From 9f1a29d056af90fe859d4daab71cd2f3503feaaf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 4 Dec 2020 20:34:15 -0500 Subject: [PATCH 001/518] Initial commit --- .gitattributes | 2 + LICENSE | 674 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 676 insertions(+) create mode 100644 .gitattributes create mode 100644 LICENSE diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfe07704 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e62ec04c --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From 003081ff2a89bd61acdede5623619304ee37d791 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 4 Dec 2020 20:34:55 -0500 Subject: [PATCH 002/518] Gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..355f79fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +__pycache__/ +*.pyc +*.pyo +*.bak +*.wxg# From 16aae9e8679d083b0e30dcbb8bd8b3255478d987 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 4 Dec 2020 20:42:41 -0500 Subject: [PATCH 003/518] Starting! --- .gitignore | 1 + ro_py/__init__.py | 1 + ro_py/examples/test.py | 1 + ro_py/users.py | 17 +++++++++++++++++ 4 files changed, 20 insertions(+) create mode 100644 ro_py/__init__.py create mode 100644 ro_py/examples/test.py create mode 100644 ro_py/users.py diff --git a/.gitignore b/.gitignore index 355f79fd..525a02ba 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ *.pyo *.bak *.wxg# +*.??~ diff --git a/ro_py/__init__.py b/ro_py/__init__.py new file mode 100644 index 00000000..d8981a9c --- /dev/null +++ b/ro_py/__init__.py @@ -0,0 +1 @@ +from ro_py.users import * diff --git a/ro_py/examples/test.py b/ro_py/examples/test.py new file mode 100644 index 00000000..6960f3c0 --- /dev/null +++ b/ro_py/examples/test.py @@ -0,0 +1 @@ +import ro_py as roblox diff --git a/ro_py/users.py b/ro_py/users.py new file mode 100644 index 00000000..6c0a4ec9 --- /dev/null +++ b/ro_py/users.py @@ -0,0 +1,17 @@ +import requests + +endpoint = "http://users.roblox.com/" + + +class User: + def __init__(self, user_id): + self.user_id = user_id + user_info_req = requests.get(endpoint + f"v1/users/{user_id}") + user_info = user_info_req.json() + self.description = user_info["description"] + self.is_banned = user_info["isBanned"] + self.name = user_info["name"] + self.display_name = user_info["displayName"] + + def get_status(self): + status_req = requests.get(endpoint + f"v1/users/{self.user_id}/status") \ No newline at end of file From ebb4ed07c6cb5ada627a1b964a8ea9ea8e25b004 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 4 Dec 2020 20:49:53 -0500 Subject: [PATCH 004/518] Users and badges! --- ro_py/badges.py | 29 +++++++++++++++++++++++++++++ ro_py/users.py | 7 +++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 ro_py/badges.py diff --git a/ro_py/badges.py b/ro_py/badges.py new file mode 100644 index 00000000..66e4c191 --- /dev/null +++ b/ro_py/badges.py @@ -0,0 +1,29 @@ +from roa import read_only_attributes +import requests + +endpoint = "http://badges.roblox.com/" + + +class BadgeStatistics: + def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage): + self.past_date_awarded_count = past_date_awarded_count + self.awarded_count = awarded_count + self.win_rate_percentage = win_rate_percentage + + +class Badge: + def __init__(self, badge_id): + self.id = badge_id + badge_info_req = requests.get(endpoint + f"v1/badges/{badge_id}") + badge_info = badge_info_req.json() + self.name = badge_info["name"] + self.description = badge_info["description"] + self.display_name = badge_info["displayName"] + self.display_description = badge_info["displayDescription"] + self.enabled = badge_info["enabled"] + statistics_info = badge_info["statistics"] + self.statistics = BadgeStatistics( + statistics_info["pastDayAwardedCount"], + statistics_info["awardedCount"], + statistics_info["winRatePercentage"] + ) diff --git a/ro_py/users.py b/ro_py/users.py index 6c0a4ec9..b64025ee 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,11 +1,13 @@ +from roa import read_only_attributes import requests endpoint = "http://users.roblox.com/" +@read_only_attributes("user_id", "description", "is_banned", "name", "display_name") class User: def __init__(self, user_id): - self.user_id = user_id + self.id = user_id user_info_req = requests.get(endpoint + f"v1/users/{user_id}") user_info = user_info_req.json() self.description = user_info["description"] @@ -14,4 +16,5 @@ def __init__(self, user_id): self.display_name = user_info["displayName"] def get_status(self): - status_req = requests.get(endpoint + f"v1/users/{self.user_id}/status") \ No newline at end of file + status_req = requests.get(endpoint + f"v1/users/{self.user_id}/status") + return status_req.json()["status"] From b7b0e1996aba6ed2dc48dd637ec1e046fc305d0a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 4 Dec 2020 21:10:31 -0500 Subject: [PATCH 005/518] Updates + Game --- ro_py/__init__.py | 4 +++- ro_py/badges.py | 3 +-- ro_py/games.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ ro_py/users.py | 10 +++++----- 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 ro_py/games.py diff --git a/ro_py/__init__.py b/ro_py/__init__.py index d8981a9c..46ffd089 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -1 +1,3 @@ -from ro_py.users import * +from ro_py.users import User +from ro_py.badges import Badge +from ro_py.games import Game \ No newline at end of file diff --git a/ro_py/badges.py b/ro_py/badges.py index 66e4c191..b349343f 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -1,7 +1,6 @@ -from roa import read_only_attributes import requests -endpoint = "http://badges.roblox.com/" +endpoint = "https://badges.roblox.com/" class BadgeStatistics: diff --git a/ro_py/games.py b/ro_py/games.py new file mode 100644 index 00000000..aee1eaab --- /dev/null +++ b/ro_py/games.py @@ -0,0 +1,45 @@ +from ro_py import User +import requests + +endpoint = "https://games.roblox.com/" + + +class Votes: + def __init__(self, votes_data): + self.up_votes = votes_data["upVotes"] + self.down_votes = votes_data["downVotes"] + + +class Game: + def __init__(self, universe_id): + self.id = universe_id + game_info_req = requests.get( + url=endpoint + "v1/games", + params={ + "universeIds": str(self.id) + } + ) + game_info = game_info_req.json() + game_info = game_info["data"][0] + self.name = game_info["name"] + self.description = game_info["description"] + if game_info["creator"]["type"] == "User": + self.creator = User(game_info["creator"]["id"]) + self.price = game_info["price"] + self.allowed_gear_genres = game_info["allowedGearGenres"] + self.allowed_gear_categories = game_info["allowedGearCategories"] + self.max_players = game_info["maxPlayers"] + self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] + self.create_vip_servers_allowed = game_info["createVipServersAllowed"] + + def get_votes(self): + votes_info_req = requests.get( + url=endpoint + "v1/games/votes", + params={ + "universeIds": str(self.id) + } + ) + votes_info = votes_info_req.json() + votes_info = votes_info["data"][0] + votes = Votes(votes_info) + return votes diff --git a/ro_py/users.py b/ro_py/users.py index b64025ee..487f4038 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,20 +1,20 @@ -from roa import read_only_attributes import requests -endpoint = "http://users.roblox.com/" +endpoint = "https://users.roblox.com/" -@read_only_attributes("user_id", "description", "is_banned", "name", "display_name") class User: def __init__(self, user_id): self.id = user_id - user_info_req = requests.get(endpoint + f"v1/users/{user_id}") + user_info_req = requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] + has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + self.has_premium = has_premium_req def get_status(self): - status_req = requests.get(endpoint + f"v1/users/{self.user_id}/status") + status_req = requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] From 63802a0368e0e1c9416c00a9208ad69edb18821c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:08:36 -0500 Subject: [PATCH 006/518] Groups! --- ro_py/__init__.py | 3 ++- ro_py/groups.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 ro_py/groups.py diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 46ffd089..13613494 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -1,3 +1,4 @@ from ro_py.users import User from ro_py.badges import Badge -from ro_py.games import Game \ No newline at end of file +from ro_py.games import Game +from ro_py.groups import Group diff --git a/ro_py/groups.py b/ro_py/groups.py new file mode 100644 index 00000000..b0df759a --- /dev/null +++ b/ro_py/groups.py @@ -0,0 +1,18 @@ +from ro_py import User +import requests + +endpoint = "https://groups.roblox.com/" + + +class Group: + def __init__(self, group_id): + self.id = group_id + group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") + group_info = group_info_req.json() + self.name = group_info["name"] + self.description = group_info["description"] + self.owner = User(group_info["owner"]["userId"]) + self.member_count = group_info["memberCount"] + self.is_builders_club_only = group_info["isBuildersClubOnly"] + self.public_entry_allowed = group_info["public_entry_allowed"] + self.is_locked = group_info["isLocked"] From 2d3914dad583a5936d8f27897b36b45dd40ad1a6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:10:46 -0500 Subject: [PATCH 007/518] Group Shout! --- ro_py/groups.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index b0df759a..a8a495b2 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -4,6 +4,12 @@ endpoint = "https://groups.roblox.com/" +class Shout: + def __init__(self, shout_data): + self.body = shout_data["body"] + self.poster = User(shout_data["poster"]["userId"]) + + class Group: def __init__(self, group_id): self.id = group_id @@ -12,6 +18,7 @@ def __init__(self, group_id): self.name = group_info["name"] self.description = group_info["description"] self.owner = User(group_info["owner"]["userId"]) + self.shout = Shout(group_info["shout"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["public_entry_allowed"] From 61334d82e40be80d725fb4050a5f6c274c294f3b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:17:44 -0500 Subject: [PATCH 008/518] Game + Group updates Games now support Group ownership, and group shouts have better support. --- ro_py/games.py | 4 +++- ro_py/groups.py | 9 ++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index aee1eaab..e9ef9aad 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -1,4 +1,4 @@ -from ro_py import User +from ro_py import User, Group import requests endpoint = "https://games.roblox.com/" @@ -25,6 +25,8 @@ def __init__(self, universe_id): self.description = game_info["description"] if game_info["creator"]["type"] == "User": self.creator = User(game_info["creator"]["id"]) + elif game_info["creator"]["type"] == "Group": + self.creator = Group(game_info["creator"]["id"]) self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] diff --git a/ro_py/groups.py b/ro_py/groups.py index a8a495b2..1c43b0d1 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -18,8 +18,11 @@ def __init__(self, group_id): self.name = group_info["name"] self.description = group_info["description"] self.owner = User(group_info["owner"]["userId"]) - self.shout = Shout(group_info["shout"]) + if group_info["shout"]: + self.shout = Shout(group_info["shout"]) + else: + self.shout = None self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] - self.public_entry_allowed = group_info["public_entry_allowed"] - self.is_locked = group_info["isLocked"] + self.public_entry_allowed = group_info["publicEntryAllowed"] + # self.is_locked = group_info["isLocked"] From 633dce99a81c6357d613a48f7051b0162b26210f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:28:24 -0500 Subject: [PATCH 009/518] Fixed imports + shout --- ro_py/__init__.py | 2 +- ro_py/groups.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 13613494..d9f2577c 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -1,4 +1,4 @@ from ro_py.users import User +from ro_py.groups import Group from ro_py.badges import Badge from ro_py.games import Game -from ro_py.groups import Group diff --git a/ro_py/groups.py b/ro_py/groups.py index 1c43b0d1..0fe8dd29 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -18,11 +18,18 @@ def __init__(self, group_id): self.name = group_info["name"] self.description = group_info["description"] self.owner = User(group_info["owner"]["userId"]) - if group_info["shout"]: - self.shout = Shout(group_info["shout"]) - else: - self.shout = None + self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] # self.is_locked = group_info["isLocked"] + + @property + def shout(self): + group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") + group_info = group_info_req.json() + + if group_info["shout"]: + return Shout(group_info["shout"]) + else: + return None From e898e5391a07018fb5e1656e2a5f4ac60fddb900 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:29:48 -0500 Subject: [PATCH 010/518] Votes are now a @property --- ro_py/games.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index e9ef9aad..c676f9db 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -33,8 +33,9 @@ def __init__(self, universe_id): self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - - def get_votes(self): + + @property + def votes(self): votes_info_req = requests.get( url=endpoint + "v1/games/votes", params={ From 03c242e4ed0a8a3090feeefb3422fe65c56eaef7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 12:43:51 -0500 Subject: [PATCH 011/518] =?UTF-8?q?Asset=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/assets.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 ro_py/assets.py diff --git a/ro_py/assets.py b/ro_py/assets.py new file mode 100644 index 00000000..bde78b19 --- /dev/null +++ b/ro_py/assets.py @@ -0,0 +1,45 @@ +from ro_py import User, Group +import requests + +endpoint = "https://api.roblox.com/" + + +class Asset: + def __init__(self, asset_id): + asset_info_req = requests.get( + url=endpoint + "marketplace/productinfo", + params={ + "assetId": asset_id + } + ) + asset_info = asset_info_req.json() + self.target_id = asset_info["TargetId"] + self.product_type = asset_info["ProductType"] + self.asset_id = asset_info["AssetId"] + self.product_id = asset_info["ProductId"] + self.name = asset_info["Name"] + self.description = asset_info["Description"] + self.asset_type_id = asset_info["AssetTypeId"] + if asset_info["Creator"]["CreatorType"] == "User": + self.creator = User(asset_info["Creator"]["Id"]) + elif asset_info["Creator"]["CreatorType"] == "Group": + self.creator = Group(asset_info["Creator"]["Id"]) + self.price = asset_info["PriceInRobux"] + self.is_new = asset_info["IsNew"] + self.is_for_sale = asset_info["IsForSale"] + self.is_public_domain = asset_info["IsPublicDomain"] + self.is_limited = asset_info["IsLimited"] + self.is_limited_unique = asset_info["IsLimitedUnique"] + self.minimum_membership_level = asset_info["MinimumMembershipLevel"] + self.content_rating_type_id = asset_info["ContentRatingTypeId"] + + @property + def remaining(self): + asset_info_req = requests.get( + url=endpoint + "marketplace/productinfo", + params={ + "assetId": self.asset_id + } + ) + asset_info = asset_info_req.json() + return asset_info["Remaining"] From 78d28761ab2e4c7c9fb1caa859fa82f8b9455420 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 13:14:51 -0500 Subject: [PATCH 012/518] Asset added to __init__ + date parsing Asset dates are now parsed from iso8601. --- ro_py/__init__.py | 1 + ro_py/assets.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index d9f2577c..b53ce2f4 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -2,3 +2,4 @@ from ro_py.groups import Group from ro_py.badges import Badge from ro_py.games import Game +from ro_py.assets import Asset diff --git a/ro_py/assets.py b/ro_py/assets.py index bde78b19..191818fb 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -1,4 +1,5 @@ from ro_py import User, Group +import iso8601 import requests endpoint = "https://api.roblox.com/" @@ -23,7 +24,9 @@ def __init__(self, asset_id): if asset_info["Creator"]["CreatorType"] == "User": self.creator = User(asset_info["Creator"]["Id"]) elif asset_info["Creator"]["CreatorType"] == "Group": - self.creator = Group(asset_info["Creator"]["Id"]) + self.creator = Group(asset_info["Creator"]["CreatorTargetId"]) + self.created = iso8601.parse_date(asset_info["Created"]) + self.updated = iso8601.parse_date(asset_info["Updated"]) self.price = asset_info["PriceInRobux"] self.is_new = asset_info["IsNew"] self.is_for_sale = asset_info["IsForSale"] From f34c639d7292fa26b12766e8c7624cca390b82a3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 13:43:23 -0500 Subject: [PATCH 013/518] Example updated + more Errors added, asset_example was created, LimitedResaleData was added. --- README.md | 0 ro_py/assets.py | 18 ++++++++++++++++++ ro_py/errors.py | 3 +++ ro_py/examples/asset_example.py | 20 ++++++++++++++++++++ ro_py/examples/test.py | 1 - 5 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 README.md create mode 100644 ro_py/errors.py create mode 100644 ro_py/examples/asset_example.py delete mode 100644 ro_py/examples/test.py diff --git a/README.md b/README.md new file mode 100644 index 00000000..e69de29b diff --git a/ro_py/assets.py b/ro_py/assets.py index 191818fb..002417e1 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -1,10 +1,20 @@ from ro_py import User, Group +from ro_py.errors import NotLimitedError import iso8601 import requests endpoint = "https://api.roblox.com/" +class LimitedResaleData: + def __init__(self, resale_data): + self.asset_stock = resale_data["assetStock"] + self.sales = resale_data["sales"] + self.number_remaining = resale_data["numberRemaining"] + self.recent_average_price = resale_data["recentAveragePrice"] + self.original_price = resale_data["originalPrice"] + + class Asset: def __init__(self, asset_id): asset_info_req = requests.get( @@ -46,3 +56,11 @@ def remaining(self): ) asset_info = asset_info_req.json() return asset_info["Remaining"] + + @property + def limited_resale_data(self): + if self.is_limited: + resale_data_req = requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") + return LimitedResaleData(resale_data_req.json()) + else: + raise NotLimitedError("You can only read this information on limited items.") diff --git a/ro_py/errors.py b/ro_py/errors.py new file mode 100644 index 00000000..822deef5 --- /dev/null +++ b/ro_py/errors.py @@ -0,0 +1,3 @@ +class NotLimitedError(Exception): + """Called when code attempts to read limited-only information.""" + pass diff --git a/ro_py/examples/asset_example.py b/ro_py/examples/asset_example.py new file mode 100644 index 00000000..3050ec05 --- /dev/null +++ b/ro_py/examples/asset_example.py @@ -0,0 +1,20 @@ +from ro_py import Asset + +asset_id = "130213380" +print(f"Loading asset {asset_id}...") +asset = Asset(asset_id) +print("Loaded assset.") +print(f"Name: {asset.name}") +print(f"Description: {asset.description}") +print(f"Limited: {asset.is_limited}") +if asset.is_limited: + resale_data = asset.limited_resale_data + print(f"Original Price: {resale_data.original_price}") + print(f"Number Remaining: {resale_data.number_remaining}") + print(f"Recent Average Price: {resale_data.recent_average_price}") + print(f"Stock: {resale_data.asset_stock}") + print(f"Sales: {resale_data.sales}") +else: + print(f"Price: {asset.price} R$") +print(f"Created: {asset.created.strftime('%b %d %Y %H:%M:%S')}") +print(f"Updated: {asset.updated.strftime('%b %d %Y %H:%M:%S')}") diff --git a/ro_py/examples/test.py b/ro_py/examples/test.py deleted file mode 100644 index 6960f3c0..00000000 --- a/ro_py/examples/test.py +++ /dev/null @@ -1 +0,0 @@ -import ro_py as roblox From 7b0b6446067b449778ed61b812f35fcd09280bb0 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 13:51:27 -0500 Subject: [PATCH 014/518] Updated Readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index e69de29b..0850ae3e 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,21 @@ +# Welcome to ro.py +ro.py is a Python wrapper for the Roblox web API. +## Examples +Reading a user's description: +```python +from ro_py import User +user = User(576059883) +print(f"Username: {user.name}") +print(f"Description: {user.description}") +``` +Reading a game's votes: +```python +from ro_py import Game +game = Game(1732173541) # This takes in a Universe ID and not a Place ID +votes = game.votes +print(f"Likes: {votes.up_votes}") +print(f"Dislikes: {votes.down_votes}") +``` +## Other Libraries +https://github.com/RbxAPI/Pyblox +https://github.com/iranathan/robloxapi \ No newline at end of file From bdb04c04bcd1a482e53a972bb381cf956aa6cc32 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 13:52:48 -0500 Subject: [PATCH 015/518] Update Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0850ae3e..2cc4fb7a 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ votes = game.votes print(f"Likes: {votes.up_votes}") print(f"Dislikes: {votes.down_votes}") ``` +You can read more examples in the `examples` directory. ## Other Libraries https://github.com/RbxAPI/Pyblox https://github.com/iranathan/robloxapi \ No newline at end of file From 3eb59aef3b3ea6032eb043ff85f58fc4efe6f2ec Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 14:10:37 -0500 Subject: [PATCH 016/518] New example and status updated "status" is now a property instead of being a "get_status" function. --- README.md | 6 +++--- ro_py/examples/asset_example.py | 2 +- ro_py/examples/user_example.py | 11 +++++++++++ ro_py/users.py | 3 ++- 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 ro_py/examples/user_example.py diff --git a/README.md b/README.md index 2cc4fb7a..d2eefab5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ user = User(576059883) print(f"Username: {user.name}") print(f"Description: {user.description}") ``` -Reading a game's votes: +Reading a game's votes: ```python from ro_py import Game game = Game(1732173541) # This takes in a Universe ID and not a Place ID @@ -18,5 +18,5 @@ print(f"Dislikes: {votes.down_votes}") ``` You can read more examples in the `examples` directory. ## Other Libraries -https://github.com/RbxAPI/Pyblox -https://github.com/iranathan/robloxapi \ No newline at end of file +https://github.com/RbxAPI/Pyblox +https://github.com/iranathan/robloxapi \ No newline at end of file diff --git a/ro_py/examples/asset_example.py b/ro_py/examples/asset_example.py index 3050ec05..ca747307 100644 --- a/ro_py/examples/asset_example.py +++ b/ro_py/examples/asset_example.py @@ -1,6 +1,6 @@ from ro_py import Asset -asset_id = "130213380" +asset_id = 130213380 print(f"Loading asset {asset_id}...") asset = Asset(asset_id) print("Loaded assset.") diff --git a/ro_py/examples/user_example.py b/ro_py/examples/user_example.py new file mode 100644 index 00000000..098e3bd2 --- /dev/null +++ b/ro_py/examples/user_example.py @@ -0,0 +1,11 @@ +from ro_py import User + +user_id = 576059883 +print(f"Loading user {user_id}...") +user = User(user_id) +print("Loaded user.") + +print(f"Username: {user.name}") +print(f"Display Name: {user.display_name}") +print(f"Description: {user.description}") +print(f"Status: {user.status}") diff --git a/ro_py/users.py b/ro_py/users.py index 487f4038..d7abd35f 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -15,6 +15,7 @@ def __init__(self, user_id): has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") self.has_premium = has_premium_req - def get_status(self): + @property + def status(self): status_req = requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] From 740b58020e566f8ef05912a8b94a00c20a15c1cf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 14:31:53 -0500 Subject: [PATCH 017/518] New Gitignore + setup.py --- .gitignore | 3 +++ setup.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 525a02ba..bf14a317 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __pycache__/ *.bak *.wxg# *.??~ +build/ +dist/ +ro_py.egg-info/ diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..ebcc4891 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="ro-py", # Replace with your own username + version="0.0.1", + author="jmkdev", + author_email="jmk@jmksite.dev", + description="ro.py is a Python wrapper for the Roblox web API.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/jmk-developer/ro.py", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', +) \ No newline at end of file From 4fa8b0162978e8f077a3eccecb477d99b5a299bd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 14:38:47 -0500 Subject: [PATCH 018/518] Updated Readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d2eefab5..94314bbc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Welcome to ro.py ro.py is a Python wrapper for the Roblox web API. +## Installation +You can install ro.py from pip: +``` +pip3 install ro-py +``` ## Examples Reading a user's description: ```python From bf7fbde51ca54788394144d7f222e7cdc87c24b9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 15:39:42 -0500 Subject: [PATCH 019/518] User now has a creation date --- ro_py/users.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index d7abd35f..129b7388 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,14 +1,38 @@ import requests +import iso8601 endpoint = "https://users.roblox.com/" class User: - def __init__(self, user_id): - self.id = user_id + def __init__(self, ui): + if isinstance(ui, str): + is_id = False + try: + int(str) + is_id = True + except TypeError: + is_id = False + if is_id: + self.id = int(ui) + else: + user_id_req = requests.post( + url="https://users.roblox.com/v1/usernames/users", + json={ + "usernames": [ + ui + ] + } + ) + user_id = user_id_req.json()["data"][0]["id"] + self.id = user_id + elif isinstance(ui, int): + self.id = ui + user_info_req = requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] + self.created = iso8601.parse_date(user_info["created"]) self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] From 8962fb961310fdefd3885e6cb0d0cfea50488880 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 15:46:43 -0500 Subject: [PATCH 020/518] Assset type names --- ro_py/asset_type.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ ro_py/assets.py | 2 ++ 2 files changed, 50 insertions(+) create mode 100644 ro_py/asset_type.py diff --git a/ro_py/asset_type.py b/ro_py/asset_type.py new file mode 100644 index 00000000..4b5726c7 --- /dev/null +++ b/ro_py/asset_type.py @@ -0,0 +1,48 @@ +asset_types = [ + None, + "Image", + "TeeShirt", + "Audio", + "Mesh", + "Lua", + "Hat", + "Place", + "Model", + "Shirt", + "Pants", + "Decal", + "Head", + "Face", + "Gear", + "Badge", + "Animation", + "Torso", + "RightArm", + "LeftArm", + "LeftLeg", + "RightLeg", + "Package", + "GamePass", + "Plugin", + "MeshPart", + "HairAccessory", + "FaceAccessory", + "NeckAccessory", + "ShoulderAccessory", + "FrontAccesory", + "BackAccessory", + "WaistAccessory", + "ClimbAnimation", + "DeathAnimation", + "FallAnimation", + "IdleAnimation", + "JumpAnimation", + "RunAnimation", + "SwimAnimation", + "WalkAnimation", + "PoseAnimation", + "EarAccessory", + "EyeAccessory", + "EmoteAnimation", + "Video" +] diff --git a/ro_py/assets.py b/ro_py/assets.py index 002417e1..911254d8 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -1,5 +1,6 @@ from ro_py import User, Group from ro_py.errors import NotLimitedError +from ro_py.asset_type import asset_types import iso8601 import requests @@ -31,6 +32,7 @@ def __init__(self, asset_id): self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] + self.asset_type_name = asset_types[self.asset_type_id] if asset_info["Creator"]["CreatorType"] == "User": self.creator = User(asset_info["Creator"]["Id"]) elif asset_info["Creator"]["CreatorType"] == "Group": From fd377006e8d96d8987e2367a09a0dceca920a136 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 15:59:44 -0500 Subject: [PATCH 021/518] Roblox Badges added --- ro_py/robloxbadges.py | 6 ++++++ ro_py/users.py | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 ro_py/robloxbadges.py diff --git a/ro_py/robloxbadges.py b/ro_py/robloxbadges.py new file mode 100644 index 00000000..b96da4c2 --- /dev/null +++ b/ro_py/robloxbadges.py @@ -0,0 +1,6 @@ +class RobloxBadge: + def __init__(self, roblox_badge_data): + self.id = roblox_badge_data["id"] + self.name = roblox_badge_data["name"] + self.description = roblox_badge_data["description"] + self.image_url = roblox_badge_data["imageUrl"] diff --git a/ro_py/users.py b/ro_py/users.py index 129b7388..d42d4627 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,3 +1,4 @@ +from ro_py.robloxbadges import RobloxBadge import requests import iso8601 @@ -43,3 +44,11 @@ def __init__(self, ui): def status(self): status_req = requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] + + @property + def roblox_badges(self): + roblox_badges_req = requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges = [] + for roblox_badge_data in roblox_badges_req.json(): + roblox_badges.append(RobloxBadge(roblox_badge_data)) + return roblox_badges From fc42ac9572e786e298a23572e1e7f6e231bd458f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 16:00:49 -0500 Subject: [PATCH 022/518] Version identifier updated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ebcc4891..eeec490d 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", # Replace with your own username - version="0.0.1", + version="0.0.2", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From b9e3043f7f614c55d5d696e45f15e1cda5dfcf9b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 16:02:47 -0500 Subject: [PATCH 023/518] Added dist_old to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bf14a317..24875371 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__/ *.??~ build/ dist/ +dist_old/ ro_py.egg-info/ From 8280a18958a676c62d478929e33eed7e821e4388 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 16:16:39 -0500 Subject: [PATCH 024/518] Group Icons You can now get icons from groups. --- ro_py/groups.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index 0fe8dd29..74ba82e8 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -33,3 +33,16 @@ def shout(self): return Shout(group_info["shout"]) else: return None + + def get_icon(self, size="150x150", format="Png", is_circular=False): + group_icon_req = requests.get( + url="https://thumbnails.roblox.com/v1/groups/icons", + params={ + "groupIds": str(self.id), + "size": size, + "format": format, + "isCircular": is_circular + } + ) + group_icon = group_icon_req.json()["data"][0]["imageUrl"] + return group_icon From 67af99ebd630c49196706165c5dee937a60927cc Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 16:38:23 -0500 Subject: [PATCH 025/518] =?UTF-8?q?Thumbnails!=20=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thumbnail-related functions are now centralized in a thumbnail section. (Also added: new error for invalid icon size.) --- ro_py/errors.py | 5 ++++ ro_py/groups.py | 16 +++--------- ro_py/thumbnails.py | 64 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 ro_py/thumbnails.py diff --git a/ro_py/errors.py b/ro_py/errors.py index 822deef5..4d490a4c 100644 --- a/ro_py/errors.py +++ b/ro_py/errors.py @@ -1,3 +1,8 @@ class NotLimitedError(Exception): """Called when code attempts to read limited-only information.""" pass + + +class InvalidIconSizeError(Exception): + """Called when code attempts to pass in an improper size to a thumbnail function.""" + pass diff --git a/ro_py/groups.py b/ro_py/groups.py index 74ba82e8..1f028d9b 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -1,4 +1,4 @@ -from ro_py import User +from ro_py import User, thumbnails import requests endpoint = "https://groups.roblox.com/" @@ -34,15 +34,5 @@ def shout(self): else: return None - def get_icon(self, size="150x150", format="Png", is_circular=False): - group_icon_req = requests.get( - url="https://thumbnails.roblox.com/v1/groups/icons", - params={ - "groupIds": str(self.id), - "size": size, - "format": format, - "isCircular": is_circular - } - ) - group_icon = group_icon_req.json()["data"][0]["imageUrl"] - return group_icon + def get_icon(self, size=thumbnails.size_150x150, format=thumbnails.format_png, is_circular=False): + return thumbnails.get_group_icon(self.id, size, format, is_circular) diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py new file mode 100644 index 00000000..883b7204 --- /dev/null +++ b/ro_py/thumbnails.py @@ -0,0 +1,64 @@ +from ro_py import errors +import requests + +endpoint = "https://thumbnails.roblox.com/" + +PlaceHolder = "PlaceHolder" +AutoGenerated = "AutoGenerated" +ForceAutoGenerated = "ForceAutoGenerated" + +size_50x50 = "50x50" +size_128x128 = "128x128" +size_150x150 = "150x150" +size_256x256 = "256x256" +size_420x420 = "420x420" +size_512x512 = "512x512" +format_png = "Png" +format_jpg = "Jpeg" +format_jpeg = "Jpeg" + + +def get_group_icon(group_id, size=size_150x150, format=format_png, is_circular=False): + """ + Gets a game's icon. + :param group_id: The group's ID. + :param size: The thumbnail size, formatted widthxheight. + :param format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + group_icon_req = requests.get( + url=endpoint + "v1/groups/icons", + params={ + "groupIds": str(group_id), + "size": size, + "format": format, + "isCircular": is_circular + } + ) + group_icon = group_icon_req.json()["data"][0]["imageUrl"] + return group_icon + + +def get_game_icon(game_id, return_policy=ForceAutoGenerated, size=size_256x256, format=format_png, is_circular=False): + """ + Gets a game's icon. + :param game_id: The game's UniverseID. + :param return_policy: Optional policy to use in selecting game icon to return. + :param size: The thumbnail size, formatted widthxheight. + :param format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + game_icon_req = requests.get( + url=endpoint + "v1/games/icons", + params={ + "universeIds": str(game_id), + "returnPolicy": return_policy, + "size": size, + "format": format, + "isCircular": is_circular + } + ) + game_icon = game_icon_req.json()["data"][0]["imageUrl"] + return game_icon From d77c86bad37530c40c129aae288b372b859155f3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 17:06:17 -0500 Subject: [PATCH 026/518] Game icons + more size values --- ro_py/games.py | 7 +++++-- ro_py/thumbnails.py | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index c676f9db..54e27387 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -1,4 +1,4 @@ -from ro_py import User, Group +from ro_py import User, Group, thumbnails import requests endpoint = "https://games.roblox.com/" @@ -33,7 +33,7 @@ def __init__(self, universe_id): self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - + @property def votes(self): votes_info_req = requests.get( @@ -46,3 +46,6 @@ def votes(self): votes_info = votes_info["data"][0] votes = Votes(votes_info) return votes + + def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): + return thumbnails.get_game_icon(self.id, size, format, is_circular) diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 883b7204..7f7c0e3f 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -7,12 +7,27 @@ AutoGenerated = "AutoGenerated" ForceAutoGenerated = "ForceAutoGenerated" +size_30x30 = "30x30" +size_42x24 = "42x42" size_50x50 = "50x50" +size_60x62 = "60x62" +size_75x75 = "75x75" +size_110x110 = "110x110" size_128x128 = "128x128" +size_140x140 = "140x140" size_150x150 = "150x150" +size_160x100 = "160x100" +size_250x250 = "250x250" +size_256x144 = "256x144" size_256x256 = "256x256" +size_300x250 = "300x240" +size_304x166 = "304x166" +size_384x216 = "384x216" +size_396x216 = "396x216" size_420x420 = "420x420" +size_480x270 = "480x270" size_512x512 = "512x512" +size_720x720 = "720x720" format_png = "Png" format_jpg = "Jpeg" format_jpeg = "Jpeg" @@ -40,11 +55,10 @@ def get_group_icon(group_id, size=size_150x150, format=format_png, is_circular=F return group_icon -def get_game_icon(game_id, return_policy=ForceAutoGenerated, size=size_256x256, format=format_png, is_circular=False): +def get_game_icon(game_id, size=size_256x256, format=format_png, is_circular=False): """ Gets a game's icon. :param game_id: The game's UniverseID. - :param return_policy: Optional policy to use in selecting game icon to return. :param size: The thumbnail size, formatted widthxheight. :param format: The thumbnail format :param is_circular: The circle thumbnail output parameter. @@ -54,7 +68,7 @@ def get_game_icon(game_id, return_policy=ForceAutoGenerated, size=size_256x256, url=endpoint + "v1/games/icons", params={ "universeIds": str(game_id), - "returnPolicy": return_policy, + "returnPolicy": PlaceHolder, "size": size, "format": format, "isCircular": is_circular From 12467297df04835698989b7990a88540bf1ef5f1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 17:24:30 -0500 Subject: [PATCH 027/518] Comments, pt 1. --- ro_py/games.py | 13 +++++++++++++ ro_py/robloxbadges.py | 5 +++++ ro_py/users.py | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/ro_py/games.py b/ro_py/games.py index 54e27387..644c2730 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -5,12 +5,19 @@ class Votes: + """ + Represents a game's votes. + """ def __init__(self, votes_data): self.up_votes = votes_data["upVotes"] self.down_votes = votes_data["downVotes"] class Game: + """ + Represents a Roblox game universe. + This class represents multiple game-related endpoints. + """ def __init__(self, universe_id): self.id = universe_id game_info_req = requests.get( @@ -36,6 +43,9 @@ def __init__(self, universe_id): @property def votes(self): + """ + :return: An instance of Votes + """ votes_info_req = requests.get( url=endpoint + "v1/games/votes", params={ @@ -48,4 +58,7 @@ def votes(self): return votes def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): + """ + Equivalent to thumbnails.get_game_icon + """ return thumbnails.get_game_icon(self.id, size, format, is_circular) diff --git a/ro_py/robloxbadges.py b/ro_py/robloxbadges.py index b96da4c2..4b1d968d 100644 --- a/ro_py/robloxbadges.py +++ b/ro_py/robloxbadges.py @@ -1,4 +1,9 @@ class RobloxBadge: + """ + Represents a Roblox badge. + This is not equivalent to a badge you would earn from a game. + This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges. + """ def __init__(self, roblox_badge_data): self.id = roblox_badge_data["id"] self.name = roblox_badge_data["name"] diff --git a/ro_py/users.py b/ro_py/users.py index d42d4627..e6dd02eb 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -6,6 +6,10 @@ class User: + """ + Represents a Roblox user and their profile. + Can be initialized with either a user ID or a username. + """ def __init__(self, ui): if isinstance(ui, str): is_id = False From ee40bfa148e43a03a0c95a828e085c246a7f78ae Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 18:11:07 -0500 Subject: [PATCH 028/518] Comments, pt 2. --- ro_py/assets.py | 6 ++++++ ro_py/badges.py | 6 ++++++ ro_py/groups.py | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/ro_py/assets.py b/ro_py/assets.py index 911254d8..3c99fbeb 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -8,6 +8,9 @@ class LimitedResaleData: + """ + Represents the resale data of a limited item. + """ def __init__(self, resale_data): self.asset_stock = resale_data["assetStock"] self.sales = resale_data["sales"] @@ -17,6 +20,9 @@ def __init__(self, resale_data): class Asset: + """ + Represents an asset. + """ def __init__(self, asset_id): asset_info_req = requests.get( url=endpoint + "marketplace/productinfo", diff --git a/ro_py/badges.py b/ro_py/badges.py index b349343f..5ba71322 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -4,6 +4,9 @@ class BadgeStatistics: + """ + Represents a badge's statistics. + """ def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage): self.past_date_awarded_count = past_date_awarded_count self.awarded_count = awarded_count @@ -11,6 +14,9 @@ def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage): class Badge: + """ + Represents a game-awarded badge. + """ def __init__(self, badge_id): self.id = badge_id badge_info_req = requests.get(endpoint + f"v1/badges/{badge_id}") diff --git a/ro_py/groups.py b/ro_py/groups.py index 1f028d9b..2d2252bd 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -5,12 +5,18 @@ class Shout: + """ + Represents a group shout. + """ def __init__(self, shout_data): self.body = shout_data["body"] self.poster = User(shout_data["poster"]["userId"]) class Group: + """ + Represents a group. + """ def __init__(self, group_id): self.id = group_id group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") @@ -26,6 +32,9 @@ def __init__(self, group_id): @property def shout(self): + """ + :return: An instance of Shout + """ group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() @@ -35,4 +44,7 @@ def shout(self): return None def get_icon(self, size=thumbnails.size_150x150, format=thumbnails.format_png, is_circular=False): + """ + Equivalent to thumbnails.get_group_icon + """ return thumbnails.get_group_icon(self.id, size, format, is_circular) From 2c7202012de08d39172777cf49bd6fe35579cb23 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 18:28:56 -0500 Subject: [PATCH 029/518] Comments, pt 3. --- ro_py/__init__.py | 14 ++++++++++++++ ro_py/asset_type.py | 8 ++++++++ ro_py/assets.py | 8 ++++++++ ro_py/badges.py | 8 ++++++++ ro_py/errors.py | 9 +++++++++ ro_py/games.py | 8 ++++++++ ro_py/groups.py | 8 ++++++++ ro_py/robloxbadges.py | 9 +++++++++ ro_py/thumbnails.py | 9 ++++++++- ro_py/users.py | 8 ++++++++ 10 files changed, 88 insertions(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index b53ce2f4..07cb05af 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -1,3 +1,17 @@ +r""" + + _ __ ___ _ __ _ _ + | '__/ _ \ | '_ \| | | | + | | | (_) || |_) | |_| | + |_| \___(_) .__/ \__, | + | | __/ | + |_| |___/ + +ro.py +by jmkdev + +""" + from ro_py.users import User from ro_py.groups import Group from ro_py.badges import Badge diff --git a/ro_py/asset_type.py b/ro_py/asset_type.py index 4b5726c7..64510b83 100644 --- a/ro_py/asset_type.py +++ b/ro_py/asset_type.py @@ -1,3 +1,11 @@ +""" + +ro.py > asset_type.py + +This file is a conversion table for asset type IDs to asset type names. + +""" + asset_types = [ None, "Image", diff --git a/ro_py/assets.py b/ro_py/assets.py index 3c99fbeb..9e872d27 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -1,3 +1,11 @@ +""" + +ro.py > assets.py + +This file houses functions and classes that pertain to Roblox assets. + +""" + from ro_py import User, Group from ro_py.errors import NotLimitedError from ro_py.asset_type import asset_types diff --git a/ro_py/badges.py b/ro_py/badges.py index 5ba71322..e3467d28 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -1,3 +1,11 @@ +""" + +ro.py > badges.py + +This file houses functions and classes that pertain to game-awarded badges. + +""" + import requests endpoint = "https://badges.roblox.com/" diff --git a/ro_py/errors.py b/ro_py/errors.py index 4d490a4c..59bdc9cc 100644 --- a/ro_py/errors.py +++ b/ro_py/errors.py @@ -1,3 +1,12 @@ +""" + +ro.py > errors.py + +This file houses custom exceptions unique to this module. + +""" + + class NotLimitedError(Exception): """Called when code attempts to read limited-only information.""" pass diff --git a/ro_py/games.py b/ro_py/games.py index 644c2730..d4820865 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -1,3 +1,11 @@ +""" + +ro.py > games.py + +This file houses functions and classes that pertain to Roblox universes and places. + +""" + from ro_py import User, Group, thumbnails import requests diff --git a/ro_py/groups.py b/ro_py/groups.py index 2d2252bd..9a104a07 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -1,3 +1,11 @@ +""" + +ro.py > groups.py + +This file houses functions and classes that pertain to Roblox groups. + +""" + from ro_py import User, thumbnails import requests diff --git a/ro_py/robloxbadges.py b/ro_py/robloxbadges.py index 4b1d968d..662480a1 100644 --- a/ro_py/robloxbadges.py +++ b/ro_py/robloxbadges.py @@ -1,3 +1,12 @@ +""" + +ro.py > robloxbadges.py + +This file houses functions and classes that pertain to roblox-awarded badges. + +""" + + class RobloxBadge: """ Represents a Roblox badge. diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 7f7c0e3f..bc36a571 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -1,4 +1,11 @@ -from ro_py import errors +""" + +ro.py > thumbnails.py + +This file houses functions and classes that pertain to Roblox icons and thumbnails. + +""" + import requests endpoint = "https://thumbnails.roblox.com/" diff --git a/ro_py/users.py b/ro_py/users.py index e6dd02eb..5de71b43 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,3 +1,11 @@ +""" + +ro.py > users.py + +This file houses functions and classes that pertain to Roblox users and profiles. + +""" + from ro_py.robloxbadges import RobloxBadge import requests import iso8601 From e8a4befa6b9767b972b181720883d7f2cb21ba7f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 18:33:58 -0500 Subject: [PATCH 030/518] Update version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index eeec490d..db4ba5e4 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", # Replace with your own username - version="0.0.2", + version="0.0.3", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 81b321d7fb38fc6002019d8b993595b1e02da26b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 19:35:28 -0500 Subject: [PATCH 031/518] Games (sort of) have badges! Sadly this can only receive the first 100 badges due to the API's page behavior. This will be expanded soon. --- ro_py/games.py | 25 ++++++++++++++++++++++++- setup.py | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index d4820865..8bf291d2 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -6,7 +6,7 @@ """ -from ro_py import User, Group, thumbnails +from ro_py import User, Group, Badge, thumbnails import requests endpoint = "https://games.roblox.com/" @@ -48,6 +48,7 @@ def __init__(self, universe_id): self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] + self.__cached_badges = False @property def votes(self): @@ -70,3 +71,25 @@ def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, i Equivalent to thumbnails.get_game_icon """ return thumbnails.get_game_icon(self.id, size, format, is_circular) + + @property + def badges(self): + """ + Note: this has a limit of 100 badges due to paging. This will be expanded soon. + :return: A list of Badge instances + """ + if not self.__cached_badges: + badges_req = requests.get( + url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", + params={ + "limit": 100, + "sortOrder": "Asc" + } + ) + badges_data = badges_req.json()["data"] + badges = [] + for badge in badges_data: + badges.append(Badge(badge["id"])) + self.__cached_badges = badges + + return self.__cached_badges diff --git a/setup.py b/setup.py index db4ba5e4..040104e1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ long_description = fh.read() setuptools.setup( - name="ro-py", # Replace with your own username + name="ro-py", version="0.0.3", author="jmkdev", author_email="jmk@jmksite.dev", From 96ca3991b4ef3fc814dd407c4f3191c0ec2fd3d9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 19:44:23 -0500 Subject: [PATCH 032/518] New example + fixed formatting --- ro_py/examples/asset_example.py | 2 ++ ro_py/examples/game_badge_example.py | 18 ++++++++++++++++++ ro_py/examples/user_example.py | 1 + 3 files changed, 21 insertions(+) create mode 100644 ro_py/examples/game_badge_example.py diff --git a/ro_py/examples/asset_example.py b/ro_py/examples/asset_example.py index ca747307..a87cdaa6 100644 --- a/ro_py/examples/asset_example.py +++ b/ro_py/examples/asset_example.py @@ -1,9 +1,11 @@ from ro_py import Asset asset_id = 130213380 + print(f"Loading asset {asset_id}...") asset = Asset(asset_id) print("Loaded assset.") + print(f"Name: {asset.name}") print(f"Description: {asset.description}") print(f"Limited: {asset.is_limited}") diff --git a/ro_py/examples/game_badge_example.py b/ro_py/examples/game_badge_example.py new file mode 100644 index 00000000..ba7a1615 --- /dev/null +++ b/ro_py/examples/game_badge_example.py @@ -0,0 +1,18 @@ +from ro_py import Game + +universe_id = 1605107130 + +print(f"Loading game {universe_id}...") +game = Game(universe_id) +print("Loaded game.") + +print(f"Name: {game.name}") + +print("Loading badges...") +badges = game.badges +print("Loaded badges.") +print(f"Badge count: {len(badges)}") +for badge in badges: + badge_tab = " "*(32-len(badge.name)) + badge_stats = badge.statistics + print(f"{badge.name}{badge_tab}Rarity: {badge_stats.win_rate_percentage}% Won Yesterday: {badge_stats.past_date_awarded_count} Won Ever: {badge_stats.awarded_count}") diff --git a/ro_py/examples/user_example.py b/ro_py/examples/user_example.py index 098e3bd2..b999bfc2 100644 --- a/ro_py/examples/user_example.py +++ b/ro_py/examples/user_example.py @@ -1,6 +1,7 @@ from ro_py import User user_id = 576059883 + print(f"Loading user {user_id}...") user = User(user_id) print("Loaded user.") From f48188f398ff5b3c67ecbe7499d832340a123fd9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 19:45:00 -0500 Subject: [PATCH 033/518] Updated example formatting again --- ro_py/examples/game_badge_example.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ro_py/examples/game_badge_example.py b/ro_py/examples/game_badge_example.py index ba7a1615..92872459 100644 --- a/ro_py/examples/game_badge_example.py +++ b/ro_py/examples/game_badge_example.py @@ -15,4 +15,7 @@ for badge in badges: badge_tab = " "*(32-len(badge.name)) badge_stats = badge.statistics - print(f"{badge.name}{badge_tab}Rarity: {badge_stats.win_rate_percentage}% Won Yesterday: {badge_stats.past_date_awarded_count} Won Ever: {badge_stats.awarded_count}") + print(f"{badge.name}{badge_tab}" + f"Rarity: {badge_stats.win_rate_percentage}% " + f"Won Yesterday: {badge_stats.past_date_awarded_count} " + f"Won Ever: {badge_stats.awarded_count}") From a9c2ce59c529d3a2c1bc9eae42c24d6925e14ec1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 21:10:13 -0500 Subject: [PATCH 034/518] Thumbnails updated + more Thumbnails now uses User, Group, and Game instances instead of IDs. You can now generate a Game from a place ID instead of a universe ID. An error has been added to accompany the new avatar image thumbnails. --- ro_py/__init__.py | 2 +- ro_py/errors.py | 5 +++++ ro_py/games.py | 27 +++++++++++++++++++++++- ro_py/groups.py | 2 +- ro_py/thumbnails.py | 51 +++++++++++++++++++++++++++++++++++++++------ 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 07cb05af..e200f5bb 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -15,5 +15,5 @@ from ro_py.users import User from ro_py.groups import Group from ro_py.badges import Badge -from ro_py.games import Game +from ro_py.games import Game, game_from_place_id from ro_py.assets import Asset diff --git a/ro_py/errors.py b/ro_py/errors.py index 59bdc9cc..f8d3d173 100644 --- a/ro_py/errors.py +++ b/ro_py/errors.py @@ -15,3 +15,8 @@ class NotLimitedError(Exception): class InvalidIconSizeError(Exception): """Called when code attempts to pass in an improper size to a thumbnail function.""" pass + + +class InvalidShotTypeError(Exception): + """Called when code attempts to pass in an improper avatar image type to a thumbnail function.""" + pass diff --git a/ro_py/games.py b/ro_py/games.py index 8bf291d2..6079a551 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -70,7 +70,7 @@ def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, i """ Equivalent to thumbnails.get_game_icon """ - return thumbnails.get_game_icon(self.id, size, format, is_circular) + return thumbnails.get_game_icon(self, size, format, is_circular) @property def badges(self): @@ -93,3 +93,28 @@ def badges(self): self.__cached_badges = badges return self.__cached_badges + + +def place_id_to_universe_id(place_id): + """ + Returns the containing universe ID of a place ID. + :param place_id: Place ID + :return: Universe ID + """ + universe_id_req = requests.get( + url="https://api.roblox.com/universes/get-universe-containing-place", + params={ + "placeId": place_id + } + ) + universe_id = universe_id_req.json()["UniverseId"] + return universe_id + + +def game_from_place_id(place_id): + """ + Generates an instance of Game with a place ID instead of a game ID. + :param place_id: Place ID + :return: Instace of Game + """ + return Game(place_id_to_universe_id(place_id)) diff --git a/ro_py/groups.py b/ro_py/groups.py index 9a104a07..fa112385 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -55,4 +55,4 @@ def get_icon(self, size=thumbnails.size_150x150, format=thumbnails.format_png, i """ Equivalent to thumbnails.get_group_icon """ - return thumbnails.get_group_icon(self.id, size, format, is_circular) + return thumbnails.get_group_icon(self, size, format, is_circular) diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index bc36a571..8674bc86 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -6,6 +6,7 @@ """ +from ro_py.errors import InvalidShotTypeError import requests endpoint = "https://thumbnails.roblox.com/" @@ -14,6 +15,10 @@ AutoGenerated = "AutoGenerated" ForceAutoGenerated = "ForceAutoGenerated" +AvatarFullBody = 0 +AvatarBust = 1 +AvatarHeadshot = 2 + size_30x30 = "30x30" size_42x24 = "42x42" size_50x50 = "50x50" @@ -34,16 +39,19 @@ size_420x420 = "420x420" size_480x270 = "480x270" size_512x512 = "512x512" +size_576x324 = "576x324" size_720x720 = "720x720" +size_768x432 = "768x432" + format_png = "Png" format_jpg = "Jpeg" format_jpeg = "Jpeg" -def get_group_icon(group_id, size=size_150x150, format=format_png, is_circular=False): +def get_group_icon(group, size=size_150x150, format=format_png, is_circular=False): """ Gets a game's icon. - :param group_id: The group's ID. + :param group: The group. :param size: The thumbnail size, formatted widthxheight. :param format: The thumbnail format :param is_circular: The circle thumbnail output parameter. @@ -52,7 +60,7 @@ def get_group_icon(group_id, size=size_150x150, format=format_png, is_circular=F group_icon_req = requests.get( url=endpoint + "v1/groups/icons", params={ - "groupIds": str(group_id), + "groupIds": str(group.id), "size": size, "format": format, "isCircular": is_circular @@ -62,10 +70,10 @@ def get_group_icon(group_id, size=size_150x150, format=format_png, is_circular=F return group_icon -def get_game_icon(game_id, size=size_256x256, format=format_png, is_circular=False): +def get_game_icon(game, size=size_256x256, format=format_png, is_circular=False): """ Gets a game's icon. - :param game_id: The game's UniverseID. + :param game: The game. :param size: The thumbnail size, formatted widthxheight. :param format: The thumbnail format :param is_circular: The circle thumbnail output parameter. @@ -74,7 +82,7 @@ def get_game_icon(game_id, size=size_256x256, format=format_png, is_circular=Fal game_icon_req = requests.get( url=endpoint + "v1/games/icons", params={ - "universeIds": str(game_id), + "universeIds": str(game.id), "returnPolicy": PlaceHolder, "size": size, "format": format, @@ -83,3 +91,34 @@ def get_game_icon(game_id, size=size_256x256, format=format_png, is_circular=Fal ) game_icon = game_icon_req.json()["data"][0]["imageUrl"] return game_icon + + +def get_avatar_image(user, shot_type=AvatarFullBody, size=size_250x250, format=format_png, is_circular=False): + """ + Gets a full body, bust, or headshot image of a user. + :param user: User to use for avatar. + :param shot_type: Type of shot. + :param size: The thumbnail size, formatted widthxheight. + :param format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + shot_endpoint = "v1/users/" + if shot_type == AvatarFullBody: + shot_endpoint = shot_endpoint + "avatar" + elif shot_type == AvatarBust: + shot_endpoint = shot_endpoint + "avatar-bust" + elif shot_type == AvatarHeadshot: + shot_endpoint = shot_endpoint + "avatar-headshot" + else: + raise InvalidShotTypeError + shot_req = requests.get( + url=shot_endpoint, + params={ + "userIds": str(user.id), + "size": size, + "format": format, + "isCircular": is_circular + } + ) + return shot_req.json()["data"][0]["imageUrl"] From 572f5fe71cb1a324c23ca39f331ff0d2fe56614f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 21:18:31 -0500 Subject: [PATCH 035/518] Some updates to avatar images --- ro_py/thumbnails.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 8674bc86..c04cd66d 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -20,7 +20,8 @@ AvatarHeadshot = 2 size_30x30 = "30x30" -size_42x24 = "42x42" +size_42x42 = "42x42" +size_48x48 = "48x48" size_50x50 = "50x50" size_60x62 = "60x62" size_75x75 = "75x75" @@ -93,7 +94,7 @@ def get_game_icon(game, size=size_256x256, format=format_png, is_circular=False) return game_icon -def get_avatar_image(user, shot_type=AvatarFullBody, size=size_250x250, format=format_png, is_circular=False): +def get_avatar_image(user, shot_type=AvatarFullBody, size=None, format=format_png, is_circular=False): """ Gets a full body, bust, or headshot image of a user. :param user: User to use for avatar. @@ -103,15 +104,18 @@ def get_avatar_image(user, shot_type=AvatarFullBody, size=size_250x250, format=f :param is_circular: The circle thumbnail output parameter. :return: Image URL """ - shot_endpoint = "v1/users/" + shot_endpoint = endpoint + "v1/users/" if shot_type == AvatarFullBody: shot_endpoint = shot_endpoint + "avatar" + size = size or size_30x30 elif shot_type == AvatarBust: shot_endpoint = shot_endpoint + "avatar-bust" + size = size or size_50x50 elif shot_type == AvatarHeadshot: + size = size or size_48x48 shot_endpoint = shot_endpoint + "avatar-headshot" else: - raise InvalidShotTypeError + raise InvalidShotTypeError("Invalid shot type.") shot_req = requests.get( url=shot_endpoint, params={ From c1ae34de8ede65a3502a3d60e824ec7041df91dd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 21:31:06 -0500 Subject: [PATCH 036/518] Errors are handled properly + new requests module ro_py_requests now replaces requests and raises proper errors instead of generic KeyErrors. --- ro_py/assets.py | 2 +- ro_py/badges.py | 2 +- ro_py/errors.py | 5 +++++ ro_py/games.py | 2 +- ro_py/groups.py | 2 +- ro_py/ro_py_requests.py | 32 ++++++++++++++++++++++++++++++++ ro_py/thumbnails.py | 2 +- ro_py/users.py | 2 +- 8 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 ro_py/ro_py_requests.py diff --git a/ro_py/assets.py b/ro_py/assets.py index 9e872d27..7a60d2de 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -10,7 +10,7 @@ from ro_py.errors import NotLimitedError from ro_py.asset_type import asset_types import iso8601 -import requests +import ro_py.ro_py_requests as requests endpoint = "https://api.roblox.com/" diff --git a/ro_py/badges.py b/ro_py/badges.py index e3467d28..9c429f2e 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -6,7 +6,7 @@ """ -import requests +import ro_py.ro_py_requests as requests endpoint = "https://badges.roblox.com/" diff --git a/ro_py/errors.py b/ro_py/errors.py index f8d3d173..977b56f0 100644 --- a/ro_py/errors.py +++ b/ro_py/errors.py @@ -20,3 +20,8 @@ class InvalidIconSizeError(Exception): class InvalidShotTypeError(Exception): """Called when code attempts to pass in an improper avatar image type to a thumbnail function.""" pass + + +class ApiError(Exception): + """Called in ro_py_requets when an API request fails.""" + pass diff --git a/ro_py/games.py b/ro_py/games.py index 6079a551..4e593459 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -7,7 +7,7 @@ """ from ro_py import User, Group, Badge, thumbnails -import requests +import ro_py.ro_py_requests as requests endpoint = "https://games.roblox.com/" diff --git a/ro_py/groups.py b/ro_py/groups.py index fa112385..6014cbd7 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -7,7 +7,7 @@ """ from ro_py import User, thumbnails -import requests +import ro_py.ro_py_requests as requests endpoint = "https://groups.roblox.com/" diff --git a/ro_py/ro_py_requests.py b/ro_py/ro_py_requests.py new file mode 100644 index 00000000..b0804a88 --- /dev/null +++ b/ro_py/ro_py_requests.py @@ -0,0 +1,32 @@ +""" + +ro.py > ro_py_requests.py + +This file houses functions and classes that pertain to web requests. +It is essentially a very limited Roblox-specific version of requests. + +""" + +from ro_py.errors import ApiError +import requests + + +def get(*args, **kwargs): + get_request = requests.get(*args, **kwargs) + try: + get_request_error = get_request.json()["errors"] + except KeyError: + return get_request + + raise ApiError(get_request_error[0]["message"]) + + +def post(*args, **kwargs): + post_request = requests.post(*args, **kwargs) + try: + post_request_error = post_request.json()["errors"] + except KeyError: + return post_request + + raise ApiError(post_request_error[0]["message"]) + diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index c04cd66d..702dc1e7 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -7,7 +7,7 @@ """ from ro_py.errors import InvalidShotTypeError -import requests +import ro_py.ro_py_requests as requests endpoint = "https://thumbnails.roblox.com/" diff --git a/ro_py/users.py b/ro_py/users.py index 5de71b43..537ddd06 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,7 +7,7 @@ """ from ro_py.robloxbadges import RobloxBadge -import requests +import ro_py.ro_py_requests as requests import iso8601 endpoint = "https://users.roblox.com/" From 77a8cc35f7f06b088f0bac9e3abcb7fab1c83359 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 21:32:26 -0500 Subject: [PATCH 037/518] has_premium has been removed --- ro_py/users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index 537ddd06..4add769f 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -49,8 +49,8 @@ def __init__(self, ui): self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") - self.has_premium = has_premium_req + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req @property def status(self): From 1761821aa4846f0791ce29d09852de58bd9c97f1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 21:42:39 -0500 Subject: [PATCH 038/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 040104e1..3c34cad9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.0.3", + version="0.0.4", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 5d744932d73c5d6f989924369b2775309b1a3c6e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 5 Dec 2020 22:09:53 -0500 Subject: [PATCH 039/518] Status codes! --- ro_py/ro_py_requests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/ro_py_requests.py b/ro_py/ro_py_requests.py index b0804a88..c1af9445 100644 --- a/ro_py/ro_py_requests.py +++ b/ro_py/ro_py_requests.py @@ -13,12 +13,13 @@ def get(*args, **kwargs): get_request = requests.get(*args, **kwargs) + try: get_request_error = get_request.json()["errors"] except KeyError: return get_request - raise ApiError(get_request_error[0]["message"]) + raise ApiError(str(get_request.status_code) + ": " + get_request_error[0]["message"]) def post(*args, **kwargs): From 90743738a43dc4e6968a293bdad6a8f2dc5be307 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 10:24:57 -0500 Subject: [PATCH 040/518] Requests formatting update --- ro_py/ro_py_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/ro_py_requests.py b/ro_py/ro_py_requests.py index c1af9445..73139f81 100644 --- a/ro_py/ro_py_requests.py +++ b/ro_py/ro_py_requests.py @@ -19,7 +19,7 @@ def get(*args, **kwargs): except KeyError: return get_request - raise ApiError(str(get_request.status_code) + ": " + get_request_error[0]["message"]) + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") def post(*args, **kwargs): From 6fcf23d595316c85f04b9730631830939796ec12 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:01:00 -0500 Subject: [PATCH 041/518] Moved from @property to get methods After some discussion with other developers, I moved from @property to normal methods due to the fact that they make web requests. --- ro_py/examples/user_example.py | 2 +- ro_py/users.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ro_py/examples/user_example.py b/ro_py/examples/user_example.py index b999bfc2..327f6822 100644 --- a/ro_py/examples/user_example.py +++ b/ro_py/examples/user_example.py @@ -9,4 +9,4 @@ print(f"Username: {user.name}") print(f"Display Name: {user.display_name}") print(f"Description: {user.description}") -print(f"Status: {user.status}") +print(f"Status: {user.get_status()}") diff --git a/ro_py/users.py b/ro_py/users.py index 4add769f..b242d691 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -52,13 +52,18 @@ def __init__(self, ui): # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - @property - def status(self): + def get_status(self): + """ + Gets the user's status. + :return: A string + """ status_req = requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - @property - def roblox_badges(self): + def get_roblox_badges(self): + """ + :return: A list of RobloxBadge instances + """ roblox_badges_req = requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): From 169f6f22add0865b1750a11b25259e9a78239b95 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:19:17 -0500 Subject: [PATCH 042/518] Friends, followers, and followings! --- ro_py/games.py | 30 +++++++++++++----------------- ro_py/users.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index 4e593459..e5e82397 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -72,27 +72,23 @@ def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, i """ return thumbnails.get_game_icon(self, size, format, is_circular) - @property - def badges(self): + def get_badges(self): """ Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances """ - if not self.__cached_badges: - badges_req = requests.get( - url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", - params={ - "limit": 100, - "sortOrder": "Asc" - } - ) - badges_data = badges_req.json()["data"] - badges = [] - for badge in badges_data: - badges.append(Badge(badge["id"])) - self.__cached_badges = badges - - return self.__cached_badges + badges_req = requests.get( + url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", + params={ + "limit": 100, + "sortOrder": "Asc" + } + ) + badges_data = badges_req.json()["data"] + badges = [] + for badge in badges_data: + badges.append(Badge(badge["id"])) + return badges def place_id_to_universe_id(place_id): diff --git a/ro_py/users.py b/ro_py/users.py index b242d691..ab78fe74 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -69,3 +69,44 @@ def get_roblox_badges(self): for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges + + def get_friends_count(self): + """ + Gets the user's friends count. + :return: An integer + """ + friends_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count = friends_count_req.json()["count"] + return friends_count + + def get_followers_count(self): + """ + Gets the user's followers count. + :return: An integer + """ + followers_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count = followers_count_req.json()["count"] + return followers_count + + def get_followings_count(self): + """ + Gets the user's followings count. + :return: An integer + """ + followings_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count = followings_count_req.json()["count"] + return followings_count + + def get_friends(self): + """ + Gets the user's friends. + :return: A list of User instances. + """ + friends_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_raw = friends_req.json()["data"] + friends_list = [] + for friend_raw in friends_raw: + friends_list.append( + User(friend_raw["id"]) + ) + return friends_list From c3fd23a7864df2d20b8453507932fb4c2b15f1ae Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:22:09 -0500 Subject: [PATCH 043/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3c34cad9..7d963368 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.0.4", + version="0.0.5", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 8dbb8797656a94c88216b4d8efac7cac07642ee8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:50:13 -0500 Subject: [PATCH 044/518] Updated votes --- ro_py/games.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index e5e82397..482ccf50 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -50,8 +50,7 @@ def __init__(self, universe_id): self.create_vip_servers_allowed = game_info["createVipServersAllowed"] self.__cached_badges = False - @property - def votes(self): + def get_votes(self): """ :return: An instance of Votes """ From d95ef2e6974d131a9d40db01c1e019b122cab9a5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:50:28 -0500 Subject: [PATCH 045/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94314bbc..f0ead90c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Reading a game's votes: ```python from ro_py import Game game = Game(1732173541) # This takes in a Universe ID and not a Place ID -votes = game.votes +votes = game.get_votes() print(f"Likes: {votes.up_votes}") print(f"Dislikes: {votes.down_votes}") ``` From 191c719857a6f541d18eda01c3a9d358f92cf796 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:53:08 -0500 Subject: [PATCH 046/518] More swapping from property to get --- ro_py/assets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index 7a60d2de..bf8a815b 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -62,8 +62,7 @@ def __init__(self, asset_id): self.minimum_membership_level = asset_info["MinimumMembershipLevel"] self.content_rating_type_id = asset_info["ContentRatingTypeId"] - @property - def remaining(self): + def get_remaining(self): asset_info_req = requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -73,8 +72,7 @@ def remaining(self): asset_info = asset_info_req.json() return asset_info["Remaining"] - @property - def limited_resale_data(self): + def get_limited_resale_data(self): if self.is_limited: resale_data_req = requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) From 4a4615c5494fd5fae257fec81299af68ff8b4ba5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 12:53:26 -0500 Subject: [PATCH 047/518] Update asset_example.py --- ro_py/examples/asset_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/examples/asset_example.py b/ro_py/examples/asset_example.py index a87cdaa6..73d6cd6d 100644 --- a/ro_py/examples/asset_example.py +++ b/ro_py/examples/asset_example.py @@ -10,7 +10,7 @@ print(f"Description: {asset.description}") print(f"Limited: {asset.is_limited}") if asset.is_limited: - resale_data = asset.limited_resale_data + resale_data = asset.get_limited_resale_data() print(f"Original Price: {resale_data.original_price}") print(f"Number Remaining: {resale_data.number_remaining}") print(f"Recent Average Price: {resale_data.recent_average_price}") From 83000e2395ce62ab0a5ebace3275625a8470c7ba Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 16:56:58 -0500 Subject: [PATCH 048/518] About to begin client --- .gitignore | 1 + ro_py/authentication.py | 8 ++++++++ ro_py/ro_py_requests.py | 1 - 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 ro_py/authentication.py diff --git a/.gitignore b/.gitignore index 24875371..011b2b05 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ dist/ dist_old/ ro_py.egg-info/ +tests/ diff --git a/ro_py/authentication.py b/ro_py/authentication.py new file mode 100644 index 00000000..57287ba8 --- /dev/null +++ b/ro_py/authentication.py @@ -0,0 +1,8 @@ +""" + +ro.py > authentication.py + +This file houses functions and classes that pertain to authentication. +Some code adapted from https://github.com/iranathan/robloxapi/blob/master/robloxapi/auth.py + +""" \ No newline at end of file diff --git a/ro_py/ro_py_requests.py b/ro_py/ro_py_requests.py index 73139f81..1006b1f5 100644 --- a/ro_py/ro_py_requests.py +++ b/ro_py/ro_py_requests.py @@ -13,7 +13,6 @@ def get(*args, **kwargs): get_request = requests.get(*args, **kwargs) - try: get_request_error = get_request.json()["errors"] except KeyError: From 77ce0e479e5070e7efcf78dbfa7491bea5abed0a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 19:36:10 -0500 Subject: [PATCH 049/518] Moved a bit around --- ro_py/assets.py | 6 +++--- ro_py/badges.py | 2 +- ro_py/games.py | 2 +- ro_py/groups.py | 2 +- ro_py/thumbnails.py | 4 ++-- ro_py/users.py | 2 +- ro_py/utilities/__init__.py | 0 ro_py/{ => utilities}/asset_type.py | 0 ro_py/{ => utilities}/errors.py | 0 ro_py/{ro_py_requests.py => utilities/rorequests.py} | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 ro_py/utilities/__init__.py rename ro_py/{ => utilities}/asset_type.py (100%) rename ro_py/{ => utilities}/errors.py (100%) rename ro_py/{ro_py_requests.py => utilities/rorequests.py} (94%) diff --git a/ro_py/assets.py b/ro_py/assets.py index bf8a815b..75bf1668 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -7,10 +7,10 @@ """ from ro_py import User, Group -from ro_py.errors import NotLimitedError -from ro_py.asset_type import asset_types +from ro_py.utilities.errors import NotLimitedError +from ro_py.utilities.asset_type import asset_types import iso8601 -import ro_py.ro_py_requests as requests +import ro_py.utilities.rorequests as requests endpoint = "https://api.roblox.com/" diff --git a/ro_py/badges.py b/ro_py/badges.py index 9c429f2e..48639d26 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -6,7 +6,7 @@ """ -import ro_py.ro_py_requests as requests +import ro_py.utilities.rorequests as requests endpoint = "https://badges.roblox.com/" diff --git a/ro_py/games.py b/ro_py/games.py index 482ccf50..5cacb5f0 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -7,7 +7,7 @@ """ from ro_py import User, Group, Badge, thumbnails -import ro_py.ro_py_requests as requests +import ro_py.utilities.rorequests as requests endpoint = "https://games.roblox.com/" diff --git a/ro_py/groups.py b/ro_py/groups.py index 6014cbd7..df23e811 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -7,7 +7,7 @@ """ from ro_py import User, thumbnails -import ro_py.ro_py_requests as requests +import ro_py.utilities.rorequests as requests endpoint = "https://groups.roblox.com/" diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 702dc1e7..7f89393d 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -6,8 +6,8 @@ """ -from ro_py.errors import InvalidShotTypeError -import ro_py.ro_py_requests as requests +from ro_py.utilities.errors import InvalidShotTypeError +import ro_py.utilities.rorequests as requests endpoint = "https://thumbnails.roblox.com/" diff --git a/ro_py/users.py b/ro_py/users.py index ab78fe74..f28e6fd0 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,7 +7,7 @@ """ from ro_py.robloxbadges import RobloxBadge -import ro_py.ro_py_requests as requests +import ro_py.rorequests as requests import iso8601 endpoint = "https://users.roblox.com/" diff --git a/ro_py/utilities/__init__.py b/ro_py/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ro_py/asset_type.py b/ro_py/utilities/asset_type.py similarity index 100% rename from ro_py/asset_type.py rename to ro_py/utilities/asset_type.py diff --git a/ro_py/errors.py b/ro_py/utilities/errors.py similarity index 100% rename from ro_py/errors.py rename to ro_py/utilities/errors.py diff --git a/ro_py/ro_py_requests.py b/ro_py/utilities/rorequests.py similarity index 94% rename from ro_py/ro_py_requests.py rename to ro_py/utilities/rorequests.py index 1006b1f5..cd3ac3cf 100644 --- a/ro_py/ro_py_requests.py +++ b/ro_py/utilities/rorequests.py @@ -7,7 +7,7 @@ """ -from ro_py.errors import ApiError +from ro_py.utilities.errors import ApiError import requests From 190058d3778475b8f4ebb537f7270a9554f49e0e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 6 Dec 2020 19:36:48 -0500 Subject: [PATCH 050/518] Fixed users.py issue --- ro_py/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/users.py b/ro_py/users.py index f28e6fd0..fc59e5fa 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,7 +7,7 @@ """ from ro_py.robloxbadges import RobloxBadge -import ro_py.rorequests as requests +import ro_py.utilities.rorequests as requests import iso8601 endpoint = "https://users.roblox.com/" From fdfef996d7fd1a70345ecf2a1296fc90bf0c5cf2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 09:10:27 -0500 Subject: [PATCH 051/518] Everything has changed - New version identifier (0.1.0) - Client model (and authentication) - New request system - + So much more! No more examples for now as I am still rewriting them. --- .gitignore | 1 + README.md | 17 +-- ro_py/__init__.py | 8 +- ro_py/accountinformation.py | 35 +++++++ ro_py/assets.py | 42 ++++++-- ro_py/authentication.py | 8 -- ro_py/badges.py | 16 ++- ro_py/client.py | 20 ++++ ro_py/examples/asset_example.py | 22 ---- ro_py/examples/game_badge_example.py | 21 ---- ro_py/examples/user_example.py | 12 --- ro_py/games.py | 48 ++++++--- ro_py/groups.py | 48 +++++---- ro_py/thumbnails.py | 151 ++++++++++++++------------- ro_py/users.py | 38 ++++--- ro_py/utilities/__init__.py | 0 ro_py/utilities/requests.py | 36 +++++++ ro_py/utilities/rorequests.py | 32 ------ setup.py | 2 +- 19 files changed, 299 insertions(+), 258 deletions(-) create mode 100644 ro_py/accountinformation.py delete mode 100644 ro_py/authentication.py create mode 100644 ro_py/client.py delete mode 100644 ro_py/examples/asset_example.py delete mode 100644 ro_py/examples/game_badge_example.py delete mode 100644 ro_py/examples/user_example.py delete mode 100644 ro_py/utilities/__init__.py create mode 100644 ro_py/utilities/requests.py delete mode 100644 ro_py/utilities/rorequests.py diff --git a/.gitignore b/.gitignore index 011b2b05..5c291095 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ dist/ dist_old/ ro_py.egg-info/ tests/ +ro_py_old/ diff --git a/README.md b/README.md index f0ead90c..c94ad3ad 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,7 @@ You can install ro.py from pip: pip3 install ro-py ``` ## Examples -Reading a user's description: -```python -from ro_py import User -user = User(576059883) -print(f"Username: {user.name}") -print(f"Description: {user.description}") -``` -Reading a game's votes: -```python -from ro_py import Game -game = Game(1732173541) # This takes in a Universe ID and not a Place ID -votes = game.get_votes() -print(f"Likes: {votes.up_votes}") -print(f"Dislikes: {votes.down_votes}") -``` -You can read more examples in the `examples` directory. +These examples are gone for now as I rewrite them for version 0.1.0. ## Other Libraries https://github.com/RbxAPI/Pyblox https://github.com/iranathan/robloxapi \ No newline at end of file diff --git a/ro_py/__init__.py b/ro_py/__init__.py index e200f5bb..768a3b26 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -10,10 +10,4 @@ ro.py by jmkdev -""" - -from ro_py.users import User -from ro_py.groups import Group -from ro_py.badges import Badge -from ro_py.games import Game, game_from_place_id -from ro_py.assets import Asset +""" \ No newline at end of file diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py new file mode 100644 index 00000000..56c18e0f --- /dev/null +++ b/ro_py/accountinformation.py @@ -0,0 +1,35 @@ +from datetime import datetime + +endpoint = "https://accountinformation.roblox.com/" + + +class AccountInformationMetadata: + def __init__(self): + pass + + +class AccountInformation: + def __init__(self, requests): + self.requests = requests + + def get_gender(self): + """ + Returns the user's gender. + :return: An integer. + """ + gender_req = self.requests.get(endpoint + "v1/birthdate") + return gender_req.json()["gender"] + + def get_birthdate(self): + """ + Returns the user's birthdate. + :return: datetime + """ + birthdate_req = self.requests.get(endpoint + "v1/birthdate") + birthdate_raw = birthdate_req.json() + birthdate = datetime( + year=birthdate_raw["birthYear"], + month=birthdate_raw["birthMonth"], + day=birthdate_raw["birthDay"] + ) + return birthdate diff --git a/ro_py/assets.py b/ro_py/assets.py index 75bf1668..b6fa5136 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -6,11 +6,11 @@ """ -from ro_py import User, Group +from ro_py.users import User +from ro_py.groups import Group from ro_py.utilities.errors import NotLimitedError from ro_py.utilities.asset_type import asset_types import iso8601 -import ro_py.utilities.rorequests as requests endpoint = "https://api.roblox.com/" @@ -31,11 +31,35 @@ class Asset: """ Represents an asset. """ - def __init__(self, asset_id): - asset_info_req = requests.get( + def __init__(self, requests, asset_id): + self.id = asset_id + self.requests = requests + self.target_id = None + self.product_type = None + self.asset_id = None + self.product_id = None + self.name = None + self.description = None + self.asset_type_id = None + self.asset_type_name = None + self.creator = None + self.created = None + self.updated = None + self.price = None + self.is_new = None + self.is_for_sale = None + self.is_public_domain = None + self.is_limited = None + self.is_limited_unique = None + self.minimum_membership_level = None + self.content_rating_type_id = None + self.update() + + def update(self): + asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ - "assetId": asset_id + "assetId": self.id } ) asset_info = asset_info_req.json() @@ -48,9 +72,9 @@ def __init__(self, asset_id): self.asset_type_id = asset_info["AssetTypeId"] self.asset_type_name = asset_types[self.asset_type_id] if asset_info["Creator"]["CreatorType"] == "User": - self.creator = User(asset_info["Creator"]["Id"]) + self.creator = User(self.requests, asset_info["Creator"]["Id"]) elif asset_info["Creator"]["CreatorType"] == "Group": - self.creator = Group(asset_info["Creator"]["CreatorTargetId"]) + self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"]) self.created = iso8601.parse_date(asset_info["Created"]) self.updated = iso8601.parse_date(asset_info["Updated"]) self.price = asset_info["PriceInRobux"] @@ -63,7 +87,7 @@ def __init__(self, asset_id): self.content_rating_type_id = asset_info["ContentRatingTypeId"] def get_remaining(self): - asset_info_req = requests.get( + asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.asset_id @@ -74,7 +98,7 @@ def get_remaining(self): def get_limited_resale_data(self): if self.is_limited: - resale_data_req = requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") + resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) else: raise NotLimitedError("You can only read this information on limited items.") diff --git a/ro_py/authentication.py b/ro_py/authentication.py deleted file mode 100644 index 57287ba8..00000000 --- a/ro_py/authentication.py +++ /dev/null @@ -1,8 +0,0 @@ -""" - -ro.py > authentication.py - -This file houses functions and classes that pertain to authentication. -Some code adapted from https://github.com/iranathan/robloxapi/blob/master/robloxapi/auth.py - -""" \ No newline at end of file diff --git a/ro_py/badges.py b/ro_py/badges.py index 48639d26..c0059375 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -6,8 +6,6 @@ """ -import ro_py.utilities.rorequests as requests - endpoint = "https://badges.roblox.com/" @@ -25,9 +23,19 @@ class Badge: """ Represents a game-awarded badge. """ - def __init__(self, badge_id): + def __init__(self, requests, badge_id): self.id = badge_id - badge_info_req = requests.get(endpoint + f"v1/badges/{badge_id}") + self.requests = requests + self.name = None + self.description = None + self.display_name = None + self.display_description = None + self.enabled = None + self.statistics = None + self.update() + + def update(self): + badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] diff --git a/ro_py/client.py b/ro_py/client.py new file mode 100644 index 00000000..910d682f --- /dev/null +++ b/ro_py/client.py @@ -0,0 +1,20 @@ +from ro_py.users import User +from ro_py.groups import Group +from ro_py.utilities.requests import Requests +from ro_py.accountinformation import AccountInformation + + +class Client: + def __init__(self, token=None): + self.token = token + self.requests = Requests() + if token: + self.requests.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.requests) + self.requests.update_xsrf() + + def get_user(self, user_identifier): + return User(self.requests, user_identifier) + + def get_group(self, group_id): + return Group(self.requests, group_id) diff --git a/ro_py/examples/asset_example.py b/ro_py/examples/asset_example.py deleted file mode 100644 index 73d6cd6d..00000000 --- a/ro_py/examples/asset_example.py +++ /dev/null @@ -1,22 +0,0 @@ -from ro_py import Asset - -asset_id = 130213380 - -print(f"Loading asset {asset_id}...") -asset = Asset(asset_id) -print("Loaded assset.") - -print(f"Name: {asset.name}") -print(f"Description: {asset.description}") -print(f"Limited: {asset.is_limited}") -if asset.is_limited: - resale_data = asset.get_limited_resale_data() - print(f"Original Price: {resale_data.original_price}") - print(f"Number Remaining: {resale_data.number_remaining}") - print(f"Recent Average Price: {resale_data.recent_average_price}") - print(f"Stock: {resale_data.asset_stock}") - print(f"Sales: {resale_data.sales}") -else: - print(f"Price: {asset.price} R$") -print(f"Created: {asset.created.strftime('%b %d %Y %H:%M:%S')}") -print(f"Updated: {asset.updated.strftime('%b %d %Y %H:%M:%S')}") diff --git a/ro_py/examples/game_badge_example.py b/ro_py/examples/game_badge_example.py deleted file mode 100644 index 92872459..00000000 --- a/ro_py/examples/game_badge_example.py +++ /dev/null @@ -1,21 +0,0 @@ -from ro_py import Game - -universe_id = 1605107130 - -print(f"Loading game {universe_id}...") -game = Game(universe_id) -print("Loaded game.") - -print(f"Name: {game.name}") - -print("Loading badges...") -badges = game.badges -print("Loaded badges.") -print(f"Badge count: {len(badges)}") -for badge in badges: - badge_tab = " "*(32-len(badge.name)) - badge_stats = badge.statistics - print(f"{badge.name}{badge_tab}" - f"Rarity: {badge_stats.win_rate_percentage}% " - f"Won Yesterday: {badge_stats.past_date_awarded_count} " - f"Won Ever: {badge_stats.awarded_count}") diff --git a/ro_py/examples/user_example.py b/ro_py/examples/user_example.py deleted file mode 100644 index 327f6822..00000000 --- a/ro_py/examples/user_example.py +++ /dev/null @@ -1,12 +0,0 @@ -from ro_py import User - -user_id = 576059883 - -print(f"Loading user {user_id}...") -user = User(user_id) -print("Loaded user.") - -print(f"Username: {user.name}") -print(f"Display Name: {user.display_name}") -print(f"Description: {user.description}") -print(f"Status: {user.get_status()}") diff --git a/ro_py/games.py b/ro_py/games.py index 5cacb5f0..3a70cba9 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -6,8 +6,10 @@ """ -from ro_py import User, Group, Badge, thumbnails -import ro_py.utilities.rorequests as requests +from ro_py.users import User +from ro_py.groups import Group +from ro_py.badges import Badge +from ro_py import thumbnails endpoint = "https://games.roblox.com/" @@ -26,9 +28,22 @@ class Game: Represents a Roblox game universe. This class represents multiple game-related endpoints. """ - def __init__(self, universe_id): + def __init__(self, requests, universe_id): self.id = universe_id - game_info_req = requests.get( + self.requests = requests + self.name = None + self.description = None + self.creator = None + self.price = None + self.allowed_gear_genres = None + self.allowed_gear_categories = None + self.max_players = None + self.studio_access_to_apis_allowed = None + self.create_vip_servers_allowed = None + self.update() + + def update(self): + game_info_req = self.requests.get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -39,22 +54,21 @@ def __init__(self, universe_id): self.name = game_info["name"] self.description = game_info["description"] if game_info["creator"]["type"] == "User": - self.creator = User(game_info["creator"]["id"]) + self.creator = User(self.requests, game_info["creator"]["id"]) elif game_info["creator"]["type"] == "Group": - self.creator = Group(game_info["creator"]["id"]) + self.creator = Group(self.requests, game_info["creator"]["id"]) self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - self.__cached_badges = False def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = requests.get( + votes_info_req = self.requests.get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -76,7 +90,7 @@ def get_badges(self): Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances """ - badges_req = requests.get( + badges_req = self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -86,17 +100,18 @@ def get_badges(self): badges_data = badges_req.json()["data"] badges = [] for badge in badges_data: - badges.append(Badge(badge["id"])) + badges.append(Badge(self.requests, badge["id"])) return badges +""" def place_id_to_universe_id(place_id): - """ + \""" Returns the containing universe ID of a place ID. :param place_id: Place ID :return: Universe ID - """ - universe_id_req = requests.get( + \""" + universe_id_req = self.requests.get( url="https://api.roblox.com/universes/get-universe-containing-place", params={ "placeId": place_id @@ -107,9 +122,10 @@ def place_id_to_universe_id(place_id): def game_from_place_id(place_id): - """ + \""" Generates an instance of Game with a place ID instead of a game ID. :param place_id: Place ID :return: Instace of Game - """ - return Game(place_id_to_universe_id(place_id)) + \""" + return Game(self.requests, place_id_to_universe_id(place_id)) +""" \ No newline at end of file diff --git a/ro_py/groups.py b/ro_py/groups.py index df23e811..1e81e3fc 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -1,13 +1,4 @@ -""" - -ro.py > groups.py - -This file houses functions and classes that pertain to Roblox groups. - -""" - -from ro_py import User, thumbnails -import ro_py.utilities.rorequests as requests +from ro_py.users import User endpoint = "https://groups.roblox.com/" @@ -16,23 +7,34 @@ class Shout: """ Represents a group shout. """ - def __init__(self, shout_data): + def __init__(self, requests, shout_data): self.body = shout_data["body"] - self.poster = User(shout_data["poster"]["userId"]) + self.poster = User(requests, shout_data["poster"]["userId"]) class Group: """ Represents a group. """ - def __init__(self, group_id): + def __init__(self, requests, group_id): + self.requests = requests self.id = group_id - group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") + + self.name = None + self.description = None + self.owner = None + self.member_count = None + self.is_builders_club_only = None + self.public_entry_allowed = None + + self.update() + + def update(self): + group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(group_info["owner"]["userId"]) - + self.owner = User(self.requests, group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] @@ -43,16 +45,16 @@ def shout(self): """ :return: An instance of Shout """ - group_info_req = requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() if group_info["shout"]: - return Shout(group_info["shout"]) + return Shout(self.requests, group_info["shout"]) else: return None - def get_icon(self, size=thumbnails.size_150x150, format=thumbnails.format_png, is_circular=False): - """ - Equivalent to thumbnails.get_group_icon - """ - return thumbnails.get_group_icon(self, size, format, is_circular) + # def get_icon(self, size=thumbnails.size_150x150, file_format=thumbnails.format_png, is_circular=False): + # """ + # Equivalent to thumbnails.get_group_icon + # """ + # return thumbnails.get_group_icon(self, size, file_format, is_circular) diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 7f89393d..2d1c74bd 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -7,7 +7,6 @@ """ from ro_py.utilities.errors import InvalidShotTypeError -import ro_py.utilities.rorequests as requests endpoint = "https://thumbnails.roblox.com/" @@ -49,80 +48,82 @@ format_jpeg = "Jpeg" -def get_group_icon(group, size=size_150x150, format=format_png, is_circular=False): - """ - Gets a game's icon. - :param group: The group. - :param size: The thumbnail size, formatted widthxheight. - :param format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL - """ - group_icon_req = requests.get( - url=endpoint + "v1/groups/icons", - params={ - "groupIds": str(group.id), - "size": size, - "format": format, - "isCircular": is_circular - } - ) - group_icon = group_icon_req.json()["data"][0]["imageUrl"] - return group_icon +class ThumbnailGenerator: + def __init__(self, requests): + self.requests = requests + def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): + """ + Gets a game's icon. + :param group: The group. + :param size: The thumbnail size, formatted widthxheight. + :param file_format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + group_icon_req = self.requests.get( + url=endpoint + "v1/groups/icons", + params={ + "groupIds": str(group.id), + "size": size, + "file_format": file_format, + "isCircular": is_circular + } + ) + group_icon = group_icon_req.json()["data"][0]["imageUrl"] + return group_icon -def get_game_icon(game, size=size_256x256, format=format_png, is_circular=False): - """ - Gets a game's icon. - :param game: The game. - :param size: The thumbnail size, formatted widthxheight. - :param format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL - """ - game_icon_req = requests.get( - url=endpoint + "v1/games/icons", - params={ - "universeIds": str(game.id), - "returnPolicy": PlaceHolder, - "size": size, - "format": format, - "isCircular": is_circular - } - ) - game_icon = game_icon_req.json()["data"][0]["imageUrl"] - return game_icon + def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False): + """ + Gets a game's icon. + :param game: The game. + :param size: The thumbnail size, formatted widthxheight. + :param file_format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + game_icon_req = self.requests.get( + url=endpoint + "v1/games/icons", + params={ + "universeIds": str(game.id), + "returnPolicy": PlaceHolder, + "size": size, + "file_format": file_format, + "isCircular": is_circular + } + ) + game_icon = game_icon_req.json()["data"][0]["imageUrl"] + return game_icon - -def get_avatar_image(user, shot_type=AvatarFullBody, size=None, format=format_png, is_circular=False): - """ - Gets a full body, bust, or headshot image of a user. - :param user: User to use for avatar. - :param shot_type: Type of shot. - :param size: The thumbnail size, formatted widthxheight. - :param format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL - """ - shot_endpoint = endpoint + "v1/users/" - if shot_type == AvatarFullBody: - shot_endpoint = shot_endpoint + "avatar" - size = size or size_30x30 - elif shot_type == AvatarBust: - shot_endpoint = shot_endpoint + "avatar-bust" - size = size or size_50x50 - elif shot_type == AvatarHeadshot: - size = size or size_48x48 - shot_endpoint = shot_endpoint + "avatar-headshot" - else: - raise InvalidShotTypeError("Invalid shot type.") - shot_req = requests.get( - url=shot_endpoint, - params={ - "userIds": str(user.id), - "size": size, - "format": format, - "isCircular": is_circular - } - ) - return shot_req.json()["data"][0]["imageUrl"] + def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False): + """ + Gets a full body, bust, or headshot image of a user. + :param user: User to use for avatar. + :param shot_type: Type of shot. + :param size: The thumbnail size, formatted widthxheight. + :param file_format: The thumbnail format + :param is_circular: The circle thumbnail output parameter. + :return: Image URL + """ + shot_endpoint = endpoint + "v1/users/" + if shot_type == AvatarFullBody: + shot_endpoint = shot_endpoint + "avatar" + size = size or size_30x30 + elif shot_type == AvatarBust: + shot_endpoint = shot_endpoint + "avatar-bust" + size = size or size_50x50 + elif shot_type == AvatarHeadshot: + size = size or size_48x48 + shot_endpoint = shot_endpoint + "avatar-headshot" + else: + raise InvalidShotTypeError("Invalid shot type.") + shot_req = self.requests.get( + url=shot_endpoint, + params={ + "userIds": str(user.id), + "size": size, + "file_format": file_format, + "isCircular": is_circular + } + ) + return shot_req.json()["data"][0]["imageUrl"] diff --git a/ro_py/users.py b/ro_py/users.py index fc59e5fa..02ab5353 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,7 +7,6 @@ """ from ro_py.robloxbadges import RobloxBadge -import ro_py.utilities.rorequests as requests import iso8601 endpoint = "https://users.roblox.com/" @@ -18,9 +17,11 @@ class User: Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. """ - def __init__(self, ui): + def __init__(self, requests, ui): + + self.requests = requests + if isinstance(ui, str): - is_id = False try: int(str) is_id = True @@ -29,7 +30,7 @@ def __init__(self, ui): if is_id: self.id = int(ui) else: - user_id_req = requests.post( + user_id_req = self.requests.post( url="https://users.roblox.com/v1/usernames/users", json={ "usernames": [ @@ -42,7 +43,20 @@ def __init__(self, ui): elif isinstance(ui, int): self.id = ui - user_info_req = requests.get(endpoint + f"v1/users/{self.id}") + self.description = None + self.created = None + self.is_banned = None + self.name = None + self.display_name = None + + self.update() + + def update(self): + """ + Updates some class values. + :return: Nothing + """ + user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -57,14 +71,14 @@ def get_status(self): Gets the user's status. :return: A string """ - status_req = requests.get(endpoint + f"v1/users/{self.id}/status") + status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] def get_roblox_badges(self): """ :return: A list of RobloxBadge instances """ - roblox_badges_req = requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) @@ -75,7 +89,7 @@ def get_friends_count(self): Gets the user's friends count. :return: An integer """ - friends_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count @@ -84,7 +98,7 @@ def get_followers_count(self): Gets the user's followers count. :return: An integer """ - followers_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count @@ -93,7 +107,7 @@ def get_followings_count(self): Gets the user's followings count. :return: An integer """ - followings_count_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -102,11 +116,11 @@ def get_friends(self): Gets the user's friends. :return: A list of User instances. """ - friends_req = requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(friend_raw["id"]) + User(self.requests, friend_raw["id"]) ) return friends_list diff --git a/ro_py/utilities/__init__.py b/ro_py/utilities/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py new file mode 100644 index 00000000..9c04a25f --- /dev/null +++ b/ro_py/utilities/requests.py @@ -0,0 +1,36 @@ +from ro_py.utilities.errors import ApiError +import requests + + +class Requests: + def __init__(self): + self.cookies = {} + self.headers = {} + + def get(self, *args, **kwargs): + kwargs["cookies"] = self.cookies + kwargs["headers"] = self.headers + + get_request = requests.get(*args, **kwargs) + try: + get_request_error = get_request.json()["errors"] + except KeyError: + return get_request + + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + + def post(self, *args, **kwargs): + kwargs["cookies"] = self.cookies + kwargs["headers"] = self.headers + + post_request = requests.post(*args, **kwargs) + try: + post_request_error = post_request.json()["errors"] + except KeyError: + return post_request + + raise ApiError(post_request_error[0]["message"]) + + def update_xsrf(self): + xsrf_req = requests.post('https://www.roblox.com/favorite/toggle') + self.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] diff --git a/ro_py/utilities/rorequests.py b/ro_py/utilities/rorequests.py deleted file mode 100644 index cd3ac3cf..00000000 --- a/ro_py/utilities/rorequests.py +++ /dev/null @@ -1,32 +0,0 @@ -""" - -ro.py > ro_py_requests.py - -This file houses functions and classes that pertain to web requests. -It is essentially a very limited Roblox-specific version of requests. - -""" - -from ro_py.utilities.errors import ApiError -import requests - - -def get(*args, **kwargs): - get_request = requests.get(*args, **kwargs) - try: - get_request_error = get_request.json()["errors"] - except KeyError: - return get_request - - raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") - - -def post(*args, **kwargs): - post_request = requests.post(*args, **kwargs) - try: - post_request_error = post_request.json()["errors"] - except KeyError: - return post_request - - raise ApiError(post_request_error[0]["message"]) - diff --git a/setup.py b/setup.py index 7d963368..290252a7 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.0.5", + version="0.1.0", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 10a749f5fd2eb86403b1ed872cac74394cf0decf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 09:31:31 -0500 Subject: [PATCH 052/518] More account information! --- ro_py/accountinformation.py | 59 +++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 56c18e0f..37172ebe 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -4,20 +4,73 @@ class AccountInformationMetadata: - def __init__(self): - pass + """ + Represents account information metadata. + """ + def __init__(self, metadata_raw): + self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + + +class PromotionChannels: + """ + Represents account information promotion channels. + """ + def __init__(self, promotion_raw): + self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + self.facebook = promotion_raw["facebook"] + self.twitter = promotion_raw["twitter"] + self.youtube = promotion_raw["youtube"] + self.twitch = promotion_raw["twitch"] class AccountInformation: + """ + Represents authenticated client account information (https://accountinformation.roblox.com/) + This is only available for authenticated clients as it cannot be accessed otherwise. + """ def __init__(self, requests): self.requests = requests + self.account_information_metadata = None + self.promotion_channels = None + self.update() + + def update(self): + """ + Updates the account information. + :return: Nothing + """ + account_information_req = self.requests.get("https://accountinformation.roblox.com/v1/metadata") + self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) + promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") + self.promotion_channels = PromotionChannels(promotion_channels_req.json()) def get_gender(self): """ Returns the user's gender. + 1: Other + 2: Male + 3: Female :return: An integer. """ - gender_req = self.requests.get(endpoint + "v1/birthdate") + gender_req = self.requests.get(endpoint + "v1/gender") + return gender_req.json()["gender"] + + def set_gender(self, gender): + """ + Sets the user's gender. + :return: Nothing + """ + gender_req = self.requests.post( + url=endpoint + "v1/gender", + data={ + "gender": str(gender) + } + ) return gender_req.json()["gender"] def get_birthdate(self): From 5d0ea65741ea69b42529da25d92ae6054bf9c890 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 09:41:26 -0500 Subject: [PATCH 053/518] set_birthdate (not working) --- ro_py/accountinformation.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 37172ebe..c9f9b1eb 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -71,7 +71,6 @@ def set_gender(self, gender): "gender": str(gender) } ) - return gender_req.json()["gender"] def get_birthdate(self): """ @@ -86,3 +85,18 @@ def get_birthdate(self): day=birthdate_raw["birthDay"] ) return birthdate + + def set_birthdate(self, birthdate): + """ + Sets the user's birthdate. + :param birthdate: A datetime object. + :return: Nothing + """ + birthdate_req = self.requests.post( + url=endpoint + "v1/birthdate", + data={ + "birthMonth": birthdate.month, + "birthDay": birthdate.day, + "birthYear": birthdate.year + } + ) From 7bbc55d84fc4ac7555a52f0488d02015013cbfd2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 11:27:02 -0500 Subject: [PATCH 054/518] More X-CSRF-TOKEN Support --- ro_py/accountinformation.py | 8 ++++++++ ro_py/gender.py | 7 +++++++ ro_py/utilities/requests.py | 11 ++++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 ro_py/gender.py diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index c9f9b1eb..42e1c8ed 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -1,3 +1,11 @@ +""" + +ro.py > accountinformation.py + +This file houses functions and classes that pertain to Roblox client . + +""" + from datetime import datetime endpoint = "https://accountinformation.roblox.com/" diff --git a/ro_py/gender.py b/ro_py/gender.py new file mode 100644 index 00000000..6f0296a6 --- /dev/null +++ b/ro_py/gender.py @@ -0,0 +1,7 @@ +import enum + + +class RobloxGender(enum.Enum): + Other = 1 + Female = 2 + Male = 3 diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 9c04a25f..3074c6d2 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -24,13 +24,18 @@ def post(self, *args, **kwargs): kwargs["headers"] = self.headers post_request = requests.post(*args, **kwargs) + if post_request.status_code == 403: + if "X-CSRF-TOKEN" in post_request.headers: + self.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) + try: post_request_error = post_request.json()["errors"] except KeyError: return post_request - raise ApiError(post_request_error[0]["message"]) + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") - def update_xsrf(self): - xsrf_req = requests.post('https://www.roblox.com/favorite/toggle') + def update_xsrf(self, url="https://www.roblox.com/favorite/toggle"): + xsrf_req = requests.post(url) self.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] From 0de6fa4bc4b5873327df2b55cd181b8c234d4383 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 11:37:52 -0500 Subject: [PATCH 055/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 290252a7..2afec0a9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.0", + version="0.1.1", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 3a87f7a4c08ddd9f9034d7776bc3fb77ba7f94e1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 12:43:42 -0500 Subject: [PATCH 056/518] Client updates + user example --- ro_py/client.py | 16 +++++++++++++++- ro_py/examples/user.py | 14 ++++++++++++++ ro_py/gender.py | 8 ++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 ro_py/examples/user.py diff --git a/ro_py/client.py b/ro_py/client.py index 910d682f..c4a3e7eb 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -1,5 +1,8 @@ from ro_py.users import User +from ro_py.games import Game from ro_py.groups import Group +from ro_py.assets import Asset +from ro_py.badges import Badge from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation @@ -10,7 +13,9 @@ def __init__(self, token=None): self.requests = Requests() if token: self.requests.cookies[".ROBLOSECURITY"] = token - self.accountinformation = AccountInformation(self.requests) + self.accountinformation = AccountInformation(self.requests) + else: + self.accountinformation = None self.requests.update_xsrf() def get_user(self, user_identifier): @@ -18,3 +23,12 @@ def get_user(self, user_identifier): def get_group(self, group_id): return Group(self.requests, group_id) + + def get_game(self, game_id): + return Game(self.requests, game_id) + + def get_asset(self, asset_id): + return Asset(self.requests, asset_id) + + def get_badge(self, badge_id): + return Badge(self.requests, badge_id) diff --git a/ro_py/examples/user.py b/ro_py/examples/user.py new file mode 100644 index 00000000..dbd99aad --- /dev/null +++ b/ro_py/examples/user.py @@ -0,0 +1,14 @@ +from ro_py.client import Client + +client = Client() + +user_id = 576059883 + +print(f"Loading user {user_id}...") +user = client.get_user(user_id) +print("Loaded user.") + +print(f"Username: {user.name}") +print(f"Display Name: {user.display_name}") +print(f"Description: {user.description}") +print(f"Status: {user.get_status() or 'None.'}") diff --git a/ro_py/gender.py b/ro_py/gender.py index 6f0296a6..ad3eb082 100644 --- a/ro_py/gender.py +++ b/ro_py/gender.py @@ -1,3 +1,11 @@ +""" + +ro.py > gender.py + +I hate how Roblox stores gender at all, it's really strange as it's not used for anything. + +""" + import enum From 1a633744ad93726d10ad9165adde5b80c72f007f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 14:08:27 -0500 Subject: [PATCH 057/518] RobloxGender is now fully supported --- ro_py/accountinformation.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 42e1c8ed..ecd1e8dd 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -7,6 +7,7 @@ """ from datetime import datetime +from ro_py.gender import RobloxGender endpoint = "https://accountinformation.roblox.com/" @@ -60,23 +61,21 @@ def update(self): def get_gender(self): """ Returns the user's gender. - 1: Other - 2: Male - 3: Female - :return: An integer. + :return: RobloxGender """ gender_req = self.requests.get(endpoint + "v1/gender") - return gender_req.json()["gender"] + return RobloxGender(gender_req.json()["gender"]) def set_gender(self, gender): """ Sets the user's gender. + :param gender: RobloxGender :return: Nothing """ gender_req = self.requests.post( url=endpoint + "v1/gender", data={ - "gender": str(gender) + "gender": str(gender.value) } ) From b1c730f0e757fdbf81b0ea53ea9b99bc3ef44e12 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 15:35:27 -0500 Subject: [PATCH 058/518] AccountSettings is coming! --- ro_py/accountsettings.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 ro_py/accountsettings.py diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py new file mode 100644 index 00000000..30888755 --- /dev/null +++ b/ro_py/accountsettings.py @@ -0,0 +1 @@ +endpoint = "https://accountsettings.roblox.com/" From 572b4585b05d12fe3a7d4f323f1292b16d6f23af Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 15:35:58 -0500 Subject: [PATCH 059/518] ropy_icon --- resources/logo.png | Bin 0 -> 113917 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/logo.png diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..def07499d372a360b2a1415acc9022316adc8baa GIT binary patch literal 113917 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

Y`klYGu{jkoMRn(FvE>_=9vH!m4T50j^ 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≥r`Z)~DcpoRi-cUh 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 Date: Mon, 7 Dec 2020 16:42:27 -0500 Subject: [PATCH 060/518] Some new examples. --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c94ad3ad..b2d2c709 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,22 @@ You can install ro.py from pip: pip3 install ro-py ``` ## Examples -These examples are gone for now as I rewrite them for version 0.1.0. +Using the client: +```python +from ro_py.client import Client +client = Client("Token goes here") # Token is optional, but allows for authentication! +``` +Viewing a user's info: +```python +from ro_py.client import Client +client = Client() +user_id = 576059883 +user = client.get_user(user_id) +print(f"Username: {user.name}") +print(f"Status: {user.get_status() or 'None.'}") +``` +Find more examples in the examples folder. + ## Other Libraries https://github.com/RbxAPI/Pyblox https://github.com/iranathan/robloxapi \ No newline at end of file From e7653abbc791956e416fd322bf60615c059fed31 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 16:48:25 -0500 Subject: [PATCH 061/518] Updated version identifier + new install requirements --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2afec0a9..6c143ac5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.1", + version="0.1.2", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", @@ -19,4 +19,7 @@ "Operating System :: OS Independent", ], python_requires='>=3.6', -) \ No newline at end of file + install_requires=[ + "iso8601" + ] +) From 9e0bd9f12b6170bfe5fc550081fa8fcd3dd13401 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 7 Dec 2020 21:38:05 -0500 Subject: [PATCH 062/518] =?UTF-8?q?AccountSettings=20=F0=9F=8E=89?= =?UTF-8?q?=F0=9F=A5=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/accountinformation.py | 2 +- ro_py/accountsettings.py | 58 +++++++++++++++++++++++++++++++++++++ ro_py/client.py | 3 ++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index ecd1e8dd..cdd15c6a 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -2,7 +2,7 @@ ro.py > accountinformation.py -This file houses functions and classes that pertain to Roblox client . +This file houses functions and classes that pertain to Roblox authenticated user account information. """ diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 30888755..71f76371 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -1 +1,59 @@ +""" + +ro.py > accountsettings.py + +This file houses functions and classes that pertain to Roblox client . + +""" + +import enum + endpoint = "https://accountsettings.roblox.com/" + + +class PrivacyLevel(enum.Enum): + NoOne = "NoOne" + Friends = "Friends", + Everyone = "AllUsers" + + +class PrivacySettings(enum.Enum): + app_chat_privacy = 0 + game_chat_privacy = 1 + inventory_privacy = 2 + phone_discovery = 3 + phone_discovery_enabled = 4 + private_message_privacy = 5 + + +class RobloxEmail: + def __init__(self, email_data): + self.email_address = email_data["emailAddress"] + self.verified = email_data["verified"] + + +class AccountSettings: + def __init__(self, requests): + self.requests = requests + + def get_privacy_setting(self, privacy_setting): + privacy_setting = privacy_setting.value + privacy_endpoint = [ + "app-chat-privacy", + "game-chat-privacy", + "inventory-privacy", + "privacy", + "privacy/info", + "private-message-privacy" + ][privacy_setting] + privacy_key = [ + "appChatPrivacy", + "gameChatPrivacy", + "inventoryPrivacy", + "phoneDiscovery", + "isPhoneDiscoveryEnabled", + "privateMessagePrivacy" + ][privacy_setting] + privacy_endpoint = endpoint + "v1/" + privacy_endpoint + privacy_req = self.requests.get(privacy_endpoint) + return privacy_req.json()[privacy_key] diff --git a/ro_py/client.py b/ro_py/client.py index c4a3e7eb..f4e553e2 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -5,6 +5,7 @@ from ro_py.badges import Badge from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation +from ro_py.accountsettings import AccountSettings class Client: @@ -14,8 +15,10 @@ def __init__(self, token=None): if token: self.requests.cookies[".ROBLOSECURITY"] = token self.accountinformation = AccountInformation(self.requests) + self.accountsettings = AccountSettings(self.requests) else: self.accountinformation = None + self.accountsettings = None self.requests.update_xsrf() def get_user(self, user_identifier): From 0fbd9bfc8ccc35387d78ce26b2ab6331414f751f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 9 Dec 2020 10:22:33 -0500 Subject: [PATCH 063/518] Some request modifications --- ro_py/notifications.py | 0 ro_py/utilities/requests.py | 21 ++++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 ro_py/notifications.py diff --git a/ro_py/notifications.py b/ro_py/notifications.py new file mode 100644 index 00000000..e69de29b diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 3074c6d2..74c5ec98 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -12,9 +12,13 @@ def get(self, *args, **kwargs): kwargs["headers"] = self.headers get_request = requests.get(*args, **kwargs) - try: - get_request_error = get_request.json()["errors"] - except KeyError: + get_request_json = get_request.json() + if isinstance(get_request_json, dict): + try: + get_request_error = get_request_json["errors"] + except KeyError: + return get_request + else: return get_request raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") @@ -28,10 +32,13 @@ def post(self, *args, **kwargs): if "X-CSRF-TOKEN" in post_request.headers: self.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] post_request = requests.post(*args, **kwargs) - - try: - post_request_error = post_request.json()["errors"] - except KeyError: + post_request_json = post_request.json() + if isinstance(post_request_json, dict): + try: + post_request_error = post_request_json["errors"] + except KeyError: + return post_request + else: return post_request raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") From 8aa456397660eb57230edb0e02f9010449a95303 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 9 Dec 2020 13:15:49 -0500 Subject: [PATCH 064/518] Create chat.py --- ro_py/chat.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 ro_py/chat.py diff --git a/ro_py/chat.py b/ro_py/chat.py new file mode 100644 index 00000000..3aa01418 --- /dev/null +++ b/ro_py/chat.py @@ -0,0 +1,12 @@ +""" + +ro.py > chat.py + +This file houses functions and classes that pertain to chatting and messaging. + +""" + + +class ChatWrapper: + def __init__(self, requests): + self.requests = requests From d88c7b032516d3e209cc4da5cb2351b75f143a40 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 09:10:41 -0500 Subject: [PATCH 065/518] Notifications!!! plus some logging stuff --- README.md | 3 +++ ro_py/client.py | 8 ++++++++ ro_py/notifications.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/README.md b/README.md index b2d2c709..eb59d7b0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ print(f"Status: {user.get_status() or 'None.'}") ``` Find more examples in the examples folder. +## Credits + + ## Other Libraries https://github.com/RbxAPI/Pyblox https://github.com/iranathan/robloxapi \ No newline at end of file diff --git a/ro_py/client.py b/ro_py/client.py index f4e553e2..c2e9d86a 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -7,19 +7,27 @@ from ro_py.accountinformation import AccountInformation from ro_py.accountsettings import AccountSettings +import logging + class Client: def __init__(self, token=None): self.token = token self.requests = Requests() + logging.info("Initialized requests.") if token: + logging.info("Found token.") self.requests.cookies[".ROBLOSECURITY"] = token + logging.info("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) + logging.info("Initialized AccountInformation and AccountSettings.") else: self.accountinformation = None self.accountsettings = None + logging.info("Updating XSRF...") self.requests.update_xsrf() + logging.info("Done updating XSRF.") def get_user(self, user_identifier): return User(self.requests, user_identifier) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index e69de29b..a354ef5f 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -0,0 +1,44 @@ +from signalrcore.hub_connection_builder import HubConnectionBuilder + + +class NotificationReceiver: + def __init__(self, requests, on_open, on_close, on_error): + self.requests = requests + + self.on_open = on_open + self.on_close = on_close + self.on_error = on_error + + self.roblosecurity = self.requests.cookies[".ROBLOSECURITY"] + self.negotiate_request = self.requests.get( + url="https://realtime.roblox.com/notifications/negotiate" + "?clientProtocol=1.5" + "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", + cookies={ + ".ROBLOSECURITY": self.roblosecurity + } + ) + self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ + f"&connectionToken={self.negotiate_request.json()['ConnectionToken']}" \ + f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D" + self.connection = HubConnectionBuilder() + self.connection.with_url( + self.wss_url, + options={ + "headers": { + "Cookie": f".ROBLOSECURITY={self.roblosecurity};" + } + } + ) + + self.connection.with_automatic_reconnect({ + "type": "raw", + "keep_alive_interval": 10, + "reconnect_interval": 5, + "max_attempts": 5 + }).build() + + self.connection.on_open(self.on_open) + self.connection.on_close(self.on_close) + self.connection.on_error(self.on_error) + self.connection.start() From 7409c564d1e8863249746f40bd0ab2ee505d90bd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 10:25:05 -0500 Subject: [PATCH 066/518] I seem to have fixed notifications. It's going to spit out a bunch of garbage since it's on debug mode. --- ro_py/notifications.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index a354ef5f..5d1f1b89 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,4 +1,6 @@ from signalrcore.hub_connection_builder import HubConnectionBuilder +from urllib.parse import quote +import logging class NotificationReceiver: @@ -19,7 +21,7 @@ def __init__(self, requests, on_open, on_close, on_error): } ) self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ - f"&connectionToken={self.negotiate_request.json()['ConnectionToken']}" \ + f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D" self.connection = HubConnectionBuilder() self.connection.with_url( @@ -30,7 +32,7 @@ def __init__(self, requests, on_open, on_close, on_error): } } ) - + self.connection.configure_logging(logging.DEBUG) self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, From 43bfae7cf5bee8c27854b8d3613c46d1bbc26c0a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 10:35:02 -0500 Subject: [PATCH 067/518] Removed extra negotiation --- ro_py/notifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 5d1f1b89..0f644ab4 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -29,7 +29,8 @@ def __init__(self, requests, on_open, on_close, on_error): options={ "headers": { "Cookie": f".ROBLOSECURITY={self.roblosecurity};" - } + }, + "skip_negotiation": False } ) self.connection.configure_logging(logging.DEBUG) From 14c527434050f6cc2f47f252c5723c43a135b0c2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 12:03:28 -0500 Subject: [PATCH 068/518] Caseconvert + new notification stuff --- ro_py/notifications.py | 12 ++++++++++++ ro_py/utilities/caseconvert.py | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100644 ro_py/utilities/caseconvert.py diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 0f644ab4..f5ea59c6 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,6 +1,17 @@ from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import logging +import json + + +class Notification: + def __init__(self, notification_data): + self.identifier = notification_data["C"] + self.hub = notification_data["M"][0]["H"] + self.type = notification_data["M"][0]["M"] + self.atype = notification_data["M"][0]["A"][0] + self.raw_data = json.loads(notification_data["M"][0]["A"][1]) + class NotificationReceiver: @@ -44,4 +55,5 @@ def __init__(self, requests, on_open, on_close, on_error): self.connection.on_open(self.on_open) self.connection.on_close(self.on_close) self.connection.on_error(self.on_error) + self.connection.on("UserNotificationHub", lambda b: print(b)) self.connection.start() diff --git a/ro_py/utilities/caseconvert.py b/ro_py/utilities/caseconvert.py new file mode 100644 index 00000000..08c4120d --- /dev/null +++ b/ro_py/utilities/caseconvert.py @@ -0,0 +1,7 @@ +import re + +pattern = re.compile(r'(? Date: Fri, 11 Dec 2020 12:35:47 -0500 Subject: [PATCH 069/518] Updated version identifier + some new notification stuff --- ro_py/notifications.py | 5 ++++- setup.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index f5ea59c6..c90c2846 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,3 +1,4 @@ +from ro_py.utilities.caseconvert import to_snake_case from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import logging @@ -11,7 +12,9 @@ def __init__(self, notification_data): self.type = notification_data["M"][0]["M"] self.atype = notification_data["M"][0]["A"][0] self.raw_data = json.loads(notification_data["M"][0]["A"][1]) - + self.data = {} + for key, value in self.raw_data.items(): + self.data[to_snake_case(key)] = value class NotificationReceiver: diff --git a/setup.py b/setup.py index 6c143ac5..97d4e73f 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.2", + version="0.1.3", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From ec40cfc937a6064d194a3bfdcc1445b88d1660b8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 19:02:32 -0500 Subject: [PATCH 070/518] Updated some notifications + readme credits --- README.md | 2 +- ro_py/notifications.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eb59d7b0..50eef5fb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ print(f"Status: {user.get_status() or 'None.'}") Find more examples in the examples folder. ## Credits - +@mfd-co - helped with endpoints ## Other Libraries https://github.com/RbxAPI/Pyblox diff --git a/ro_py/notifications.py b/ro_py/notifications.py index c90c2846..930afbe1 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -47,7 +47,7 @@ def __init__(self, requests, on_open, on_close, on_error): "skip_negotiation": False } ) - self.connection.configure_logging(logging.DEBUG) + # self.connection.configure_logging(logging.DEBUG) self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, @@ -58,5 +58,7 @@ def __init__(self, requests, on_open, on_close, on_error): self.connection.on_open(self.on_open) self.connection.on_close(self.on_close) self.connection.on_error(self.on_error) - self.connection.on("UserNotificationHub", lambda b: print(b)) self.connection.start() + + def close(self): + self.connection.stop() From 03cac73beaf10fc56d6d88b374bd12362d921760 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 11 Dec 2020 21:40:55 -0500 Subject: [PATCH 071/518] Removed useless deps + added deps to pip --- ro_py/notifications.py | 1 - setup.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 930afbe1..b70c4036 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,7 +1,6 @@ from ro_py.utilities.caseconvert import to_snake_case from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote -import logging import json diff --git a/setup.py b/setup.py index 97d4e73f..253457c3 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,8 @@ ], python_requires='>=3.6', install_requires=[ - "iso8601" + "iso8601", + "signalrcore", + "urllib" ] ) From a0cc397d3a06ca69fc46b5d444c22e5674e8a282 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 12 Dec 2020 21:55:18 -0500 Subject: [PATCH 072/518] Notifications are coming together --- ro_py/notifications.py | 53 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index b70c4036..da2cf665 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,28 +1,51 @@ from ro_py.utilities.caseconvert import to_snake_case + from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json +import logging class Notification: def __init__(self, notification_data): self.identifier = notification_data["C"] self.hub = notification_data["M"][0]["H"] - self.type = notification_data["M"][0]["M"] + self.type = None + self.rtype = notification_data["M"][0]["M"] self.atype = notification_data["M"][0]["A"][0] self.raw_data = json.loads(notification_data["M"][0]["A"][1]) - self.data = {} - for key, value in self.raw_data.items(): - self.data[to_snake_case(key)] = value + self.data = None + + if isinstance(self.raw_data, dict): + self.data = {} + for key, value in self.raw_data.items(): + self.data[to_snake_case(key)] = value + + if "type" in self.data: + self.type = self.data["type"] + elif "Type" in self.data: + self.type = self.data["Type"] + + elif isinstance(self.raw_data, list): + self.data = [] + for value in self.raw_data: + self.data.append(value) + + if len(self.data) > 0: + if "type" in self.data[0]: + self.type = self.data[0]["type"] + elif "Type" in self.data[0]: + self.type = self.data[0]["Type"] class NotificationReceiver: - def __init__(self, requests, on_open, on_close, on_error): + def __init__(self, requests, on_open, on_close, on_error, on_notification): self.requests = requests self.on_open = on_open self.on_close = on_close self.on_error = on_error + self.on_notification = on_notification self.roblosecurity = self.requests.cookies[".ROBLOSECURITY"] self.negotiate_request = self.requests.get( @@ -46,7 +69,23 @@ def __init__(self, requests, on_open, on_close, on_error): "skip_negotiation": False } ) - # self.connection.configure_logging(logging.DEBUG) + + def on_message(in_self, raw_notification): + try: + notification_json = json.loads(raw_notification) + except: + return + if len(notification_json) > 0: + notification = Notification(notification_json) + self.on_notification(notification) + logging.debug( + f"""Notification: +Type: {notification.type} +Data: {notification.data}""" + ) + else: + return + self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, @@ -57,6 +96,8 @@ def __init__(self, requests, on_open, on_close, on_error): self.connection.on_open(self.on_open) self.connection.on_close(self.on_close) self.connection.on_error(self.on_error) + self.connection.hub.on_message = on_message + self.connection.start() def close(self): From b5b4c48be448f7f4a19b3635724e6968a5a72bea Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 12 Dec 2020 22:02:50 -0500 Subject: [PATCH 073/518] Updated logging + added authenticated user --- ro_py/client.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index c2e9d86a..ea9453a7 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -14,20 +14,25 @@ class Client: def __init__(self, token=None): self.token = token self.requests = Requests() - logging.info("Initialized requests.") + + logging.debug("Initialized requests.") if token: - logging.info("Found token.") + logging.debug("Found token.") self.requests.cookies[".ROBLOSECURITY"] = token - logging.info("Initialized token.") + logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) - logging.info("Initialized AccountInformation and AccountSettings.") + logging.debug("Initialized AccountInformation and AccountSettings.") else: self.accountinformation = None self.accountsettings = None - logging.info("Updating XSRF...") + + logging.debug("Updating XSRF...") self.requests.update_xsrf() - logging.info("Done updating XSRF.") + logging.debug("Done updating XSRF.") + + auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") + self.user = User(self.requests, auth_user_req.json()["id"]) def get_user(self, user_identifier): return User(self.requests, user_identifier) From 649c320f63c220f5aa140a36f2f67f7a843a6a30 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 12 Dec 2020 22:40:34 -0500 Subject: [PATCH 074/518] Added some comments --- ro_py/notifications.py | 6 ++++++ ro_py/thumbnails.py | 5 ++++- ro_py/users.py | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index da2cf665..2bef4a3d 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -7,6 +7,9 @@ class Notification: + """ + Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client. + """ def __init__(self, notification_data): self.identifier = notification_data["C"] self.hub = notification_data["M"][0]["H"] @@ -39,6 +42,9 @@ def __init__(self, notification_data): class NotificationReceiver: + """ + This object is used to receive notifications. This should only be generated once per client. + """ def __init__(self, requests, on_open, on_close, on_error, on_notification): self.requests = requests diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 2d1c74bd..ae035a45 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -49,12 +49,15 @@ class ThumbnailGenerator: + """ + This object is used to generate thumbnails. + """ def __init__(self, requests): self.requests = requests def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ - Gets a game's icon. + Gets a group's icon. :param group: The group. :param size: The thumbnail size, formatted widthxheight. :param file_format: The thumbnail format diff --git a/ro_py/users.py b/ro_py/users.py index 02ab5353..c6f77309 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -76,6 +76,7 @@ def get_status(self): def get_roblox_badges(self): """ + Gets the user's roblox badges. :return: A list of RobloxBadge instances """ roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") From c6fff728ce6fcca59108a3aeec0f635af9d43608 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 09:34:26 -0500 Subject: [PATCH 075/518] Async, part 1 --- ro_py/accountinformation.py | 10 +++++----- ro_py/accountsettings.py | 2 +- ro_py/assets.py | 6 +++--- ro_py/badges.py | 2 +- ro_py/client.py | 10 +++++----- ro_py/games.py | 8 ++++---- ro_py/groups.py | 4 ++-- ro_py/notifications.py | 2 +- ro_py/thumbnails.py | 6 +++--- ro_py/users.py | 14 +++++++------- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index cdd15c6a..6bee2f84 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -48,7 +48,7 @@ def __init__(self, requests): self.promotion_channels = None self.update() - def update(self): + async def update(self): """ Updates the account information. :return: Nothing @@ -58,7 +58,7 @@ def update(self): promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) - def get_gender(self): + async def get_gender(self): """ Returns the user's gender. :return: RobloxGender @@ -66,7 +66,7 @@ def get_gender(self): gender_req = self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) - def set_gender(self, gender): + async def set_gender(self, gender): """ Sets the user's gender. :param gender: RobloxGender @@ -79,7 +79,7 @@ def set_gender(self, gender): } ) - def get_birthdate(self): + async def get_birthdate(self): """ Returns the user's birthdate. :return: datetime @@ -93,7 +93,7 @@ def get_birthdate(self): ) return birthdate - def set_birthdate(self, birthdate): + async def set_birthdate(self, birthdate): """ Sets the user's birthdate. :param birthdate: A datetime object. diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 71f76371..f9637302 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -36,7 +36,7 @@ class AccountSettings: def __init__(self, requests): self.requests = requests - def get_privacy_setting(self, privacy_setting): + async def get_privacy_setting(self, privacy_setting): privacy_setting = privacy_setting.value privacy_endpoint = [ "app-chat-privacy", diff --git a/ro_py/assets.py b/ro_py/assets.py index b6fa5136..4c5130d0 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -55,7 +55,7 @@ def __init__(self, requests, asset_id): self.content_rating_type_id = None self.update() - def update(self): + async def update(self): asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -86,7 +86,7 @@ def update(self): self.minimum_membership_level = asset_info["MinimumMembershipLevel"] self.content_rating_type_id = asset_info["ContentRatingTypeId"] - def get_remaining(self): + async def get_remaining(self): asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -96,7 +96,7 @@ def get_remaining(self): asset_info = asset_info_req.json() return asset_info["Remaining"] - def get_limited_resale_data(self): + async def get_limited_resale_data(self): if self.is_limited: resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) diff --git a/ro_py/badges.py b/ro_py/badges.py index c0059375..b503773a 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -34,7 +34,7 @@ def __init__(self, requests, badge_id): self.statistics = None self.update() - def update(self): + async def update(self): badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] diff --git a/ro_py/client.py b/ro_py/client.py index ea9453a7..92b8c5bc 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -34,17 +34,17 @@ def __init__(self, token=None): auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") self.user = User(self.requests, auth_user_req.json()["id"]) - def get_user(self, user_identifier): + async def get_user(self, user_identifier): return User(self.requests, user_identifier) - def get_group(self, group_id): + async def get_group(self, group_id): return Group(self.requests, group_id) - def get_game(self, game_id): + async def get_game(self, game_id): return Game(self.requests, game_id) - def get_asset(self, asset_id): + async def get_asset(self, asset_id): return Asset(self.requests, asset_id) - def get_badge(self, badge_id): + async def get_badge(self, badge_id): return Badge(self.requests, badge_id) diff --git a/ro_py/games.py b/ro_py/games.py index 3a70cba9..2874be01 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -42,7 +42,7 @@ def __init__(self, requests, universe_id): self.create_vip_servers_allowed = None self.update() - def update(self): + async def update(self): game_info_req = self.requests.get( url=endpoint + "v1/games", params={ @@ -64,7 +64,7 @@ def update(self): self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - def get_votes(self): + async def get_votes(self): """ :return: An instance of Votes """ @@ -79,13 +79,13 @@ def get_votes(self): votes = Votes(votes_info) return votes - def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): + async def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): """ Equivalent to thumbnails.get_game_icon """ return thumbnails.get_game_icon(self, size, format, is_circular) - def get_badges(self): + async def get_badges(self): """ Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances diff --git a/ro_py/groups.py b/ro_py/groups.py index 1e81e3fc..08a05c6d 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -29,7 +29,7 @@ def __init__(self, requests, group_id): self.update() - def update(self): + async def update(self): group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] @@ -41,7 +41,7 @@ def update(self): # self.is_locked = group_info["isLocked"] @property - def shout(self): + async def shout(self): """ :return: An instance of Shout """ diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 2bef4a3d..e8d07752 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -106,5 +106,5 @@ def on_message(in_self, raw_notification): self.connection.start() - def close(self): + async def close(self): self.connection.stop() diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index ae035a45..6d7d942f 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -55,7 +55,7 @@ class ThumbnailGenerator: def __init__(self, requests): self.requests = requests - def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): + async def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ Gets a group's icon. :param group: The group. @@ -76,7 +76,7 @@ def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_ci group_icon = group_icon_req.json()["data"][0]["imageUrl"] return group_icon - def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False): + async def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False): """ Gets a game's icon. :param game: The game. @@ -98,7 +98,7 @@ def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circ game_icon = game_icon_req.json()["data"][0]["imageUrl"] return game_icon - def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False): + async def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False): """ Gets a full body, bust, or headshot image of a user. :param user: User to use for avatar. diff --git a/ro_py/users.py b/ro_py/users.py index c6f77309..6349b1ed 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -51,7 +51,7 @@ def __init__(self, requests, ui): self.update() - def update(self): + async def update(self): """ Updates some class values. :return: Nothing @@ -66,7 +66,7 @@ def update(self): # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - def get_status(self): + async def get_status(self): """ Gets the user's status. :return: A string @@ -74,7 +74,7 @@ def get_status(self): status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - def get_roblox_badges(self): + async def get_roblox_badges(self): """ Gets the user's roblox badges. :return: A list of RobloxBadge instances @@ -85,7 +85,7 @@ def get_roblox_badges(self): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - def get_friends_count(self): + async def get_friends_count(self): """ Gets the user's friends count. :return: An integer @@ -94,7 +94,7 @@ def get_friends_count(self): friends_count = friends_count_req.json()["count"] return friends_count - def get_followers_count(self): + async def get_followers_count(self): """ Gets the user's followers count. :return: An integer @@ -103,7 +103,7 @@ def get_followers_count(self): followers_count = followers_count_req.json()["count"] return followers_count - def get_followings_count(self): + async def get_followings_count(self): """ Gets the user's followings count. :return: An integer @@ -112,7 +112,7 @@ def get_followings_count(self): followings_count = followings_count_req.json()["count"] return followings_count - def get_friends(self): + async def get_friends(self): """ Gets the user's friends. :return: A list of User instances. From fae286ebda999fa85f3835799e03b99b89ed135f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 12:20:22 -0500 Subject: [PATCH 076/518] Revert "Async, part 1" This reverts commit c6fff728ce6fcca59108a3aeec0f635af9d43608. --- ro_py/accountinformation.py | 10 +++++----- ro_py/accountsettings.py | 2 +- ro_py/assets.py | 6 +++--- ro_py/badges.py | 2 +- ro_py/client.py | 10 +++++----- ro_py/games.py | 8 ++++---- ro_py/groups.py | 4 ++-- ro_py/notifications.py | 2 +- ro_py/thumbnails.py | 6 +++--- ro_py/users.py | 14 +++++++------- 10 files changed, 32 insertions(+), 32 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 6bee2f84..cdd15c6a 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -48,7 +48,7 @@ def __init__(self, requests): self.promotion_channels = None self.update() - async def update(self): + def update(self): """ Updates the account information. :return: Nothing @@ -58,7 +58,7 @@ async def update(self): promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) - async def get_gender(self): + def get_gender(self): """ Returns the user's gender. :return: RobloxGender @@ -66,7 +66,7 @@ async def get_gender(self): gender_req = self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) - async def set_gender(self, gender): + def set_gender(self, gender): """ Sets the user's gender. :param gender: RobloxGender @@ -79,7 +79,7 @@ async def set_gender(self, gender): } ) - async def get_birthdate(self): + def get_birthdate(self): """ Returns the user's birthdate. :return: datetime @@ -93,7 +93,7 @@ async def get_birthdate(self): ) return birthdate - async def set_birthdate(self, birthdate): + def set_birthdate(self, birthdate): """ Sets the user's birthdate. :param birthdate: A datetime object. diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index f9637302..71f76371 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -36,7 +36,7 @@ class AccountSettings: def __init__(self, requests): self.requests = requests - async def get_privacy_setting(self, privacy_setting): + def get_privacy_setting(self, privacy_setting): privacy_setting = privacy_setting.value privacy_endpoint = [ "app-chat-privacy", diff --git a/ro_py/assets.py b/ro_py/assets.py index 4c5130d0..b6fa5136 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -55,7 +55,7 @@ def __init__(self, requests, asset_id): self.content_rating_type_id = None self.update() - async def update(self): + def update(self): asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -86,7 +86,7 @@ async def update(self): self.minimum_membership_level = asset_info["MinimumMembershipLevel"] self.content_rating_type_id = asset_info["ContentRatingTypeId"] - async def get_remaining(self): + def get_remaining(self): asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -96,7 +96,7 @@ async def get_remaining(self): asset_info = asset_info_req.json() return asset_info["Remaining"] - async def get_limited_resale_data(self): + def get_limited_resale_data(self): if self.is_limited: resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) diff --git a/ro_py/badges.py b/ro_py/badges.py index b503773a..c0059375 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -34,7 +34,7 @@ def __init__(self, requests, badge_id): self.statistics = None self.update() - async def update(self): + def update(self): badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] diff --git a/ro_py/client.py b/ro_py/client.py index 92b8c5bc..ea9453a7 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -34,17 +34,17 @@ def __init__(self, token=None): auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") self.user = User(self.requests, auth_user_req.json()["id"]) - async def get_user(self, user_identifier): + def get_user(self, user_identifier): return User(self.requests, user_identifier) - async def get_group(self, group_id): + def get_group(self, group_id): return Group(self.requests, group_id) - async def get_game(self, game_id): + def get_game(self, game_id): return Game(self.requests, game_id) - async def get_asset(self, asset_id): + def get_asset(self, asset_id): return Asset(self.requests, asset_id) - async def get_badge(self, badge_id): + def get_badge(self, badge_id): return Badge(self.requests, badge_id) diff --git a/ro_py/games.py b/ro_py/games.py index 2874be01..3a70cba9 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -42,7 +42,7 @@ def __init__(self, requests, universe_id): self.create_vip_servers_allowed = None self.update() - async def update(self): + def update(self): game_info_req = self.requests.get( url=endpoint + "v1/games", params={ @@ -64,7 +64,7 @@ async def update(self): self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - async def get_votes(self): + def get_votes(self): """ :return: An instance of Votes """ @@ -79,13 +79,13 @@ async def get_votes(self): votes = Votes(votes_info) return votes - async def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): + def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): """ Equivalent to thumbnails.get_game_icon """ return thumbnails.get_game_icon(self, size, format, is_circular) - async def get_badges(self): + def get_badges(self): """ Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances diff --git a/ro_py/groups.py b/ro_py/groups.py index 08a05c6d..1e81e3fc 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -29,7 +29,7 @@ def __init__(self, requests, group_id): self.update() - async def update(self): + def update(self): group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] @@ -41,7 +41,7 @@ async def update(self): # self.is_locked = group_info["isLocked"] @property - async def shout(self): + def shout(self): """ :return: An instance of Shout """ diff --git a/ro_py/notifications.py b/ro_py/notifications.py index e8d07752..2bef4a3d 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -106,5 +106,5 @@ def on_message(in_self, raw_notification): self.connection.start() - async def close(self): + def close(self): self.connection.stop() diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 6d7d942f..ae035a45 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -55,7 +55,7 @@ class ThumbnailGenerator: def __init__(self, requests): self.requests = requests - async def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): + def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ Gets a group's icon. :param group: The group. @@ -76,7 +76,7 @@ async def get_group_icon(self, group, size=size_150x150, file_format=format_png, group_icon = group_icon_req.json()["data"][0]["imageUrl"] return group_icon - async def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False): + def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False): """ Gets a game's icon. :param game: The game. @@ -98,7 +98,7 @@ async def get_game_icon(self, game, size=size_256x256, file_format=format_png, i game_icon = game_icon_req.json()["data"][0]["imageUrl"] return game_icon - async def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False): + def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False): """ Gets a full body, bust, or headshot image of a user. :param user: User to use for avatar. diff --git a/ro_py/users.py b/ro_py/users.py index 6349b1ed..c6f77309 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -51,7 +51,7 @@ def __init__(self, requests, ui): self.update() - async def update(self): + def update(self): """ Updates some class values. :return: Nothing @@ -66,7 +66,7 @@ async def update(self): # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - async def get_status(self): + def get_status(self): """ Gets the user's status. :return: A string @@ -74,7 +74,7 @@ async def get_status(self): status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - async def get_roblox_badges(self): + def get_roblox_badges(self): """ Gets the user's roblox badges. :return: A list of RobloxBadge instances @@ -85,7 +85,7 @@ async def get_roblox_badges(self): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - async def get_friends_count(self): + def get_friends_count(self): """ Gets the user's friends count. :return: An integer @@ -94,7 +94,7 @@ async def get_friends_count(self): friends_count = friends_count_req.json()["count"] return friends_count - async def get_followers_count(self): + def get_followers_count(self): """ Gets the user's followers count. :return: An integer @@ -103,7 +103,7 @@ async def get_followers_count(self): followers_count = followers_count_req.json()["count"] return followers_count - async def get_followings_count(self): + def get_followings_count(self): """ Gets the user's followings count. :return: An integer @@ -112,7 +112,7 @@ async def get_followings_count(self): followings_count = followings_count_req.json()["count"] return followings_count - async def get_friends(self): + def get_friends(self): """ Gets the user's friends. :return: A list of User instances. From e70c4000b0457843278856dfb951f62ab73e636a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 12:21:19 -0500 Subject: [PATCH 077/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 253457c3..d7f704c6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.3", + version="0.1.4", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 5a3916218243e01cf781cf8851ee317b227165bc Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 12:55:28 -0500 Subject: [PATCH 078/518] =?UTF-8?q?Chat=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/chat.py | 26 ++++++++++++++++++++++++++ ro_py/utilities/errors.py | 6 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 3aa01418..07f9e01d 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -6,6 +6,32 @@ """ +from ro_py.utilities.errors import ChatError + + +class Conversation: + def __init__(self, requests, conversation_id): + self.requests = requests + self.id = conversation_id + + def send_message(self, message): + send_message_req = self.requests.post( + url="https://chat.roblox.com/v2/send-message", + data={ + "message": message, + "conversationId": self.id + } + ) + send_message_json = send_message_req.json() + if send_message_json["sent"]: + return + else: + raise ChatError(send_message_json["statusMessage"]) + + +class Message: + pass + class ChatWrapper: def __init__(self, requests): diff --git a/ro_py/utilities/errors.py b/ro_py/utilities/errors.py index 977b56f0..09b1c485 100644 --- a/ro_py/utilities/errors.py +++ b/ro_py/utilities/errors.py @@ -23,5 +23,9 @@ class InvalidShotTypeError(Exception): class ApiError(Exception): - """Called in ro_py_requets when an API request fails.""" + """Called in requests when an API request fails.""" pass + + +class ChatError(Exception): + """Called in chat when a chat action fails.""" From 916b780cbaf3ffc3e3679f8e6cc78d09cca72fff Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 13:06:07 -0500 Subject: [PATCH 079/518] ConversationTyping + endpoint --- ro_py/chat.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 07f9e01d..cb2e3911 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -8,15 +8,42 @@ from ro_py.utilities.errors import ChatError +endpoint = "https://chat.roblox.com/" + + +class ConversationTyping: + def __init__(self, requests, conversation_id): + self.requests = requests + self.id = conversation_id + + def __enter__(self): + self.requests.post( + url=endpoint + "v2/update-user-typing-status", + data={ + "conversationId": self.id, + "isTyping": "true" + } + ) + + def __exit__(self): + self.requests.post( + url=endpoint + "v2/update-user-typing-status", + data={ + "conversationId": self.id, + "isTyping": "false" + } + ) + class Conversation: def __init__(self, requests, conversation_id): self.requests = requests self.id = conversation_id + self.typing = ConversationTyping(self.requests, conversation_id) def send_message(self, message): send_message_req = self.requests.post( - url="https://chat.roblox.com/v2/send-message", + url=endpoint + "v2/send-message", data={ "message": message, "conversationId": self.id From 00263bcf6fd50a6c8325d6e0c3f6b56142df761f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 14:54:58 -0500 Subject: [PATCH 080/518] =?UTF-8?q?Message=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/chat.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index cb2e3911..18039694 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -7,6 +7,7 @@ """ from ro_py.utilities.errors import ChatError +from ro_py.users import User endpoint = "https://chat.roblox.com/" @@ -17,7 +18,7 @@ def __init__(self, requests, conversation_id): self.id = conversation_id def __enter__(self): - self.requests.post( + enter_req = self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -25,8 +26,8 @@ def __enter__(self): } ) - def __exit__(self): - self.requests.post( + def __exit__(self, *args, **kwargs): + exit_req = self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -51,13 +52,37 @@ def send_message(self, message): ) send_message_json = send_message_req.json() if send_message_json["sent"]: - return + return Message(self.requests, send_message_json["messageId"]) else: raise ChatError(send_message_json["statusMessage"]) class Message: - pass + def __init__(self, requests, message_id, conversation_id): + self.requests = requests + self.id = message_id + self.conversation_id = conversation_id + + self.content = None + self.sender = None + self.read = None + + self.update() + + def update(self): + message_req = self.requests.get( + url="https://chat.roblox.com/v2/get-messages", + params={ + "conversationId": self.conversation_id, + "pageSize": 1, + "exclusiveStartMessageId": self.id + } + ) + + message_json = message_req.json() + self.content = message_json["content"] + self.sender = User(self.requests, message_json["senderTargetId"]) + self.read = message_json["read"] class ChatWrapper: From 74565c865660979872bf98d609c8ed96967a7613 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 15:12:52 -0500 Subject: [PATCH 081/518] Cache --- ro_py/utilities/cache.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ro_py/utilities/cache.py diff --git a/ro_py/utilities/cache.py b/ro_py/utilities/cache.py new file mode 100644 index 00000000..c25375cc --- /dev/null +++ b/ro_py/utilities/cache.py @@ -0,0 +1,3 @@ +cache = { + "users": {} +} From d7b07231ef5bf063683cb3e799237d1f4901c09a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 21:40:11 -0500 Subject: [PATCH 082/518] Cache implementation + fixed auth --- ro_py/client.py | 17 ++++++++++++----- ro_py/users.py | 23 +---------------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index ea9453a7..5f1c0935 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -3,6 +3,7 @@ from ro_py.groups import Group from ro_py.assets import Asset from ro_py.badges import Badge +from ro_py.utilities.cache import cache from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation from ro_py.accountsettings import AccountSettings @@ -23,19 +24,25 @@ def __init__(self, token=None): self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") + auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") + self.user = User(self.requests, auth_user_req.json()["id"]) + logging.debug("Initialized authenticated user.") else: self.accountinformation = None self.accountsettings = None + self.user = None logging.debug("Updating XSRF...") self.requests.update_xsrf() logging.debug("Done updating XSRF.") - auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") - self.user = User(self.requests, auth_user_req.json()["id"]) - - def get_user(self, user_identifier): - return User(self.requests, user_identifier) + def get_user(self, user_id): + try: + cache["users"][str(user_id)] + except KeyError: + user = User(self.requests, user_id) + cache["users"][str(user_id)] = user + return cache["users"][str(user_id)] def get_group(self, group_id): return Group(self.requests, group_id) diff --git a/ro_py/users.py b/ro_py/users.py index c6f77309..01a6881e 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -20,28 +20,7 @@ class User: def __init__(self, requests, ui): self.requests = requests - - if isinstance(ui, str): - try: - int(str) - is_id = True - except TypeError: - is_id = False - if is_id: - self.id = int(ui) - else: - user_id_req = self.requests.post( - url="https://users.roblox.com/v1/usernames/users", - json={ - "usernames": [ - ui - ] - } - ) - user_id = user_id_req.json()["data"][0]["id"] - self.id = user_id - elif isinstance(ui, int): - self.id = ui + self.id = ui self.description = None self.created = None From 560802ee162bbd199ce1306b9d91e3b064364edf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 21:43:48 -0500 Subject: [PATCH 083/518] Cache implementation updated --- ro_py/client.py | 27 +++++++++++++++++++++------ ro_py/utilities/cache.py | 6 +++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 5f1c0935..cdce9b8a 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -40,18 +40,33 @@ def get_user(self, user_id): try: cache["users"][str(user_id)] except KeyError: - user = User(self.requests, user_id) - cache["users"][str(user_id)] = user + cache["users"][str(user_id)] = User(self.requests, user_id) return cache["users"][str(user_id)] def get_group(self, group_id): - return Group(self.requests, group_id) + try: + cache["groups"][str(group_id)] + except KeyError: + cache["groups"][str(group_id)] = Group(self.requests, group_id) + return cache["groups"][str(group_id)] def get_game(self, game_id): - return Game(self.requests, game_id) + try: + cache["games"][str(game_id)] + except KeyError: + cache["games"][str(game_id)] = Game(self.requests, game_id) + return cache["games"][str(game_id)] def get_asset(self, asset_id): - return Asset(self.requests, asset_id) + try: + cache["assets"][str(asset_id)] + except KeyError: + cache["assets"][str(asset_id)] = Asset(self.requests, asset_id) + return cache["assets"][str(asset_id)] def get_badge(self, badge_id): - return Badge(self.requests, badge_id) + try: + cache["badges"][str(badge_id)] + except KeyError: + cache["badges"][str(badge_id)] = Badge(self.requests, badge_id) + return cache["badges"][str(badge_id)] diff --git a/ro_py/utilities/cache.py b/ro_py/utilities/cache.py index c25375cc..64bec568 100644 --- a/ro_py/utilities/cache.py +++ b/ro_py/utilities/cache.py @@ -1,3 +1,7 @@ cache = { - "users": {} + "users": {}, + "groups": {}, + "games": {}, + "assets": {}, + "badges": {} } From e7121490c2bf812df2b893212768063b67644170 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 21:47:51 -0500 Subject: [PATCH 084/518] Fixed chat --- ro_py/chat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 18039694..2a519a0c 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -52,7 +52,7 @@ def send_message(self, message): ) send_message_json = send_message_req.json() if send_message_json["sent"]: - return Message(self.requests, send_message_json["messageId"]) + return Message(self.requests, send_message_json["messageId"], self.id) else: raise ChatError(send_message_json["statusMessage"]) @@ -79,7 +79,7 @@ def update(self): } ) - message_json = message_req.json() + message_json = message_req.json()[0] self.content = message_json["content"] self.sender = User(self.requests, message_json["senderTargetId"]) self.read = message_json["read"] From 561e95043d4329052f33df6749fd6e62b0b58ae7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 22:11:42 -0500 Subject: [PATCH 085/518] Some work on chat --- ro_py/chat.py | 19 +++++++++++++++++-- ro_py/client.py | 4 ++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 2a519a0c..857c3293 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -42,11 +42,14 @@ def __init__(self, requests, conversation_id): self.id = conversation_id self.typing = ConversationTyping(self.requests, conversation_id) - def send_message(self, message): + def get_message(self, message_id): + return Message(self.requests, message_id, self.id) + + def send_message(self, content): send_message_req = self.requests.post( url=endpoint + "v2/send-message", data={ - "message": message, + "message": content, "conversationId": self.id } ) @@ -88,3 +91,15 @@ def update(self): class ChatWrapper: def __init__(self, requests): self.requests = requests + + def get_conversation(self, conversation_id): + return Conversation(self.requests, conversation_id) + + def get_conversations(self, page_number=1, page_size=10): + conversations_req = self.requests.get( + url="https://chat.roblox.com/v2/get-user-conversations", + params={ + "pageNumber": page_number, + "pageSize": page_size + } + ) diff --git a/ro_py/client.py b/ro_py/client.py index cdce9b8a..86b5d7f9 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -3,6 +3,7 @@ from ro_py.groups import Group from ro_py.assets import Asset from ro_py.badges import Badge +from ro_py.chat import ChatWrapper from ro_py.utilities.cache import cache from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation @@ -27,10 +28,13 @@ def __init__(self, token=None): auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") self.user = User(self.requests, auth_user_req.json()["id"]) logging.debug("Initialized authenticated user.") + self.chat = ChatWrapper(self.requests) + logging.debug("Initialized chat wrapper.") else: self.accountinformation = None self.accountsettings = None self.user = None + self.chat = None logging.debug("Updating XSRF...") self.requests.update_xsrf() From b902fdaefd42552283cca0e137623969876f0674 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 13 Dec 2020 22:28:01 -0500 Subject: [PATCH 086/518] =?UTF-8?q?almost=20done=20=F0=9F=91=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/chat.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 857c3293..5eec591d 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -37,9 +37,26 @@ def __exit__(self, *args, **kwargs): class Conversation: - def __init__(self, requests, conversation_id): + def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): self.requests = requests - self.id = conversation_id + + if raw: + data = raw_data + self.id = data["id"] + else: + self.id = conversation_id + conversation_req = requests.get( + url="https://chat.roblox.com/v2/get-conversations", + params={ + "conversationIds": self.id + } + ) + data = conversation_req.json()[0] + + self.title = data["title"] + self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.type = data["conversationType"] + self.typing = ConversationTyping(self.requests, conversation_id) def get_message(self, message_id): @@ -103,3 +120,12 @@ def get_conversations(self, page_number=1, page_size=10): "pageSize": page_size } ) + conversations_json = conversations_req.json() + conversations = [] + for conversation_raw in conversations_json: + conversations.append(Conversation( + requests=self.requests, + raw=True, + raw_data=conversation_raw + )) + return conversations From 0e5a02cc4d595e5581a653373d74493cdb282d69 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 14 Dec 2020 10:18:14 -0500 Subject: [PATCH 087/518] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5c291095..677541bc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist_old/ ro_py.egg-info/ tests/ ro_py_old/ +other/ From 4f588b981faaa3b68b2f3c461e56ab7a2a945957 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 14 Dec 2020 18:08:47 -0500 Subject: [PATCH 088/518] Comments, comments, everywhere! --- ro_py/accountinformation.py | 2 +- ro_py/accountsettings.py | 22 +++++++++++++++++++--- ro_py/assets.py | 11 +++++++++++ ro_py/games.py | 3 +++ ro_py/gender.py | 3 +++ ro_py/groups.py | 27 ++++++++------------------- ro_py/notifications.py | 11 +++++++++-- 7 files changed, 54 insertions(+), 25 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index cdd15c6a..7b08cab6 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -60,7 +60,7 @@ def update(self): def get_gender(self): """ - Returns the user's gender. + Gets the user's gender. :return: RobloxGender """ gender_req = self.requests.get(endpoint + "v1/gender") diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 71f76371..c6db261e 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -12,12 +12,18 @@ class PrivacyLevel(enum.Enum): - NoOne = "NoOne" - Friends = "Friends", - Everyone = "AllUsers" + """ + Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy. + """ + no_one = "NoOne" + friends = "Friends", + everyone = "AllUsers" class PrivacySettings(enum.Enum): + """ + Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy. + """ app_chat_privacy = 0 game_chat_privacy = 1 inventory_privacy = 2 @@ -27,16 +33,26 @@ class PrivacySettings(enum.Enum): class RobloxEmail: + """ + Represents an obfuscated version of the email you have set on your account. + """ def __init__(self, email_data): self.email_address = email_data["emailAddress"] self.verified = email_data["verified"] class AccountSettings: + """ + Represents authenticated client account settings (https://accountsettings.roblox.com/) + This is only available for authenticated clients as it cannot be accessed otherwise. + """ def __init__(self, requests): self.requests = requests def get_privacy_setting(self, privacy_setting): + """ + Gets the value of a privacy setting. + """ privacy_setting = privacy_setting.value privacy_endpoint = [ "app-chat-privacy", diff --git a/ro_py/assets.py b/ro_py/assets.py index b6fa5136..a1923610 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -56,6 +56,9 @@ def __init__(self, requests, asset_id): self.update() def update(self): + """ + Updates the asset's information. + """ asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -87,6 +90,10 @@ def update(self): self.content_rating_type_id = asset_info["ContentRatingTypeId"] def get_remaining(self): + """ + Gets the remaining amount of this asset. (used for Limited U items) + :returns: Amount remaining + """ asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ @@ -97,6 +104,10 @@ def get_remaining(self): return asset_info["Remaining"] def get_limited_resale_data(self): + """ + Gets the limited resale data + :returns: LimitedResaleData + """ if self.is_limited: resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) diff --git a/ro_py/games.py b/ro_py/games.py index 3a70cba9..4e24100f 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -43,6 +43,9 @@ def __init__(self, requests, universe_id): self.update() def update(self): + """ + Updates the game's information. + """ game_info_req = self.requests.get( url=endpoint + "v1/games", params={ diff --git a/ro_py/gender.py b/ro_py/gender.py index ad3eb082..0e7d53aa 100644 --- a/ro_py/gender.py +++ b/ro_py/gender.py @@ -10,6 +10,9 @@ class RobloxGender(enum.Enum): + """ + Represents the gender of the authenticated Roblox client. + """ Other = 1 Female = 2 Male = 3 diff --git a/ro_py/groups.py b/ro_py/groups.py index 1e81e3fc..d3896255 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -26,10 +26,14 @@ def __init__(self, requests, group_id): self.member_count = None self.is_builders_club_only = None self.public_entry_allowed = None + self.shout = None self.update() def update(self): + """ + Updates the group's information. + """ group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] @@ -38,23 +42,8 @@ def update(self): self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] - # self.is_locked = group_info["isLocked"] - - @property - def shout(self): - """ - :return: An instance of Shout - """ - group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") - group_info = group_info_req.json() - - if group_info["shout"]: - return Shout(self.requests, group_info["shout"]) + if "shout" in group_info: + self.shout = group_info["shout"] else: - return None - - # def get_icon(self, size=thumbnails.size_150x150, file_format=thumbnails.format_png, is_circular=False): - # """ - # Equivalent to thumbnails.get_group_icon - # """ - # return thumbnails.get_group_icon(self, size, file_format, is_circular) + self.shout = None + # self.is_locked = group_info["isLocked"] diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 2bef4a3d..0d8d4adb 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -43,7 +43,8 @@ def __init__(self, notification_data): class NotificationReceiver: """ - This object is used to receive notifications. This should only be generated once per client. + This object is used to receive notifications. + This should only be generated once per client as to not duplicate notifications. """ def __init__(self, requests, on_open, on_close, on_error, on_notification): self.requests = requests @@ -76,7 +77,10 @@ def __init__(self, requests, on_open, on_close, on_error, on_notification): } ) - def on_message(in_self, raw_notification): + def on_message(_self, raw_notification): + """ + Internal callback when a message is received. + """ try: notification_json = json.loads(raw_notification) except: @@ -107,4 +111,7 @@ def on_message(in_self, raw_notification): self.connection.start() def close(self): + """ + Closes the connection and stops receiving notifications. + """ self.connection.stop() From c9fcecd9309089beb44a05b4faa4b5e18d9393dd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 14 Dec 2020 18:15:28 -0500 Subject: [PATCH 089/518] Client comments --- ro_py/client.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index 86b5d7f9..5a2bbcb6 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -13,6 +13,9 @@ class Client: + """ + Represents an authenticated Roblox client. + """ def __init__(self, token=None): self.token = token self.requests = Requests() @@ -41,6 +44,10 @@ def __init__(self, token=None): logging.debug("Done updating XSRF.") def get_user(self, user_id): + """ + Gets a Roblox user. + :returns: Instance of User + """ try: cache["users"][str(user_id)] except KeyError: @@ -48,6 +55,10 @@ def get_user(self, user_id): return cache["users"][str(user_id)] def get_group(self, group_id): + """ + Gets a Roblox group. + :returns: Instance of Group + """ try: cache["groups"][str(group_id)] except KeyError: @@ -55,6 +66,10 @@ def get_group(self, group_id): return cache["groups"][str(group_id)] def get_game(self, game_id): + """ + Gets a Roblox game. + :returns: Instance of Game + """ try: cache["games"][str(game_id)] except KeyError: @@ -62,6 +77,10 @@ def get_game(self, game_id): return cache["games"][str(game_id)] def get_asset(self, asset_id): + """ + Gets a Roblox asset. + :returns: Instance of Asset + """ try: cache["assets"][str(asset_id)] except KeyError: @@ -69,6 +88,10 @@ def get_asset(self, asset_id): return cache["assets"][str(asset_id)] def get_badge(self, badge_id): + """ + Gets a Roblox badge. + :returns: Instance of Badge + """ try: cache["badges"][str(badge_id)] except KeyError: From fbfa0871a9c25610fba2245266bab6edcfcaf5ac Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 14 Dec 2020 19:51:31 -0500 Subject: [PATCH 090/518] Fixed minor PEP issues --- ro_py/accountinformation.py | 4 ++-- ro_py/chat.py | 4 ++-- ro_py/games.py | 7 ------- ro_py/notifications.py | 2 ++ 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 7b08cab6..2ef37507 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -72,7 +72,7 @@ def set_gender(self, gender): :param gender: RobloxGender :return: Nothing """ - gender_req = self.requests.post( + self.requests.post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) @@ -99,7 +99,7 @@ def set_birthdate(self, birthdate): :param birthdate: A datetime object. :return: Nothing """ - birthdate_req = self.requests.post( + self.requests.post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, diff --git a/ro_py/chat.py b/ro_py/chat.py index 5eec591d..beb72f4f 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -18,7 +18,7 @@ def __init__(self, requests, conversation_id): self.id = conversation_id def __enter__(self): - enter_req = self.requests.post( + self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -27,7 +27,7 @@ def __enter__(self): ) def __exit__(self, *args, **kwargs): - exit_req = self.requests.post( + self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, diff --git a/ro_py/games.py b/ro_py/games.py index 4e24100f..f4d1e4da 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -9,7 +9,6 @@ from ro_py.users import User from ro_py.groups import Group from ro_py.badges import Badge -from ro_py import thumbnails endpoint = "https://games.roblox.com/" @@ -82,12 +81,6 @@ def get_votes(self): votes = Votes(votes_info) return votes - def get_icon(self, size=thumbnails.size_256x256, format=thumbnails.format_png, is_circular=False): - """ - Equivalent to thumbnails.get_game_icon - """ - return thumbnails.get_game_icon(self, size, format, is_circular) - def get_badges(self): """ Note: this has a limit of 100 badges due to paging. This will be expanded soon. diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 0d8d4adb..b94d9d82 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -10,6 +10,7 @@ class Notification: """ Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client. """ + def __init__(self, notification_data): self.identifier = notification_data["C"] self.hub = notification_data["M"][0]["H"] @@ -46,6 +47,7 @@ class NotificationReceiver: This object is used to receive notifications. This should only be generated once per client as to not duplicate notifications. """ + def __init__(self, requests, on_open, on_close, on_error, on_notification): self.requests = requests From 15ef48be0d5b1be0ea94ced01b138f6c3a9fc318 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 14 Dec 2020 19:54:40 -0500 Subject: [PATCH 091/518] Fixed another PEP issue --- ro_py/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index b94d9d82..65b72e7a 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -85,7 +85,7 @@ def on_message(_self, raw_notification): """ try: notification_json = json.loads(raw_notification) - except: + except json.decoder.JSONDecodeError: return if len(notification_json) > 0: notification = Notification(notification_json) From f75c8ee513cec3039e6c3e0ce49fc826a1af783e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 15 Dec 2020 14:14:24 -0500 Subject: [PATCH 092/518] Added patch requests --- ro_py/utilities/requests.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 74c5ec98..3d0e64bd 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -41,7 +41,25 @@ def post(self, *args, **kwargs): else: return post_request - raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + def patch(self, *args, **kwargs): + kwargs["cookies"] = self.cookies + kwargs["headers"] = self.headers + + patch_request = requests.post(*args, **kwargs) + if patch_request.status_code == 403: + if "X-CSRF-TOKEN" in patch_request.headers: + self.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] + patch_request = requests.patch(*args, **kwargs) + patch_request_json = patch_request.json() + if isinstance(patch_request_json, dict): + try: + patch_request_error = patch_request_json["errors"] + except KeyError: + return patch_request + else: + return patch_request + + raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") def update_xsrf(self, url="https://www.roblox.com/favorite/toggle"): xsrf_req = requests.post(url) From 4c8c2ccf32f8884351c4087c96d424562e389120 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 15 Dec 2020 14:17:13 -0500 Subject: [PATCH 093/518] Fixed typo Wrote "post", meant "patch" --- ro_py/utilities/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 3d0e64bd..6402e555 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -45,7 +45,7 @@ def patch(self, *args, **kwargs): kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers - patch_request = requests.post(*args, **kwargs) + patch_request = requests.patch(*args, **kwargs) if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: self.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] From 24e2db38bd54fab4fa3fc2340ab6f4235272befa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 15 Dec 2020 14:30:07 -0500 Subject: [PATCH 094/518] You can now update group shouts --- ro_py/groups.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index d3896255..291e3267 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -47,3 +47,11 @@ def update(self): else: self.shout = None # self.is_locked = group_info["isLocked"] + + def update_shout(self, message): + self.requests.patch( + url=f"https://groups.roblox.com/v1/groups/{self.id}/status", + data={ + "message": message + } + ) From 6db7cd3732da7e464718de7d21d83edeea593997 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 15 Dec 2020 14:30:33 -0500 Subject: [PATCH 095/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d7f704c6..8e4633fd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.4", + version="0.1.5", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 0ff26b9f9b0026e9cfca2c229e39bb6b58b9b6e4 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 15 Dec 2020 14:31:15 -0500 Subject: [PATCH 096/518] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 677541bc..4aed4069 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ ro_py.egg-info/ tests/ ro_py_old/ other/ +build.bat From 87586f7bbec669435b72ea057c73ad32a8741930 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 17 Dec 2020 12:27:51 -0500 Subject: [PATCH 097/518] Patched JSON --- ro_py/utilities/requests.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 6402e555..d7633d4f 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -1,4 +1,5 @@ from ro_py.utilities.errors import ApiError +from json.decoder import JSONDecodeError import requests @@ -12,7 +13,12 @@ def get(self, *args, **kwargs): kwargs["headers"] = self.headers get_request = requests.get(*args, **kwargs) - get_request_json = get_request.json() + + try: + get_request_json = get_request.json() + except JSONDecodeError: + return get_request + if isinstance(get_request_json, dict): try: get_request_error = get_request_json["errors"] @@ -28,11 +34,16 @@ def post(self, *args, **kwargs): kwargs["headers"] = self.headers post_request = requests.post(*args, **kwargs) + if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: self.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] post_request = requests.post(*args, **kwargs) - post_request_json = post_request.json() + try: + post_request_json = post_request.json() + except JSONDecodeError: + return post_request + if isinstance(post_request_json, dict): try: post_request_error = post_request_json["errors"] From ac52660f572627abab802d30c6471184cd2ed905 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 18 Dec 2020 11:57:31 -0500 Subject: [PATCH 098/518] We are now using a Session object. --- ro_py/utilities/requests.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index d7633d4f..0fd6a369 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -7,12 +7,13 @@ class Requests: def __init__(self): self.cookies = {} self.headers = {} + self.session = requests.Session() def get(self, *args, **kwargs): kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers - get_request = requests.get(*args, **kwargs) + get_request = self.session.get(*args, **kwargs) try: get_request_json = get_request.json() @@ -33,12 +34,13 @@ def post(self, *args, **kwargs): kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers - post_request = requests.post(*args, **kwargs) + post_request = self.session.post(*args, **kwargs) if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: self.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = requests.post(*args, **kwargs) + post_request = self.session.post(*args, **kwargs) + try: post_request_json = post_request.json() except JSONDecodeError: @@ -56,12 +58,15 @@ def patch(self, *args, **kwargs): kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers - patch_request = requests.patch(*args, **kwargs) + patch_request = self.session.patch(*args, **kwargs) + if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: self.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = requests.patch(*args, **kwargs) + patch_request = self.session.patch(*args, **kwargs) + patch_request_json = patch_request.json() + if isinstance(patch_request_json, dict): try: patch_request_error = patch_request_json["errors"] @@ -73,5 +78,5 @@ def patch(self, *args, **kwargs): raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") def update_xsrf(self, url="https://www.roblox.com/favorite/toggle"): - xsrf_req = requests.post(url) + xsrf_req = self.session.post(url) self.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] From c22c2b60969743d2d13c3f7d8ee12c9d133cd5f9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 18 Dec 2020 12:00:45 -0500 Subject: [PATCH 099/518] Fixed session issue + fixed auth --- ro_py/client.py | 2 +- ro_py/utilities/requests.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 5a2bbcb6..015059ac 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -23,7 +23,7 @@ def __init__(self, token=None): logging.debug("Initialized requests.") if token: logging.debug("Found token.") - self.requests.cookies[".ROBLOSECURITY"] = token + self.requests.session.cookies[".ROBLOSECURITY"] = token logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 0fd6a369..fa921a58 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -5,12 +5,10 @@ class Requests: def __init__(self): - self.cookies = {} self.headers = {} self.session = requests.Session() def get(self, *args, **kwargs): - kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers get_request = self.session.get(*args, **kwargs) @@ -31,7 +29,6 @@ def get(self, *args, **kwargs): raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") def post(self, *args, **kwargs): - kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers post_request = self.session.post(*args, **kwargs) @@ -48,14 +45,13 @@ def post(self, *args, **kwargs): if isinstance(post_request_json, dict): try: - post_request_error = post_request_json["errors"] + post_request_json["errors"] except KeyError: return post_request else: return post_request def patch(self, *args, **kwargs): - kwargs["cookies"] = self.cookies kwargs["headers"] = self.headers patch_request = self.session.patch(*args, **kwargs) From 6846380f811ef7991f724252926eb50aaa18846a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 19 Dec 2020 20:31:56 -0500 Subject: [PATCH 100/518] Experimental Roblox Status.io support --- ro_py/robloxstatus.py | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 ro_py/robloxstatus.py diff --git a/ro_py/robloxstatus.py b/ro_py/robloxstatus.py new file mode 100644 index 00000000..3d4780c3 --- /dev/null +++ b/ro_py/robloxstatus.py @@ -0,0 +1,61 @@ +""" + +ro.py > robloxstatus.py + +This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) + +""" + +import iso8601 + +endpoint = "https://4277980205320394.hostedstatus.com/1.0/status/59db90dbcdeb2f04dadcf16d" + + +class RobloxStatusContainer: + """ + Represents a tab or item in a tab on the Roblox status site. + The tab items are internally called "containers" so that's what I call them here. + I don't see any difference between the data in tabs and data in containers, so I use the same object here. + """ + def __init__(self, container_data): + self.id = container_data["id"] + self.name = container_data["name"] + self.updated = iso8601.parse_date(container_data["updated"]) + self.status = container_data["status"] + self.status_code = container_data["status_code"] + + +class RobloxStatusOverall: + """ + Represents the overall status on the Roblox status site. + """ + def __init__(self, overall_data): + self.updated = iso8601.parse_date(overall_data["updated"]) + self.status = overall_data["status"] + self.status_code = overall_data["status_code"] + + +class RobloxStatus: + def __init__(self, requests): + self.requests = requests + + self.overall = None + self.user = None + self.player = None + self.creator = None + + self.update() + + def update(self): + status_req = self.requests.get( + url=endpoint + ) + status_data = status_req.json()["result"] + + self.overall = RobloxStatusOverall(status_data["status_overall"]) + self.user = RobloxStatusContainer(status_data["status"][0]) + self.player = RobloxStatusContainer(status_data["status"][1]) + self.creator = RobloxStatusContainer(status_data["status"][2]) + + + From f6d939e7740f68ed5f708262381dc506d6e6d4cf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 13:36:46 -0500 Subject: [PATCH 101/518] Economy! --- ro_py/economy.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ro_py/economy.py diff --git a/ro_py/economy.py b/ro_py/economy.py new file mode 100644 index 00000000..e7f69326 --- /dev/null +++ b/ro_py/economy.py @@ -0,0 +1,9 @@ +""" + +ro.py > economy.py + +This file houses functions and classes that pertain to the Roblox economy endpoints. + +""" + +endpoint = "https://economy.roblox.com/" From 8874103289ecc6e7de93c6e69454a6d501c54fdc Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 13:40:47 -0500 Subject: [PATCH 102/518] LimitedResaleData moved --- ro_py/assets.py | 13 +------------ ro_py/economy.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index a1923610..c2ff5ffc 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -9,24 +9,13 @@ from ro_py.users import User from ro_py.groups import Group from ro_py.utilities.errors import NotLimitedError +from ro_py.economy import LimitedResaleData from ro_py.utilities.asset_type import asset_types import iso8601 endpoint = "https://api.roblox.com/" -class LimitedResaleData: - """ - Represents the resale data of a limited item. - """ - def __init__(self, resale_data): - self.asset_stock = resale_data["assetStock"] - self.sales = resale_data["sales"] - self.number_remaining = resale_data["numberRemaining"] - self.recent_average_price = resale_data["recentAveragePrice"] - self.original_price = resale_data["originalPrice"] - - class Asset: """ Represents an asset. diff --git a/ro_py/economy.py b/ro_py/economy.py index e7f69326..8fe17dec 100644 --- a/ro_py/economy.py +++ b/ro_py/economy.py @@ -7,3 +7,23 @@ """ endpoint = "https://economy.roblox.com/" + + +class Currency: + """ + Represents currency data. + """ + def __init__(self, currency_data): + self.robux = currency_data["robux"] + + +class LimitedResaleData: + """ + Represents the resale data of a limited item. + """ + def __init__(self, resale_data): + self.asset_stock = resale_data["assetStock"] + self.sales = resale_data["sales"] + self.number_remaining = resale_data["numberRemaining"] + self.recent_average_price = resale_data["recentAveragePrice"] + self.original_price = resale_data["originalPrice"] From b8ad6db98fc727bf011502600ba36685b0336d85 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 15:01:19 -0500 Subject: [PATCH 103/518] Fixed headers + added special user agent --- ro_py/utilities/requests.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index fa921a58..7e74a7dc 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -5,12 +5,15 @@ class Requests: def __init__(self): - self.headers = {} self.session = requests.Session() + """ + Thank you @nsg for letting me know about this! + This allows us to access some extra content. + ▼▼▼ + """ + self.session.headers["User-Agent"] = "Roblox/WinInet" def get(self, *args, **kwargs): - kwargs["headers"] = self.headers - get_request = self.session.get(*args, **kwargs) try: @@ -29,13 +32,11 @@ def get(self, *args, **kwargs): raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") def post(self, *args, **kwargs): - kwargs["headers"] = self.headers - post_request = self.session.post(*args, **kwargs) if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: - self.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] post_request = self.session.post(*args, **kwargs) try: @@ -52,13 +53,11 @@ def post(self, *args, **kwargs): return post_request def patch(self, *args, **kwargs): - kwargs["headers"] = self.headers - patch_request = self.session.patch(*args, **kwargs) if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: - self.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] + self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] patch_request = self.session.patch(*args, **kwargs) patch_request_json = patch_request.json() @@ -75,4 +74,4 @@ def patch(self, *args, **kwargs): def update_xsrf(self, url="https://www.roblox.com/favorite/toggle"): xsrf_req = self.session.post(url) - self.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] + self.session.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] From 488338d250228e0792f61b28c5f1dfdcfbac9a67 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 15:44:04 -0500 Subject: [PATCH 104/518] X-CSRF is no longer updated on client init I removed this because it's updated on PATCH and PUT anyways, so it's not really useful. --- ro_py/client.py | 4 ---- ro_py/utilities/requests.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 015059ac..284e95b6 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -39,10 +39,6 @@ def __init__(self, token=None): self.user = None self.chat = None - logging.debug("Updating XSRF...") - self.requests.update_xsrf() - logging.debug("Done updating XSRF.") - def get_user(self, user_id): """ Gets a Roblox user. diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 7e74a7dc..f8382f78 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -71,7 +71,3 @@ def patch(self, *args, **kwargs): return patch_request raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") - - def update_xsrf(self, url="https://www.roblox.com/favorite/toggle"): - xsrf_req = self.session.post(url) - self.session.headers['X-CSRF-TOKEN'] = xsrf_req.headers["X-CSRF-TOKEN"] From 7577a1e2745a09131890f97629c1cbc642f8777d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 18:56:01 -0500 Subject: [PATCH 105/518] Fixed notifications --- .gitignore | 1 + ro_py/notifications.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4aed4069..cc34eb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ tests/ ro_py_old/ other/ build.bat +chat.py diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 65b72e7a..08204606 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -56,7 +56,7 @@ def __init__(self, requests, on_open, on_close, on_error, on_notification): self.on_error = on_error self.on_notification = on_notification - self.roblosecurity = self.requests.cookies[".ROBLOSECURITY"] + self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] self.negotiate_request = self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" From 9b7bd154a5045a24c947ac5220c10f784af39cca Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 19:26:16 -0500 Subject: [PATCH 106/518] Some notification events are no longer required. --- ro_py/notifications.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 08204606..532d96e6 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -105,9 +105,12 @@ def on_message(_self, raw_notification): "max_attempts": 5 }).build() - self.connection.on_open(self.on_open) - self.connection.on_close(self.on_close) - self.connection.on_error(self.on_error) + if self.on_open: + self.connection.on_open(self.on_open) + if self.on_close: + self.connection.on_close(self.on_close) + if self.on_error: + self.connection.on_error(self.on_error) self.connection.hub.on_message = on_message self.connection.start() From 2c7a39c21c32fe748d13d10ea31574933dad7015 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 20:19:28 -0500 Subject: [PATCH 107/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e4633fd..e0a98bce 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.5", + version="0.1.6", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From b43d9779986c231c84f941d408a3de1864c1378b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 22:19:42 -0500 Subject: [PATCH 108/518] Added ChatSettings object This should be the last change before making everything @property. --- ro_py/chat.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ro_py/chat.py b/ro_py/chat.py index beb72f4f..6d76c3d3 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -12,6 +12,12 @@ endpoint = "https://chat.roblox.com/" +class ChatSettings: + def __init__(self, settings_data): + self.enabled = settings_data["chatEnabled"] + self.is_active_chat_user = settings_data["isActiveChatUser"] + + class ConversationTyping: def __init__(self, requests, conversation_id): self.requests = requests From f41bca684476e8de6a5530f3b8e0593f6d403154 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 22:35:48 -0500 Subject: [PATCH 109/518] Read only class --- ro_py/client.py | 54 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 284e95b6..11a48167 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -17,27 +17,47 @@ class Client: Represents an authenticated Roblox client. """ def __init__(self, token=None): - self.token = token - self.requests = Requests() + self.__dict__["token"] = token + self.__dict__["requests"] = Requests() logging.debug("Initialized requests.") if token: logging.debug("Found token.") - self.requests.session.cookies[".ROBLOSECURITY"] = token + self.__dict__["requests"].session.cookies[".ROBLOSECURITY"] = token logging.debug("Initialized token.") - self.accountinformation = AccountInformation(self.requests) - self.accountsettings = AccountSettings(self.requests) + self.__dict__["accountinformation"] = AccountInformation(self.__dict__["requests"]) + self.__dict__["accountsettings"] = AccountSettings(self.__dict__["requests"]) logging.debug("Initialized AccountInformation and AccountSettings.") - auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") - self.user = User(self.requests, auth_user_req.json()["id"]) + auth_user_req = self.__dict__["requests"].get("https://users.roblox.com/v1/users/authenticated") + self.__dict__["user"] = User(self.__dict__["requests"], auth_user_req.json()["id"]) logging.debug("Initialized authenticated user.") - self.chat = ChatWrapper(self.requests) + self.__dict__["chat"] = ChatWrapper(self.__dict__["requests"]) logging.debug("Initialized chat wrapper.") else: - self.accountinformation = None - self.accountsettings = None - self.user = None - self.chat = None + self.__dict__["accountinformation"] = None + self.__dict__["accountsettings"] = None + self.__dict__["user"] = None + self.__dict__["chat"] = None + + @property + def requests(self): + return self.__dict__["requests"] + + @property + def accountinformation(self): + return self.__dict__["accountinformation"] + + @property + def accountsettings(self): + return self.__dict__["accountsettings"] + + @property + def user(self): + return self.__dict__["user"] + + @property + def chat(self): + return self.__dict__["chat"] def get_user(self, user_id): """ @@ -47,7 +67,7 @@ def get_user(self, user_id): try: cache["users"][str(user_id)] except KeyError: - cache["users"][str(user_id)] = User(self.requests, user_id) + cache["users"][str(user_id)] = User(self.__dict__["requests"], user_id) return cache["users"][str(user_id)] def get_group(self, group_id): @@ -58,7 +78,7 @@ def get_group(self, group_id): try: cache["groups"][str(group_id)] except KeyError: - cache["groups"][str(group_id)] = Group(self.requests, group_id) + cache["groups"][str(group_id)] = Group(self.__dict__["requests"], group_id) return cache["groups"][str(group_id)] def get_game(self, game_id): @@ -69,7 +89,7 @@ def get_game(self, game_id): try: cache["games"][str(game_id)] except KeyError: - cache["games"][str(game_id)] = Game(self.requests, game_id) + cache["games"][str(game_id)] = Game(self.__dict__["requests"], game_id) return cache["games"][str(game_id)] def get_asset(self, asset_id): @@ -80,7 +100,7 @@ def get_asset(self, asset_id): try: cache["assets"][str(asset_id)] except KeyError: - cache["assets"][str(asset_id)] = Asset(self.requests, asset_id) + cache["assets"][str(asset_id)] = Asset(self.__dict__["requests"], asset_id) return cache["assets"][str(asset_id)] def get_badge(self, badge_id): @@ -91,5 +111,5 @@ def get_badge(self, badge_id): try: cache["badges"][str(badge_id)] except KeyError: - cache["badges"][str(badge_id)] = Badge(self.requests, badge_id) + cache["badges"][str(badge_id)] = Badge(self.__dict__["requests"], badge_id) return cache["badges"][str(badge_id)] From 5a7703ffd1a2d08f61fecd994a1e543f9a5d3634 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 20 Dec 2020 22:38:50 -0500 Subject: [PATCH 110/518] Read only requests --- ro_py/accountinformation.py | 14 +++++++------- ro_py/accountsettings.py | 4 ++-- ro_py/assets.py | 12 ++++++------ ro_py/badges.py | 4 ++-- ro_py/chat.py | 32 ++++++++++++++++---------------- ro_py/games.py | 18 +++++++++--------- ro_py/groups.py | 8 ++++---- ro_py/notifications.py | 6 +++--- ro_py/robloxstatus.py | 4 ++-- ro_py/thumbnails.py | 8 ++++---- ro_py/users.py | 18 +++++++++--------- 11 files changed, 64 insertions(+), 64 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 2ef37507..fb230ad5 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -43,7 +43,7 @@ class AccountInformation: This is only available for authenticated clients as it cannot be accessed otherwise. """ def __init__(self, requests): - self.requests = requests + self.__dict__["requests"] = requests self.account_information_metadata = None self.promotion_channels = None self.update() @@ -53,9 +53,9 @@ def update(self): Updates the account information. :return: Nothing """ - account_information_req = self.requests.get("https://accountinformation.roblox.com/v1/metadata") + account_information_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/metadata") self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") + promotion_channels_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) def get_gender(self): @@ -63,7 +63,7 @@ def get_gender(self): Gets the user's gender. :return: RobloxGender """ - gender_req = self.requests.get(endpoint + "v1/gender") + gender_req = self.__dict__["requests"].get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) def set_gender(self, gender): @@ -72,7 +72,7 @@ def set_gender(self, gender): :param gender: RobloxGender :return: Nothing """ - self.requests.post( + self.__dict__["requests"].post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) @@ -84,7 +84,7 @@ def get_birthdate(self): Returns the user's birthdate. :return: datetime """ - birthdate_req = self.requests.get(endpoint + "v1/birthdate") + birthdate_req = self.__dict__["requests"].get(endpoint + "v1/birthdate") birthdate_raw = birthdate_req.json() birthdate = datetime( year=birthdate_raw["birthYear"], @@ -99,7 +99,7 @@ def set_birthdate(self, birthdate): :param birthdate: A datetime object. :return: Nothing """ - self.requests.post( + self.__dict__["requests"].post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index c6db261e..9c9e555d 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -47,7 +47,7 @@ class AccountSettings: This is only available for authenticated clients as it cannot be accessed otherwise. """ def __init__(self, requests): - self.requests = requests + self.__dict__["requests"] = requests def get_privacy_setting(self, privacy_setting): """ @@ -71,5 +71,5 @@ def get_privacy_setting(self, privacy_setting): "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.requests.get(privacy_endpoint) + privacy_req = self.__dict__["requests"].get(privacy_endpoint) return privacy_req.json()[privacy_key] diff --git a/ro_py/assets.py b/ro_py/assets.py index c2ff5ffc..1a3ef9bf 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -22,7 +22,7 @@ class Asset: """ def __init__(self, requests, asset_id): self.id = asset_id - self.requests = requests + self.__dict__["requests"] = requests self.target_id = None self.product_type = None self.asset_id = None @@ -48,7 +48,7 @@ def update(self): """ Updates the asset's information. """ - asset_info_req = self.requests.get( + asset_info_req = self.__dict__["requests"].get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.id @@ -64,9 +64,9 @@ def update(self): self.asset_type_id = asset_info["AssetTypeId"] self.asset_type_name = asset_types[self.asset_type_id] if asset_info["Creator"]["CreatorType"] == "User": - self.creator = User(self.requests, asset_info["Creator"]["Id"]) + self.creator = User(self.__dict__["requests"], asset_info["Creator"]["Id"]) elif asset_info["Creator"]["CreatorType"] == "Group": - self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"]) + self.creator = Group(self.__dict__["requests"], asset_info["Creator"]["CreatorTargetId"]) self.created = iso8601.parse_date(asset_info["Created"]) self.updated = iso8601.parse_date(asset_info["Updated"]) self.price = asset_info["PriceInRobux"] @@ -83,7 +83,7 @@ def get_remaining(self): Gets the remaining amount of this asset. (used for Limited U items) :returns: Amount remaining """ - asset_info_req = self.requests.get( + asset_info_req = self.__dict__["requests"].get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.asset_id @@ -98,7 +98,7 @@ def get_limited_resale_data(self): :returns: LimitedResaleData """ if self.is_limited: - resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") + resale_data_req = self.__dict__["requests"].get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) else: raise NotLimitedError("You can only read this information on limited items.") diff --git a/ro_py/badges.py b/ro_py/badges.py index c0059375..8bed74f0 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -25,7 +25,7 @@ class Badge: """ def __init__(self, requests, badge_id): self.id = badge_id - self.requests = requests + self.__dict__["requests"] = requests self.name = None self.description = None self.display_name = None @@ -35,7 +35,7 @@ def __init__(self, requests, badge_id): self.update() def update(self): - badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") + badge_info_req = self.__dict__["requests"].get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] diff --git a/ro_py/chat.py b/ro_py/chat.py index 6d76c3d3..ae62613d 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -20,11 +20,11 @@ def __init__(self, settings_data): class ConversationTyping: def __init__(self, requests, conversation_id): - self.requests = requests + self.__dict__["requests"] = requests self.id = conversation_id def __enter__(self): - self.requests.post( + self.__dict__["requests"].post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -33,7 +33,7 @@ def __enter__(self): ) def __exit__(self, *args, **kwargs): - self.requests.post( + self.__dict__["requests"].post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -44,7 +44,7 @@ def __exit__(self, *args, **kwargs): class Conversation: def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): - self.requests = requests + self.__dict__["requests"] = requests if raw: data = raw_data @@ -60,16 +60,16 @@ def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): data = conversation_req.json()[0] self.title = data["title"] - self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.initiator = User(self.__dict__["requests"], data["initiator"]["targetId"]) self.type = data["conversationType"] - self.typing = ConversationTyping(self.requests, conversation_id) + self.typing = ConversationTyping(self.__dict__["requests"], conversation_id) def get_message(self, message_id): - return Message(self.requests, message_id, self.id) + return Message(self.__dict__["requests"], message_id, self.id) def send_message(self, content): - send_message_req = self.requests.post( + send_message_req = self.__dict__["requests"].post( url=endpoint + "v2/send-message", data={ "message": content, @@ -78,14 +78,14 @@ def send_message(self, content): ) send_message_json = send_message_req.json() if send_message_json["sent"]: - return Message(self.requests, send_message_json["messageId"], self.id) + return Message(self.__dict__["requests"], send_message_json["messageId"], self.id) else: raise ChatError(send_message_json["statusMessage"]) class Message: def __init__(self, requests, message_id, conversation_id): - self.requests = requests + self.__dict__["requests"] = requests self.id = message_id self.conversation_id = conversation_id @@ -96,7 +96,7 @@ def __init__(self, requests, message_id, conversation_id): self.update() def update(self): - message_req = self.requests.get( + message_req = self.__dict__["requests"].get( url="https://chat.roblox.com/v2/get-messages", params={ "conversationId": self.conversation_id, @@ -107,19 +107,19 @@ def update(self): message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.requests, message_json["senderTargetId"]) + self.sender = User(self.__dict__["requests"], message_json["senderTargetId"]) self.read = message_json["read"] class ChatWrapper: def __init__(self, requests): - self.requests = requests + self.__dict__["requests"] = requests def get_conversation(self, conversation_id): - return Conversation(self.requests, conversation_id) + return Conversation(self.__dict__["requests"], conversation_id) def get_conversations(self, page_number=1, page_size=10): - conversations_req = self.requests.get( + conversations_req = self.__dict__["requests"].get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, @@ -130,7 +130,7 @@ def get_conversations(self, page_number=1, page_size=10): conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.requests, + requests=self.__dict__["requests"], raw=True, raw_data=conversation_raw )) diff --git a/ro_py/games.py b/ro_py/games.py index f4d1e4da..286b6403 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -29,7 +29,7 @@ class Game: """ def __init__(self, requests, universe_id): self.id = universe_id - self.requests = requests + self.__dict__["requests"] = requests self.name = None self.description = None self.creator = None @@ -45,7 +45,7 @@ def update(self): """ Updates the game's information. """ - game_info_req = self.requests.get( + game_info_req = self.__dict__["requests"].get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -56,9 +56,9 @@ def update(self): self.name = game_info["name"] self.description = game_info["description"] if game_info["creator"]["type"] == "User": - self.creator = User(self.requests, game_info["creator"]["id"]) + self.creator = User(self.__dict__["requests"], game_info["creator"]["id"]) elif game_info["creator"]["type"] == "Group": - self.creator = Group(self.requests, game_info["creator"]["id"]) + self.creator = Group(self.__dict__["requests"], game_info["creator"]["id"]) self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] @@ -70,7 +70,7 @@ def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = self.requests.get( + votes_info_req = self.__dict__["requests"].get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -86,7 +86,7 @@ def get_badges(self): Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances """ - badges_req = self.requests.get( + badges_req = self.__dict__["requests"].get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -96,7 +96,7 @@ def get_badges(self): badges_data = badges_req.json()["data"] badges = [] for badge in badges_data: - badges.append(Badge(self.requests, badge["id"])) + badges.append(Badge(self.__dict__["requests"], badge["id"])) return badges @@ -107,7 +107,7 @@ def place_id_to_universe_id(place_id): :param place_id: Place ID :return: Universe ID \""" - universe_id_req = self.requests.get( + universe_id_req = self.__dict__["requests"].get( url="https://api.roblox.com/universes/get-universe-containing-place", params={ "placeId": place_id @@ -123,5 +123,5 @@ def game_from_place_id(place_id): :param place_id: Place ID :return: Instace of Game \""" - return Game(self.requests, place_id_to_universe_id(place_id)) + return Game(self.__dict__["requests"], place_id_to_universe_id(place_id)) """ \ No newline at end of file diff --git a/ro_py/groups.py b/ro_py/groups.py index 291e3267..9ec9a642 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -17,7 +17,7 @@ class Group: Represents a group. """ def __init__(self, requests, group_id): - self.requests = requests + self.__dict__["requests"] = requests self.id = group_id self.name = None @@ -34,11 +34,11 @@ def update(self): """ Updates the group's information. """ - group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = self.__dict__["requests"].get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.requests, group_info["owner"]["userId"]) + self.owner = User(self.__dict__["requests"], group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] @@ -49,7 +49,7 @@ def update(self): # self.is_locked = group_info["isLocked"] def update_shout(self, message): - self.requests.patch( + self.__dict__["requests"].patch( url=f"https://groups.roblox.com/v1/groups/{self.id}/status", data={ "message": message diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 532d96e6..73e4a730 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -49,15 +49,15 @@ class NotificationReceiver: """ def __init__(self, requests, on_open, on_close, on_error, on_notification): - self.requests = requests + self.__dict__["requests"] = requests self.on_open = on_open self.on_close = on_close self.on_error = on_error self.on_notification = on_notification - self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.negotiate_request = self.requests.get( + self.roblosecurity = self.__dict__["requests"].session.cookies[".ROBLOSECURITY"] + self.negotiate_request = self.__dict__["requests"].get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", diff --git a/ro_py/robloxstatus.py b/ro_py/robloxstatus.py index 3d4780c3..369b6ad0 100644 --- a/ro_py/robloxstatus.py +++ b/ro_py/robloxstatus.py @@ -37,7 +37,7 @@ def __init__(self, overall_data): class RobloxStatus: def __init__(self, requests): - self.requests = requests + self.__dict__["requests"] = requests self.overall = None self.user = None @@ -47,7 +47,7 @@ def __init__(self, requests): self.update() def update(self): - status_req = self.requests.get( + status_req = self.__dict__["requests"].get( url=endpoint ) status_data = status_req.json()["result"] diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index ae035a45..53890621 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -53,7 +53,7 @@ class ThumbnailGenerator: This object is used to generate thumbnails. """ def __init__(self, requests): - self.requests = requests + self.__dict__["requests"] = requests def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ @@ -64,7 +64,7 @@ def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_ci :param is_circular: The circle thumbnail output parameter. :return: Image URL """ - group_icon_req = self.requests.get( + group_icon_req = self.__dict__["requests"].get( url=endpoint + "v1/groups/icons", params={ "groupIds": str(group.id), @@ -85,7 +85,7 @@ def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circ :param is_circular: The circle thumbnail output parameter. :return: Image URL """ - game_icon_req = self.requests.get( + game_icon_req = self.__dict__["requests"].get( url=endpoint + "v1/games/icons", params={ "universeIds": str(game.id), @@ -120,7 +120,7 @@ def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_forma shot_endpoint = shot_endpoint + "avatar-headshot" else: raise InvalidShotTypeError("Invalid shot type.") - shot_req = self.requests.get( + shot_req = self.__dict__["requests"].get( url=shot_endpoint, params={ "userIds": str(user.id), diff --git a/ro_py/users.py b/ro_py/users.py index 01a6881e..0e081f74 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -19,7 +19,7 @@ class User: """ def __init__(self, requests, ui): - self.requests = requests + self.__dict__["requests"] = requests self.id = ui self.description = None @@ -35,7 +35,7 @@ def update(self): Updates some class values. :return: Nothing """ - user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") + user_info_req = self.__dict__["requests"].get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -50,7 +50,7 @@ def get_status(self): Gets the user's status. :return: A string """ - status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") + status_req = self.__dict__["requests"].get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] def get_roblox_badges(self): @@ -58,7 +58,7 @@ def get_roblox_badges(self): Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = self.__dict__["requests"].get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) @@ -69,7 +69,7 @@ def get_friends_count(self): Gets the user's friends count. :return: An integer """ - friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count @@ -78,7 +78,7 @@ def get_followers_count(self): Gets the user's followers count. :return: An integer """ - followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count @@ -87,7 +87,7 @@ def get_followings_count(self): Gets the user's followings count. :return: An integer """ - followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -96,11 +96,11 @@ def get_friends(self): Gets the user's friends. :return: A list of User instances. """ - friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.requests, friend_raw["id"]) + User(self.__dict__["requests"], friend_raw["id"]) ) return friends_list From f5741406a7c738c60bfc8b2cf2d56c1a7414f408 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 21 Dec 2020 11:41:43 -0500 Subject: [PATCH 111/518] AccountInformation and AccountSettings are read-only I'm working on making everything that should be read-only. --- ro_py/accountinformation.py | 68 +++++++++++++++++++++++++++---------- ro_py/accountsettings.py | 14 ++++++-- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index fb230ad5..55839265 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -17,12 +17,13 @@ class AccountInformationMetadata: Represents account information metadata. """ def __init__(self, metadata_raw): - self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] - self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] - self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] - self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] - self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ + metadata_raw["isAllowedNotificationsEndpointDisabled"] + self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] + self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] + self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] + self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] + self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"] class PromotionChannels: @@ -30,11 +31,31 @@ class PromotionChannels: Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.facebook = promotion_raw["facebook"] - self.twitter = promotion_raw["twitter"] - self.youtube = promotion_raw["youtube"] - self.twitch = promotion_raw["twitch"] + self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] + self.__dict__["facebook"] = promotion_raw["facebook"] + self.__dict__["twitter"] = promotion_raw["twitter"] + self.__dict__["youtube"] = promotion_raw["youtube"] + self.__dict__["twitch"] = promotion_raw["twitch"] + + @property + def promotion_channels_visibility_privacy(self): + return self.__dict__["promotion_channels_visibility_privacy"] + + @property + def facebook(self): + return self.__dict__["facebook"] + + @property + def twitter(self): + return self.__dict__["twitter"] + + @property + def youtube(self): + return self.__dict__["youtube"] + + @property + def twitch(self): + return self.__dict__["twitch"] class AccountInformation: @@ -44,8 +65,8 @@ class AccountInformation: """ def __init__(self, requests): self.__dict__["requests"] = requests - self.account_information_metadata = None - self.promotion_channels = None + self.__dict__["account_information_metadata"] = None + self.__dict__["promotion_channels"] = None self.update() def update(self): @@ -53,10 +74,23 @@ def update(self): Updates the account information. :return: Nothing """ - account_information_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/metadata") - self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/promotion-channels") - self.promotion_channels = PromotionChannels(promotion_channels_req.json()) + account_information_req = self.__dict__["requests"].get( + url="https://accountinformation.roblox.com/v1/metadata" + ) + self.__dict__["account_information_metadata"] = AccountInformationMetadata(account_information_req.json()) + + promotion_channels_req = self.__dict__["requests"].get( + url="https://accountinformation.roblox.com/v1/promotion-channels" + ) + self.__dict__["promotion_channels"] = PromotionChannels(promotion_channels_req.json()) + + @property + def account_information_metadata(self): + return self.__dict__["account_information_metadata"] + + @property + def promotion_channels(self): + return self.__dict__["promotion_channels"] def get_gender(self): """ diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 9c9e555d..dd0071e0 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -16,7 +16,7 @@ class PrivacyLevel(enum.Enum): Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy. """ no_one = "NoOne" - friends = "Friends", + friends = "Friends" everyone = "AllUsers" @@ -37,8 +37,16 @@ class RobloxEmail: Represents an obfuscated version of the email you have set on your account. """ def __init__(self, email_data): - self.email_address = email_data["emailAddress"] - self.verified = email_data["verified"] + self.__dict__["email_address"] = email_data["emailAddress"] + self.__dict__["verified"] = email_data["verified"] + + @property + def email_address(self): + return self.__dict__["email_address"] + + @property + def verified(self): + return self.__dict__["verified"] class AccountSettings: From 1dbb7ba66d64c377f0aaff724b7ad6682011d660 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 21 Dec 2020 14:37:37 -0500 Subject: [PATCH 112/518] Fixed setup --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e0a98bce..1eabff1b 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ python_requires='>=3.6', install_requires=[ "iso8601", - "signalrcore", - "urllib" + "signalrcore" ] ) From 653ffcd6b60b909c984c5f55180762407293412f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 21 Dec 2020 14:37:58 -0500 Subject: [PATCH 113/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1eabff1b..dd8a5097 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.6", + version="0.1.6.5", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From f8db8254b8cbe7d4f34489ea2c8d27c065a7a869 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 21 Dec 2020 19:25:12 -0500 Subject: [PATCH 114/518] Incorrect license --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dd8a5097..4a1e927e 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: GPL License", "Operating System :: OS Independent", ], python_requires='>=3.6', From 9ac8a6d436b7ffaa87da95d3dc5ecbe6c14a673b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:41:19 -0500 Subject: [PATCH 115/518] Starting trade support --- ro_py/trades.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ro_py/trades.py diff --git a/ro_py/trades.py b/ro_py/trades.py new file mode 100644 index 00000000..e69de29b From bcb4bb5ac061dc290fa4a9e68539313117ee31a4 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:42:34 -0500 Subject: [PATCH 116/518] Trades docstring --- ro_py/trades.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index e69de29b..636802d9 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -0,0 +1,7 @@ +""" + +ro.py > trades.py + +This file houses functions and classes that pertain to Roblox trades. + +""" From 3767b57a5905944f450679075ae483d9a59f5440 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:43:49 -0500 Subject: [PATCH 117/518] Trades endpoint --- ro_py/trades.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index 636802d9..3acae736 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -5,3 +5,5 @@ This file houses functions and classes that pertain to Roblox trades. """ + +endpoint = "https://trades.roblox.com/" From ba15cfcfae3c49d4369cadddc34f87317dda3d86 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:45:44 -0500 Subject: [PATCH 118/518] trying to revert some things --- ro_py/accountinformation.py | 21 +++++++++++++++++---- ro_py/accountsettings.py | 4 ++-- ro_py/assets.py | 12 ++++++------ ro_py/badges.py | 4 ++-- ro_py/chat.py | 32 ++++++++++++++++---------------- ro_py/games.py | 18 +++++++++--------- ro_py/groups.py | 8 ++++---- ro_py/notifications.py | 6 +++--- ro_py/robloxstatus.py | 4 ++-- ro_py/thumbnails.py | 8 ++++---- ro_py/users.py | 18 +++++++++--------- 11 files changed, 74 insertions(+), 61 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 55839265..f2a817b4 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -64,9 +64,15 @@ class AccountInformation: This is only available for authenticated clients as it cannot be accessed otherwise. """ def __init__(self, requests): +<<<<<<< HEAD self.__dict__["requests"] = requests self.__dict__["account_information_metadata"] = None self.__dict__["promotion_channels"] = None +======= + self.requests = requests + self.account_information_metadata = None + self.promotion_channels = None +>>>>>>> parent of 5a7703f... Read only requests self.update() def update(self): @@ -74,6 +80,7 @@ def update(self): Updates the account information. :return: Nothing """ +<<<<<<< HEAD account_information_req = self.__dict__["requests"].get( url="https://accountinformation.roblox.com/v1/metadata" ) @@ -91,13 +98,19 @@ def account_information_metadata(self): @property def promotion_channels(self): return self.__dict__["promotion_channels"] +======= + account_information_req = self.requests.get("https://accountinformation.roblox.com/v1/metadata") + self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) + promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") + self.promotion_channels = PromotionChannels(promotion_channels_req.json()) +>>>>>>> parent of 5a7703f... Read only requests def get_gender(self): """ Gets the user's gender. :return: RobloxGender """ - gender_req = self.__dict__["requests"].get(endpoint + "v1/gender") + gender_req = self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) def set_gender(self, gender): @@ -106,7 +119,7 @@ def set_gender(self, gender): :param gender: RobloxGender :return: Nothing """ - self.__dict__["requests"].post( + self.requests.post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) @@ -118,7 +131,7 @@ def get_birthdate(self): Returns the user's birthdate. :return: datetime """ - birthdate_req = self.__dict__["requests"].get(endpoint + "v1/birthdate") + birthdate_req = self.requests.get(endpoint + "v1/birthdate") birthdate_raw = birthdate_req.json() birthdate = datetime( year=birthdate_raw["birthYear"], @@ -133,7 +146,7 @@ def set_birthdate(self, birthdate): :param birthdate: A datetime object. :return: Nothing """ - self.__dict__["requests"].post( + self.requests.post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index dd0071e0..3ab8a8a0 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -55,7 +55,7 @@ class AccountSettings: This is only available for authenticated clients as it cannot be accessed otherwise. """ def __init__(self, requests): - self.__dict__["requests"] = requests + self.requests = requests def get_privacy_setting(self, privacy_setting): """ @@ -79,5 +79,5 @@ def get_privacy_setting(self, privacy_setting): "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.__dict__["requests"].get(privacy_endpoint) + privacy_req = self.requests.get(privacy_endpoint) return privacy_req.json()[privacy_key] diff --git a/ro_py/assets.py b/ro_py/assets.py index 1a3ef9bf..c2ff5ffc 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -22,7 +22,7 @@ class Asset: """ def __init__(self, requests, asset_id): self.id = asset_id - self.__dict__["requests"] = requests + self.requests = requests self.target_id = None self.product_type = None self.asset_id = None @@ -48,7 +48,7 @@ def update(self): """ Updates the asset's information. """ - asset_info_req = self.__dict__["requests"].get( + asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.id @@ -64,9 +64,9 @@ def update(self): self.asset_type_id = asset_info["AssetTypeId"] self.asset_type_name = asset_types[self.asset_type_id] if asset_info["Creator"]["CreatorType"] == "User": - self.creator = User(self.__dict__["requests"], asset_info["Creator"]["Id"]) + self.creator = User(self.requests, asset_info["Creator"]["Id"]) elif asset_info["Creator"]["CreatorType"] == "Group": - self.creator = Group(self.__dict__["requests"], asset_info["Creator"]["CreatorTargetId"]) + self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"]) self.created = iso8601.parse_date(asset_info["Created"]) self.updated = iso8601.parse_date(asset_info["Updated"]) self.price = asset_info["PriceInRobux"] @@ -83,7 +83,7 @@ def get_remaining(self): Gets the remaining amount of this asset. (used for Limited U items) :returns: Amount remaining """ - asset_info_req = self.__dict__["requests"].get( + asset_info_req = self.requests.get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.asset_id @@ -98,7 +98,7 @@ def get_limited_resale_data(self): :returns: LimitedResaleData """ if self.is_limited: - resale_data_req = self.__dict__["requests"].get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") + resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) else: raise NotLimitedError("You can only read this information on limited items.") diff --git a/ro_py/badges.py b/ro_py/badges.py index 8bed74f0..c0059375 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -25,7 +25,7 @@ class Badge: """ def __init__(self, requests, badge_id): self.id = badge_id - self.__dict__["requests"] = requests + self.requests = requests self.name = None self.description = None self.display_name = None @@ -35,7 +35,7 @@ def __init__(self, requests, badge_id): self.update() def update(self): - badge_info_req = self.__dict__["requests"].get(endpoint + f"v1/badges/{self.id}") + badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] diff --git a/ro_py/chat.py b/ro_py/chat.py index ae62613d..6d76c3d3 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -20,11 +20,11 @@ def __init__(self, settings_data): class ConversationTyping: def __init__(self, requests, conversation_id): - self.__dict__["requests"] = requests + self.requests = requests self.id = conversation_id def __enter__(self): - self.__dict__["requests"].post( + self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -33,7 +33,7 @@ def __enter__(self): ) def __exit__(self, *args, **kwargs): - self.__dict__["requests"].post( + self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -44,7 +44,7 @@ def __exit__(self, *args, **kwargs): class Conversation: def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): - self.__dict__["requests"] = requests + self.requests = requests if raw: data = raw_data @@ -60,16 +60,16 @@ def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): data = conversation_req.json()[0] self.title = data["title"] - self.initiator = User(self.__dict__["requests"], data["initiator"]["targetId"]) + self.initiator = User(self.requests, data["initiator"]["targetId"]) self.type = data["conversationType"] - self.typing = ConversationTyping(self.__dict__["requests"], conversation_id) + self.typing = ConversationTyping(self.requests, conversation_id) def get_message(self, message_id): - return Message(self.__dict__["requests"], message_id, self.id) + return Message(self.requests, message_id, self.id) def send_message(self, content): - send_message_req = self.__dict__["requests"].post( + send_message_req = self.requests.post( url=endpoint + "v2/send-message", data={ "message": content, @@ -78,14 +78,14 @@ def send_message(self, content): ) send_message_json = send_message_req.json() if send_message_json["sent"]: - return Message(self.__dict__["requests"], send_message_json["messageId"], self.id) + return Message(self.requests, send_message_json["messageId"], self.id) else: raise ChatError(send_message_json["statusMessage"]) class Message: def __init__(self, requests, message_id, conversation_id): - self.__dict__["requests"] = requests + self.requests = requests self.id = message_id self.conversation_id = conversation_id @@ -96,7 +96,7 @@ def __init__(self, requests, message_id, conversation_id): self.update() def update(self): - message_req = self.__dict__["requests"].get( + message_req = self.requests.get( url="https://chat.roblox.com/v2/get-messages", params={ "conversationId": self.conversation_id, @@ -107,19 +107,19 @@ def update(self): message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.__dict__["requests"], message_json["senderTargetId"]) + self.sender = User(self.requests, message_json["senderTargetId"]) self.read = message_json["read"] class ChatWrapper: def __init__(self, requests): - self.__dict__["requests"] = requests + self.requests = requests def get_conversation(self, conversation_id): - return Conversation(self.__dict__["requests"], conversation_id) + return Conversation(self.requests, conversation_id) def get_conversations(self, page_number=1, page_size=10): - conversations_req = self.__dict__["requests"].get( + conversations_req = self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, @@ -130,7 +130,7 @@ def get_conversations(self, page_number=1, page_size=10): conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.__dict__["requests"], + requests=self.requests, raw=True, raw_data=conversation_raw )) diff --git a/ro_py/games.py b/ro_py/games.py index 286b6403..f4d1e4da 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -29,7 +29,7 @@ class Game: """ def __init__(self, requests, universe_id): self.id = universe_id - self.__dict__["requests"] = requests + self.requests = requests self.name = None self.description = None self.creator = None @@ -45,7 +45,7 @@ def update(self): """ Updates the game's information. """ - game_info_req = self.__dict__["requests"].get( + game_info_req = self.requests.get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -56,9 +56,9 @@ def update(self): self.name = game_info["name"] self.description = game_info["description"] if game_info["creator"]["type"] == "User": - self.creator = User(self.__dict__["requests"], game_info["creator"]["id"]) + self.creator = User(self.requests, game_info["creator"]["id"]) elif game_info["creator"]["type"] == "Group": - self.creator = Group(self.__dict__["requests"], game_info["creator"]["id"]) + self.creator = Group(self.requests, game_info["creator"]["id"]) self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] @@ -70,7 +70,7 @@ def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = self.__dict__["requests"].get( + votes_info_req = self.requests.get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -86,7 +86,7 @@ def get_badges(self): Note: this has a limit of 100 badges due to paging. This will be expanded soon. :return: A list of Badge instances """ - badges_req = self.__dict__["requests"].get( + badges_req = self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -96,7 +96,7 @@ def get_badges(self): badges_data = badges_req.json()["data"] badges = [] for badge in badges_data: - badges.append(Badge(self.__dict__["requests"], badge["id"])) + badges.append(Badge(self.requests, badge["id"])) return badges @@ -107,7 +107,7 @@ def place_id_to_universe_id(place_id): :param place_id: Place ID :return: Universe ID \""" - universe_id_req = self.__dict__["requests"].get( + universe_id_req = self.requests.get( url="https://api.roblox.com/universes/get-universe-containing-place", params={ "placeId": place_id @@ -123,5 +123,5 @@ def game_from_place_id(place_id): :param place_id: Place ID :return: Instace of Game \""" - return Game(self.__dict__["requests"], place_id_to_universe_id(place_id)) + return Game(self.requests, place_id_to_universe_id(place_id)) """ \ No newline at end of file diff --git a/ro_py/groups.py b/ro_py/groups.py index 9ec9a642..291e3267 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -17,7 +17,7 @@ class Group: Represents a group. """ def __init__(self, requests, group_id): - self.__dict__["requests"] = requests + self.requests = requests self.id = group_id self.name = None @@ -34,11 +34,11 @@ def update(self): """ Updates the group's information. """ - group_info_req = self.__dict__["requests"].get(endpoint + f"v1/groups/{self.id}") + group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.__dict__["requests"], group_info["owner"]["userId"]) + self.owner = User(self.requests, group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] @@ -49,7 +49,7 @@ def update(self): # self.is_locked = group_info["isLocked"] def update_shout(self, message): - self.__dict__["requests"].patch( + self.requests.patch( url=f"https://groups.roblox.com/v1/groups/{self.id}/status", data={ "message": message diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 73e4a730..532d96e6 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -49,15 +49,15 @@ class NotificationReceiver: """ def __init__(self, requests, on_open, on_close, on_error, on_notification): - self.__dict__["requests"] = requests + self.requests = requests self.on_open = on_open self.on_close = on_close self.on_error = on_error self.on_notification = on_notification - self.roblosecurity = self.__dict__["requests"].session.cookies[".ROBLOSECURITY"] - self.negotiate_request = self.__dict__["requests"].get( + self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] + self.negotiate_request = self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", diff --git a/ro_py/robloxstatus.py b/ro_py/robloxstatus.py index 369b6ad0..3d4780c3 100644 --- a/ro_py/robloxstatus.py +++ b/ro_py/robloxstatus.py @@ -37,7 +37,7 @@ def __init__(self, overall_data): class RobloxStatus: def __init__(self, requests): - self.__dict__["requests"] = requests + self.requests = requests self.overall = None self.user = None @@ -47,7 +47,7 @@ def __init__(self, requests): self.update() def update(self): - status_req = self.__dict__["requests"].get( + status_req = self.requests.get( url=endpoint ) status_data = status_req.json()["result"] diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 53890621..ae035a45 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -53,7 +53,7 @@ class ThumbnailGenerator: This object is used to generate thumbnails. """ def __init__(self, requests): - self.__dict__["requests"] = requests + self.requests = requests def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ @@ -64,7 +64,7 @@ def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_ci :param is_circular: The circle thumbnail output parameter. :return: Image URL """ - group_icon_req = self.__dict__["requests"].get( + group_icon_req = self.requests.get( url=endpoint + "v1/groups/icons", params={ "groupIds": str(group.id), @@ -85,7 +85,7 @@ def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circ :param is_circular: The circle thumbnail output parameter. :return: Image URL """ - game_icon_req = self.__dict__["requests"].get( + game_icon_req = self.requests.get( url=endpoint + "v1/games/icons", params={ "universeIds": str(game.id), @@ -120,7 +120,7 @@ def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_forma shot_endpoint = shot_endpoint + "avatar-headshot" else: raise InvalidShotTypeError("Invalid shot type.") - shot_req = self.__dict__["requests"].get( + shot_req = self.requests.get( url=shot_endpoint, params={ "userIds": str(user.id), diff --git a/ro_py/users.py b/ro_py/users.py index 0e081f74..01a6881e 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -19,7 +19,7 @@ class User: """ def __init__(self, requests, ui): - self.__dict__["requests"] = requests + self.requests = requests self.id = ui self.description = None @@ -35,7 +35,7 @@ def update(self): Updates some class values. :return: Nothing """ - user_info_req = self.__dict__["requests"].get(endpoint + f"v1/users/{self.id}") + user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -50,7 +50,7 @@ def get_status(self): Gets the user's status. :return: A string """ - status_req = self.__dict__["requests"].get(endpoint + f"v1/users/{self.id}/status") + status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] def get_roblox_badges(self): @@ -58,7 +58,7 @@ def get_roblox_badges(self): Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = self.__dict__["requests"].get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) @@ -69,7 +69,7 @@ def get_friends_count(self): Gets the user's friends count. :return: An integer """ - friends_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count @@ -78,7 +78,7 @@ def get_followers_count(self): Gets the user's followers count. :return: An integer """ - followers_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count @@ -87,7 +87,7 @@ def get_followings_count(self): Gets the user's followings count. :return: An integer """ - followings_count_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -96,11 +96,11 @@ def get_friends(self): Gets the user's friends. :return: A list of User instances. """ - friends_req = self.__dict__["requests"].get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.__dict__["requests"], friend_raw["id"]) + User(self.requests, friend_raw["id"]) ) return friends_list From 051bdd847fb90ba861bfaa701fbb8bf47a31487c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:46:09 -0500 Subject: [PATCH 119/518] trying to revert some things, again --- ro_py/accountinformation.py | 55 ++++++++++++++++--------------------- ro_py/accountsettings.py | 14 ++-------- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index f2a817b4..eee1c861 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -17,13 +17,12 @@ class AccountInformationMetadata: Represents account information metadata. """ def __init__(self, metadata_raw): - self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ - metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] - self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] - self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] - self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] - self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"] + self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] class PromotionChannels: @@ -31,31 +30,11 @@ class PromotionChannels: Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.__dict__["facebook"] = promotion_raw["facebook"] - self.__dict__["twitter"] = promotion_raw["twitter"] - self.__dict__["youtube"] = promotion_raw["youtube"] - self.__dict__["twitch"] = promotion_raw["twitch"] - - @property - def promotion_channels_visibility_privacy(self): - return self.__dict__["promotion_channels_visibility_privacy"] - - @property - def facebook(self): - return self.__dict__["facebook"] - - @property - def twitter(self): - return self.__dict__["twitter"] - - @property - def youtube(self): - return self.__dict__["youtube"] - - @property - def twitch(self): - return self.__dict__["twitch"] + self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + self.facebook = promotion_raw["facebook"] + self.twitter = promotion_raw["twitter"] + self.youtube = promotion_raw["youtube"] + self.twitch = promotion_raw["twitch"] class AccountInformation: @@ -66,6 +45,7 @@ class AccountInformation: def __init__(self, requests): <<<<<<< HEAD self.__dict__["requests"] = requests +<<<<<<< HEAD self.__dict__["account_information_metadata"] = None self.__dict__["promotion_channels"] = None ======= @@ -73,6 +53,10 @@ def __init__(self, requests): self.account_information_metadata = None self.promotion_channels = None >>>>>>> parent of 5a7703f... Read only requests +======= + self.account_information_metadata = None + self.promotion_channels = None +>>>>>>> parent of f574140... AccountInformation and AccountSettings are read-only self.update() def update(self): @@ -80,6 +64,7 @@ def update(self): Updates the account information. :return: Nothing """ +<<<<<<< HEAD <<<<<<< HEAD account_information_req = self.__dict__["requests"].get( url="https://accountinformation.roblox.com/v1/metadata" @@ -104,6 +89,12 @@ def promotion_channels(self): promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) >>>>>>> parent of 5a7703f... Read only requests +======= + account_information_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/metadata") + self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) + promotion_channels_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/promotion-channels") + self.promotion_channels = PromotionChannels(promotion_channels_req.json()) +>>>>>>> parent of f574140... AccountInformation and AccountSettings are read-only def get_gender(self): """ diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 3ab8a8a0..c6db261e 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -16,7 +16,7 @@ class PrivacyLevel(enum.Enum): Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy. """ no_one = "NoOne" - friends = "Friends" + friends = "Friends", everyone = "AllUsers" @@ -37,16 +37,8 @@ class RobloxEmail: Represents an obfuscated version of the email you have set on your account. """ def __init__(self, email_data): - self.__dict__["email_address"] = email_data["emailAddress"] - self.__dict__["verified"] = email_data["verified"] - - @property - def email_address(self): - return self.__dict__["email_address"] - - @property - def verified(self): - return self.__dict__["verified"] + self.email_address = email_data["emailAddress"] + self.verified = email_data["verified"] class AccountSettings: From 603e142046462e4ba3428284488d593c1d8c16ee Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:47:41 -0500 Subject: [PATCH 120/518] Revert "trying to revert some things, again" This reverts commit 051bdd847fb90ba861bfaa701fbb8bf47a31487c. --- ro_py/accountinformation.py | 55 +++++++++++++++++++++---------------- ro_py/accountsettings.py | 14 ++++++++-- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index eee1c861..f2a817b4 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -17,12 +17,13 @@ class AccountInformationMetadata: Represents account information metadata. """ def __init__(self, metadata_raw): - self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] - self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] - self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] - self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] - self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ + metadata_raw["isAllowedNotificationsEndpointDisabled"] + self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] + self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] + self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] + self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] + self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"] class PromotionChannels: @@ -30,11 +31,31 @@ class PromotionChannels: Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.facebook = promotion_raw["facebook"] - self.twitter = promotion_raw["twitter"] - self.youtube = promotion_raw["youtube"] - self.twitch = promotion_raw["twitch"] + self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] + self.__dict__["facebook"] = promotion_raw["facebook"] + self.__dict__["twitter"] = promotion_raw["twitter"] + self.__dict__["youtube"] = promotion_raw["youtube"] + self.__dict__["twitch"] = promotion_raw["twitch"] + + @property + def promotion_channels_visibility_privacy(self): + return self.__dict__["promotion_channels_visibility_privacy"] + + @property + def facebook(self): + return self.__dict__["facebook"] + + @property + def twitter(self): + return self.__dict__["twitter"] + + @property + def youtube(self): + return self.__dict__["youtube"] + + @property + def twitch(self): + return self.__dict__["twitch"] class AccountInformation: @@ -45,7 +66,6 @@ class AccountInformation: def __init__(self, requests): <<<<<<< HEAD self.__dict__["requests"] = requests -<<<<<<< HEAD self.__dict__["account_information_metadata"] = None self.__dict__["promotion_channels"] = None ======= @@ -53,10 +73,6 @@ def __init__(self, requests): self.account_information_metadata = None self.promotion_channels = None >>>>>>> parent of 5a7703f... Read only requests -======= - self.account_information_metadata = None - self.promotion_channels = None ->>>>>>> parent of f574140... AccountInformation and AccountSettings are read-only self.update() def update(self): @@ -64,7 +80,6 @@ def update(self): Updates the account information. :return: Nothing """ -<<<<<<< HEAD <<<<<<< HEAD account_information_req = self.__dict__["requests"].get( url="https://accountinformation.roblox.com/v1/metadata" @@ -89,12 +104,6 @@ def promotion_channels(self): promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) >>>>>>> parent of 5a7703f... Read only requests -======= - account_information_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/metadata") - self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.__dict__["requests"].get("https://accountinformation.roblox.com/v1/promotion-channels") - self.promotion_channels = PromotionChannels(promotion_channels_req.json()) ->>>>>>> parent of f574140... AccountInformation and AccountSettings are read-only def get_gender(self): """ diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index c6db261e..3ab8a8a0 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -16,7 +16,7 @@ class PrivacyLevel(enum.Enum): Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy. """ no_one = "NoOne" - friends = "Friends", + friends = "Friends" everyone = "AllUsers" @@ -37,8 +37,16 @@ class RobloxEmail: Represents an obfuscated version of the email you have set on your account. """ def __init__(self, email_data): - self.email_address = email_data["emailAddress"] - self.verified = email_data["verified"] + self.__dict__["email_address"] = email_data["emailAddress"] + self.__dict__["verified"] = email_data["verified"] + + @property + def email_address(self): + return self.__dict__["email_address"] + + @property + def verified(self): + return self.__dict__["verified"] class AccountSettings: From 2dae0426dfb28cbb031be6e060218566338de405 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:49:01 -0500 Subject: [PATCH 121/518] Trying to resolve really weird conflicts --- ro_py/accountinformation.py | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index f2a817b4..5b0b146a 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -64,15 +64,9 @@ class AccountInformation: This is only available for authenticated clients as it cannot be accessed otherwise. """ def __init__(self, requests): -<<<<<<< HEAD - self.__dict__["requests"] = requests - self.__dict__["account_information_metadata"] = None - self.__dict__["promotion_channels"] = None -======= self.requests = requests self.account_information_metadata = None self.promotion_channels = None ->>>>>>> parent of 5a7703f... Read only requests self.update() def update(self): @@ -80,30 +74,14 @@ def update(self): Updates the account information. :return: Nothing """ -<<<<<<< HEAD - account_information_req = self.__dict__["requests"].get( + account_information_req = self.requests.get( url="https://accountinformation.roblox.com/v1/metadata" ) - self.__dict__["account_information_metadata"] = AccountInformationMetadata(account_information_req.json()) - - promotion_channels_req = self.__dict__["requests"].get( + self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) + promotion_channels_req = self.requests.get( url="https://accountinformation.roblox.com/v1/promotion-channels" ) - self.__dict__["promotion_channels"] = PromotionChannels(promotion_channels_req.json()) - - @property - def account_information_metadata(self): - return self.__dict__["account_information_metadata"] - - @property - def promotion_channels(self): - return self.__dict__["promotion_channels"] -======= - account_information_req = self.requests.get("https://accountinformation.roblox.com/v1/metadata") - self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.requests.get("https://accountinformation.roblox.com/v1/promotion-channels") self.promotion_channels = PromotionChannels(promotion_channels_req.json()) ->>>>>>> parent of 5a7703f... Read only requests def get_gender(self): """ From 40891fbe4301cd48c0a510539fbd1319158ff476 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:49:56 -0500 Subject: [PATCH 122/518] Revert "Read only class" This reverts commit f41bca684476e8de6a5530f3b8e0593f6d403154. --- ro_py/client.py | 54 ++++++++++++++++--------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 11a48167..284e95b6 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -17,47 +17,27 @@ class Client: Represents an authenticated Roblox client. """ def __init__(self, token=None): - self.__dict__["token"] = token - self.__dict__["requests"] = Requests() + self.token = token + self.requests = Requests() logging.debug("Initialized requests.") if token: logging.debug("Found token.") - self.__dict__["requests"].session.cookies[".ROBLOSECURITY"] = token + self.requests.session.cookies[".ROBLOSECURITY"] = token logging.debug("Initialized token.") - self.__dict__["accountinformation"] = AccountInformation(self.__dict__["requests"]) - self.__dict__["accountsettings"] = AccountSettings(self.__dict__["requests"]) + self.accountinformation = AccountInformation(self.requests) + self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") - auth_user_req = self.__dict__["requests"].get("https://users.roblox.com/v1/users/authenticated") - self.__dict__["user"] = User(self.__dict__["requests"], auth_user_req.json()["id"]) + auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") + self.user = User(self.requests, auth_user_req.json()["id"]) logging.debug("Initialized authenticated user.") - self.__dict__["chat"] = ChatWrapper(self.__dict__["requests"]) + self.chat = ChatWrapper(self.requests) logging.debug("Initialized chat wrapper.") else: - self.__dict__["accountinformation"] = None - self.__dict__["accountsettings"] = None - self.__dict__["user"] = None - self.__dict__["chat"] = None - - @property - def requests(self): - return self.__dict__["requests"] - - @property - def accountinformation(self): - return self.__dict__["accountinformation"] - - @property - def accountsettings(self): - return self.__dict__["accountsettings"] - - @property - def user(self): - return self.__dict__["user"] - - @property - def chat(self): - return self.__dict__["chat"] + self.accountinformation = None + self.accountsettings = None + self.user = None + self.chat = None def get_user(self, user_id): """ @@ -67,7 +47,7 @@ def get_user(self, user_id): try: cache["users"][str(user_id)] except KeyError: - cache["users"][str(user_id)] = User(self.__dict__["requests"], user_id) + cache["users"][str(user_id)] = User(self.requests, user_id) return cache["users"][str(user_id)] def get_group(self, group_id): @@ -78,7 +58,7 @@ def get_group(self, group_id): try: cache["groups"][str(group_id)] except KeyError: - cache["groups"][str(group_id)] = Group(self.__dict__["requests"], group_id) + cache["groups"][str(group_id)] = Group(self.requests, group_id) return cache["groups"][str(group_id)] def get_game(self, game_id): @@ -89,7 +69,7 @@ def get_game(self, game_id): try: cache["games"][str(game_id)] except KeyError: - cache["games"][str(game_id)] = Game(self.__dict__["requests"], game_id) + cache["games"][str(game_id)] = Game(self.requests, game_id) return cache["games"][str(game_id)] def get_asset(self, asset_id): @@ -100,7 +80,7 @@ def get_asset(self, asset_id): try: cache["assets"][str(asset_id)] except KeyError: - cache["assets"][str(asset_id)] = Asset(self.__dict__["requests"], asset_id) + cache["assets"][str(asset_id)] = Asset(self.requests, asset_id) return cache["assets"][str(asset_id)] def get_badge(self, badge_id): @@ -111,5 +91,5 @@ def get_badge(self, badge_id): try: cache["badges"][str(badge_id)] except KeyError: - cache["badges"][str(badge_id)] = Badge(self.__dict__["requests"], badge_id) + cache["badges"][str(badge_id)] = Badge(self.requests, badge_id) return cache["badges"][str(badge_id)] From 5e6dd0178173d482cbef44f2fd6107e7d8c985db Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:50:38 -0500 Subject: [PATCH 123/518] Fixed token --- ro_py/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index 284e95b6..84215cd9 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -17,7 +17,6 @@ class Client: Represents an authenticated Roblox client. """ def __init__(self, token=None): - self.token = token self.requests = Requests() logging.debug("Initialized requests.") From 40ef4da29187061983e6377bbcd08678f15c7693 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:51:54 -0500 Subject: [PATCH 124/518] TradesMetadata --- ro_py/trades.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index 3acae736..85d3baf7 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -7,3 +7,11 @@ """ endpoint = "https://trades.roblox.com/" + + +class TradesMetadata: + def __init__(self, trades_metadata_data): + self.max_items_per_side = trades_metadata_data["maxItemsPerSide"] + self.min_value_ratio = trades_metadata_data["minValueRatio"] + self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"] + self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"] From fbaee0a77df65425eea5ce92b2b436688e12bfdd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 11:53:45 -0500 Subject: [PATCH 125/518] TradesWrapper --- ro_py/trades.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index 85d3baf7..be5ada08 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -15,3 +15,8 @@ def __init__(self, trades_metadata_data): self.min_value_ratio = trades_metadata_data["minValueRatio"] self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"] self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"] + + +class TradesWrapper: + def __init__(self, requests): + self.requests = requests From 1cea3cf4d6c7234b03c150678f787addbe5a0009 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 12:01:35 -0500 Subject: [PATCH 126/518] TradeStatusType and more --- ro_py/trades.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index be5ada08..2aac634f 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -6,10 +6,25 @@ """ +import enum + endpoint = "https://trades.roblox.com/" +class TradeStatusType(enum.Enum): + """ + Represents a trade status type. + """ + Inbound = "Inbound" + Outbound = "Outbound" + Completed = "Completed" + Inactive = "Inactive" + + class TradesMetadata: + """ + Represents trade system metadata at /v1/trades/metadata + """ def __init__(self, trades_metadata_data): self.max_items_per_side = trades_metadata_data["maxItemsPerSide"] self.min_value_ratio = trades_metadata_data["minValueRatio"] @@ -18,5 +33,12 @@ def __init__(self, trades_metadata_data): class TradesWrapper: + """ + Represents the Roblox trades page. + """ def __init__(self, requests): self.requests = requests + + def send_trade(self): + pass + From 42f96d1f296ea705afc3bd8ddf1008d65ee343aa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 12:23:16 -0500 Subject: [PATCH 127/518] Working on pages --- ro_py/utilities/pages.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ro_py/utilities/pages.py diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py new file mode 100644 index 00000000..4ccdb37c --- /dev/null +++ b/ro_py/utilities/pages.py @@ -0,0 +1,16 @@ +import enum + + +class SortOrder(enum.Enum): + Ascending = "Asc" + Descending = "Desc" + + +class PagedObject: + def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10): + self.requests = requests + self.url = url + self.page = 0 + + def _get_page(self): + From 96f2b2a0755551081e7984f98c32624317f90113 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 12:31:26 -0500 Subject: [PATCH 128/518] _get_page --- ro_py/utilities/pages.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 4ccdb37c..87da4836 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -7,10 +7,24 @@ class SortOrder(enum.Enum): class PagedObject: - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10): + def __init__(self, requests, url, extra_parameters=None, sort_order=SortOrder.Ascending, limit=10): + if extra_parameters is None: + extra_parameters = {} + + extra_parameters["sortOrder"] = sort_order.value + extra_parameters["limit"] = limit + + self.parameters = extra_parameters self.requests = requests self.url = url self.page = 0 - def _get_page(self): + def _get_page(self, cursor=None): + this_parameters = self.parameters + if cursor: + this_parameters["cursor"] = cursor + self.requests.get( + url=self.url, + params=this_parameters + ) From 099ba7710a0ce71e4457ea5f2b989177b091ee8d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 12:43:49 -0500 Subject: [PATCH 129/518] InvalidPageError --- ro_py/utilities/errors.py | 4 ++++ ro_py/utilities/pages.py | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ro_py/utilities/errors.py b/ro_py/utilities/errors.py index 09b1c485..91ad4d3a 100644 --- a/ro_py/utilities/errors.py +++ b/ro_py/utilities/errors.py @@ -29,3 +29,7 @@ class ApiError(Exception): class ChatError(Exception): """Called in chat when a chat action fails.""" + + +class InvalidPageError(Exception): + """Called when an invalid page is requested.""" diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 87da4836..e4d670ef 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -1,3 +1,4 @@ +from ro_py.utilities.errors import InvalidPageError import enum @@ -18,13 +19,27 @@ def __init__(self, requests, url, extra_parameters=None, sort_order=SortOrder.As self.requests = requests self.url = url self.page = 0 + self.data = self._get_page() def _get_page(self, cursor=None): this_parameters = self.parameters if cursor: this_parameters["cursor"] = cursor - - self.requests.get( + + page_req = self.requests.get( url=self.url, params=this_parameters ) + return page_req.json() + + def previous(self): + if self.data["previousPageCursor"]: + self.data = self._get_page(self.data["previousPageCursor"]) + else: + raise InvalidPageError + + def next(self): + if self.data["nextPageCursor"]: + self.data = self._get_page(self.data["nextPageCursor"]) + else: + raise InvalidPageError From 889e3183602b1dbc9021b04243fbfaf3e7dd6164 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:01:54 -0500 Subject: [PATCH 130/518] Pages are mostly ready! --- ro_py/utilities/pages.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index e4d670ef..76c617d1 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -7,7 +7,14 @@ class SortOrder(enum.Enum): Descending = "Desc" -class PagedObject: +class Page: + def __init__(self, data): + self.previous_page_cursor = data["previousPageCursor"] + self.next_page_cursor = data["nextPageCursor"] + self.data = data["data"] + + +class Pages: def __init__(self, requests, url, extra_parameters=None, sort_order=SortOrder.Ascending, limit=10): if extra_parameters is None: extra_parameters = {} @@ -19,6 +26,8 @@ def __init__(self, requests, url, extra_parameters=None, sort_order=SortOrder.As self.requests = requests self.url = url self.page = 0 + + print(self.parameters) self.data = self._get_page() def _get_page(self, cursor=None): @@ -30,16 +39,16 @@ def _get_page(self, cursor=None): url=self.url, params=this_parameters ) - return page_req.json() + return Page(page_req.json()) def previous(self): - if self.data["previousPageCursor"]: - self.data = self._get_page(self.data["previousPageCursor"]) + if self.data.previous_page_cursor: + self.data = self._get_page(self.data.previous_page_cursor) else: raise InvalidPageError def next(self): - if self.data["nextPageCursor"]: - self.data = self._get_page(self.data["nextPageCursor"]) + if self.data.next_page_cursor: + self.data = self._get_page(self.data.next_page_cursor) else: raise InvalidPageError From df9ebf7348ed6f2d57963ed75b11be96cc9e73e2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:15:24 -0500 Subject: [PATCH 131/518] TradesWrapper added to Client + more - Pages was modified a bit with a handler and more. - Trades was updated with the new Pages object. - TradesWrapper added to Client --- ro_py/client.py | 4 ++++ ro_py/trades.py | 10 ++++++++++ ro_py/utilities/pages.py | 16 ++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 84215cd9..b89e393b 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -4,6 +4,7 @@ from ro_py.assets import Asset from ro_py.badges import Badge from ro_py.chat import ChatWrapper +from ro_py.trades import TradesWrapper from ro_py.utilities.cache import cache from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation @@ -32,11 +33,14 @@ def __init__(self, token=None): logging.debug("Initialized authenticated user.") self.chat = ChatWrapper(self.requests) logging.debug("Initialized chat wrapper.") + self.trade = TradesWrapper(self.requests) + logging.debug("Initialized trade wrapper.") else: self.accountinformation = None self.accountsettings = None self.user = None self.chat = None + self.trade = None def get_user(self, user_id): """ diff --git a/ro_py/trades.py b/ro_py/trades.py index 2aac634f..4cf199f5 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -6,6 +6,7 @@ """ +from ro_py.utilities.pages import Pages, SortOrder import enum endpoint = "https://trades.roblox.com/" @@ -39,6 +40,15 @@ class TradesWrapper: def __init__(self, requests): self.requests = requests + def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10): + trades = Pages( + requests=self.requests, + url=endpoint + f"/v1/trades/{trade_status_type.value}", + sort_order=sort_order, + limit=limit + ) + return trades + def send_trade(self): pass diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 76c617d1..0ca48b2b 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -8,17 +8,22 @@ class SortOrder(enum.Enum): class Page: - def __init__(self, data): + def __init__(self, data, handler=None): self.previous_page_cursor = data["previousPageCursor"] self.next_page_cursor = data["nextPageCursor"] - self.data = data["data"] + if handler: + self.data = handler(data["data"]) + else: + self.data = data["data"] class Pages: - def __init__(self, requests, url, extra_parameters=None, sort_order=SortOrder.Ascending, limit=10): + def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None): if extra_parameters is None: extra_parameters = {} + self.handler = handler + extra_parameters["sortOrder"] = sort_order.value extra_parameters["limit"] = limit @@ -39,7 +44,10 @@ def _get_page(self, cursor=None): url=self.url, params=this_parameters ) - return Page(page_req.json()) + return Page( + data=page_req.json(), + handler=self.handler + ) def previous(self): if self.data.previous_page_cursor: From 66982d1f1ff8800ba74b7b97a7447832fbf3bafd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:16:37 -0500 Subject: [PATCH 132/518] Added no-auth warning --- ro_py/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index b89e393b..d49052f8 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -22,7 +22,6 @@ def __init__(self, token=None): logging.debug("Initialized requests.") if token: - logging.debug("Found token.") self.requests.session.cookies[".ROBLOSECURITY"] = token logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) @@ -36,6 +35,7 @@ def __init__(self, token=None): self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") else: + logging.warning("The active client is not authenticated, so some features will not be enabled.") self.accountinformation = None self.accountsettings = None self.user = None From 52fc0842fe79120922652f6e7ec37e64eb52a6e5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:27:17 -0500 Subject: [PATCH 133/518] Trade page handler + Trade object --- ro_py/trades.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ro_py/trades.py b/ro_py/trades.py index 4cf199f5..1aa9974f 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -7,11 +7,34 @@ """ from ro_py.utilities.pages import Pages, SortOrder +from ro_py.users import User +import iso8601 import enum endpoint = "https://trades.roblox.com/" +def trade_page_handler(this_page): + trades_out = [] + for raw_trade in this_page: + pass + return this_page + + +class Trade: + def __init__(self, requests, trade_id): + self.requests = requests + trade_req = self.requests.get( + url=endpoint + f"v1/trades/{trade_id}" + ) + trade_data = trade_req.json() + self.id = trade_data["id"] + self.user = User(self.requests, trade_data["user"]["id"]) + self.created = iso8601.parse_date(trade_data["created"]) + self.is_active = trade_data["isActive"] + self.status = trade_data["status"] + + class TradeStatusType(enum.Enum): """ Represents a trade status type. From f947ef8e59d616492d9ab9dfdce0d6403c700be3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:35:15 -0500 Subject: [PATCH 134/518] Trade page handler fixed --- ro_py/trades.py | 4 ++-- ro_py/utilities/pages.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 1aa9974f..4c66e4b8 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -14,10 +14,10 @@ endpoint = "https://trades.roblox.com/" -def trade_page_handler(this_page): +def trade_page_handler(requests, this_page): trades_out = [] for raw_trade in this_page: - pass + Trade(requests, raw_trade["id"]) return this_page diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 0ca48b2b..56b53999 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -8,11 +8,11 @@ class SortOrder(enum.Enum): class Page: - def __init__(self, data, handler=None): + def __init__(self, requests, data, handler=None): self.previous_page_cursor = data["previousPageCursor"] self.next_page_cursor = data["nextPageCursor"] if handler: - self.data = handler(data["data"]) + self.data = handler(requests, data["data"]) else: self.data = data["data"] @@ -45,6 +45,7 @@ def _get_page(self, cursor=None): params=this_parameters ) return Page( + requests=self.requests, data=page_req.json(), handler=self.handler ) From 8f5b477933bec0fd70102c41182b2b386f2455ad Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 13:46:07 -0500 Subject: [PATCH 135/518] Enabled wrapper --- ro_py/trades.py | 7 ++++--- ro_py/utilities/pages.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 4c66e4b8..25592f43 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -17,8 +17,8 @@ def trade_page_handler(requests, this_page): trades_out = [] for raw_trade in this_page: - Trade(requests, raw_trade["id"]) - return this_page + trades_out.append(Trade(requests, raw_trade["id"])) + return trades_out class Trade: @@ -68,7 +68,8 @@ def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.As requests=self.requests, url=endpoint + f"/v1/trades/{trade_status_type.value}", sort_order=sort_order, - limit=limit + limit=limit, + handler=trade_page_handler ) return trades diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 56b53999..6bcae584 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -32,7 +32,6 @@ def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extr self.url = url self.page = 0 - print(self.parameters) self.data = self._get_page() def _get_page(self, cursor=None): From 6700c738a12f67560b071167d41d89a773a04ada Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:24:02 -0500 Subject: [PATCH 136/518] Banner --- resources/banner.svg | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/banner.svg diff --git a/resources/banner.svg b/resources/banner.svg new file mode 100644 index 00000000..e69de29b From aad25667d4dc9725f43427c5b31871a851774d48 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:24:55 -0500 Subject: [PATCH 137/518] Update banner.svg --- resources/banner.svg | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/resources/banner.svg b/resources/banner.svg index e69de29b..14a39931 100644 --- a/resources/banner.svg +++ b/resources/banner.svg @@ -0,0 +1,9 @@ + + +
+ +

Test test test

+
+
+
From ca511b370bc2decccb5afcab9baf2e456aff9824 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:26:12 -0500 Subject: [PATCH 138/518] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50eef5fb..52c5df96 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +![Banner image for ro.py](https://raw.githubusercontent.com/jmk-developer/ro.py/main/resources/banner.svg) + # Welcome to ro.py ro.py is a Python wrapper for the Roblox web API. ## Installation @@ -27,4 +29,4 @@ Find more examples in the examples folder. ## Other Libraries https://github.com/RbxAPI/Pyblox -https://github.com/iranathan/robloxapi \ No newline at end of file +https://github.com/iranathan/robloxapi From 78b3e9a543ca9dc71e27f38919d8301090cb5604 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:27:58 -0500 Subject: [PATCH 139/518] Update banner.svg --- resources/banner.svg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/banner.svg b/resources/banner.svg index 14a39931..cbf2fdbd 100644 --- a/resources/banner.svg +++ b/resources/banner.svg @@ -2,6 +2,9 @@

From 118193857d33653356c995c85c58fed9031a2cbc Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:29:44 -0500 Subject: [PATCH 140/518] Update banner.svg --- resources/banner.svg | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/banner.svg b/resources/banner.svg index cbf2fdbd..f5a88fc9 100644 --- a/resources/banner.svg +++ b/resources/banner.svg @@ -2,9 +2,14 @@

Test test test

From 1442a8f76f08b3242393b382951684b70b8b00bb Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:30:00 -0500 Subject: [PATCH 141/518] Update banner.svg --- resources/banner.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/banner.svg b/resources/banner.svg index f5a88fc9..e138c031 100644 --- a/resources/banner.svg +++ b/resources/banner.svg @@ -8,7 +8,7 @@ html, body { } body { color: white; - background: transparent; + background: red; }

Test test test

From 261fa0c568605e1cddfb5c3563e8cc84d834cb18 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 17:32:56 -0500 Subject: [PATCH 142/518] Update banner.svg --- resources/banner.svg | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/resources/banner.svg b/resources/banner.svg index e138c031..8a62f183 100644 --- a/resources/banner.svg +++ b/resources/banner.svg @@ -1,12 +1,10 @@ -
+

Test test test

From b3fdbd357fcd8a8fa4c7b514a1238ee07eff4f34 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 18:19:31 -0500 Subject: [PATCH 148/518] Optional caching option. --- ro_py/client.py | 6 ++++-- ro_py/utilities/requests.py | 5 ++++- setup.py | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index d49052f8..6301c7e7 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -17,8 +17,10 @@ class Client: """ Represents an authenticated Roblox client. """ - def __init__(self, token=None): - self.requests = Requests() + def __init__(self, token=None, requests_cache=False): + self.requests = Requests( + cache=requests_cache + ) logging.debug("Initialized requests.") if token: diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index f8382f78..8db7ba2f 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -1,11 +1,14 @@ from ro_py.utilities.errors import ApiError from json.decoder import JSONDecodeError +from cachecontrol import CacheControl import requests class Requests: - def __init__(self): + def __init__(self, cache=True): self.session = requests.Session() + if cache: + self.session = CacheControl(self.session) """ Thank you @nsg for letting me know about this! This allows us to access some extra content. diff --git a/setup.py b/setup.py index 4a1e927e..6e406738 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ python_requires='>=3.6', install_requires=[ "iso8601", - "signalrcore" + "signalrcore", + "cachecontrol" ] ) From 3e77e0987738e4af014a6b9311991023ae7a02b3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 20:02:05 -0500 Subject: [PATCH 149/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6e406738..b5b81815 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.6.5", + version="0.1.7", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 6df818a7ca43e75b876aa95c2ea3b5aa56bd9612 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 22 Dec 2020 20:03:38 -0500 Subject: [PATCH 150/518] Fixed classifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b5b81815..9a19eb34 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", - "License :: OSI Approved :: GPL License", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], python_requires='>=3.6', From 8b05772ead40a2babaee5709f7c62990fbfe3c13 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 23 Dec 2020 20:24:36 -0500 Subject: [PATCH 151/518] Added dropshadow resource --- resources/dropshadow.png | Bin 0 -> 40041 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/dropshadow.png diff --git a/resources/dropshadow.png b/resources/dropshadow.png new file mode 100644 index 0000000000000000000000000000000000000000..456fdc2c8b60b154efa4c4392712d75f171d13a2 GIT binary patch literal 40041 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

pwedLdnD=rT$vlp<$6uZ$E z3$JyfBXecKMz3M7Z{+k8#4!uvAbB$q?XUts5bO^o>}g(DH96z`r=)OhIQjTo5~#ua zm250w_a|8{Yu_y=FxQZa_hSJ)xw7mVD;Q|Z!F!RtJ~5PCX$^Z3qMQ1df~IaVrM}B_ zlJgKrAD;S_7yd;AOBIHkZaIFtZmWL%IaN-5_vN}-%6V5!r<;#5gAGX++{G%j;r3h< z3!x-pe1;-EwkKdshYVh&E+y00%phwWOXgF=jlVnI!EU1AN0wK7;UDd$V5g(Tp^rN9 zF?l;fNw8&UKKE_7gY7lvX*{jRm2(&2qw541>YRDOX3&n|ZtSF5iwE#8*ZxmoR~pvT zm4k^+KzoXGx?W1=bYSg@3+3^e&4HszlO6o*w!4z zclI5P_y2bOnwwz-{@I#OLA5}PfoR!UxJzpCyLn>AzK4jT#rFScE@|=Yv~%xm-J-V~ zP`OJ%ww*k9?Czd2%lj+N*Z=j}Ig4PvrJEk*0xxQIVfhx9QfeCd1*kHy4p}==+e`tx zM%UQQ_VPpJXmTQQc9-cNv|sNo(mLoBO0-(0Xj-|~sYX%%;ChIob%}hgWd+}L*&Q=T7*{FRjJ5+qPuV)@xY)$|y;>ySrzz83F>DPs`SLD90%zx=UKv8)R8-_`_ zJ4qMOeYdDZ5`WLkZphOX6>Bc$&tQ}=qZ zMm5C}-?m?3>;|f3=f*mYlDdm@KjfwEYq_)IRnvn#o3kdar00kI)45N_yktD)jxl#v zvOsrX6)}3lDzT!|0lXjom<;Vx-TBN9giaqUMqYJc+uYb(d5Iclqj9KA4h3H}LyT=RcTF1yB)mFa;^tgoP-A=r0{X zAYv&BXTn`Ko$%5da{4D=dY%%LJ)ud3=U>AfsR*i*BkdSB z^djEyT-7lpQJ?1=rHk__;OPLuZaL6q!3M#1PA;P;%?$2r$l|lbN=~>8&_MVi0w487{&^B!hynOD&F#1prjTSPsgeK zpzut-bpZXhUuk}=wAQRza=(q{-LMY7$>h`mpE2oyE zsR(46xd8dVF~4zTmn|S%BM59X`Kmb+0}3LRKvpbRf9XS+bA|KEb^0umi^ zA~YkmZ&Px^o>bx%Bsz+nAcmkEU6^%|*!TdG+MNw5%h%wvpY4-@TWxI522h6VQntbt zaEHXxD0?3%n2u%&PUsHFv$aiC&isv<3bGaAXcY?7ySc@vpqAY}9-TqCt;jqQqii~>anxpBfDKmNNj8Q3H7ORFyy$A?+`EF+uDk&?GVWeqXTo`&Jy z4$pcB-lv)Zj;P#h>GMN>0%THnKD$Qe_rg;FZDHTYb@olSC4=>SQzCUe=0h_$g$<5q z3Jnfo+`DA$Swyb2tpb6ONJ~*~SE3Gj?DLzFzIhosgC3 zjN*0*!*gKBe|>zX-zLN(4Vxq;4|rWh74r457!WFk*vvm0YmO_X4i+4#6bCL;EO)r@ zGG{P8#R>YbLx2NpHs!b;?|$84-O}Vl$j;FvcFEGK0x5fmGW5g*=s=qTciWvp9K=U4 z@yYw^%8Vb5lepGZ+)V5X7f~vcE=s(O(!4z;q_z#PwgKA8IZ<6N5!OoL1uxX3e@o>sQhpR_eJZ=^4mdQ5 z(ey^5wl-8KZ3H6JUE`hZ04{ z=#r>r6;}tHe8K{g-1UoTX<-c0a=?5TTl8W&H~E-;aEvP_a6@6kR)=$^PKO_C$-Bw{ zSA;ogQi+#8v}wr>xpKI%H|*CMz0P57@B*H#Z``V^w!zb0{iOsvJ6r}VH^<#9aeF{Z z^12?S|C6@6il-3y%s}Ax-|b!cenWA@j`p&rKtDVeF*x?C70kw=O~H*NV+=rs0e;OM a==fK3>)Of7!aJ@)2z+?HL7WzEq2%8pg~USu literal 0 HcmV?d00001 From 53879b59866812ccb8d3cbdaccf9b328526e43b3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 13:21:12 -0500 Subject: [PATCH 152/518] Added catalog.py and AppStore The AppStore enum is used for grabbing app-store specific avatar items. --- ro_py/catalog.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 ro_py/catalog.py diff --git a/ro_py/catalog.py b/ro_py/catalog.py new file mode 100644 index 00000000..5ae84e69 --- /dev/null +++ b/ro_py/catalog.py @@ -0,0 +1,16 @@ +""" + +ro.py > catalog.py + +This file houses functions and classes that pertain to the Roblox catalog. + +""" + +import enum + + +class AppStore(enum.Enum): + google_play = "GooglePlay" + amazon = "Amazon" + ios = "iOS" + xbox = "Xbox" From 33659dffa6ebc2e025542fab909a5cc93ddba0fa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 13:51:13 -0500 Subject: [PATCH 153/518] Updated library docstrings --- ro_py/accountinformation.py | 2 -- ro_py/accountsettings.py | 2 -- ro_py/assets.py | 2 -- ro_py/badges.py | 2 -- ro_py/catalog.py | 2 -- ro_py/chat.py | 2 -- ro_py/client.py | 6 ++++++ ro_py/economy.py | 2 -- ro_py/games.py | 2 -- ro_py/gender.py | 3 +-- ro_py/groups.py | 6 ++++++ ro_py/notifications.py | 11 +++++++++++ ro_py/robloxbadges.py | 4 +--- ro_py/robloxstatus.py | 7 ++----- ro_py/thumbnails.py | 3 +-- ro_py/trades.py | 4 +--- ro_py/users.py | 2 -- 17 files changed, 29 insertions(+), 33 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 5b0b146a..79f7dbe6 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -1,7 +1,5 @@ """ -ro.py > accountinformation.py - This file houses functions and classes that pertain to Roblox authenticated user account information. """ diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 3ab8a8a0..196694c6 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -1,7 +1,5 @@ """ -ro.py > accountsettings.py - This file houses functions and classes that pertain to Roblox client . """ diff --git a/ro_py/assets.py b/ro_py/assets.py index c2ff5ffc..e2df4f1f 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -1,7 +1,5 @@ """ -ro.py > assets.py - This file houses functions and classes that pertain to Roblox assets. """ diff --git a/ro_py/badges.py b/ro_py/badges.py index c0059375..bfec4a1c 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -1,7 +1,5 @@ """ -ro.py > badges.py - This file houses functions and classes that pertain to game-awarded badges. """ diff --git a/ro_py/catalog.py b/ro_py/catalog.py index 5ae84e69..ad087128 100644 --- a/ro_py/catalog.py +++ b/ro_py/catalog.py @@ -1,7 +1,5 @@ """ -ro.py > catalog.py - This file houses functions and classes that pertain to the Roblox catalog. """ diff --git a/ro_py/chat.py b/ro_py/chat.py index 6d76c3d3..507fc64f 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -1,7 +1,5 @@ """ -ro.py > chat.py - This file houses functions and classes that pertain to chatting and messaging. """ diff --git a/ro_py/client.py b/ro_py/client.py index 6301c7e7..50f841d9 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -1,3 +1,9 @@ +""" + +This file houses functions and classes that represent the core Roblox web client. + +""" + from ro_py.users import User from ro_py.games import Game from ro_py.groups import Group diff --git a/ro_py/economy.py b/ro_py/economy.py index 8fe17dec..35506e03 100644 --- a/ro_py/economy.py +++ b/ro_py/economy.py @@ -1,7 +1,5 @@ """ -ro.py > economy.py - This file houses functions and classes that pertain to the Roblox economy endpoints. """ diff --git a/ro_py/games.py b/ro_py/games.py index f4d1e4da..80a35a92 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -1,7 +1,5 @@ """ -ro.py > games.py - This file houses functions and classes that pertain to Roblox universes and places. """ diff --git a/ro_py/gender.py b/ro_py/gender.py index 0e7d53aa..e1e2a70d 100644 --- a/ro_py/gender.py +++ b/ro_py/gender.py @@ -1,8 +1,7 @@ """ -ro.py > gender.py - I hate how Roblox stores gender at all, it's really strange as it's not used for anything. +There's literally no point in storing this information. """ diff --git a/ro_py/groups.py b/ro_py/groups.py index 291e3267..8d3f925e 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -1,3 +1,9 @@ +""" + +This file houses functions and classes that pertain to Roblox groups. + +""" + from ro_py.users import User endpoint = "https://groups.roblox.com/" diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 532d96e6..4f17c972 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -1,3 +1,14 @@ +""" + +This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger +notification menu on the Roblox web client. + +.. warning:: + This part of ro.py may have bugs and I don't recommend relying on it for daily use. + Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond + to Roblox chat messages, which is pretty neat. +""" + from ro_py.utilities.caseconvert import to_snake_case from signalrcore.hub_connection_builder import HubConnectionBuilder diff --git a/ro_py/robloxbadges.py b/ro_py/robloxbadges.py index 662480a1..2f0bff86 100644 --- a/ro_py/robloxbadges.py +++ b/ro_py/robloxbadges.py @@ -1,8 +1,6 @@ """ -ro.py > robloxbadges.py - -This file houses functions and classes that pertain to roblox-awarded badges. +This file houses functions and classes that pertain to Roblox-awarded badges. """ diff --git a/ro_py/robloxstatus.py b/ro_py/robloxstatus.py index 3d4780c3..97bd9fc8 100644 --- a/ro_py/robloxstatus.py +++ b/ro_py/robloxstatus.py @@ -1,8 +1,8 @@ """ -ro.py > robloxstatus.py - This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) +I don't know if this is really that useful, but I was able to find the status API endpoint by looking in the status +page source and some of the status.io documentation. """ @@ -56,6 +56,3 @@ def update(self): self.user = RobloxStatusContainer(status_data["status"][0]) self.player = RobloxStatusContainer(status_data["status"][1]) self.creator = RobloxStatusContainer(status_data["status"][2]) - - - diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index ae035a45..0aca9b30 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -1,7 +1,5 @@ """ -ro.py > thumbnails.py - This file houses functions and classes that pertain to Roblox icons and thumbnails. """ @@ -10,6 +8,7 @@ endpoint = "https://thumbnails.roblox.com/" +# TODO: turn these into enums PlaceHolder = "PlaceHolder" AutoGenerated = "AutoGenerated" ForceAutoGenerated = "ForceAutoGenerated" diff --git a/ro_py/trades.py b/ro_py/trades.py index 25592f43..3de6f59a 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -1,8 +1,6 @@ """ -ro.py > trades.py - -This file houses functions and classes that pertain to Roblox trades. +This file houses functions and classes that pertain to Roblox trades and trading. """ diff --git a/ro_py/users.py b/ro_py/users.py index 01a6881e..d7483df0 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -1,7 +1,5 @@ """ -ro.py > users.py - This file houses functions and classes that pertain to Roblox users and profiles. """ From 019773c333fffbcf248853e09d53b348503ece20 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 15:22:24 -0500 Subject: [PATCH 154/518] Added docs for examples and utilities. --- ro_py/examples/__init__.py | 5 +++++ ro_py/utilities/__init__.py | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 ro_py/examples/__init__.py create mode 100644 ro_py/utilities/__init__.py diff --git a/ro_py/examples/__init__.py b/ro_py/examples/__init__.py new file mode 100644 index 00000000..12790069 --- /dev/null +++ b/ro_py/examples/__init__.py @@ -0,0 +1,5 @@ +""" + +This folder houses ro.py examples. + +""" \ No newline at end of file diff --git a/ro_py/utilities/__init__.py b/ro_py/utilities/__init__.py new file mode 100644 index 00000000..79c8c475 --- /dev/null +++ b/ro_py/utilities/__init__.py @@ -0,0 +1,5 @@ +""" + +This folder houses utilities that are used internally for ro.py. + +""" \ No newline at end of file From 65345d87e044b5a778b10b3cca0d7c7237b09269 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 15:50:24 -0500 Subject: [PATCH 155/518] Updated main doc --- ro_py/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 768a3b26..8d1ee992 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -1,13 +1,11 @@ -r""" - - _ __ ___ _ __ _ _ - | '__/ _ \ | '_ \| | | | - | | | (_) || |_) | |_| | - |_| \___(_) .__/ \__, | - | | __/ | - |_| |___/ +""" ro.py by jmkdev +Welcome to ro.py! +ro.py is a powerful wrapper for the Roblox web API. +It can be used to create (almost) anything from chat bots to group management systems. +ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at + """ \ No newline at end of file From 02df60eb61ce887545d738b092ce94c7334b02dc Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 17:19:55 -0500 Subject: [PATCH 156/518] Updated docstrings --- ro_py/chat.py | 6 ++++++ ro_py/client.py | 17 +++++++---------- ro_py/games.py | 4 ++-- ro_py/utilities/pages.py | 38 ++++++++++++++++++++++++++++++++++--- ro_py/utilities/requests.py | 12 ++++++++++++ 5 files changed, 62 insertions(+), 15 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 507fc64f..ff921243 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -114,9 +114,15 @@ def __init__(self, requests): self.requests = requests def get_conversation(self, conversation_id): + """ + Gets a conversation by the conversation ID. + """ return Conversation(self.requests, conversation_id) def get_conversations(self, page_number=1, page_size=10): + """ + Gets the list of conversations. This will be updated soon to use the new Pages object. + """ conversations_req = self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ diff --git a/ro_py/client.py b/ro_py/client.py index 50f841d9..ab3929aa 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -29,6 +29,13 @@ def __init__(self, token=None, requests_cache=False): ) logging.debug("Initialized requests.") + + self.accountinformation = None + self.accountsettings = None + self.user = None + self.chat = None + self.trade = None + if token: self.requests.session.cookies[".ROBLOSECURITY"] = token logging.debug("Initialized token.") @@ -44,16 +51,10 @@ def __init__(self, token=None, requests_cache=False): logging.debug("Initialized trade wrapper.") else: logging.warning("The active client is not authenticated, so some features will not be enabled.") - self.accountinformation = None - self.accountsettings = None - self.user = None - self.chat = None - self.trade = None def get_user(self, user_id): """ Gets a Roblox user. - :returns: Instance of User """ try: cache["users"][str(user_id)] @@ -64,7 +65,6 @@ def get_user(self, user_id): def get_group(self, group_id): """ Gets a Roblox group. - :returns: Instance of Group """ try: cache["groups"][str(group_id)] @@ -75,7 +75,6 @@ def get_group(self, group_id): def get_game(self, game_id): """ Gets a Roblox game. - :returns: Instance of Game """ try: cache["games"][str(game_id)] @@ -86,7 +85,6 @@ def get_game(self, game_id): def get_asset(self, asset_id): """ Gets a Roblox asset. - :returns: Instance of Asset """ try: cache["assets"][str(asset_id)] @@ -97,7 +95,6 @@ def get_asset(self, asset_id): def get_badge(self, badge_id): """ Gets a Roblox badge. - :returns: Instance of Badge """ try: cache["badges"][str(badge_id)] diff --git a/ro_py/games.py b/ro_py/games.py index 80a35a92..34f50cf2 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -81,8 +81,8 @@ def get_votes(self): def get_badges(self): """ - Note: this has a limit of 100 badges due to paging. This will be expanded soon. - :return: A list of Badge instances + Gets the game's badges. + This will be updated soon to use the new Page object. """ badges_req = self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 6bcae584..12379c58 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -3,38 +3,64 @@ class SortOrder(enum.Enum): + """ + Order in which page data should load in. + """ Ascending = "Asc" Descending = "Desc" class Page: + """ + Represents a single page from a Pages object. + """ def __init__(self, requests, data, handler=None): self.previous_page_cursor = data["previousPageCursor"] + """Cursor to navigate to the previous page.""" self.next_page_cursor = data["nextPageCursor"] + """Cursor to navigate to the next page.""" + + self.data = data["data"] + """Raw data from this page.""" + if handler: - self.data = handler(requests, data["data"]) - else: - self.data = data["data"] + self.data = handler(requests, self.data) class Pages: + """ + Represents a paged object. + + !!! warning + This object is *slow*, especially with a custom handler. + Automatic page caching will be added in the future. It is suggested to + cache the pages yourself if speed is required. + """ def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None): if extra_parameters is None: extra_parameters = {} self.handler = handler + """Function that is passed to Page as data handler.""" extra_parameters["sortOrder"] = sort_order.value extra_parameters["limit"] = limit self.parameters = extra_parameters + """Extra parameters for the request.""" self.requests = requests + """Requests object.""" self.url = url + """URL containing the paginated data, accessible with a GET request.""" self.page = 0 + """Current page number.""" self.data = self._get_page() def _get_page(self, cursor=None): + """ + Gets a page at the specified cursor position. + """ this_parameters = self.parameters if cursor: this_parameters["cursor"] = cursor @@ -50,12 +76,18 @@ def _get_page(self, cursor=None): ) def previous(self): + """ + Moves to the previous page. + """ if self.data.previous_page_cursor: self.data = self._get_page(self.data.previous_page_cursor) else: raise InvalidPageError def next(self): + """ + Moves to the next page. + """ if self.data.next_page_cursor: self.data = self._get_page(self.data.next_page_cursor) else: diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 8db7ba2f..28835a24 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -17,6 +17,10 @@ def __init__(self, cache=True): self.session.headers["User-Agent"] = "Roblox/WinInet" def get(self, *args, **kwargs): + """ + Essentially identical to requests.Session.get. + """ + get_request = self.session.get(*args, **kwargs) try: @@ -35,6 +39,10 @@ def get(self, *args, **kwargs): raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") def post(self, *args, **kwargs): + """ + Essentially identical to requests.Session.post. + """ + post_request = self.session.post(*args, **kwargs) if post_request.status_code == 403: @@ -56,6 +64,10 @@ def post(self, *args, **kwargs): return post_request def patch(self, *args, **kwargs): + """ + Essentially identical to requests.Session.patch. + """ + patch_request = self.session.patch(*args, **kwargs) if patch_request.status_code == 403: From 7aa90d03f56501dd9a56219f04a7964d33b3393c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 17:30:52 -0500 Subject: [PATCH 157/518] Client parameters + init variable docs --- ro_py/client.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index ab3929aa..86e2d0bb 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -22,6 +22,13 @@ class Client: """ Represents an authenticated Roblox client. + + Parameters + ---------- + token : str + Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser. + requests_cache: bool + Toggle for cached requests using CacheControl. """ def __init__(self, token=None, requests_cache=False): self.requests = Requests( @@ -31,10 +38,15 @@ def __init__(self, token=None, requests_cache=False): logging.debug("Initialized requests.") self.accountinformation = None + """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None + """AccountSettings object. Only available for authenticated clients.""" self.user = None + """User object. Only available for authenticated clients.""" self.chat = None + """ChatWrapper object. Only available for authenticated clients.""" self.trade = None + """TradesWrapper object. Only available for authenticated clients.""" if token: self.requests.session.cookies[".ROBLOSECURITY"] = token From 6cf1c61ec61aa88bf837ed116b9e4a21af2396fa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 18:20:04 -0500 Subject: [PATCH 158/518] Moved Examples out of ro.py module --- {ro_py/examples => examples}/user.py | 0 ro_py/examples/__init__.py | 5 ----- 2 files changed, 5 deletions(-) rename {ro_py/examples => examples}/user.py (100%) delete mode 100644 ro_py/examples/__init__.py diff --git a/ro_py/examples/user.py b/examples/user.py similarity index 100% rename from ro_py/examples/user.py rename to examples/user.py diff --git a/ro_py/examples/__init__.py b/ro_py/examples/__init__.py deleted file mode 100644 index 12790069..00000000 --- a/ro_py/examples/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - -This folder houses ro.py examples. - -""" \ No newline at end of file From 1b6f2e074837333c9236763c41d04f33e73b5a79 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 19:38:39 -0500 Subject: [PATCH 159/518] Documentation added (using pdoc3) --- docs/ro_py/accountinformation.html | 561 ++++++++++++++++++++++++++ docs/ro_py/accountsettings.html | 407 +++++++++++++++++++ docs/ro_py/assets.html | 368 +++++++++++++++++ docs/ro_py/badges.html | 212 ++++++++++ docs/ro_py/catalog.html | 129 ++++++ docs/ro_py/chat.html | 532 ++++++++++++++++++++++++ docs/ro_py/client.html | 435 ++++++++++++++++++++ docs/ro_py/economy.html | 139 +++++++ docs/ro_py/games.html | 402 ++++++++++++++++++ docs/ro_py/gender.html | 131 ++++++ docs/ro_py/groups.html | 261 ++++++++++++ docs/ro_py/index.html | 171 ++++++++ docs/ro_py/notifications.html | 372 +++++++++++++++++ docs/ro_py/robloxbadges.html | 112 +++++ docs/ro_py/robloxstatus.html | 242 +++++++++++ docs/ro_py/thumbnails.html | 424 +++++++++++++++++++ docs/ro_py/trades.html | 342 ++++++++++++++++ docs/ro_py/users.html | 440 ++++++++++++++++++++ docs/ro_py/utilities/asset_type.html | 116 ++++++ docs/ro_py/utilities/cache.html | 65 +++ docs/ro_py/utilities/caseconvert.html | 86 ++++ docs/ro_py/utilities/errors.html | 238 +++++++++++ docs/ro_py/utilities/index.html | 101 +++++ docs/ro_py/utilities/pages.html | 408 +++++++++++++++++++ docs/ro_py/utilities/requests.html | 359 ++++++++++++++++ 25 files changed, 7053 insertions(+) create mode 100644 docs/ro_py/accountinformation.html create mode 100644 docs/ro_py/accountsettings.html create mode 100644 docs/ro_py/assets.html create mode 100644 docs/ro_py/badges.html create mode 100644 docs/ro_py/catalog.html create mode 100644 docs/ro_py/chat.html create mode 100644 docs/ro_py/client.html create mode 100644 docs/ro_py/economy.html create mode 100644 docs/ro_py/games.html create mode 100644 docs/ro_py/gender.html create mode 100644 docs/ro_py/groups.html create mode 100644 docs/ro_py/index.html create mode 100644 docs/ro_py/notifications.html create mode 100644 docs/ro_py/robloxbadges.html create mode 100644 docs/ro_py/robloxstatus.html create mode 100644 docs/ro_py/thumbnails.html create mode 100644 docs/ro_py/trades.html create mode 100644 docs/ro_py/users.html create mode 100644 docs/ro_py/utilities/asset_type.html create mode 100644 docs/ro_py/utilities/cache.html create mode 100644 docs/ro_py/utilities/caseconvert.html create mode 100644 docs/ro_py/utilities/errors.html create mode 100644 docs/ro_py/utilities/index.html create mode 100644 docs/ro_py/utilities/pages.html create mode 100644 docs/ro_py/utilities/requests.html diff --git a/docs/ro_py/accountinformation.html b/docs/ro_py/accountinformation.html new file mode 100644 index 00000000..d5312db1 --- /dev/null +++ b/docs/ro_py/accountinformation.html @@ -0,0 +1,561 @@ + + + + + + +ro_py.accountinformation API documentation + + + + + + + + + + + +

+
+
+

Module ro_py.accountinformation

+
+
+

This file houses functions and classes that pertain to Roblox authenticated user account information.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox authenticated user account information.
+
+"""
+
+from datetime import datetime
+from ro_py.gender import RobloxGender
+
+endpoint = "https://accountinformation.roblox.com/"
+
+
+class AccountInformationMetadata:
+    """
+    Represents account information metadata.
+    """
+    def __init__(self, metadata_raw):
+        self.__dict__["is_allowed_notifications_endpoint_disabled"] = \
+            metadata_raw["isAllowedNotificationsEndpointDisabled"]
+        self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"]
+        self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"]
+        self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"]
+        self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"]
+        self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"]
+
+
+class PromotionChannels:
+    """
+    Represents account information promotion channels.
+    """
+    def __init__(self, promotion_raw):
+        self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"]
+        self.__dict__["facebook"] = promotion_raw["facebook"]
+        self.__dict__["twitter"] = promotion_raw["twitter"]
+        self.__dict__["youtube"] = promotion_raw["youtube"]
+        self.__dict__["twitch"] = promotion_raw["twitch"]
+
+    @property
+    def promotion_channels_visibility_privacy(self):
+        return self.__dict__["promotion_channels_visibility_privacy"]
+
+    @property
+    def facebook(self):
+        return self.__dict__["facebook"]
+
+    @property
+    def twitter(self):
+        return self.__dict__["twitter"]
+
+    @property
+    def youtube(self):
+        return self.__dict__["youtube"]
+
+    @property
+    def twitch(self):
+        return self.__dict__["twitch"]
+
+
+class AccountInformation:
+    """
+    Represents authenticated client account information (https://accountinformation.roblox.com/)
+    This is only available for authenticated clients as it cannot be accessed otherwise.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+        self.account_information_metadata = None
+        self.promotion_channels = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the account information.
+        :return: Nothing
+        """
+        account_information_req = self.requests.get(
+            url="https://accountinformation.roblox.com/v1/metadata"
+        )
+        self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
+        promotion_channels_req = self.requests.get(
+            url="https://accountinformation.roblox.com/v1/promotion-channels"
+        )
+        self.promotion_channels = PromotionChannels(promotion_channels_req.json())
+
+    def get_gender(self):
+        """
+        Gets the user's gender.
+        :return: RobloxGender
+        """
+        gender_req = self.requests.get(endpoint + "v1/gender")
+        return RobloxGender(gender_req.json()["gender"])
+
+    def set_gender(self, gender):
+        """
+        Sets the user's gender.
+        :param gender: RobloxGender
+        :return: Nothing
+        """
+        self.requests.post(
+            url=endpoint + "v1/gender",
+            data={
+                "gender": str(gender.value)
+            }
+        )
+
+    def get_birthdate(self):
+        """
+        Returns the user's birthdate.
+        :return: datetime
+        """
+        birthdate_req = self.requests.get(endpoint + "v1/birthdate")
+        birthdate_raw = birthdate_req.json()
+        birthdate = datetime(
+            year=birthdate_raw["birthYear"],
+            month=birthdate_raw["birthMonth"],
+            day=birthdate_raw["birthDay"]
+        )
+        return birthdate
+
+    def set_birthdate(self, birthdate):
+        """
+        Sets the user's birthdate.
+        :param birthdate: A datetime object.
+        :return: Nothing
+        """
+        self.requests.post(
+            url=endpoint + "v1/birthdate",
+            data={
+              "birthMonth": birthdate.month,
+              "birthDay": birthdate.day,
+              "birthYear": birthdate.year
+            }
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AccountInformation +(requests) +
+
+

Represents authenticated client account information (https://accountinformation.roblox.com/) +This is only available for authenticated clients as it cannot be accessed otherwise.

+
+ +Expand source code + +
class AccountInformation:
+    """
+    Represents authenticated client account information (https://accountinformation.roblox.com/)
+    This is only available for authenticated clients as it cannot be accessed otherwise.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+        self.account_information_metadata = None
+        self.promotion_channels = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the account information.
+        :return: Nothing
+        """
+        account_information_req = self.requests.get(
+            url="https://accountinformation.roblox.com/v1/metadata"
+        )
+        self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
+        promotion_channels_req = self.requests.get(
+            url="https://accountinformation.roblox.com/v1/promotion-channels"
+        )
+        self.promotion_channels = PromotionChannels(promotion_channels_req.json())
+
+    def get_gender(self):
+        """
+        Gets the user's gender.
+        :return: RobloxGender
+        """
+        gender_req = self.requests.get(endpoint + "v1/gender")
+        return RobloxGender(gender_req.json()["gender"])
+
+    def set_gender(self, gender):
+        """
+        Sets the user's gender.
+        :param gender: RobloxGender
+        :return: Nothing
+        """
+        self.requests.post(
+            url=endpoint + "v1/gender",
+            data={
+                "gender": str(gender.value)
+            }
+        )
+
+    def get_birthdate(self):
+        """
+        Returns the user's birthdate.
+        :return: datetime
+        """
+        birthdate_req = self.requests.get(endpoint + "v1/birthdate")
+        birthdate_raw = birthdate_req.json()
+        birthdate = datetime(
+            year=birthdate_raw["birthYear"],
+            month=birthdate_raw["birthMonth"],
+            day=birthdate_raw["birthDay"]
+        )
+        return birthdate
+
+    def set_birthdate(self, birthdate):
+        """
+        Sets the user's birthdate.
+        :param birthdate: A datetime object.
+        :return: Nothing
+        """
+        self.requests.post(
+            url=endpoint + "v1/birthdate",
+            data={
+              "birthMonth": birthdate.month,
+              "birthDay": birthdate.day,
+              "birthYear": birthdate.year
+            }
+        )
+
+

Methods

+
+
+def get_birthdate(self) +
+
+

Returns the user's birthdate. +:return: datetime

+
+ +Expand source code + +
def get_birthdate(self):
+    """
+    Returns the user's birthdate.
+    :return: datetime
+    """
+    birthdate_req = self.requests.get(endpoint + "v1/birthdate")
+    birthdate_raw = birthdate_req.json()
+    birthdate = datetime(
+        year=birthdate_raw["birthYear"],
+        month=birthdate_raw["birthMonth"],
+        day=birthdate_raw["birthDay"]
+    )
+    return birthdate
+
+
+
+def get_gender(self) +
+
+

Gets the user's gender. +:return: RobloxGender

+
+ +Expand source code + +
def get_gender(self):
+    """
+    Gets the user's gender.
+    :return: RobloxGender
+    """
+    gender_req = self.requests.get(endpoint + "v1/gender")
+    return RobloxGender(gender_req.json()["gender"])
+
+
+
+def set_birthdate(self, birthdate) +
+
+

Sets the user's birthdate. +:param birthdate: A datetime object. +:return: Nothing

+
+ +Expand source code + +
def set_birthdate(self, birthdate):
+    """
+    Sets the user's birthdate.
+    :param birthdate: A datetime object.
+    :return: Nothing
+    """
+    self.requests.post(
+        url=endpoint + "v1/birthdate",
+        data={
+          "birthMonth": birthdate.month,
+          "birthDay": birthdate.day,
+          "birthYear": birthdate.year
+        }
+    )
+
+
+
+def set_gender(self, gender) +
+
+

Sets the user's gender. +:param gender: RobloxGender +:return: Nothing

+
+ +Expand source code + +
def set_gender(self, gender):
+    """
+    Sets the user's gender.
+    :param gender: RobloxGender
+    :return: Nothing
+    """
+    self.requests.post(
+        url=endpoint + "v1/gender",
+        data={
+            "gender": str(gender.value)
+        }
+    )
+
+
+
+def update(self) +
+
+

Updates the account information. +:return: Nothing

+
+ +Expand source code + +
def update(self):
+    """
+    Updates the account information.
+    :return: Nothing
+    """
+    account_information_req = self.requests.get(
+        url="https://accountinformation.roblox.com/v1/metadata"
+    )
+    self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
+    promotion_channels_req = self.requests.get(
+        url="https://accountinformation.roblox.com/v1/promotion-channels"
+    )
+    self.promotion_channels = PromotionChannels(promotion_channels_req.json())
+
+
+
+
+
+class AccountInformationMetadata +(metadata_raw) +
+
+

Represents account information metadata.

+
+ +Expand source code + +
class AccountInformationMetadata:
+    """
+    Represents account information metadata.
+    """
+    def __init__(self, metadata_raw):
+        self.__dict__["is_allowed_notifications_endpoint_disabled"] = \
+            metadata_raw["isAllowedNotificationsEndpointDisabled"]
+        self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"]
+        self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"]
+        self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"]
+        self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"]
+        self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"]
+
+
+
+class PromotionChannels +(promotion_raw) +
+
+

Represents account information promotion channels.

+
+ +Expand source code + +
class PromotionChannels:
+    """
+    Represents account information promotion channels.
+    """
+    def __init__(self, promotion_raw):
+        self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"]
+        self.__dict__["facebook"] = promotion_raw["facebook"]
+        self.__dict__["twitter"] = promotion_raw["twitter"]
+        self.__dict__["youtube"] = promotion_raw["youtube"]
+        self.__dict__["twitch"] = promotion_raw["twitch"]
+
+    @property
+    def promotion_channels_visibility_privacy(self):
+        return self.__dict__["promotion_channels_visibility_privacy"]
+
+    @property
+    def facebook(self):
+        return self.__dict__["facebook"]
+
+    @property
+    def twitter(self):
+        return self.__dict__["twitter"]
+
+    @property
+    def youtube(self):
+        return self.__dict__["youtube"]
+
+    @property
+    def twitch(self):
+        return self.__dict__["twitch"]
+
+

Instance variables

+
+
var facebook
+
+
+
+ +Expand source code + +
@property
+def facebook(self):
+    return self.__dict__["facebook"]
+
+
+
var promotion_channels_visibility_privacy
+
+
+
+ +Expand source code + +
@property
+def promotion_channels_visibility_privacy(self):
+    return self.__dict__["promotion_channels_visibility_privacy"]
+
+
+
var twitch
+
+
+
+ +Expand source code + +
@property
+def twitch(self):
+    return self.__dict__["twitch"]
+
+
+
var twitter
+
+
+
+ +Expand source code + +
@property
+def twitter(self):
+    return self.__dict__["twitter"]
+
+
+
var youtube
+
+
+
+ +Expand source code + +
@property
+def youtube(self):
+    return self.__dict__["youtube"]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/accountsettings.html b/docs/ro_py/accountsettings.html new file mode 100644 index 00000000..0a81a06c --- /dev/null +++ b/docs/ro_py/accountsettings.html @@ -0,0 +1,407 @@ + + + + + + +ro_py.accountsettings API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.accountsettings

+
+
+

This file houses functions and classes that pertain to Roblox client .

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox client .
+
+"""
+
+import enum
+
+endpoint = "https://accountsettings.roblox.com/"
+
+
+class PrivacyLevel(enum.Enum):
+    """
+    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.
+    """
+    no_one = "NoOne"
+    friends = "Friends"
+    everyone = "AllUsers"
+
+
+class PrivacySettings(enum.Enum):
+    """
+    Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.
+    """
+    app_chat_privacy = 0
+    game_chat_privacy = 1
+    inventory_privacy = 2
+    phone_discovery = 3
+    phone_discovery_enabled = 4
+    private_message_privacy = 5
+
+
+class RobloxEmail:
+    """
+    Represents an obfuscated version of the email you have set on your account.
+    """
+    def __init__(self, email_data):
+        self.__dict__["email_address"] = email_data["emailAddress"]
+        self.__dict__["verified"] = email_data["verified"]
+
+    @property
+    def email_address(self):
+        return self.__dict__["email_address"]
+
+    @property
+    def verified(self):
+        return self.__dict__["verified"]
+
+
+class AccountSettings:
+    """
+    Represents authenticated client account settings (https://accountsettings.roblox.com/)
+    This is only available for authenticated clients as it cannot be accessed otherwise.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_privacy_setting(self, privacy_setting):
+        """
+        Gets the value of a privacy setting.
+        """
+        privacy_setting = privacy_setting.value
+        privacy_endpoint = [
+            "app-chat-privacy",
+            "game-chat-privacy",
+            "inventory-privacy",
+            "privacy",
+            "privacy/info",
+            "private-message-privacy"
+        ][privacy_setting]
+        privacy_key = [
+            "appChatPrivacy",
+            "gameChatPrivacy",
+            "inventoryPrivacy",
+            "phoneDiscovery",
+            "isPhoneDiscoveryEnabled",
+            "privateMessagePrivacy"
+        ][privacy_setting]
+        privacy_endpoint = endpoint + "v1/" + privacy_endpoint
+        privacy_req = self.requests.get(privacy_endpoint)
+        return privacy_req.json()[privacy_key]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AccountSettings +(requests) +
+
+

Represents authenticated client account settings (https://accountsettings.roblox.com/) +This is only available for authenticated clients as it cannot be accessed otherwise.

+
+ +Expand source code + +
class AccountSettings:
+    """
+    Represents authenticated client account settings (https://accountsettings.roblox.com/)
+    This is only available for authenticated clients as it cannot be accessed otherwise.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_privacy_setting(self, privacy_setting):
+        """
+        Gets the value of a privacy setting.
+        """
+        privacy_setting = privacy_setting.value
+        privacy_endpoint = [
+            "app-chat-privacy",
+            "game-chat-privacy",
+            "inventory-privacy",
+            "privacy",
+            "privacy/info",
+            "private-message-privacy"
+        ][privacy_setting]
+        privacy_key = [
+            "appChatPrivacy",
+            "gameChatPrivacy",
+            "inventoryPrivacy",
+            "phoneDiscovery",
+            "isPhoneDiscoveryEnabled",
+            "privateMessagePrivacy"
+        ][privacy_setting]
+        privacy_endpoint = endpoint + "v1/" + privacy_endpoint
+        privacy_req = self.requests.get(privacy_endpoint)
+        return privacy_req.json()[privacy_key]
+
+

Methods

+
+
+def get_privacy_setting(self, privacy_setting) +
+
+

Gets the value of a privacy setting.

+
+ +Expand source code + +
def get_privacy_setting(self, privacy_setting):
+    """
+    Gets the value of a privacy setting.
+    """
+    privacy_setting = privacy_setting.value
+    privacy_endpoint = [
+        "app-chat-privacy",
+        "game-chat-privacy",
+        "inventory-privacy",
+        "privacy",
+        "privacy/info",
+        "private-message-privacy"
+    ][privacy_setting]
+    privacy_key = [
+        "appChatPrivacy",
+        "gameChatPrivacy",
+        "inventoryPrivacy",
+        "phoneDiscovery",
+        "isPhoneDiscoveryEnabled",
+        "privateMessagePrivacy"
+    ][privacy_setting]
+    privacy_endpoint = endpoint + "v1/" + privacy_endpoint
+    privacy_req = self.requests.get(privacy_endpoint)
+    return privacy_req.json()[privacy_key]
+
+
+
+
+
+class PrivacyLevel +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.

+
+ +Expand source code + +
class PrivacyLevel(enum.Enum):
+    """
+    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.
+    """
+    no_one = "NoOne"
+    friends = "Friends"
+    everyone = "AllUsers"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var everyone
+
+
+
+
var friends
+
+
+
+
var no_one
+
+
+
+
+
+
+class PrivacySettings +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.

+
+ +Expand source code + +
class PrivacySettings(enum.Enum):
+    """
+    Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.
+    """
+    app_chat_privacy = 0
+    game_chat_privacy = 1
+    inventory_privacy = 2
+    phone_discovery = 3
+    phone_discovery_enabled = 4
+    private_message_privacy = 5
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var app_chat_privacy
+
+
+
+
var game_chat_privacy
+
+
+
+
var inventory_privacy
+
+
+
+
var phone_discovery
+
+
+
+
var phone_discovery_enabled
+
+
+
+
var private_message_privacy
+
+
+
+
+
+
+class RobloxEmail +(email_data) +
+
+

Represents an obfuscated version of the email you have set on your account.

+
+ +Expand source code + +
class RobloxEmail:
+    """
+    Represents an obfuscated version of the email you have set on your account.
+    """
+    def __init__(self, email_data):
+        self.__dict__["email_address"] = email_data["emailAddress"]
+        self.__dict__["verified"] = email_data["verified"]
+
+    @property
+    def email_address(self):
+        return self.__dict__["email_address"]
+
+    @property
+    def verified(self):
+        return self.__dict__["verified"]
+
+

Instance variables

+
+
var email_address
+
+
+
+ +Expand source code + +
@property
+def email_address(self):
+    return self.__dict__["email_address"]
+
+
+
var verified
+
+
+
+ +Expand source code + +
@property
+def verified(self):
+    return self.__dict__["verified"]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/assets.html b/docs/ro_py/assets.html new file mode 100644 index 00000000..741197a5 --- /dev/null +++ b/docs/ro_py/assets.html @@ -0,0 +1,368 @@ + + + + + + +ro_py.assets API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.assets

+
+
+

This file houses functions and classes that pertain to Roblox assets.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox assets.
+
+"""
+
+from ro_py.users import User
+from ro_py.groups import Group
+from ro_py.utilities.errors import NotLimitedError
+from ro_py.economy import LimitedResaleData
+from ro_py.utilities.asset_type import asset_types
+import iso8601
+
+endpoint = "https://api.roblox.com/"
+
+
+class Asset:
+    """
+    Represents an asset.
+    """
+    def __init__(self, requests, asset_id):
+        self.id = asset_id
+        self.requests = requests
+        self.target_id = None
+        self.product_type = None
+        self.asset_id = None
+        self.product_id = None
+        self.name = None
+        self.description = None
+        self.asset_type_id = None
+        self.asset_type_name = None
+        self.creator = None
+        self.created = None
+        self.updated = None
+        self.price = None
+        self.is_new = None
+        self.is_for_sale = None
+        self.is_public_domain = None
+        self.is_limited = None
+        self.is_limited_unique = None
+        self.minimum_membership_level = None
+        self.content_rating_type_id = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the asset's information.
+        """
+        asset_info_req = self.requests.get(
+            url=endpoint + "marketplace/productinfo",
+            params={
+                "assetId": self.id
+            }
+        )
+        asset_info = asset_info_req.json()
+        self.target_id = asset_info["TargetId"]
+        self.product_type = asset_info["ProductType"]
+        self.asset_id = asset_info["AssetId"]
+        self.product_id = asset_info["ProductId"]
+        self.name = asset_info["Name"]
+        self.description = asset_info["Description"]
+        self.asset_type_id = asset_info["AssetTypeId"]
+        self.asset_type_name = asset_types[self.asset_type_id]
+        if asset_info["Creator"]["CreatorType"] == "User":
+            self.creator = User(self.requests, asset_info["Creator"]["Id"])
+        elif asset_info["Creator"]["CreatorType"] == "Group":
+            self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
+        self.created = iso8601.parse_date(asset_info["Created"])
+        self.updated = iso8601.parse_date(asset_info["Updated"])
+        self.price = asset_info["PriceInRobux"]
+        self.is_new = asset_info["IsNew"]
+        self.is_for_sale = asset_info["IsForSale"]
+        self.is_public_domain = asset_info["IsPublicDomain"]
+        self.is_limited = asset_info["IsLimited"]
+        self.is_limited_unique = asset_info["IsLimitedUnique"]
+        self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
+        self.content_rating_type_id = asset_info["ContentRatingTypeId"]
+
+    def get_remaining(self):
+        """
+        Gets the remaining amount of this asset. (used for Limited U items)
+        :returns: Amount remaining
+        """
+        asset_info_req = self.requests.get(
+            url=endpoint + "marketplace/productinfo",
+            params={
+                "assetId": self.asset_id
+            }
+        )
+        asset_info = asset_info_req.json()
+        return asset_info["Remaining"]
+
+    def get_limited_resale_data(self):
+        """
+        Gets the limited resale data
+        :returns: LimitedResaleData
+        """
+        if self.is_limited:
+            resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
+            return LimitedResaleData(resale_data_req.json())
+        else:
+            raise NotLimitedError("You can only read this information on limited items.")
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Asset +(requests, asset_id) +
+
+

Represents an asset.

+
+ +Expand source code + +
class Asset:
+    """
+    Represents an asset.
+    """
+    def __init__(self, requests, asset_id):
+        self.id = asset_id
+        self.requests = requests
+        self.target_id = None
+        self.product_type = None
+        self.asset_id = None
+        self.product_id = None
+        self.name = None
+        self.description = None
+        self.asset_type_id = None
+        self.asset_type_name = None
+        self.creator = None
+        self.created = None
+        self.updated = None
+        self.price = None
+        self.is_new = None
+        self.is_for_sale = None
+        self.is_public_domain = None
+        self.is_limited = None
+        self.is_limited_unique = None
+        self.minimum_membership_level = None
+        self.content_rating_type_id = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the asset's information.
+        """
+        asset_info_req = self.requests.get(
+            url=endpoint + "marketplace/productinfo",
+            params={
+                "assetId": self.id
+            }
+        )
+        asset_info = asset_info_req.json()
+        self.target_id = asset_info["TargetId"]
+        self.product_type = asset_info["ProductType"]
+        self.asset_id = asset_info["AssetId"]
+        self.product_id = asset_info["ProductId"]
+        self.name = asset_info["Name"]
+        self.description = asset_info["Description"]
+        self.asset_type_id = asset_info["AssetTypeId"]
+        self.asset_type_name = asset_types[self.asset_type_id]
+        if asset_info["Creator"]["CreatorType"] == "User":
+            self.creator = User(self.requests, asset_info["Creator"]["Id"])
+        elif asset_info["Creator"]["CreatorType"] == "Group":
+            self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
+        self.created = iso8601.parse_date(asset_info["Created"])
+        self.updated = iso8601.parse_date(asset_info["Updated"])
+        self.price = asset_info["PriceInRobux"]
+        self.is_new = asset_info["IsNew"]
+        self.is_for_sale = asset_info["IsForSale"]
+        self.is_public_domain = asset_info["IsPublicDomain"]
+        self.is_limited = asset_info["IsLimited"]
+        self.is_limited_unique = asset_info["IsLimitedUnique"]
+        self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
+        self.content_rating_type_id = asset_info["ContentRatingTypeId"]
+
+    def get_remaining(self):
+        """
+        Gets the remaining amount of this asset. (used for Limited U items)
+        :returns: Amount remaining
+        """
+        asset_info_req = self.requests.get(
+            url=endpoint + "marketplace/productinfo",
+            params={
+                "assetId": self.asset_id
+            }
+        )
+        asset_info = asset_info_req.json()
+        return asset_info["Remaining"]
+
+    def get_limited_resale_data(self):
+        """
+        Gets the limited resale data
+        :returns: LimitedResaleData
+        """
+        if self.is_limited:
+            resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
+            return LimitedResaleData(resale_data_req.json())
+        else:
+            raise NotLimitedError("You can only read this information on limited items.")
+
+

Methods

+
+
+def get_limited_resale_data(self) +
+
+

Gets the limited resale data +:returns: LimitedResaleData

+
+ +Expand source code + +
def get_limited_resale_data(self):
+    """
+    Gets the limited resale data
+    :returns: LimitedResaleData
+    """
+    if self.is_limited:
+        resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
+        return LimitedResaleData(resale_data_req.json())
+    else:
+        raise NotLimitedError("You can only read this information on limited items.")
+
+
+
+def get_remaining(self) +
+
+

Gets the remaining amount of this asset. (used for Limited U items) +:returns: Amount remaining

+
+ +Expand source code + +
def get_remaining(self):
+    """
+    Gets the remaining amount of this asset. (used for Limited U items)
+    :returns: Amount remaining
+    """
+    asset_info_req = self.requests.get(
+        url=endpoint + "marketplace/productinfo",
+        params={
+            "assetId": self.asset_id
+        }
+    )
+    asset_info = asset_info_req.json()
+    return asset_info["Remaining"]
+
+
+
+def update(self) +
+
+

Updates the asset's information.

+
+ +Expand source code + +
def update(self):
+    """
+    Updates the asset's information.
+    """
+    asset_info_req = self.requests.get(
+        url=endpoint + "marketplace/productinfo",
+        params={
+            "assetId": self.id
+        }
+    )
+    asset_info = asset_info_req.json()
+    self.target_id = asset_info["TargetId"]
+    self.product_type = asset_info["ProductType"]
+    self.asset_id = asset_info["AssetId"]
+    self.product_id = asset_info["ProductId"]
+    self.name = asset_info["Name"]
+    self.description = asset_info["Description"]
+    self.asset_type_id = asset_info["AssetTypeId"]
+    self.asset_type_name = asset_types[self.asset_type_id]
+    if asset_info["Creator"]["CreatorType"] == "User":
+        self.creator = User(self.requests, asset_info["Creator"]["Id"])
+    elif asset_info["Creator"]["CreatorType"] == "Group":
+        self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
+    self.created = iso8601.parse_date(asset_info["Created"])
+    self.updated = iso8601.parse_date(asset_info["Updated"])
+    self.price = asset_info["PriceInRobux"]
+    self.is_new = asset_info["IsNew"]
+    self.is_for_sale = asset_info["IsForSale"]
+    self.is_public_domain = asset_info["IsPublicDomain"]
+    self.is_limited = asset_info["IsLimited"]
+    self.is_limited_unique = asset_info["IsLimitedUnique"]
+    self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
+    self.content_rating_type_id = asset_info["ContentRatingTypeId"]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/badges.html b/docs/ro_py/badges.html new file mode 100644 index 00000000..40ee435f --- /dev/null +++ b/docs/ro_py/badges.html @@ -0,0 +1,212 @@ + + + + + + +ro_py.badges API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.badges

+
+
+

This file houses functions and classes that pertain to game-awarded badges.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to game-awarded badges.
+
+"""
+
+endpoint = "https://badges.roblox.com/"
+
+
+class BadgeStatistics:
+    """
+    Represents a badge's statistics.
+    """
+    def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage):
+        self.past_date_awarded_count = past_date_awarded_count
+        self.awarded_count = awarded_count
+        self.win_rate_percentage = win_rate_percentage
+
+
+class Badge:
+    """
+    Represents a game-awarded badge.
+    """
+    def __init__(self, requests, badge_id):
+        self.id = badge_id
+        self.requests = requests
+        self.name = None
+        self.description = None
+        self.display_name = None
+        self.display_description = None
+        self.enabled = None
+        self.statistics = None
+        self.update()
+
+    def update(self):
+        badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}")
+        badge_info = badge_info_req.json()
+        self.name = badge_info["name"]
+        self.description = badge_info["description"]
+        self.display_name = badge_info["displayName"]
+        self.display_description = badge_info["displayDescription"]
+        self.enabled = badge_info["enabled"]
+        statistics_info = badge_info["statistics"]
+        self.statistics = BadgeStatistics(
+            statistics_info["pastDayAwardedCount"],
+            statistics_info["awardedCount"],
+            statistics_info["winRatePercentage"]
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Badge +(requests, badge_id) +
+
+

Represents a game-awarded badge.

+
+ +Expand source code + +
class Badge:
+    """
+    Represents a game-awarded badge.
+    """
+    def __init__(self, requests, badge_id):
+        self.id = badge_id
+        self.requests = requests
+        self.name = None
+        self.description = None
+        self.display_name = None
+        self.display_description = None
+        self.enabled = None
+        self.statistics = None
+        self.update()
+
+    def update(self):
+        badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}")
+        badge_info = badge_info_req.json()
+        self.name = badge_info["name"]
+        self.description = badge_info["description"]
+        self.display_name = badge_info["displayName"]
+        self.display_description = badge_info["displayDescription"]
+        self.enabled = badge_info["enabled"]
+        statistics_info = badge_info["statistics"]
+        self.statistics = BadgeStatistics(
+            statistics_info["pastDayAwardedCount"],
+            statistics_info["awardedCount"],
+            statistics_info["winRatePercentage"]
+        )
+
+

Methods

+
+
+def update(self) +
+
+
+
+ +Expand source code + +
def update(self):
+    badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}")
+    badge_info = badge_info_req.json()
+    self.name = badge_info["name"]
+    self.description = badge_info["description"]
+    self.display_name = badge_info["displayName"]
+    self.display_description = badge_info["displayDescription"]
+    self.enabled = badge_info["enabled"]
+    statistics_info = badge_info["statistics"]
+    self.statistics = BadgeStatistics(
+        statistics_info["pastDayAwardedCount"],
+        statistics_info["awardedCount"],
+        statistics_info["winRatePercentage"]
+    )
+
+
+
+
+
+class BadgeStatistics +(past_date_awarded_count, awarded_count, win_rate_percentage) +
+
+

Represents a badge's statistics.

+
+ +Expand source code + +
class BadgeStatistics:
+    """
+    Represents a badge's statistics.
+    """
+    def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage):
+        self.past_date_awarded_count = past_date_awarded_count
+        self.awarded_count = awarded_count
+        self.win_rate_percentage = win_rate_percentage
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/catalog.html b/docs/ro_py/catalog.html new file mode 100644 index 00000000..60f4a5bd --- /dev/null +++ b/docs/ro_py/catalog.html @@ -0,0 +1,129 @@ + + + + + + +ro_py.catalog API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.catalog

+
+
+

This file houses functions and classes that pertain to the Roblox catalog.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to the Roblox catalog.
+
+"""
+
+import enum
+
+
+class AppStore(enum.Enum):
+    google_play = "GooglePlay"
+    amazon = "Amazon"
+    ios = "iOS"
+    xbox = "Xbox"
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AppStore +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

An enumeration.

+
+ +Expand source code + +
class AppStore(enum.Enum):
+    google_play = "GooglePlay"
+    amazon = "Amazon"
+    ios = "iOS"
+    xbox = "Xbox"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var amazon
+
+
+
+
var google_play
+
+
+
+
var ios
+
+
+
+
var xbox
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/chat.html b/docs/ro_py/chat.html new file mode 100644 index 00000000..04d49bba --- /dev/null +++ b/docs/ro_py/chat.html @@ -0,0 +1,532 @@ + + + + + + +ro_py.chat API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.chat

+
+
+

This file houses functions and classes that pertain to chatting and messaging.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to chatting and messaging.
+
+"""
+
+from ro_py.utilities.errors import ChatError
+from ro_py.users import User
+
+endpoint = "https://chat.roblox.com/"
+
+
+class ChatSettings:
+    def __init__(self, settings_data):
+        self.enabled = settings_data["chatEnabled"]
+        self.is_active_chat_user = settings_data["isActiveChatUser"]
+
+
+class ConversationTyping:
+    def __init__(self, requests, conversation_id):
+        self.requests = requests
+        self.id = conversation_id
+
+    def __enter__(self):
+        self.requests.post(
+            url=endpoint + "v2/update-user-typing-status",
+            data={
+                "conversationId": self.id,
+                "isTyping": "true"
+            }
+        )
+
+    def __exit__(self, *args, **kwargs):
+        self.requests.post(
+            url=endpoint + "v2/update-user-typing-status",
+            data={
+                "conversationId": self.id,
+                "isTyping": "false"
+            }
+        )
+
+
+class Conversation:
+    def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
+        self.requests = requests
+
+        if raw:
+            data = raw_data
+            self.id = data["id"]
+        else:
+            self.id = conversation_id
+            conversation_req = requests.get(
+                url="https://chat.roblox.com/v2/get-conversations",
+                params={
+                    "conversationIds": self.id
+                }
+            )
+            data = conversation_req.json()[0]
+
+        self.title = data["title"]
+        self.initiator = User(self.requests, data["initiator"]["targetId"])
+        self.type = data["conversationType"]
+
+        self.typing = ConversationTyping(self.requests, conversation_id)
+
+    def get_message(self, message_id):
+        return Message(self.requests, message_id, self.id)
+
+    def send_message(self, content):
+        send_message_req = self.requests.post(
+            url=endpoint + "v2/send-message",
+            data={
+                "message": content,
+                "conversationId": self.id
+            }
+        )
+        send_message_json = send_message_req.json()
+        if send_message_json["sent"]:
+            return Message(self.requests, send_message_json["messageId"], self.id)
+        else:
+            raise ChatError(send_message_json["statusMessage"])
+
+
+class Message:
+    def __init__(self, requests, message_id, conversation_id):
+        self.requests = requests
+        self.id = message_id
+        self.conversation_id = conversation_id
+
+        self.content = None
+        self.sender = None
+        self.read = None
+
+        self.update()
+
+    def update(self):
+        message_req = self.requests.get(
+            url="https://chat.roblox.com/v2/get-messages",
+            params={
+                "conversationId": self.conversation_id,
+                "pageSize": 1,
+                "exclusiveStartMessageId": self.id
+            }
+        )
+
+        message_json = message_req.json()[0]
+        self.content = message_json["content"]
+        self.sender = User(self.requests, message_json["senderTargetId"])
+        self.read = message_json["read"]
+
+
+class ChatWrapper:
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_conversation(self, conversation_id):
+        """
+        Gets a conversation by the conversation ID.
+        """
+        return Conversation(self.requests, conversation_id)
+
+    def get_conversations(self, page_number=1, page_size=10):
+        """
+        Gets the list of conversations. This will be updated soon to use the new Pages object.
+        """
+        conversations_req = self.requests.get(
+            url="https://chat.roblox.com/v2/get-user-conversations",
+            params={
+                "pageNumber": page_number,
+                "pageSize": page_size
+            }
+        )
+        conversations_json = conversations_req.json()
+        conversations = []
+        for conversation_raw in conversations_json:
+            conversations.append(Conversation(
+                requests=self.requests,
+                raw=True,
+                raw_data=conversation_raw
+            ))
+        return conversations
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ChatSettings +(settings_data) +
+
+
+
+ +Expand source code + +
class ChatSettings:
+    def __init__(self, settings_data):
+        self.enabled = settings_data["chatEnabled"]
+        self.is_active_chat_user = settings_data["isActiveChatUser"]
+
+
+
+class ChatWrapper +(requests) +
+
+
+
+ +Expand source code + +
class ChatWrapper:
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_conversation(self, conversation_id):
+        """
+        Gets a conversation by the conversation ID.
+        """
+        return Conversation(self.requests, conversation_id)
+
+    def get_conversations(self, page_number=1, page_size=10):
+        """
+        Gets the list of conversations. This will be updated soon to use the new Pages object.
+        """
+        conversations_req = self.requests.get(
+            url="https://chat.roblox.com/v2/get-user-conversations",
+            params={
+                "pageNumber": page_number,
+                "pageSize": page_size
+            }
+        )
+        conversations_json = conversations_req.json()
+        conversations = []
+        for conversation_raw in conversations_json:
+            conversations.append(Conversation(
+                requests=self.requests,
+                raw=True,
+                raw_data=conversation_raw
+            ))
+        return conversations
+
+

Methods

+
+
+def get_conversation(self, conversation_id) +
+
+

Gets a conversation by the conversation ID.

+
+ +Expand source code + +
def get_conversation(self, conversation_id):
+    """
+    Gets a conversation by the conversation ID.
+    """
+    return Conversation(self.requests, conversation_id)
+
+
+
+def get_conversations(self, page_number=1, page_size=10) +
+
+

Gets the list of conversations. This will be updated soon to use the new Pages object.

+
+ +Expand source code + +
def get_conversations(self, page_number=1, page_size=10):
+    """
+    Gets the list of conversations. This will be updated soon to use the new Pages object.
+    """
+    conversations_req = self.requests.get(
+        url="https://chat.roblox.com/v2/get-user-conversations",
+        params={
+            "pageNumber": page_number,
+            "pageSize": page_size
+        }
+    )
+    conversations_json = conversations_req.json()
+    conversations = []
+    for conversation_raw in conversations_json:
+        conversations.append(Conversation(
+            requests=self.requests,
+            raw=True,
+            raw_data=conversation_raw
+        ))
+    return conversations
+
+
+
+
+
+class Conversation +(requests, conversation_id=None, raw=False, raw_data=None) +
+
+
+
+ +Expand source code + +
class Conversation:
+    def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
+        self.requests = requests
+
+        if raw:
+            data = raw_data
+            self.id = data["id"]
+        else:
+            self.id = conversation_id
+            conversation_req = requests.get(
+                url="https://chat.roblox.com/v2/get-conversations",
+                params={
+                    "conversationIds": self.id
+                }
+            )
+            data = conversation_req.json()[0]
+
+        self.title = data["title"]
+        self.initiator = User(self.requests, data["initiator"]["targetId"])
+        self.type = data["conversationType"]
+
+        self.typing = ConversationTyping(self.requests, conversation_id)
+
+    def get_message(self, message_id):
+        return Message(self.requests, message_id, self.id)
+
+    def send_message(self, content):
+        send_message_req = self.requests.post(
+            url=endpoint + "v2/send-message",
+            data={
+                "message": content,
+                "conversationId": self.id
+            }
+        )
+        send_message_json = send_message_req.json()
+        if send_message_json["sent"]:
+            return Message(self.requests, send_message_json["messageId"], self.id)
+        else:
+            raise ChatError(send_message_json["statusMessage"])
+
+

Methods

+
+
+def get_message(self, message_id) +
+
+
+
+ +Expand source code + +
def get_message(self, message_id):
+    return Message(self.requests, message_id, self.id)
+
+
+
+def send_message(self, content) +
+
+
+
+ +Expand source code + +
def send_message(self, content):
+    send_message_req = self.requests.post(
+        url=endpoint + "v2/send-message",
+        data={
+            "message": content,
+            "conversationId": self.id
+        }
+    )
+    send_message_json = send_message_req.json()
+    if send_message_json["sent"]:
+        return Message(self.requests, send_message_json["messageId"], self.id)
+    else:
+        raise ChatError(send_message_json["statusMessage"])
+
+
+
+
+
+class ConversationTyping +(requests, conversation_id) +
+
+
+
+ +Expand source code + +
class ConversationTyping:
+    def __init__(self, requests, conversation_id):
+        self.requests = requests
+        self.id = conversation_id
+
+    def __enter__(self):
+        self.requests.post(
+            url=endpoint + "v2/update-user-typing-status",
+            data={
+                "conversationId": self.id,
+                "isTyping": "true"
+            }
+        )
+
+    def __exit__(self, *args, **kwargs):
+        self.requests.post(
+            url=endpoint + "v2/update-user-typing-status",
+            data={
+                "conversationId": self.id,
+                "isTyping": "false"
+            }
+        )
+
+
+
+class Message +(requests, message_id, conversation_id) +
+
+
+
+ +Expand source code + +
class Message:
+    def __init__(self, requests, message_id, conversation_id):
+        self.requests = requests
+        self.id = message_id
+        self.conversation_id = conversation_id
+
+        self.content = None
+        self.sender = None
+        self.read = None
+
+        self.update()
+
+    def update(self):
+        message_req = self.requests.get(
+            url="https://chat.roblox.com/v2/get-messages",
+            params={
+                "conversationId": self.conversation_id,
+                "pageSize": 1,
+                "exclusiveStartMessageId": self.id
+            }
+        )
+
+        message_json = message_req.json()[0]
+        self.content = message_json["content"]
+        self.sender = User(self.requests, message_json["senderTargetId"])
+        self.read = message_json["read"]
+
+

Methods

+
+
+def update(self) +
+
+
+
+ +Expand source code + +
def update(self):
+    message_req = self.requests.get(
+        url="https://chat.roblox.com/v2/get-messages",
+        params={
+            "conversationId": self.conversation_id,
+            "pageSize": 1,
+            "exclusiveStartMessageId": self.id
+        }
+    )
+
+    message_json = message_req.json()[0]
+    self.content = message_json["content"]
+    self.sender = User(self.requests, message_json["senderTargetId"])
+    self.read = message_json["read"]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/client.html b/docs/ro_py/client.html new file mode 100644 index 00000000..34181e7d --- /dev/null +++ b/docs/ro_py/client.html @@ -0,0 +1,435 @@ + + + + + + +ro_py.client API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.client

+
+
+

This file houses functions and classes that represent the core Roblox web client.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that represent the core Roblox web client.
+
+"""
+
+from ro_py.users import User
+from ro_py.games import Game
+from ro_py.groups import Group
+from ro_py.assets import Asset
+from ro_py.badges import Badge
+from ro_py.chat import ChatWrapper
+from ro_py.trades import TradesWrapper
+from ro_py.utilities.cache import cache
+from ro_py.utilities.requests import Requests
+from ro_py.accountinformation import AccountInformation
+from ro_py.accountsettings import AccountSettings
+
+import logging
+
+
+class Client:
+    """
+    Represents an authenticated Roblox client.
+
+    Parameters
+    ----------
+    token : str
+        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
+    requests_cache: bool
+        Toggle for cached requests using CacheControl.
+    """
+    def __init__(self, token=None, requests_cache=False):
+        self.requests = Requests(
+            cache=requests_cache
+        )
+
+        logging.debug("Initialized requests.")
+
+        self.accountinformation = None
+        """AccountInformation object. Only available for authenticated clients."""
+        self.accountsettings = None
+        """AccountSettings object. Only available for authenticated clients."""
+        self.user = None
+        """User object. Only available for authenticated clients."""
+        self.chat = None
+        """ChatWrapper object. Only available for authenticated clients."""
+        self.trade = None
+        """TradesWrapper object. Only available for authenticated clients."""
+
+        if token:
+            self.requests.session.cookies[".ROBLOSECURITY"] = token
+            logging.debug("Initialized token.")
+            self.accountinformation = AccountInformation(self.requests)
+            self.accountsettings = AccountSettings(self.requests)
+            logging.debug("Initialized AccountInformation and AccountSettings.")
+            auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated")
+            self.user = User(self.requests, auth_user_req.json()["id"])
+            logging.debug("Initialized authenticated user.")
+            self.chat = ChatWrapper(self.requests)
+            logging.debug("Initialized chat wrapper.")
+            self.trade = TradesWrapper(self.requests)
+            logging.debug("Initialized trade wrapper.")
+        else:
+            logging.warning("The active client is not authenticated, so some features will not be enabled.")
+
+    def get_user(self, user_id):
+        """
+        Gets a Roblox user.
+        """
+        try:
+            cache["users"][str(user_id)]
+        except KeyError:
+            cache["users"][str(user_id)] = User(self.requests, user_id)
+        return cache["users"][str(user_id)]
+
+    def get_group(self, group_id):
+        """
+        Gets a Roblox group.
+        """
+        try:
+            cache["groups"][str(group_id)]
+        except KeyError:
+            cache["groups"][str(group_id)] = Group(self.requests, group_id)
+        return cache["groups"][str(group_id)]
+
+    def get_game(self, game_id):
+        """
+        Gets a Roblox game.
+        """
+        try:
+            cache["games"][str(game_id)]
+        except KeyError:
+            cache["games"][str(game_id)] = Game(self.requests, game_id)
+        return cache["games"][str(game_id)]
+
+    def get_asset(self, asset_id):
+        """
+        Gets a Roblox asset.
+        """
+        try:
+            cache["assets"][str(asset_id)]
+        except KeyError:
+            cache["assets"][str(asset_id)] = Asset(self.requests, asset_id)
+        return cache["assets"][str(asset_id)]
+
+    def get_badge(self, badge_id):
+        """
+        Gets a Roblox badge.
+        """
+        try:
+            cache["badges"][str(badge_id)]
+        except KeyError:
+            cache["badges"][str(badge_id)] = Badge(self.requests, badge_id)
+        return cache["badges"][str(badge_id)]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Client +(token=None, requests_cache=False) +
+
+

Represents an authenticated Roblox client.

+

Parameters

+
+
token : str
+
Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
+
requests_cache : bool
+
Toggle for cached requests using CacheControl.
+
+
+ +Expand source code + +
class Client:
+    """
+    Represents an authenticated Roblox client.
+
+    Parameters
+    ----------
+    token : str
+        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
+    requests_cache: bool
+        Toggle for cached requests using CacheControl.
+    """
+    def __init__(self, token=None, requests_cache=False):
+        self.requests = Requests(
+            cache=requests_cache
+        )
+
+        logging.debug("Initialized requests.")
+
+        self.accountinformation = None
+        """AccountInformation object. Only available for authenticated clients."""
+        self.accountsettings = None
+        """AccountSettings object. Only available for authenticated clients."""
+        self.user = None
+        """User object. Only available for authenticated clients."""
+        self.chat = None
+        """ChatWrapper object. Only available for authenticated clients."""
+        self.trade = None
+        """TradesWrapper object. Only available for authenticated clients."""
+
+        if token:
+            self.requests.session.cookies[".ROBLOSECURITY"] = token
+            logging.debug("Initialized token.")
+            self.accountinformation = AccountInformation(self.requests)
+            self.accountsettings = AccountSettings(self.requests)
+            logging.debug("Initialized AccountInformation and AccountSettings.")
+            auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated")
+            self.user = User(self.requests, auth_user_req.json()["id"])
+            logging.debug("Initialized authenticated user.")
+            self.chat = ChatWrapper(self.requests)
+            logging.debug("Initialized chat wrapper.")
+            self.trade = TradesWrapper(self.requests)
+            logging.debug("Initialized trade wrapper.")
+        else:
+            logging.warning("The active client is not authenticated, so some features will not be enabled.")
+
+    def get_user(self, user_id):
+        """
+        Gets a Roblox user.
+        """
+        try:
+            cache["users"][str(user_id)]
+        except KeyError:
+            cache["users"][str(user_id)] = User(self.requests, user_id)
+        return cache["users"][str(user_id)]
+
+    def get_group(self, group_id):
+        """
+        Gets a Roblox group.
+        """
+        try:
+            cache["groups"][str(group_id)]
+        except KeyError:
+            cache["groups"][str(group_id)] = Group(self.requests, group_id)
+        return cache["groups"][str(group_id)]
+
+    def get_game(self, game_id):
+        """
+        Gets a Roblox game.
+        """
+        try:
+            cache["games"][str(game_id)]
+        except KeyError:
+            cache["games"][str(game_id)] = Game(self.requests, game_id)
+        return cache["games"][str(game_id)]
+
+    def get_asset(self, asset_id):
+        """
+        Gets a Roblox asset.
+        """
+        try:
+            cache["assets"][str(asset_id)]
+        except KeyError:
+            cache["assets"][str(asset_id)] = Asset(self.requests, asset_id)
+        return cache["assets"][str(asset_id)]
+
+    def get_badge(self, badge_id):
+        """
+        Gets a Roblox badge.
+        """
+        try:
+            cache["badges"][str(badge_id)]
+        except KeyError:
+            cache["badges"][str(badge_id)] = Badge(self.requests, badge_id)
+        return cache["badges"][str(badge_id)]
+
+

Instance variables

+
+
var accountinformation
+
+

AccountInformation object. Only available for authenticated clients.

+
+
var accountsettings
+
+

AccountSettings object. Only available for authenticated clients.

+
+
var chat
+
+

ChatWrapper object. Only available for authenticated clients.

+
+
var trade
+
+

TradesWrapper object. Only available for authenticated clients.

+
+
var user
+
+

User object. Only available for authenticated clients.

+
+
+

Methods

+
+
+def get_asset(self, asset_id) +
+
+

Gets a Roblox asset.

+
+ +Expand source code + +
def get_asset(self, asset_id):
+    """
+    Gets a Roblox asset.
+    """
+    try:
+        cache["assets"][str(asset_id)]
+    except KeyError:
+        cache["assets"][str(asset_id)] = Asset(self.requests, asset_id)
+    return cache["assets"][str(asset_id)]
+
+
+
+def get_badge(self, badge_id) +
+
+

Gets a Roblox badge.

+
+ +Expand source code + +
def get_badge(self, badge_id):
+    """
+    Gets a Roblox badge.
+    """
+    try:
+        cache["badges"][str(badge_id)]
+    except KeyError:
+        cache["badges"][str(badge_id)] = Badge(self.requests, badge_id)
+    return cache["badges"][str(badge_id)]
+
+
+
+def get_game(self, game_id) +
+
+

Gets a Roblox game.

+
+ +Expand source code + +
def get_game(self, game_id):
+    """
+    Gets a Roblox game.
+    """
+    try:
+        cache["games"][str(game_id)]
+    except KeyError:
+        cache["games"][str(game_id)] = Game(self.requests, game_id)
+    return cache["games"][str(game_id)]
+
+
+
+def get_group(self, group_id) +
+
+

Gets a Roblox group.

+
+ +Expand source code + +
def get_group(self, group_id):
+    """
+    Gets a Roblox group.
+    """
+    try:
+        cache["groups"][str(group_id)]
+    except KeyError:
+        cache["groups"][str(group_id)] = Group(self.requests, group_id)
+    return cache["groups"][str(group_id)]
+
+
+
+def get_user(self, user_id) +
+
+

Gets a Roblox user.

+
+ +Expand source code + +
def get_user(self, user_id):
+    """
+    Gets a Roblox user.
+    """
+    try:
+        cache["users"][str(user_id)]
+    except KeyError:
+        cache["users"][str(user_id)] = User(self.requests, user_id)
+    return cache["users"][str(user_id)]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/economy.html b/docs/ro_py/economy.html new file mode 100644 index 00000000..235e47e2 --- /dev/null +++ b/docs/ro_py/economy.html @@ -0,0 +1,139 @@ + + + + + + +ro_py.economy API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.economy

+
+
+

This file houses functions and classes that pertain to the Roblox economy endpoints.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to the Roblox economy endpoints.
+
+"""
+
+endpoint = "https://economy.roblox.com/"
+
+
+class Currency:
+    """
+    Represents currency data.
+    """
+    def __init__(self, currency_data):
+        self.robux = currency_data["robux"]
+
+
+class LimitedResaleData:
+    """
+    Represents the resale data of a limited item.
+    """
+    def __init__(self, resale_data):
+        self.asset_stock = resale_data["assetStock"]
+        self.sales = resale_data["sales"]
+        self.number_remaining = resale_data["numberRemaining"]
+        self.recent_average_price = resale_data["recentAveragePrice"]
+        self.original_price = resale_data["originalPrice"]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Currency +(currency_data) +
+
+

Represents currency data.

+
+ +Expand source code + +
class Currency:
+    """
+    Represents currency data.
+    """
+    def __init__(self, currency_data):
+        self.robux = currency_data["robux"]
+
+
+
+class LimitedResaleData +(resale_data) +
+
+

Represents the resale data of a limited item.

+
+ +Expand source code + +
class LimitedResaleData:
+    """
+    Represents the resale data of a limited item.
+    """
+    def __init__(self, resale_data):
+        self.asset_stock = resale_data["assetStock"]
+        self.sales = resale_data["sales"]
+        self.number_remaining = resale_data["numberRemaining"]
+        self.recent_average_price = resale_data["recentAveragePrice"]
+        self.original_price = resale_data["originalPrice"]
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/games.html b/docs/ro_py/games.html new file mode 100644 index 00000000..10b2bf7b --- /dev/null +++ b/docs/ro_py/games.html @@ -0,0 +1,402 @@ + + + + + + +ro_py.games API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.games

+
+
+

This file houses functions and classes that pertain to Roblox universes and places.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox universes and places.
+
+"""
+
+from ro_py.users import User
+from ro_py.groups import Group
+from ro_py.badges import Badge
+
+endpoint = "https://games.roblox.com/"
+
+
+class Votes:
+    """
+    Represents a game's votes.
+    """
+    def __init__(self, votes_data):
+        self.up_votes = votes_data["upVotes"]
+        self.down_votes = votes_data["downVotes"]
+
+
+class Game:
+    """
+    Represents a Roblox game universe.
+    This class represents multiple game-related endpoints.
+    """
+    def __init__(self, requests, universe_id):
+        self.id = universe_id
+        self.requests = requests
+        self.name = None
+        self.description = None
+        self.creator = None
+        self.price = None
+        self.allowed_gear_genres = None
+        self.allowed_gear_categories = None
+        self.max_players = None
+        self.studio_access_to_apis_allowed = None
+        self.create_vip_servers_allowed = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the game's information.
+        """
+        game_info_req = self.requests.get(
+            url=endpoint + "v1/games",
+            params={
+                "universeIds": str(self.id)
+            }
+        )
+        game_info = game_info_req.json()
+        game_info = game_info["data"][0]
+        self.name = game_info["name"]
+        self.description = game_info["description"]
+        if game_info["creator"]["type"] == "User":
+            self.creator = User(self.requests, game_info["creator"]["id"])
+        elif game_info["creator"]["type"] == "Group":
+            self.creator = Group(self.requests, game_info["creator"]["id"])
+        self.price = game_info["price"]
+        self.allowed_gear_genres = game_info["allowedGearGenres"]
+        self.allowed_gear_categories = game_info["allowedGearCategories"]
+        self.max_players = game_info["maxPlayers"]
+        self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
+        self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
+
+    def get_votes(self):
+        """
+        :return: An instance of Votes
+        """
+        votes_info_req = self.requests.get(
+            url=endpoint + "v1/games/votes",
+            params={
+                "universeIds": str(self.id)
+            }
+        )
+        votes_info = votes_info_req.json()
+        votes_info = votes_info["data"][0]
+        votes = Votes(votes_info)
+        return votes
+
+    def get_badges(self):
+        """
+        Gets the game's badges.
+        This will be updated soon to use the new Page object.
+        """
+        badges_req = self.requests.get(
+            url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
+            params={
+                "limit": 100,
+                "sortOrder": "Asc"
+            }
+        )
+        badges_data = badges_req.json()["data"]
+        badges = []
+        for badge in badges_data:
+            badges.append(Badge(self.requests, badge["id"]))
+        return badges
+
+
+"""
+def place_id_to_universe_id(place_id):
+    \"""
+    Returns the containing universe ID of a place ID.
+    :param place_id: Place ID
+    :return: Universe ID
+    \"""
+    universe_id_req = self.requests.get(
+        url="https://api.roblox.com/universes/get-universe-containing-place",
+        params={
+            "placeId": place_id
+        }
+    )
+    universe_id = universe_id_req.json()["UniverseId"]
+    return universe_id
+
+
+def game_from_place_id(place_id):
+    \"""
+    Generates an instance of Game with a place ID instead of a game ID.
+    :param place_id: Place ID
+    :return: Instace of Game
+    \"""
+    return Game(self.requests, place_id_to_universe_id(place_id))
+"""
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Game +(requests, universe_id) +
+
+

Represents a Roblox game universe. +This class represents multiple game-related endpoints.

+
+ +Expand source code + +
class Game:
+    """
+    Represents a Roblox game universe.
+    This class represents multiple game-related endpoints.
+    """
+    def __init__(self, requests, universe_id):
+        self.id = universe_id
+        self.requests = requests
+        self.name = None
+        self.description = None
+        self.creator = None
+        self.price = None
+        self.allowed_gear_genres = None
+        self.allowed_gear_categories = None
+        self.max_players = None
+        self.studio_access_to_apis_allowed = None
+        self.create_vip_servers_allowed = None
+        self.update()
+
+    def update(self):
+        """
+        Updates the game's information.
+        """
+        game_info_req = self.requests.get(
+            url=endpoint + "v1/games",
+            params={
+                "universeIds": str(self.id)
+            }
+        )
+        game_info = game_info_req.json()
+        game_info = game_info["data"][0]
+        self.name = game_info["name"]
+        self.description = game_info["description"]
+        if game_info["creator"]["type"] == "User":
+            self.creator = User(self.requests, game_info["creator"]["id"])
+        elif game_info["creator"]["type"] == "Group":
+            self.creator = Group(self.requests, game_info["creator"]["id"])
+        self.price = game_info["price"]
+        self.allowed_gear_genres = game_info["allowedGearGenres"]
+        self.allowed_gear_categories = game_info["allowedGearCategories"]
+        self.max_players = game_info["maxPlayers"]
+        self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
+        self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
+
+    def get_votes(self):
+        """
+        :return: An instance of Votes
+        """
+        votes_info_req = self.requests.get(
+            url=endpoint + "v1/games/votes",
+            params={
+                "universeIds": str(self.id)
+            }
+        )
+        votes_info = votes_info_req.json()
+        votes_info = votes_info["data"][0]
+        votes = Votes(votes_info)
+        return votes
+
+    def get_badges(self):
+        """
+        Gets the game's badges.
+        This will be updated soon to use the new Page object.
+        """
+        badges_req = self.requests.get(
+            url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
+            params={
+                "limit": 100,
+                "sortOrder": "Asc"
+            }
+        )
+        badges_data = badges_req.json()["data"]
+        badges = []
+        for badge in badges_data:
+            badges.append(Badge(self.requests, badge["id"]))
+        return badges
+
+

Methods

+
+
+def get_badges(self) +
+
+

Gets the game's badges. +This will be updated soon to use the new Page object.

+
+ +Expand source code + +
def get_badges(self):
+    """
+    Gets the game's badges.
+    This will be updated soon to use the new Page object.
+    """
+    badges_req = self.requests.get(
+        url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
+        params={
+            "limit": 100,
+            "sortOrder": "Asc"
+        }
+    )
+    badges_data = badges_req.json()["data"]
+    badges = []
+    for badge in badges_data:
+        badges.append(Badge(self.requests, badge["id"]))
+    return badges
+
+
+
+def get_votes(self) +
+
+

:return: An instance of Votes

+
+ +Expand source code + +
def get_votes(self):
+    """
+    :return: An instance of Votes
+    """
+    votes_info_req = self.requests.get(
+        url=endpoint + "v1/games/votes",
+        params={
+            "universeIds": str(self.id)
+        }
+    )
+    votes_info = votes_info_req.json()
+    votes_info = votes_info["data"][0]
+    votes = Votes(votes_info)
+    return votes
+
+
+
+def update(self) +
+
+

Updates the game's information.

+
+ +Expand source code + +
def update(self):
+    """
+    Updates the game's information.
+    """
+    game_info_req = self.requests.get(
+        url=endpoint + "v1/games",
+        params={
+            "universeIds": str(self.id)
+        }
+    )
+    game_info = game_info_req.json()
+    game_info = game_info["data"][0]
+    self.name = game_info["name"]
+    self.description = game_info["description"]
+    if game_info["creator"]["type"] == "User":
+        self.creator = User(self.requests, game_info["creator"]["id"])
+    elif game_info["creator"]["type"] == "Group":
+        self.creator = Group(self.requests, game_info["creator"]["id"])
+    self.price = game_info["price"]
+    self.allowed_gear_genres = game_info["allowedGearGenres"]
+    self.allowed_gear_categories = game_info["allowedGearCategories"]
+    self.max_players = game_info["maxPlayers"]
+    self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
+    self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
+
+
+
+
+
+class Votes +(votes_data) +
+
+

Represents a game's votes.

+
+ +Expand source code + +
class Votes:
+    """
+    Represents a game's votes.
+    """
+    def __init__(self, votes_data):
+        self.up_votes = votes_data["upVotes"]
+        self.down_votes = votes_data["downVotes"]
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/gender.html b/docs/ro_py/gender.html new file mode 100644 index 00000000..e1b3511e --- /dev/null +++ b/docs/ro_py/gender.html @@ -0,0 +1,131 @@ + + + + + + +ro_py.gender API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.gender

+
+
+

I hate how Roblox stores gender at all, it's really strange as it's not used for anything. +There's literally no point in storing this information.

+
+ +Expand source code + +
"""
+
+I hate how Roblox stores gender at all, it's really strange as it's not used for anything.
+There's literally no point in storing this information.
+
+"""
+
+import enum
+
+
+class RobloxGender(enum.Enum):
+    """
+    Represents the gender of the authenticated Roblox client.
+    """
+    Other = 1
+    Female = 2
+    Male = 3
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RobloxGender +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents the gender of the authenticated Roblox client.

+
+ +Expand source code + +
class RobloxGender(enum.Enum):
+    """
+    Represents the gender of the authenticated Roblox client.
+    """
+    Other = 1
+    Female = 2
+    Male = 3
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var Female
+
+
+
+
var Male
+
+
+
+
var Other
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/groups.html b/docs/ro_py/groups.html new file mode 100644 index 00000000..2c1ddb77 --- /dev/null +++ b/docs/ro_py/groups.html @@ -0,0 +1,261 @@ + + + + + + +ro_py.groups API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.groups

+
+
+

This file houses functions and classes that pertain to Roblox groups.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox groups.
+
+"""
+
+from ro_py.users import User
+
+endpoint = "https://groups.roblox.com/"
+
+
+class Shout:
+    """
+    Represents a group shout.
+    """
+    def __init__(self, requests, shout_data):
+        self.body = shout_data["body"]
+        self.poster = User(requests, shout_data["poster"]["userId"])
+
+
+class Group:
+    """
+    Represents a group.
+    """
+    def __init__(self, requests, group_id):
+        self.requests = requests
+        self.id = group_id
+
+        self.name = None
+        self.description = None
+        self.owner = None
+        self.member_count = None
+        self.is_builders_club_only = None
+        self.public_entry_allowed = None
+        self.shout = None
+
+        self.update()
+
+    def update(self):
+        """
+        Updates the group's information.
+        """
+        group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}")
+        group_info = group_info_req.json()
+        self.name = group_info["name"]
+        self.description = group_info["description"]
+        self.owner = User(self.requests, group_info["owner"]["userId"])
+        self.member_count = group_info["memberCount"]
+        self.is_builders_club_only = group_info["isBuildersClubOnly"]
+        self.public_entry_allowed = group_info["publicEntryAllowed"]
+        if "shout" in group_info:
+            self.shout = group_info["shout"]
+        else:
+            self.shout = None
+        # self.is_locked = group_info["isLocked"]
+
+    def update_shout(self, message):
+        self.requests.patch(
+            url=f"https://groups.roblox.com/v1/groups/{self.id}/status",
+            data={
+                "message": message
+            }
+        )
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Group +(requests, group_id) +
+
+

Represents a group.

+
+ +Expand source code + +
class Group:
+    """
+    Represents a group.
+    """
+    def __init__(self, requests, group_id):
+        self.requests = requests
+        self.id = group_id
+
+        self.name = None
+        self.description = None
+        self.owner = None
+        self.member_count = None
+        self.is_builders_club_only = None
+        self.public_entry_allowed = None
+        self.shout = None
+
+        self.update()
+
+    def update(self):
+        """
+        Updates the group's information.
+        """
+        group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}")
+        group_info = group_info_req.json()
+        self.name = group_info["name"]
+        self.description = group_info["description"]
+        self.owner = User(self.requests, group_info["owner"]["userId"])
+        self.member_count = group_info["memberCount"]
+        self.is_builders_club_only = group_info["isBuildersClubOnly"]
+        self.public_entry_allowed = group_info["publicEntryAllowed"]
+        if "shout" in group_info:
+            self.shout = group_info["shout"]
+        else:
+            self.shout = None
+        # self.is_locked = group_info["isLocked"]
+
+    def update_shout(self, message):
+        self.requests.patch(
+            url=f"https://groups.roblox.com/v1/groups/{self.id}/status",
+            data={
+                "message": message
+            }
+        )
+
+

Methods

+
+
+def update(self) +
+
+

Updates the group's information.

+
+ +Expand source code + +
def update(self):
+    """
+    Updates the group's information.
+    """
+    group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}")
+    group_info = group_info_req.json()
+    self.name = group_info["name"]
+    self.description = group_info["description"]
+    self.owner = User(self.requests, group_info["owner"]["userId"])
+    self.member_count = group_info["memberCount"]
+    self.is_builders_club_only = group_info["isBuildersClubOnly"]
+    self.public_entry_allowed = group_info["publicEntryAllowed"]
+    if "shout" in group_info:
+        self.shout = group_info["shout"]
+    else:
+        self.shout = None
+
+
+
+def update_shout(self, message) +
+
+
+
+ +Expand source code + +
def update_shout(self, message):
+    self.requests.patch(
+        url=f"https://groups.roblox.com/v1/groups/{self.id}/status",
+        data={
+            "message": message
+        }
+    )
+
+
+
+
+
+class Shout +(requests, shout_data) +
+
+

Represents a group shout.

+
+ +Expand source code + +
class Shout:
+    """
+    Represents a group shout.
+    """
+    def __init__(self, requests, shout_data):
+        self.body = shout_data["body"]
+        self.poster = User(requests, shout_data["poster"]["userId"])
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/index.html b/docs/ro_py/index.html new file mode 100644 index 00000000..1506cde9 --- /dev/null +++ b/docs/ro_py/index.html @@ -0,0 +1,171 @@ + + + + + + +ro_py API documentation + + + + + + + + + + + +
+
+
+

Package ro_py

+
+
+

ro.py +by jmkdev

+

Welcome to ro.py! +ro.py is a powerful wrapper for the Roblox web API. +It can be used to create (almost) anything from chat bots to group management systems. +ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at

+
+ +Expand source code + +
"""
+
+ro.py
+by jmkdev
+
+Welcome to ro.py!
+ro.py is a powerful wrapper for the Roblox web API.
+It can be used to create (almost) anything from chat bots to group management systems.
+ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at
+
+"""
+
+
+
+

Sub-modules

+
+
ro_py.accountinformation
+
+

This file houses functions and classes that pertain to Roblox authenticated user account information.

+
+
ro_py.accountsettings
+
+

This file houses functions and classes that pertain to Roblox client .

+
+
ro_py.assets
+
+

This file houses functions and classes that pertain to Roblox assets.

+
+
ro_py.badges
+
+

This file houses functions and classes that pertain to game-awarded badges.

+
+
ro_py.catalog
+
+

This file houses functions and classes that pertain to the Roblox catalog.

+
+
ro_py.chat
+
+

This file houses functions and classes that pertain to chatting and messaging.

+
+
ro_py.client
+
+

This file houses functions and classes that represent the core Roblox web client.

+
+
ro_py.economy
+
+

This file houses functions and classes that pertain to the Roblox economy endpoints.

+
+
ro_py.games
+
+

This file houses functions and classes that pertain to Roblox universes and places.

+
+
ro_py.gender
+
+

I hate how Roblox stores gender at all, it's really strange as it's not used for anything. +There's literally no point in storing this information.

+
+
ro_py.groups
+
+

This file houses functions and classes that pertain to Roblox groups.

+
+
ro_py.notifications
+
+

This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger +notification menu on the Roblox web …

+
+
ro_py.robloxbadges
+
+

This file houses functions and classes that pertain to Roblox-awarded badges.

+
+
ro_py.robloxstatus
+
+

This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) +I don't know if this is really that useful, but I …

+
+
ro_py.thumbnails
+
+

This file houses functions and classes that pertain to Roblox icons and thumbnails.

+
+
ro_py.trades
+
+

This file houses functions and classes that pertain to Roblox trades and trading.

+
+
ro_py.users
+
+

This file houses functions and classes that pertain to Roblox users and profiles.

+
+
ro_py.utilities
+
+

This folder houses utilities that are used internally for ro.py.

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/notifications.html b/docs/ro_py/notifications.html new file mode 100644 index 00000000..20f01d68 --- /dev/null +++ b/docs/ro_py/notifications.html @@ -0,0 +1,372 @@ + + + + + + +ro_py.notifications API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.notifications

+
+
+

This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger +notification menu on the Roblox web client.

+
+

Warning

+

This part of ro.py may have bugs and I don't recommend relying on it for daily use. +Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond +to Roblox chat messages, which is pretty neat.

+
+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger
+notification menu on the Roblox web client.
+
+.. warning::
+    This part of ro.py may have bugs and I don't recommend relying on it for daily use.
+    Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond
+    to Roblox chat messages, which is pretty neat.
+"""
+
+from ro_py.utilities.caseconvert import to_snake_case
+
+from signalrcore.hub_connection_builder import HubConnectionBuilder
+from urllib.parse import quote
+import json
+import logging
+
+
+class Notification:
+    """
+    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
+    """
+
+    def __init__(self, notification_data):
+        self.identifier = notification_data["C"]
+        self.hub = notification_data["M"][0]["H"]
+        self.type = None
+        self.rtype = notification_data["M"][0]["M"]
+        self.atype = notification_data["M"][0]["A"][0]
+        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
+        self.data = None
+
+        if isinstance(self.raw_data, dict):
+            self.data = {}
+            for key, value in self.raw_data.items():
+                self.data[to_snake_case(key)] = value
+
+            if "type" in self.data:
+                self.type = self.data["type"]
+            elif "Type" in self.data:
+                self.type = self.data["Type"]
+
+        elif isinstance(self.raw_data, list):
+            self.data = []
+            for value in self.raw_data:
+                self.data.append(value)
+
+            if len(self.data) > 0:
+                if "type" in self.data[0]:
+                    self.type = self.data[0]["type"]
+                elif "Type" in self.data[0]:
+                    self.type = self.data[0]["Type"]
+
+
+class NotificationReceiver:
+    """
+    This object is used to receive notifications.
+    This should only be generated once per client as to not duplicate notifications.
+    """
+
+    def __init__(self, requests, on_open, on_close, on_error, on_notification):
+        self.requests = requests
+
+        self.on_open = on_open
+        self.on_close = on_close
+        self.on_error = on_error
+        self.on_notification = on_notification
+
+        self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"]
+        self.negotiate_request = self.requests.get(
+            url="https://realtime.roblox.com/notifications/negotiate"
+                "?clientProtocol=1.5"
+                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
+            cookies={
+                ".ROBLOSECURITY": self.roblosecurity
+            }
+        )
+        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
+                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
+                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
+        self.connection = HubConnectionBuilder()
+        self.connection.with_url(
+            self.wss_url,
+            options={
+                "headers": {
+                    "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
+                },
+                "skip_negotiation": False
+            }
+        )
+
+        def on_message(_self, raw_notification):
+            """
+            Internal callback when a message is received.
+            """
+            try:
+                notification_json = json.loads(raw_notification)
+            except json.decoder.JSONDecodeError:
+                return
+            if len(notification_json) > 0:
+                notification = Notification(notification_json)
+                self.on_notification(notification)
+                logging.debug(
+                    f"""Notification:
+Type: {notification.type}
+Data: {notification.data}"""
+                )
+            else:
+                return
+
+        self.connection.with_automatic_reconnect({
+            "type": "raw",
+            "keep_alive_interval": 10,
+            "reconnect_interval": 5,
+            "max_attempts": 5
+        }).build()
+
+        if self.on_open:
+            self.connection.on_open(self.on_open)
+        if self.on_close:
+            self.connection.on_close(self.on_close)
+        if self.on_error:
+            self.connection.on_error(self.on_error)
+        self.connection.hub.on_message = on_message
+
+        self.connection.start()
+
+    def close(self):
+        """
+        Closes the connection and stops receiving notifications.
+        """
+        self.connection.stop()
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Notification +(notification_data) +
+
+

Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.

+
+ +Expand source code + +
class Notification:
+    """
+    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
+    """
+
+    def __init__(self, notification_data):
+        self.identifier = notification_data["C"]
+        self.hub = notification_data["M"][0]["H"]
+        self.type = None
+        self.rtype = notification_data["M"][0]["M"]
+        self.atype = notification_data["M"][0]["A"][0]
+        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
+        self.data = None
+
+        if isinstance(self.raw_data, dict):
+            self.data = {}
+            for key, value in self.raw_data.items():
+                self.data[to_snake_case(key)] = value
+
+            if "type" in self.data:
+                self.type = self.data["type"]
+            elif "Type" in self.data:
+                self.type = self.data["Type"]
+
+        elif isinstance(self.raw_data, list):
+            self.data = []
+            for value in self.raw_data:
+                self.data.append(value)
+
+            if len(self.data) > 0:
+                if "type" in self.data[0]:
+                    self.type = self.data[0]["type"]
+                elif "Type" in self.data[0]:
+                    self.type = self.data[0]["Type"]
+
+
+
+class NotificationReceiver +(requests, on_open, on_close, on_error, on_notification) +
+
+

This object is used to receive notifications. +This should only be generated once per client as to not duplicate notifications.

+
+ +Expand source code + +
class NotificationReceiver:
+    """
+    This object is used to receive notifications.
+    This should only be generated once per client as to not duplicate notifications.
+    """
+
+    def __init__(self, requests, on_open, on_close, on_error, on_notification):
+        self.requests = requests
+
+        self.on_open = on_open
+        self.on_close = on_close
+        self.on_error = on_error
+        self.on_notification = on_notification
+
+        self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"]
+        self.negotiate_request = self.requests.get(
+            url="https://realtime.roblox.com/notifications/negotiate"
+                "?clientProtocol=1.5"
+                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
+            cookies={
+                ".ROBLOSECURITY": self.roblosecurity
+            }
+        )
+        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
+                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
+                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
+        self.connection = HubConnectionBuilder()
+        self.connection.with_url(
+            self.wss_url,
+            options={
+                "headers": {
+                    "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
+                },
+                "skip_negotiation": False
+            }
+        )
+
+        def on_message(_self, raw_notification):
+            """
+            Internal callback when a message is received.
+            """
+            try:
+                notification_json = json.loads(raw_notification)
+            except json.decoder.JSONDecodeError:
+                return
+            if len(notification_json) > 0:
+                notification = Notification(notification_json)
+                self.on_notification(notification)
+                logging.debug(
+                    f"""Notification:
+Type: {notification.type}
+Data: {notification.data}"""
+                )
+            else:
+                return
+
+        self.connection.with_automatic_reconnect({
+            "type": "raw",
+            "keep_alive_interval": 10,
+            "reconnect_interval": 5,
+            "max_attempts": 5
+        }).build()
+
+        if self.on_open:
+            self.connection.on_open(self.on_open)
+        if self.on_close:
+            self.connection.on_close(self.on_close)
+        if self.on_error:
+            self.connection.on_error(self.on_error)
+        self.connection.hub.on_message = on_message
+
+        self.connection.start()
+
+    def close(self):
+        """
+        Closes the connection and stops receiving notifications.
+        """
+        self.connection.stop()
+
+

Methods

+
+
+def close(self) +
+
+

Closes the connection and stops receiving notifications.

+
+ +Expand source code + +
def close(self):
+    """
+    Closes the connection and stops receiving notifications.
+    """
+    self.connection.stop()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/robloxbadges.html b/docs/ro_py/robloxbadges.html new file mode 100644 index 00000000..7046e02a --- /dev/null +++ b/docs/ro_py/robloxbadges.html @@ -0,0 +1,112 @@ + + + + + + +ro_py.robloxbadges API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.robloxbadges

+
+
+

This file houses functions and classes that pertain to Roblox-awarded badges.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox-awarded badges.
+
+"""
+
+
+class RobloxBadge:
+    """
+    Represents a Roblox badge.
+    This is not equivalent to a badge you would earn from a game.
+    This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.
+    """
+    def __init__(self, roblox_badge_data):
+        self.id = roblox_badge_data["id"]
+        self.name = roblox_badge_data["name"]
+        self.description = roblox_badge_data["description"]
+        self.image_url = roblox_badge_data["imageUrl"]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RobloxBadge +(roblox_badge_data) +
+
+

Represents a Roblox badge. +This is not equivalent to a badge you would earn from a game. +This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.

+
+ +Expand source code + +
class RobloxBadge:
+    """
+    Represents a Roblox badge.
+    This is not equivalent to a badge you would earn from a game.
+    This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.
+    """
+    def __init__(self, roblox_badge_data):
+        self.id = roblox_badge_data["id"]
+        self.name = roblox_badge_data["name"]
+        self.description = roblox_badge_data["description"]
+        self.image_url = roblox_badge_data["imageUrl"]
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/robloxstatus.html b/docs/ro_py/robloxstatus.html new file mode 100644 index 00000000..f212626a --- /dev/null +++ b/docs/ro_py/robloxstatus.html @@ -0,0 +1,242 @@ + + + + + + +ro_py.robloxstatus API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.robloxstatus

+
+
+

This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) +I don't know if this is really that useful, but I was able to find the status API endpoint by looking in the status +page source and some of the status.io documentation.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com)
+I don't know if this is really that useful, but I was able to find the status API endpoint by looking in the status
+page source and some of the status.io documentation.
+
+"""
+
+import iso8601
+
+endpoint = "https://4277980205320394.hostedstatus.com/1.0/status/59db90dbcdeb2f04dadcf16d"
+
+
+class RobloxStatusContainer:
+    """
+    Represents a tab or item in a tab on the Roblox status site.
+    The tab items are internally called "containers" so that's what I call them here.
+    I don't see any difference between the data in tabs and data in containers, so I use the same object here.
+    """
+    def __init__(self, container_data):
+        self.id = container_data["id"]
+        self.name = container_data["name"]
+        self.updated = iso8601.parse_date(container_data["updated"])
+        self.status = container_data["status"]
+        self.status_code = container_data["status_code"]
+
+
+class RobloxStatusOverall:
+    """
+    Represents the overall status on the Roblox status site.
+    """
+    def __init__(self, overall_data):
+        self.updated = iso8601.parse_date(overall_data["updated"])
+        self.status = overall_data["status"]
+        self.status_code = overall_data["status_code"]
+
+
+class RobloxStatus:
+    def __init__(self, requests):
+        self.requests = requests
+
+        self.overall = None
+        self.user = None
+        self.player = None
+        self.creator = None
+
+        self.update()
+
+    def update(self):
+        status_req = self.requests.get(
+            url=endpoint
+        )
+        status_data = status_req.json()["result"]
+
+        self.overall = RobloxStatusOverall(status_data["status_overall"])
+        self.user = RobloxStatusContainer(status_data["status"][0])
+        self.player = RobloxStatusContainer(status_data["status"][1])
+        self.creator = RobloxStatusContainer(status_data["status"][2])
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RobloxStatus +(requests) +
+
+
+
+ +Expand source code + +
class RobloxStatus:
+    def __init__(self, requests):
+        self.requests = requests
+
+        self.overall = None
+        self.user = None
+        self.player = None
+        self.creator = None
+
+        self.update()
+
+    def update(self):
+        status_req = self.requests.get(
+            url=endpoint
+        )
+        status_data = status_req.json()["result"]
+
+        self.overall = RobloxStatusOverall(status_data["status_overall"])
+        self.user = RobloxStatusContainer(status_data["status"][0])
+        self.player = RobloxStatusContainer(status_data["status"][1])
+        self.creator = RobloxStatusContainer(status_data["status"][2])
+
+

Methods

+
+
+def update(self) +
+
+
+
+ +Expand source code + +
def update(self):
+    status_req = self.requests.get(
+        url=endpoint
+    )
+    status_data = status_req.json()["result"]
+
+    self.overall = RobloxStatusOverall(status_data["status_overall"])
+    self.user = RobloxStatusContainer(status_data["status"][0])
+    self.player = RobloxStatusContainer(status_data["status"][1])
+    self.creator = RobloxStatusContainer(status_data["status"][2])
+
+
+
+
+
+class RobloxStatusContainer +(container_data) +
+
+

Represents a tab or item in a tab on the Roblox status site. +The tab items are internally called "containers" so that's what I call them here. +I don't see any difference between the data in tabs and data in containers, so I use the same object here.

+
+ +Expand source code + +
class RobloxStatusContainer:
+    """
+    Represents a tab or item in a tab on the Roblox status site.
+    The tab items are internally called "containers" so that's what I call them here.
+    I don't see any difference between the data in tabs and data in containers, so I use the same object here.
+    """
+    def __init__(self, container_data):
+        self.id = container_data["id"]
+        self.name = container_data["name"]
+        self.updated = iso8601.parse_date(container_data["updated"])
+        self.status = container_data["status"]
+        self.status_code = container_data["status_code"]
+
+
+
+class RobloxStatusOverall +(overall_data) +
+
+

Represents the overall status on the Roblox status site.

+
+ +Expand source code + +
class RobloxStatusOverall:
+    """
+    Represents the overall status on the Roblox status site.
+    """
+    def __init__(self, overall_data):
+        self.updated = iso8601.parse_date(overall_data["updated"])
+        self.status = overall_data["status"]
+        self.status_code = overall_data["status_code"]
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/thumbnails.html b/docs/ro_py/thumbnails.html new file mode 100644 index 00000000..b4e23b20 --- /dev/null +++ b/docs/ro_py/thumbnails.html @@ -0,0 +1,424 @@ + + + + + + +ro_py.thumbnails API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.thumbnails

+
+
+

This file houses functions and classes that pertain to Roblox icons and thumbnails.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox icons and thumbnails.
+
+"""
+
+from ro_py.utilities.errors import InvalidShotTypeError
+
+endpoint = "https://thumbnails.roblox.com/"
+
+# TODO: turn these into enums
+PlaceHolder = "PlaceHolder"
+AutoGenerated = "AutoGenerated"
+ForceAutoGenerated = "ForceAutoGenerated"
+
+AvatarFullBody = 0
+AvatarBust = 1
+AvatarHeadshot = 2
+
+size_30x30 = "30x30"
+size_42x42 = "42x42"
+size_48x48 = "48x48"
+size_50x50 = "50x50"
+size_60x62 = "60x62"
+size_75x75 = "75x75"
+size_110x110 = "110x110"
+size_128x128 = "128x128"
+size_140x140 = "140x140"
+size_150x150 = "150x150"
+size_160x100 = "160x100"
+size_250x250 = "250x250"
+size_256x144 = "256x144"
+size_256x256 = "256x256"
+size_300x250 = "300x240"
+size_304x166 = "304x166"
+size_384x216 = "384x216"
+size_396x216 = "396x216"
+size_420x420 = "420x420"
+size_480x270 = "480x270"
+size_512x512 = "512x512"
+size_576x324 = "576x324"
+size_720x720 = "720x720"
+size_768x432 = "768x432"
+
+format_png = "Png"
+format_jpg = "Jpeg"
+format_jpeg = "Jpeg"
+
+
+class ThumbnailGenerator:
+    """
+    This object is used to generate thumbnails.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
+        """
+        Gets a group's icon.
+        :param group: The group.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        group_icon_req = self.requests.get(
+            url=endpoint + "v1/groups/icons",
+            params={
+                "groupIds": str(group.id),
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        group_icon = group_icon_req.json()["data"][0]["imageUrl"]
+        return group_icon
+
+    def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
+        """
+        Gets a game's icon.
+        :param game: The game.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        game_icon_req = self.requests.get(
+            url=endpoint + "v1/games/icons",
+            params={
+                "universeIds": str(game.id),
+                "returnPolicy": PlaceHolder,
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        game_icon = game_icon_req.json()["data"][0]["imageUrl"]
+        return game_icon
+
+    def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
+        """
+        Gets a full body, bust, or headshot image of a user.
+        :param user: User to use for avatar.
+        :param shot_type: Type of shot.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        shot_endpoint = endpoint + "v1/users/"
+        if shot_type == AvatarFullBody:
+            shot_endpoint = shot_endpoint + "avatar"
+            size = size or size_30x30
+        elif shot_type == AvatarBust:
+            shot_endpoint = shot_endpoint + "avatar-bust"
+            size = size or size_50x50
+        elif shot_type == AvatarHeadshot:
+            size = size or size_48x48
+            shot_endpoint = shot_endpoint + "avatar-headshot"
+        else:
+            raise InvalidShotTypeError("Invalid shot type.")
+        shot_req = self.requests.get(
+            url=shot_endpoint,
+            params={
+                "userIds": str(user.id),
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        return shot_req.json()["data"][0]["imageUrl"]
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ThumbnailGenerator +(requests) +
+
+

This object is used to generate thumbnails.

+
+ +Expand source code + +
class ThumbnailGenerator:
+    """
+    This object is used to generate thumbnails.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
+        """
+        Gets a group's icon.
+        :param group: The group.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        group_icon_req = self.requests.get(
+            url=endpoint + "v1/groups/icons",
+            params={
+                "groupIds": str(group.id),
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        group_icon = group_icon_req.json()["data"][0]["imageUrl"]
+        return group_icon
+
+    def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
+        """
+        Gets a game's icon.
+        :param game: The game.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        game_icon_req = self.requests.get(
+            url=endpoint + "v1/games/icons",
+            params={
+                "universeIds": str(game.id),
+                "returnPolicy": PlaceHolder,
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        game_icon = game_icon_req.json()["data"][0]["imageUrl"]
+        return game_icon
+
+    def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
+        """
+        Gets a full body, bust, or headshot image of a user.
+        :param user: User to use for avatar.
+        :param shot_type: Type of shot.
+        :param size: The thumbnail size, formatted widthxheight.
+        :param file_format: The thumbnail format
+        :param is_circular: The circle thumbnail output parameter.
+        :return: Image URL
+        """
+        shot_endpoint = endpoint + "v1/users/"
+        if shot_type == AvatarFullBody:
+            shot_endpoint = shot_endpoint + "avatar"
+            size = size or size_30x30
+        elif shot_type == AvatarBust:
+            shot_endpoint = shot_endpoint + "avatar-bust"
+            size = size or size_50x50
+        elif shot_type == AvatarHeadshot:
+            size = size or size_48x48
+            shot_endpoint = shot_endpoint + "avatar-headshot"
+        else:
+            raise InvalidShotTypeError("Invalid shot type.")
+        shot_req = self.requests.get(
+            url=shot_endpoint,
+            params={
+                "userIds": str(user.id),
+                "size": size,
+                "file_format": file_format,
+                "isCircular": is_circular
+            }
+        )
+        return shot_req.json()["data"][0]["imageUrl"]
+
+

Methods

+
+
+def get_avatar_image(self, user, shot_type=0, size=None, file_format='Png', is_circular=False) +
+
+

Gets a full body, bust, or headshot image of a user. +:param user: User to use for avatar. +:param shot_type: Type of shot. +:param size: The thumbnail size, formatted widthxheight. +:param file_format: The thumbnail format +:param is_circular: The circle thumbnail output parameter. +:return: Image URL

+
+ +Expand source code + +
def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
+    """
+    Gets a full body, bust, or headshot image of a user.
+    :param user: User to use for avatar.
+    :param shot_type: Type of shot.
+    :param size: The thumbnail size, formatted widthxheight.
+    :param file_format: The thumbnail format
+    :param is_circular: The circle thumbnail output parameter.
+    :return: Image URL
+    """
+    shot_endpoint = endpoint + "v1/users/"
+    if shot_type == AvatarFullBody:
+        shot_endpoint = shot_endpoint + "avatar"
+        size = size or size_30x30
+    elif shot_type == AvatarBust:
+        shot_endpoint = shot_endpoint + "avatar-bust"
+        size = size or size_50x50
+    elif shot_type == AvatarHeadshot:
+        size = size or size_48x48
+        shot_endpoint = shot_endpoint + "avatar-headshot"
+    else:
+        raise InvalidShotTypeError("Invalid shot type.")
+    shot_req = self.requests.get(
+        url=shot_endpoint,
+        params={
+            "userIds": str(user.id),
+            "size": size,
+            "file_format": file_format,
+            "isCircular": is_circular
+        }
+    )
+    return shot_req.json()["data"][0]["imageUrl"]
+
+
+
+def get_game_icon(self, game, size='256x256', file_format='Png', is_circular=False) +
+
+

Gets a game's icon. +:param game: The game. +:param size: The thumbnail size, formatted widthxheight. +:param file_format: The thumbnail format +:param is_circular: The circle thumbnail output parameter. +:return: Image URL

+
+ +Expand source code + +
def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
+    """
+    Gets a game's icon.
+    :param game: The game.
+    :param size: The thumbnail size, formatted widthxheight.
+    :param file_format: The thumbnail format
+    :param is_circular: The circle thumbnail output parameter.
+    :return: Image URL
+    """
+    game_icon_req = self.requests.get(
+        url=endpoint + "v1/games/icons",
+        params={
+            "universeIds": str(game.id),
+            "returnPolicy": PlaceHolder,
+            "size": size,
+            "file_format": file_format,
+            "isCircular": is_circular
+        }
+    )
+    game_icon = game_icon_req.json()["data"][0]["imageUrl"]
+    return game_icon
+
+
+
+def get_group_icon(self, group, size='150x150', file_format='Png', is_circular=False) +
+
+

Gets a group's icon. +:param group: The group. +:param size: The thumbnail size, formatted widthxheight. +:param file_format: The thumbnail format +:param is_circular: The circle thumbnail output parameter. +:return: Image URL

+
+ +Expand source code + +
def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
+    """
+    Gets a group's icon.
+    :param group: The group.
+    :param size: The thumbnail size, formatted widthxheight.
+    :param file_format: The thumbnail format
+    :param is_circular: The circle thumbnail output parameter.
+    :return: Image URL
+    """
+    group_icon_req = self.requests.get(
+        url=endpoint + "v1/groups/icons",
+        params={
+            "groupIds": str(group.id),
+            "size": size,
+            "file_format": file_format,
+            "isCircular": is_circular
+        }
+    )
+    group_icon = group_icon_req.json()["data"][0]["imageUrl"]
+    return group_icon
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/trades.html b/docs/ro_py/trades.html new file mode 100644 index 00000000..c5c92174 --- /dev/null +++ b/docs/ro_py/trades.html @@ -0,0 +1,342 @@ + + + + + + +ro_py.trades API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.trades

+
+
+

This file houses functions and classes that pertain to Roblox trades and trading.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox trades and trading.
+
+"""
+
+from ro_py.utilities.pages import Pages, SortOrder
+from ro_py.users import User
+import iso8601
+import enum
+
+endpoint = "https://trades.roblox.com/"
+
+
+def trade_page_handler(requests, this_page):
+    trades_out = []
+    for raw_trade in this_page:
+        trades_out.append(Trade(requests, raw_trade["id"]))
+    return trades_out
+
+
+class Trade:
+    def __init__(self, requests, trade_id):
+        self.requests = requests
+        trade_req = self.requests.get(
+            url=endpoint + f"v1/trades/{trade_id}"
+        )
+        trade_data = trade_req.json()
+        self.id = trade_data["id"]
+        self.user = User(self.requests, trade_data["user"]["id"])
+        self.created = iso8601.parse_date(trade_data["created"])
+        self.is_active = trade_data["isActive"]
+        self.status = trade_data["status"]
+
+
+class TradeStatusType(enum.Enum):
+    """
+    Represents a trade status type.
+    """
+    Inbound = "Inbound"
+    Outbound = "Outbound"
+    Completed = "Completed"
+    Inactive = "Inactive"
+
+
+class TradesMetadata:
+    """
+    Represents trade system metadata at /v1/trades/metadata
+    """
+    def __init__(self, trades_metadata_data):
+        self.max_items_per_side = trades_metadata_data["maxItemsPerSide"]
+        self.min_value_ratio = trades_metadata_data["minValueRatio"]
+        self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"]
+        self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"]
+
+
+class TradesWrapper:
+    """
+    Represents the Roblox trades page.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10):
+        trades = Pages(
+            requests=self.requests,
+            url=endpoint + f"/v1/trades/{trade_status_type.value}",
+            sort_order=sort_order,
+            limit=limit,
+            handler=trade_page_handler
+        )
+        return trades
+
+    def send_trade(self):
+        pass
+
+
+
+
+
+
+
+

Functions

+
+
+def trade_page_handler(requests, this_page) +
+
+
+
+ +Expand source code + +
def trade_page_handler(requests, this_page):
+    trades_out = []
+    for raw_trade in this_page:
+        trades_out.append(Trade(requests, raw_trade["id"]))
+    return trades_out
+
+
+
+
+
+

Classes

+
+
+class Trade +(requests, trade_id) +
+
+
+
+ +Expand source code + +
class Trade:
+    def __init__(self, requests, trade_id):
+        self.requests = requests
+        trade_req = self.requests.get(
+            url=endpoint + f"v1/trades/{trade_id}"
+        )
+        trade_data = trade_req.json()
+        self.id = trade_data["id"]
+        self.user = User(self.requests, trade_data["user"]["id"])
+        self.created = iso8601.parse_date(trade_data["created"])
+        self.is_active = trade_data["isActive"]
+        self.status = trade_data["status"]
+
+
+
+class TradeStatusType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Represents a trade status type.

+
+ +Expand source code + +
class TradeStatusType(enum.Enum):
+    """
+    Represents a trade status type.
+    """
+    Inbound = "Inbound"
+    Outbound = "Outbound"
+    Completed = "Completed"
+    Inactive = "Inactive"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var Completed
+
+
+
+
var Inactive
+
+
+
+
var Inbound
+
+
+
+
var Outbound
+
+
+
+
+
+
+class TradesMetadata +(trades_metadata_data) +
+
+

Represents trade system metadata at /v1/trades/metadata

+
+ +Expand source code + +
class TradesMetadata:
+    """
+    Represents trade system metadata at /v1/trades/metadata
+    """
+    def __init__(self, trades_metadata_data):
+        self.max_items_per_side = trades_metadata_data["maxItemsPerSide"]
+        self.min_value_ratio = trades_metadata_data["minValueRatio"]
+        self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"]
+        self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"]
+
+
+
+class TradesWrapper +(requests) +
+
+

Represents the Roblox trades page.

+
+ +Expand source code + +
class TradesWrapper:
+    """
+    Represents the Roblox trades page.
+    """
+    def __init__(self, requests):
+        self.requests = requests
+
+    def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10):
+        trades = Pages(
+            requests=self.requests,
+            url=endpoint + f"/v1/trades/{trade_status_type.value}",
+            sort_order=sort_order,
+            limit=limit,
+            handler=trade_page_handler
+        )
+        return trades
+
+    def send_trade(self):
+        pass
+
+

Methods

+
+
+def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10) +
+
+
+
+ +Expand source code + +
def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10):
+    trades = Pages(
+        requests=self.requests,
+        url=endpoint + f"/v1/trades/{trade_status_type.value}",
+        sort_order=sort_order,
+        limit=limit,
+        handler=trade_page_handler
+    )
+    return trades
+
+
+
+def send_trade(self) +
+
+
+
+ +Expand source code + +
def send_trade(self):
+    pass
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/users.html b/docs/ro_py/users.html new file mode 100644 index 00000000..e1a25b72 --- /dev/null +++ b/docs/ro_py/users.html @@ -0,0 +1,440 @@ + + + + + + +ro_py.users API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.users

+
+
+

This file houses functions and classes that pertain to Roblox users and profiles.

+
+ +Expand source code + +
"""
+
+This file houses functions and classes that pertain to Roblox users and profiles.
+
+"""
+
+from ro_py.robloxbadges import RobloxBadge
+import iso8601
+
+endpoint = "https://users.roblox.com/"
+
+
+class User:
+    """
+    Represents a Roblox user and their profile.
+    Can be initialized with either a user ID or a username.
+    """
+    def __init__(self, requests, ui):
+
+        self.requests = requests
+        self.id = ui
+
+        self.description = None
+        self.created = None
+        self.is_banned = None
+        self.name = None
+        self.display_name = None
+
+        self.update()
+
+    def update(self):
+        """
+        Updates some class values.
+        :return: Nothing
+        """
+        user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}")
+        user_info = user_info_req.json()
+        self.description = user_info["description"]
+        self.created = iso8601.parse_date(user_info["created"])
+        self.is_banned = user_info["isBanned"]
+        self.name = user_info["name"]
+        self.display_name = user_info["displayName"]
+        # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
+        # self.has_premium = has_premium_req
+
+    def get_status(self):
+        """
+        Gets the user's status.
+        :return: A string
+        """
+        status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status")
+        return status_req.json()["status"]
+
+    def get_roblox_badges(self):
+        """
+        Gets the user's roblox badges.
+        :return: A list of RobloxBadge instances
+        """
+        roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
+        roblox_badges = []
+        for roblox_badge_data in roblox_badges_req.json():
+            roblox_badges.append(RobloxBadge(roblox_badge_data))
+        return roblox_badges
+
+    def get_friends_count(self):
+        """
+        Gets the user's friends count.
+        :return: An integer
+        """
+        friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
+        friends_count = friends_count_req.json()["count"]
+        return friends_count
+
+    def get_followers_count(self):
+        """
+        Gets the user's followers count.
+        :return: An integer
+        """
+        followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
+        followers_count = followers_count_req.json()["count"]
+        return followers_count
+
+    def get_followings_count(self):
+        """
+        Gets the user's followings count.
+        :return: An integer
+        """
+        followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
+        followings_count = followings_count_req.json()["count"]
+        return followings_count
+
+    def get_friends(self):
+        """
+        Gets the user's friends.
+        :return: A list of User instances.
+        """
+        friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
+        friends_raw = friends_req.json()["data"]
+        friends_list = []
+        for friend_raw in friends_raw:
+            friends_list.append(
+                User(self.requests, friend_raw["id"])
+            )
+        return friends_list
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class User +(requests, ui) +
+
+

Represents a Roblox user and their profile. +Can be initialized with either a user ID or a username.

+
+ +Expand source code + +
class User:
+    """
+    Represents a Roblox user and their profile.
+    Can be initialized with either a user ID or a username.
+    """
+    def __init__(self, requests, ui):
+
+        self.requests = requests
+        self.id = ui
+
+        self.description = None
+        self.created = None
+        self.is_banned = None
+        self.name = None
+        self.display_name = None
+
+        self.update()
+
+    def update(self):
+        """
+        Updates some class values.
+        :return: Nothing
+        """
+        user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}")
+        user_info = user_info_req.json()
+        self.description = user_info["description"]
+        self.created = iso8601.parse_date(user_info["created"])
+        self.is_banned = user_info["isBanned"]
+        self.name = user_info["name"]
+        self.display_name = user_info["displayName"]
+        # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
+        # self.has_premium = has_premium_req
+
+    def get_status(self):
+        """
+        Gets the user's status.
+        :return: A string
+        """
+        status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status")
+        return status_req.json()["status"]
+
+    def get_roblox_badges(self):
+        """
+        Gets the user's roblox badges.
+        :return: A list of RobloxBadge instances
+        """
+        roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
+        roblox_badges = []
+        for roblox_badge_data in roblox_badges_req.json():
+            roblox_badges.append(RobloxBadge(roblox_badge_data))
+        return roblox_badges
+
+    def get_friends_count(self):
+        """
+        Gets the user's friends count.
+        :return: An integer
+        """
+        friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
+        friends_count = friends_count_req.json()["count"]
+        return friends_count
+
+    def get_followers_count(self):
+        """
+        Gets the user's followers count.
+        :return: An integer
+        """
+        followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
+        followers_count = followers_count_req.json()["count"]
+        return followers_count
+
+    def get_followings_count(self):
+        """
+        Gets the user's followings count.
+        :return: An integer
+        """
+        followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
+        followings_count = followings_count_req.json()["count"]
+        return followings_count
+
+    def get_friends(self):
+        """
+        Gets the user's friends.
+        :return: A list of User instances.
+        """
+        friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
+        friends_raw = friends_req.json()["data"]
+        friends_list = []
+        for friend_raw in friends_raw:
+            friends_list.append(
+                User(self.requests, friend_raw["id"])
+            )
+        return friends_list
+
+

Methods

+
+
+def get_followers_count(self) +
+
+

Gets the user's followers count. +:return: An integer

+
+ +Expand source code + +
def get_followers_count(self):
+    """
+    Gets the user's followers count.
+    :return: An integer
+    """
+    followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
+    followers_count = followers_count_req.json()["count"]
+    return followers_count
+
+
+
+def get_followings_count(self) +
+
+

Gets the user's followings count. +:return: An integer

+
+ +Expand source code + +
def get_followings_count(self):
+    """
+    Gets the user's followings count.
+    :return: An integer
+    """
+    followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
+    followings_count = followings_count_req.json()["count"]
+    return followings_count
+
+
+
+def get_friends(self) +
+
+

Gets the user's friends. +:return: A list of User instances.

+
+ +Expand source code + +
def get_friends(self):
+    """
+    Gets the user's friends.
+    :return: A list of User instances.
+    """
+    friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
+    friends_raw = friends_req.json()["data"]
+    friends_list = []
+    for friend_raw in friends_raw:
+        friends_list.append(
+            User(self.requests, friend_raw["id"])
+        )
+    return friends_list
+
+
+
+def get_friends_count(self) +
+
+

Gets the user's friends count. +:return: An integer

+
+ +Expand source code + +
def get_friends_count(self):
+    """
+    Gets the user's friends count.
+    :return: An integer
+    """
+    friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
+    friends_count = friends_count_req.json()["count"]
+    return friends_count
+
+
+
+def get_roblox_badges(self) +
+
+

Gets the user's roblox badges. +:return: A list of RobloxBadge instances

+
+ +Expand source code + +
def get_roblox_badges(self):
+    """
+    Gets the user's roblox badges.
+    :return: A list of RobloxBadge instances
+    """
+    roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
+    roblox_badges = []
+    for roblox_badge_data in roblox_badges_req.json():
+        roblox_badges.append(RobloxBadge(roblox_badge_data))
+    return roblox_badges
+
+
+
+def get_status(self) +
+
+

Gets the user's status. +:return: A string

+
+ +Expand source code + +
def get_status(self):
+    """
+    Gets the user's status.
+    :return: A string
+    """
+    status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status")
+    return status_req.json()["status"]
+
+
+
+def update(self) +
+
+

Updates some class values. +:return: Nothing

+
+ +Expand source code + +
def update(self):
+    """
+    Updates some class values.
+    :return: Nothing
+    """
+    user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}")
+    user_info = user_info_req.json()
+    self.description = user_info["description"]
+    self.created = iso8601.parse_date(user_info["created"])
+    self.is_banned = user_info["isBanned"]
+    self.name = user_info["name"]
+    self.display_name = user_info["displayName"]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/asset_type.html b/docs/ro_py/utilities/asset_type.html new file mode 100644 index 00000000..5406f3d8 --- /dev/null +++ b/docs/ro_py/utilities/asset_type.html @@ -0,0 +1,116 @@ + + + + + + +ro_py.utilities.asset_type API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.asset_type

+
+
+

ro.py > asset_type.py

+

This file is a conversion table for asset type IDs to asset type names.

+
+ +Expand source code + +
"""
+
+ro.py > asset_type.py
+
+This file is a conversion table for asset type IDs to asset type names.
+
+"""
+
+asset_types = [
+    None,
+    "Image",
+    "TeeShirt",
+    "Audio",
+    "Mesh",
+    "Lua",
+    "Hat",
+    "Place",
+    "Model",
+    "Shirt",
+    "Pants",
+    "Decal",
+    "Head",
+    "Face",
+    "Gear",
+    "Badge",
+    "Animation",
+    "Torso",
+    "RightArm",
+    "LeftArm",
+    "LeftLeg",
+    "RightLeg",
+    "Package",
+    "GamePass",
+    "Plugin",
+    "MeshPart",
+    "HairAccessory",
+    "FaceAccessory",
+    "NeckAccessory",
+    "ShoulderAccessory",
+    "FrontAccesory",
+    "BackAccessory",
+    "WaistAccessory",
+    "ClimbAnimation",
+    "DeathAnimation",
+    "FallAnimation",
+    "IdleAnimation",
+    "JumpAnimation",
+    "RunAnimation",
+    "SwimAnimation",
+    "WalkAnimation",
+    "PoseAnimation",
+    "EarAccessory",
+    "EyeAccessory",
+    "EmoteAnimation",
+    "Video"
+]
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/cache.html b/docs/ro_py/utilities/cache.html new file mode 100644 index 00000000..f4cd45e7 --- /dev/null +++ b/docs/ro_py/utilities/cache.html @@ -0,0 +1,65 @@ + + + + + + +ro_py.utilities.cache API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.cache

+
+
+
+ +Expand source code + +
cache = {
+    "users": {},
+    "groups": {},
+    "games": {},
+    "assets": {},
+    "badges": {}
+}
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/caseconvert.html b/docs/ro_py/utilities/caseconvert.html new file mode 100644 index 00000000..232212ca --- /dev/null +++ b/docs/ro_py/utilities/caseconvert.html @@ -0,0 +1,86 @@ + + + + + + +ro_py.utilities.caseconvert API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.caseconvert

+
+
+
+ +Expand source code + +
import re
+
+pattern = re.compile(r'(?<!^)(?=[A-Z])')
+
+
+def to_snake_case(string):
+    return pattern.sub('_', string).lower()
+
+
+
+
+
+
+
+

Functions

+
+
+def to_snake_case(string) +
+
+
+
+ +Expand source code + +
def to_snake_case(string):
+    return pattern.sub('_', string).lower()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/errors.html b/docs/ro_py/utilities/errors.html new file mode 100644 index 00000000..b8da4ed1 --- /dev/null +++ b/docs/ro_py/utilities/errors.html @@ -0,0 +1,238 @@ + + + + + + +ro_py.utilities.errors API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.errors

+
+
+

ro.py > errors.py

+

This file houses custom exceptions unique to this module.

+
+ +Expand source code + +
"""
+
+ro.py > errors.py
+
+This file houses custom exceptions unique to this module.
+
+"""
+
+
+class NotLimitedError(Exception):
+    """Called when code attempts to read limited-only information."""
+    pass
+
+
+class InvalidIconSizeError(Exception):
+    """Called when code attempts to pass in an improper size to a thumbnail function."""
+    pass
+
+
+class InvalidShotTypeError(Exception):
+    """Called when code attempts to pass in an improper avatar image type to a thumbnail function."""
+    pass
+
+
+class ApiError(Exception):
+    """Called in requests when an API request fails."""
+    pass
+
+
+class ChatError(Exception):
+    """Called in chat when a chat action fails."""
+
+
+class InvalidPageError(Exception):
+    """Called when an invalid page is requested."""
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ApiError +(*args, **kwargs) +
+
+

Called in requests when an API request fails.

+
+ +Expand source code + +
class ApiError(Exception):
+    """Called in requests when an API request fails."""
+    pass
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class ChatError +(*args, **kwargs) +
+
+

Called in chat when a chat action fails.

+
+ +Expand source code + +
class ChatError(Exception):
+    """Called in chat when a chat action fails."""
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class InvalidIconSizeError +(*args, **kwargs) +
+
+

Called when code attempts to pass in an improper size to a thumbnail function.

+
+ +Expand source code + +
class InvalidIconSizeError(Exception):
+    """Called when code attempts to pass in an improper size to a thumbnail function."""
+    pass
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class InvalidPageError +(*args, **kwargs) +
+
+

Called when an invalid page is requested.

+
+ +Expand source code + +
class InvalidPageError(Exception):
+    """Called when an invalid page is requested."""
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class InvalidShotTypeError +(*args, **kwargs) +
+
+

Called when code attempts to pass in an improper avatar image type to a thumbnail function.

+
+ +Expand source code + +
class InvalidShotTypeError(Exception):
+    """Called when code attempts to pass in an improper avatar image type to a thumbnail function."""
+    pass
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class NotLimitedError +(*args, **kwargs) +
+
+

Called when code attempts to read limited-only information.

+
+ +Expand source code + +
class NotLimitedError(Exception):
+    """Called when code attempts to read limited-only information."""
+    pass
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/index.html b/docs/ro_py/utilities/index.html new file mode 100644 index 00000000..e1de3e3e --- /dev/null +++ b/docs/ro_py/utilities/index.html @@ -0,0 +1,101 @@ + + + + + + +ro_py.utilities API documentation + + + + + + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/pages.html b/docs/ro_py/utilities/pages.html new file mode 100644 index 00000000..4d39e4f0 --- /dev/null +++ b/docs/ro_py/utilities/pages.html @@ -0,0 +1,408 @@ + + + + + + +ro_py.utilities.pages API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.pages

+
+
+
+ +Expand source code + +
from ro_py.utilities.errors import InvalidPageError
+import enum
+
+
+class SortOrder(enum.Enum):
+    """
+    Order in which page data should load in.
+    """
+    Ascending = "Asc"
+    Descending = "Desc"
+
+
+class Page:
+    """
+    Represents a single page from a Pages object.
+    """
+    def __init__(self, requests, data, handler=None):
+        self.previous_page_cursor = data["previousPageCursor"]
+        """Cursor to navigate to the previous page."""
+        self.next_page_cursor = data["nextPageCursor"]
+        """Cursor to navigate to the next page."""
+
+        self.data = data["data"]
+        """Raw data from this page."""
+
+        if handler:
+            self.data = handler(requests, self.data)
+
+
+class Pages:
+    """
+    Represents a paged object.
+
+    !!! warning
+        This object is *slow*, especially with a custom handler.
+        Automatic page caching will be added in the future. It is suggested to
+        cache the pages yourself if speed is required.
+    """
+    def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None):
+        if extra_parameters is None:
+            extra_parameters = {}
+
+        self.handler = handler
+        """Function that is passed to Page as data handler."""
+
+        extra_parameters["sortOrder"] = sort_order.value
+        extra_parameters["limit"] = limit
+
+        self.parameters = extra_parameters
+        """Extra parameters for the request."""
+        self.requests = requests
+        """Requests object."""
+        self.url = url
+        """URL containing the paginated data, accessible with a GET request."""
+        self.page = 0
+        """Current page number."""
+
+        self.data = self._get_page()
+
+    def _get_page(self, cursor=None):
+        """
+        Gets a page at the specified cursor position.
+        """
+        this_parameters = self.parameters
+        if cursor:
+            this_parameters["cursor"] = cursor
+
+        page_req = self.requests.get(
+            url=self.url,
+            params=this_parameters
+        )
+        return Page(
+            requests=self.requests,
+            data=page_req.json(),
+            handler=self.handler
+        )
+
+    def previous(self):
+        """
+        Moves to the previous page.
+        """
+        if self.data.previous_page_cursor:
+            self.data = self._get_page(self.data.previous_page_cursor)
+        else:
+            raise InvalidPageError
+
+    def next(self):
+        """
+        Moves to the next page.
+        """
+        if self.data.next_page_cursor:
+            self.data = self._get_page(self.data.next_page_cursor)
+        else:
+            raise InvalidPageError
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Page +(requests, data, handler=None) +
+
+

Represents a single page from a Pages object.

+
+ +Expand source code + +
class Page:
+    """
+    Represents a single page from a Pages object.
+    """
+    def __init__(self, requests, data, handler=None):
+        self.previous_page_cursor = data["previousPageCursor"]
+        """Cursor to navigate to the previous page."""
+        self.next_page_cursor = data["nextPageCursor"]
+        """Cursor to navigate to the next page."""
+
+        self.data = data["data"]
+        """Raw data from this page."""
+
+        if handler:
+            self.data = handler(requests, self.data)
+
+

Instance variables

+
+
var data
+
+

Raw data from this page.

+
+
var next_page_cursor
+
+

Cursor to navigate to the next page.

+
+
var previous_page_cursor
+
+

Cursor to navigate to the previous page.

+
+
+
+
+class Pages +(requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None) +
+
+

Represents a paged object.

+
+

Warning

+

This object is slow, especially with a custom handler. +Automatic page caching will be added in the future. It is suggested to +cache the pages yourself if speed is required.

+
+
+ +Expand source code + +
class Pages:
+    """
+    Represents a paged object.
+
+    !!! warning
+        This object is *slow*, especially with a custom handler.
+        Automatic page caching will be added in the future. It is suggested to
+        cache the pages yourself if speed is required.
+    """
+    def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None):
+        if extra_parameters is None:
+            extra_parameters = {}
+
+        self.handler = handler
+        """Function that is passed to Page as data handler."""
+
+        extra_parameters["sortOrder"] = sort_order.value
+        extra_parameters["limit"] = limit
+
+        self.parameters = extra_parameters
+        """Extra parameters for the request."""
+        self.requests = requests
+        """Requests object."""
+        self.url = url
+        """URL containing the paginated data, accessible with a GET request."""
+        self.page = 0
+        """Current page number."""
+
+        self.data = self._get_page()
+
+    def _get_page(self, cursor=None):
+        """
+        Gets a page at the specified cursor position.
+        """
+        this_parameters = self.parameters
+        if cursor:
+            this_parameters["cursor"] = cursor
+
+        page_req = self.requests.get(
+            url=self.url,
+            params=this_parameters
+        )
+        return Page(
+            requests=self.requests,
+            data=page_req.json(),
+            handler=self.handler
+        )
+
+    def previous(self):
+        """
+        Moves to the previous page.
+        """
+        if self.data.previous_page_cursor:
+            self.data = self._get_page(self.data.previous_page_cursor)
+        else:
+            raise InvalidPageError
+
+    def next(self):
+        """
+        Moves to the next page.
+        """
+        if self.data.next_page_cursor:
+            self.data = self._get_page(self.data.next_page_cursor)
+        else:
+            raise InvalidPageError
+
+

Instance variables

+
+
var handler
+
+

Function that is passed to Page as data handler.

+
+
var page
+
+

Current page number.

+
+
var parameters
+
+

Extra parameters for the request.

+
+
var requests
+
+

Requests object.

+
+
var url
+
+

URL containing the paginated data, accessible with a GET request.

+
+
+

Methods

+
+
+def next(self) +
+
+

Moves to the next page.

+
+ +Expand source code + +
def next(self):
+    """
+    Moves to the next page.
+    """
+    if self.data.next_page_cursor:
+        self.data = self._get_page(self.data.next_page_cursor)
+    else:
+        raise InvalidPageError
+
+
+
+def previous(self) +
+
+

Moves to the previous page.

+
+ +Expand source code + +
def previous(self):
+    """
+    Moves to the previous page.
+    """
+    if self.data.previous_page_cursor:
+        self.data = self._get_page(self.data.previous_page_cursor)
+    else:
+        raise InvalidPageError
+
+
+
+
+
+class SortOrder +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
+
+

Order in which page data should load in.

+
+ +Expand source code + +
class SortOrder(enum.Enum):
+    """
+    Order in which page data should load in.
+    """
+    Ascending = "Asc"
+    Descending = "Desc"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var Ascending
+
+
+
+
var Descending
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/ro_py/utilities/requests.html b/docs/ro_py/utilities/requests.html new file mode 100644 index 00000000..5eece9ef --- /dev/null +++ b/docs/ro_py/utilities/requests.html @@ -0,0 +1,359 @@ + + + + + + +ro_py.utilities.requests API documentation + + + + + + + + + + + +
+
+
+

Module ro_py.utilities.requests

+
+
+
+ +Expand source code + +
from ro_py.utilities.errors import ApiError
+from json.decoder import JSONDecodeError
+from cachecontrol import CacheControl
+import requests
+
+
+class Requests:
+    def __init__(self, cache=True):
+        self.session = requests.Session()
+        if cache:
+            self.session = CacheControl(self.session)
+        """
+        Thank you @nsg for letting me know about this!
+        This allows us to access some extra content.
+        ▼▼▼
+        """
+        self.session.headers["User-Agent"] = "Roblox/WinInet"
+
+    def get(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.get.
+        """
+
+        get_request = self.session.get(*args, **kwargs)
+
+        try:
+            get_request_json = get_request.json()
+        except JSONDecodeError:
+            return get_request
+
+        if isinstance(get_request_json, dict):
+            try:
+                get_request_error = get_request_json["errors"]
+            except KeyError:
+                return get_request
+        else:
+            return get_request
+
+        raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
+
+    def post(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.post.
+        """
+
+        post_request = self.session.post(*args, **kwargs)
+
+        if post_request.status_code == 403:
+            if "X-CSRF-TOKEN" in post_request.headers:
+                self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
+                post_request = self.session.post(*args, **kwargs)
+
+        try:
+            post_request_json = post_request.json()
+        except JSONDecodeError:
+            return post_request
+
+        if isinstance(post_request_json, dict):
+            try:
+                post_request_json["errors"]
+            except KeyError:
+                return post_request
+        else:
+            return post_request
+
+    def patch(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.patch.
+        """
+
+        patch_request = self.session.patch(*args, **kwargs)
+
+        if patch_request.status_code == 403:
+            if "X-CSRF-TOKEN" in patch_request.headers:
+                self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
+                patch_request = self.session.patch(*args, **kwargs)
+
+        patch_request_json = patch_request.json()
+
+        if isinstance(patch_request_json, dict):
+            try:
+                patch_request_error = patch_request_json["errors"]
+            except KeyError:
+                return patch_request
+        else:
+            return patch_request
+
+        raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Requests +(cache=True) +
+
+
+
+ +Expand source code + +
class Requests:
+    def __init__(self, cache=True):
+        self.session = requests.Session()
+        if cache:
+            self.session = CacheControl(self.session)
+        """
+        Thank you @nsg for letting me know about this!
+        This allows us to access some extra content.
+        ▼▼▼
+        """
+        self.session.headers["User-Agent"] = "Roblox/WinInet"
+
+    def get(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.get.
+        """
+
+        get_request = self.session.get(*args, **kwargs)
+
+        try:
+            get_request_json = get_request.json()
+        except JSONDecodeError:
+            return get_request
+
+        if isinstance(get_request_json, dict):
+            try:
+                get_request_error = get_request_json["errors"]
+            except KeyError:
+                return get_request
+        else:
+            return get_request
+
+        raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
+
+    def post(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.post.
+        """
+
+        post_request = self.session.post(*args, **kwargs)
+
+        if post_request.status_code == 403:
+            if "X-CSRF-TOKEN" in post_request.headers:
+                self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
+                post_request = self.session.post(*args, **kwargs)
+
+        try:
+            post_request_json = post_request.json()
+        except JSONDecodeError:
+            return post_request
+
+        if isinstance(post_request_json, dict):
+            try:
+                post_request_json["errors"]
+            except KeyError:
+                return post_request
+        else:
+            return post_request
+
+    def patch(self, *args, **kwargs):
+        """
+        Essentially identical to requests.Session.patch.
+        """
+
+        patch_request = self.session.patch(*args, **kwargs)
+
+        if patch_request.status_code == 403:
+            if "X-CSRF-TOKEN" in patch_request.headers:
+                self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
+                patch_request = self.session.patch(*args, **kwargs)
+
+        patch_request_json = patch_request.json()
+
+        if isinstance(patch_request_json, dict):
+            try:
+                patch_request_error = patch_request_json["errors"]
+            except KeyError:
+                return patch_request
+        else:
+            return patch_request
+
+        raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
+
+

Methods

+
+
+def get(self, *args, **kwargs) +
+
+

Essentially identical to requests.Session.get.

+
+ +Expand source code + +
def get(self, *args, **kwargs):
+    """
+    Essentially identical to requests.Session.get.
+    """
+
+    get_request = self.session.get(*args, **kwargs)
+
+    try:
+        get_request_json = get_request.json()
+    except JSONDecodeError:
+        return get_request
+
+    if isinstance(get_request_json, dict):
+        try:
+            get_request_error = get_request_json["errors"]
+        except KeyError:
+            return get_request
+    else:
+        return get_request
+
+    raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
+
+
+
+def patch(self, *args, **kwargs) +
+
+

Essentially identical to requests.Session.patch.

+
+ +Expand source code + +
def patch(self, *args, **kwargs):
+    """
+    Essentially identical to requests.Session.patch.
+    """
+
+    patch_request = self.session.patch(*args, **kwargs)
+
+    if patch_request.status_code == 403:
+        if "X-CSRF-TOKEN" in patch_request.headers:
+            self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
+            patch_request = self.session.patch(*args, **kwargs)
+
+    patch_request_json = patch_request.json()
+
+    if isinstance(patch_request_json, dict):
+        try:
+            patch_request_error = patch_request_json["errors"]
+        except KeyError:
+            return patch_request
+    else:
+        return patch_request
+
+    raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
+
+
+
+def post(self, *args, **kwargs) +
+
+

Essentially identical to requests.Session.post.

+
+ +Expand source code + +
def post(self, *args, **kwargs):
+    """
+    Essentially identical to requests.Session.post.
+    """
+
+    post_request = self.session.post(*args, **kwargs)
+
+    if post_request.status_code == 403:
+        if "X-CSRF-TOKEN" in post_request.headers:
+            self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
+            post_request = self.session.post(*args, **kwargs)
+
+    try:
+        post_request_json = post_request.json()
+    except JSONDecodeError:
+        return post_request
+
+    if isinstance(post_request_json, dict):
+        try:
+            post_request_json["errors"]
+        except KeyError:
+            return post_request
+    else:
+        return post_request
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file From 614a9bc7a2ac2d98cb1a57f61bbd9eb4e3da2647 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 19:43:22 -0500 Subject: [PATCH 160/518] Moved things out of the subfolder --- docs/{ro_py => }/accountinformation.html | 0 docs/{ro_py => }/accountsettings.html | 0 docs/{ro_py => }/assets.html | 0 docs/{ro_py => }/badges.html | 0 docs/{ro_py => }/catalog.html | 0 docs/{ro_py => }/chat.html | 0 docs/{ro_py => }/client.html | 0 docs/{ro_py => }/economy.html | 0 docs/{ro_py => }/games.html | 0 docs/{ro_py => }/gender.html | 0 docs/{ro_py => }/groups.html | 0 docs/{ro_py => }/index.html | 0 docs/{ro_py => }/notifications.html | 0 docs/{ro_py => }/robloxbadges.html | 0 docs/{ro_py => }/robloxstatus.html | 0 docs/{ro_py => }/thumbnails.html | 0 docs/{ro_py => }/trades.html | 0 docs/{ro_py => }/users.html | 0 docs/{ro_py => }/utilities/asset_type.html | 0 docs/{ro_py => }/utilities/cache.html | 0 docs/{ro_py => }/utilities/caseconvert.html | 0 docs/{ro_py => }/utilities/errors.html | 0 docs/{ro_py => }/utilities/index.html | 0 docs/{ro_py => }/utilities/pages.html | 0 docs/{ro_py => }/utilities/requests.html | 0 25 files changed, 0 insertions(+), 0 deletions(-) rename docs/{ro_py => }/accountinformation.html (100%) rename docs/{ro_py => }/accountsettings.html (100%) rename docs/{ro_py => }/assets.html (100%) rename docs/{ro_py => }/badges.html (100%) rename docs/{ro_py => }/catalog.html (100%) rename docs/{ro_py => }/chat.html (100%) rename docs/{ro_py => }/client.html (100%) rename docs/{ro_py => }/economy.html (100%) rename docs/{ro_py => }/games.html (100%) rename docs/{ro_py => }/gender.html (100%) rename docs/{ro_py => }/groups.html (100%) rename docs/{ro_py => }/index.html (100%) rename docs/{ro_py => }/notifications.html (100%) rename docs/{ro_py => }/robloxbadges.html (100%) rename docs/{ro_py => }/robloxstatus.html (100%) rename docs/{ro_py => }/thumbnails.html (100%) rename docs/{ro_py => }/trades.html (100%) rename docs/{ro_py => }/users.html (100%) rename docs/{ro_py => }/utilities/asset_type.html (100%) rename docs/{ro_py => }/utilities/cache.html (100%) rename docs/{ro_py => }/utilities/caseconvert.html (100%) rename docs/{ro_py => }/utilities/errors.html (100%) rename docs/{ro_py => }/utilities/index.html (100%) rename docs/{ro_py => }/utilities/pages.html (100%) rename docs/{ro_py => }/utilities/requests.html (100%) diff --git a/docs/ro_py/accountinformation.html b/docs/accountinformation.html similarity index 100% rename from docs/ro_py/accountinformation.html rename to docs/accountinformation.html diff --git a/docs/ro_py/accountsettings.html b/docs/accountsettings.html similarity index 100% rename from docs/ro_py/accountsettings.html rename to docs/accountsettings.html diff --git a/docs/ro_py/assets.html b/docs/assets.html similarity index 100% rename from docs/ro_py/assets.html rename to docs/assets.html diff --git a/docs/ro_py/badges.html b/docs/badges.html similarity index 100% rename from docs/ro_py/badges.html rename to docs/badges.html diff --git a/docs/ro_py/catalog.html b/docs/catalog.html similarity index 100% rename from docs/ro_py/catalog.html rename to docs/catalog.html diff --git a/docs/ro_py/chat.html b/docs/chat.html similarity index 100% rename from docs/ro_py/chat.html rename to docs/chat.html diff --git a/docs/ro_py/client.html b/docs/client.html similarity index 100% rename from docs/ro_py/client.html rename to docs/client.html diff --git a/docs/ro_py/economy.html b/docs/economy.html similarity index 100% rename from docs/ro_py/economy.html rename to docs/economy.html diff --git a/docs/ro_py/games.html b/docs/games.html similarity index 100% rename from docs/ro_py/games.html rename to docs/games.html diff --git a/docs/ro_py/gender.html b/docs/gender.html similarity index 100% rename from docs/ro_py/gender.html rename to docs/gender.html diff --git a/docs/ro_py/groups.html b/docs/groups.html similarity index 100% rename from docs/ro_py/groups.html rename to docs/groups.html diff --git a/docs/ro_py/index.html b/docs/index.html similarity index 100% rename from docs/ro_py/index.html rename to docs/index.html diff --git a/docs/ro_py/notifications.html b/docs/notifications.html similarity index 100% rename from docs/ro_py/notifications.html rename to docs/notifications.html diff --git a/docs/ro_py/robloxbadges.html b/docs/robloxbadges.html similarity index 100% rename from docs/ro_py/robloxbadges.html rename to docs/robloxbadges.html diff --git a/docs/ro_py/robloxstatus.html b/docs/robloxstatus.html similarity index 100% rename from docs/ro_py/robloxstatus.html rename to docs/robloxstatus.html diff --git a/docs/ro_py/thumbnails.html b/docs/thumbnails.html similarity index 100% rename from docs/ro_py/thumbnails.html rename to docs/thumbnails.html diff --git a/docs/ro_py/trades.html b/docs/trades.html similarity index 100% rename from docs/ro_py/trades.html rename to docs/trades.html diff --git a/docs/ro_py/users.html b/docs/users.html similarity index 100% rename from docs/ro_py/users.html rename to docs/users.html diff --git a/docs/ro_py/utilities/asset_type.html b/docs/utilities/asset_type.html similarity index 100% rename from docs/ro_py/utilities/asset_type.html rename to docs/utilities/asset_type.html diff --git a/docs/ro_py/utilities/cache.html b/docs/utilities/cache.html similarity index 100% rename from docs/ro_py/utilities/cache.html rename to docs/utilities/cache.html diff --git a/docs/ro_py/utilities/caseconvert.html b/docs/utilities/caseconvert.html similarity index 100% rename from docs/ro_py/utilities/caseconvert.html rename to docs/utilities/caseconvert.html diff --git a/docs/ro_py/utilities/errors.html b/docs/utilities/errors.html similarity index 100% rename from docs/ro_py/utilities/errors.html rename to docs/utilities/errors.html diff --git a/docs/ro_py/utilities/index.html b/docs/utilities/index.html similarity index 100% rename from docs/ro_py/utilities/index.html rename to docs/utilities/index.html diff --git a/docs/ro_py/utilities/pages.html b/docs/utilities/pages.html similarity index 100% rename from docs/ro_py/utilities/pages.html rename to docs/utilities/pages.html diff --git a/docs/ro_py/utilities/requests.html b/docs/utilities/requests.html similarity index 100% rename from docs/ro_py/utilities/requests.html rename to docs/utilities/requests.html From 59408ad33f19f260ab0a406a8209fe183283bb25 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 19:51:07 -0500 Subject: [PATCH 161/518] Create CNAME --- docs/CNAME | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/CNAME diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 00000000..b1f93fc7 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +ro.py.jmksite.dev \ No newline at end of file From 1a224422f94f3b90a0b61f194305c954c347dcc1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 26 Dec 2020 22:13:30 -0500 Subject: [PATCH 162/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a19eb34..d6f4a5df 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.7", + version="0.1.8", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 3f15abefa0c51e7e783e03e101d20dcc47dd8cfa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 13:01:18 -0500 Subject: [PATCH 163/518] Fixed some parameters --- ro_py/client.py | 2 +- ro_py/thumbnails.py | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 86e2d0bb..210ce038 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -25,7 +25,7 @@ class Client: Parameters ---------- - token : str + token: str Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser. requests_cache: bool Toggle for cached requests using CacheControl. diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 0aca9b30..905977ad 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -50,6 +50,11 @@ class ThumbnailGenerator: """ This object is used to generate thumbnails. + + Parameters + ---------- + requests: Requests + Requests object. """ def __init__(self, requests): self.requests = requests @@ -57,11 +62,17 @@ def __init__(self, requests): def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ Gets a group's icon. - :param group: The group. - :param size: The thumbnail size, formatted widthxheight. - :param file_format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL + + Parameters + ---------- + group: Group + The group. + size: str + The thumbnail size, formatted WIDTHxHEIGHT. + file_format: str + The thumbnail format. + is_circular: bool + Whether to output a circular version of the thumbnail. """ group_icon_req = self.requests.get( url=endpoint + "v1/groups/icons", From 15cb846ef7f2a94de2197e866479fa698293782d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 15:28:08 -0500 Subject: [PATCH 164/518] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 52c5df96..c2e4038c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -![Banner image for ro.py](https://raw.githubusercontent.com/jmk-developer/ro.py/main/resources/banner.svg) - # Welcome to ro.py ro.py is a Python wrapper for the Roblox web API. ## Installation From 1f70a90429c9cc83dc345d899b4f0c604d86bff9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 16:46:27 -0500 Subject: [PATCH 165/518] Updated readme --- ro_py/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 8d1ee992..279cc12d 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -6,6 +6,7 @@ Welcome to ro.py! ro.py is a powerful wrapper for the Roblox web API. It can be used to create (almost) anything from chat bots to group management systems. -ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at +ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/ +You can view the source code at https://github.com/jmk-developer/ro.py/ """ \ No newline at end of file From d28203314f9851e28b75fd7845e98a427c04b994 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 18:39:30 -0500 Subject: [PATCH 166/518] Updated README with docs link --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c2e4038c..afaccd5e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # Welcome to ro.py ro.py is a Python wrapper for the Roblox web API. +## Documentation +You can view documentation for ro.py at [https://ro.py.jmksite.dev/](https://ro.py.jmksite.dev/) + ## Installation You can install ro.py from pip: ``` pip3 install ro-py ``` + ## Examples Using the client: ```python From 17c6261c2fbd23dd9423cfddb4cb42da851915ec Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 18:42:15 -0500 Subject: [PATCH 167/518] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..355edfe2 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '39 1 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From b3a7403cc9333f46fa63a7a2c01990ee65cd370b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 27 Dec 2020 19:56:09 -0500 Subject: [PATCH 168/518] Added docs link --- ro_py/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 279cc12d..aa38206f 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -8,5 +8,6 @@ It can be used to create (almost) anything from chat bots to group management systems. ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/ You can view the source code at https://github.com/jmk-developer/ro.py/ +You can also view the documentation at https://ro.py.jmksite.dev/. -""" \ No newline at end of file +""" From dc958744606be8d414f91289d6964a181bb8d06e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 28 Dec 2020 17:01:44 -0500 Subject: [PATCH 169/518] Fixed a bug with errors --- ro_py/utilities/requests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 28835a24..40178d2b 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -57,12 +57,14 @@ def post(self, *args, **kwargs): if isinstance(post_request_json, dict): try: - post_request_json["errors"] + post_request_error = post_request_json["errors"] except KeyError: return post_request else: return post_request + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + def patch(self, *args, **kwargs): """ Essentially identical to requests.Session.patch. From f465fd5ba2cbe01d367412217417cac15a5e95f9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 28 Dec 2020 18:07:28 -0500 Subject: [PATCH 170/518] Added JMK Endpoint I'm planning on locking certain things that can be abused or used maliciously behing this endpoint to mitigate the use of ro.py for exploits. --- ro_py/utilities/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 40178d2b..6035357a 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -5,7 +5,7 @@ class Requests: - def __init__(self, cache=True): + def __init__(self, cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): self.session = requests.Session() if cache: self.session = CacheControl(self.session) From a7d7aaa3955db427bbffa83aaf007f417bd561dd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 28 Dec 2020 18:07:51 -0500 Subject: [PATCH 171/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d6f4a5df..21c22619 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.8", + version="0.1.9", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From f95a18468ce34aef6e603d040a9ec7df1773e90a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 13:44:31 -0500 Subject: [PATCH 172/518] added type hints --- ro_py/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index 210ce038..dfa99989 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -30,7 +30,8 @@ class Client: requests_cache: bool Toggle for cached requests using CacheControl. """ - def __init__(self, token=None, requests_cache=False): + + def __init__(self, token: str = None, requests_cache: bool = False): self.requests = Requests( cache=requests_cache ) From 43ced2cf3450fcbc36063c6e4dd572d0b8a3e51f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 14:41:42 -0500 Subject: [PATCH 173/518] Working on making things asynchronous This will probably break everything --- ro_py/client.py | 22 +++++++++++++--------- ro_py/utilities/requests.py | 10 +++++----- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index dfa99989..c49e5f0e 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -42,8 +42,8 @@ def __init__(self, token: str = None, requests_cache: bool = False): """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None """AccountSettings object. Only available for authenticated clients.""" - self.user = None - """User object. Only available for authenticated clients.""" + # self.user = None + # """User object. Only available for authenticated clients.""" self.chat = None """ChatWrapper object. Only available for authenticated clients.""" self.trade = None @@ -56,8 +56,8 @@ def __init__(self, token: str = None, requests_cache: bool = False): self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") - self.user = User(self.requests, auth_user_req.json()["id"]) - logging.debug("Initialized authenticated user.") + # self.user = User(self.requests, auth_user_req.json()["id"]) + # logging.debug("Initialized authenticated user.") self.chat = ChatWrapper(self.requests) logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) @@ -65,7 +65,7 @@ def __init__(self, token: str = None, requests_cache: bool = False): else: logging.warning("The active client is not authenticated, so some features will not be enabled.") - def get_user(self, user_id): + async def get_user(self, user_id): """ Gets a Roblox user. """ @@ -73,9 +73,10 @@ def get_user(self, user_id): cache["users"][str(user_id)] except KeyError: cache["users"][str(user_id)] = User(self.requests, user_id) + await cache["users"][str(user_id)].update() return cache["users"][str(user_id)] - def get_group(self, group_id): + async def get_group(self, group_id): """ Gets a Roblox group. """ @@ -83,9 +84,10 @@ def get_group(self, group_id): cache["groups"][str(group_id)] except KeyError: cache["groups"][str(group_id)] = Group(self.requests, group_id) + await cache["groups"][str(group_id)].update() return cache["groups"][str(group_id)] - def get_game(self, game_id): + async def get_game(self, game_id): """ Gets a Roblox game. """ @@ -93,9 +95,10 @@ def get_game(self, game_id): cache["games"][str(game_id)] except KeyError: cache["games"][str(game_id)] = Game(self.requests, game_id) + await cache["games"][str(game_id)].update() return cache["games"][str(game_id)] - def get_asset(self, asset_id): + async def get_asset(self, asset_id): """ Gets a Roblox asset. """ @@ -103,9 +106,10 @@ def get_asset(self, asset_id): cache["assets"][str(asset_id)] except KeyError: cache["assets"][str(asset_id)] = Asset(self.requests, asset_id) + await cache["assets"][str(asset_id)].update() return cache["assets"][str(asset_id)] - def get_badge(self, badge_id): + async def get_badge(self, badge_id): """ Gets a Roblox badge. """ diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 6035357a..42a10658 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -1,12 +1,12 @@ from ro_py.utilities.errors import ApiError from json.decoder import JSONDecodeError from cachecontrol import CacheControl -import requests +import requests_async class Requests: def __init__(self, cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): - self.session = requests.Session() + self.session = requests_async.Session() if cache: self.session = CacheControl(self.session) """ @@ -16,7 +16,7 @@ def __init__(self, cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): """ self.session.headers["User-Agent"] = "Roblox/WinInet" - def get(self, *args, **kwargs): + async def get(self, *args, **kwargs): """ Essentially identical to requests.Session.get. """ @@ -38,7 +38,7 @@ def get(self, *args, **kwargs): raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") - def post(self, *args, **kwargs): + async def post(self, *args, **kwargs): """ Essentially identical to requests.Session.post. """ @@ -65,7 +65,7 @@ def post(self, *args, **kwargs): raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") - def patch(self, *args, **kwargs): + async def patch(self, *args, **kwargs): """ Essentially identical to requests.Session.patch. """ From e562784c7e27c689111768dffc6af2459652251a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 14:47:38 -0500 Subject: [PATCH 174/518] more async --- ro_py/users.py | 16 +++++++--------- ro_py/utilities/requests.py | 10 +++++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index d7483df0..a0aff16b 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -26,14 +26,12 @@ def __init__(self, requests, ui): self.name = None self.display_name = None - self.update() - def update(self): """ Updates some class values. :return: Nothing """ - user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") + user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -48,7 +46,7 @@ def get_status(self): Gets the user's status. :return: A string """ - status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] def get_roblox_badges(self): @@ -56,7 +54,7 @@ def get_roblox_badges(self): Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) @@ -67,7 +65,7 @@ def get_friends_count(self): Gets the user's friends count. :return: An integer """ - friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count @@ -76,7 +74,7 @@ def get_followers_count(self): Gets the user's followers count. :return: An integer """ - followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count @@ -85,7 +83,7 @@ def get_followings_count(self): Gets the user's followings count. :return: An integer """ - followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -94,7 +92,7 @@ def get_friends(self): Gets the user's friends. :return: A list of User instances. """ - friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 42a10658..653e856c 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -21,7 +21,7 @@ async def get(self, *args, **kwargs): Essentially identical to requests.Session.get. """ - get_request = self.session.get(*args, **kwargs) + get_request = await self.session.get(*args, **kwargs) try: get_request_json = get_request.json() @@ -43,12 +43,12 @@ async def post(self, *args, **kwargs): Essentially identical to requests.Session.post. """ - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -70,12 +70,12 @@ async def patch(self, *args, **kwargs): Essentially identical to requests.Session.patch. """ - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) patch_request_json = patch_request.json() From 7026d478e5e7b56a649e91f5519f4b41dd766690 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 14:50:56 -0500 Subject: [PATCH 175/518] User is updated (mostly) --- ro_py/users.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index a0aff16b..17c38932 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -26,7 +26,7 @@ def __init__(self, requests, ui): self.name = None self.display_name = None - def update(self): + async def update(self): """ Updates some class values. :return: Nothing @@ -41,7 +41,7 @@ def update(self): # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - def get_status(self): + async def get_status(self): """ Gets the user's status. :return: A string @@ -49,7 +49,7 @@ def get_status(self): status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - def get_roblox_badges(self): + async def get_roblox_badges(self): """ Gets the user's roblox badges. :return: A list of RobloxBadge instances @@ -60,7 +60,7 @@ def get_roblox_badges(self): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - def get_friends_count(self): + async def get_friends_count(self): """ Gets the user's friends count. :return: An integer @@ -69,7 +69,7 @@ def get_friends_count(self): friends_count = friends_count_req.json()["count"] return friends_count - def get_followers_count(self): + async def get_followers_count(self): """ Gets the user's followers count. :return: An integer @@ -78,7 +78,7 @@ def get_followers_count(self): followers_count = followers_count_req.json()["count"] return followers_count - def get_followings_count(self): + async def get_followings_count(self): """ Gets the user's followings count. :return: An integer @@ -87,7 +87,7 @@ def get_followings_count(self): followings_count = followings_count_req.json()["count"] return followings_count - def get_friends(self): + async def get_friends(self): """ Gets the user's friends. :return: A list of User instances. From 31b3013bb11049017c786359e0a3c1929d71cc35 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 14:56:54 -0500 Subject: [PATCH 176/518] Fixed user example --- examples/user.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/user.py b/examples/user.py index dbd99aad..43ee553a 100644 --- a/examples/user.py +++ b/examples/user.py @@ -1,14 +1,26 @@ from ro_py.client import Client +import asyncio client = Client() user_id = 576059883 -print(f"Loading user {user_id}...") -user = client.get_user(user_id) -print("Loaded user.") -print(f"Username: {user.name}") -print(f"Display Name: {user.display_name}") -print(f"Description: {user.description}") -print(f"Status: {user.get_status() or 'None.'}") +async def grab_info(): + print(f"Loading user {user_id}...") + user = await client.get_user(user_id) + print("Loaded user.") + + print(f"Username: {user.name}") + print(f"Display Name: {user.display_name}") + print(f"Description: {user.description}") + print(f"Status: {await user.get_status() or 'None.'}") + + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(grab_info()) + + +if __name__ == '__main__': + main() From c9784e302f31c3e342cc79efd3a69c5c5fb459d9 Mon Sep 17 00:00:00 2001 From: iranathan Date: Tue, 29 Dec 2020 23:00:29 +0100 Subject: [PATCH 177/518] changes for async --- ro_py/assets.py | 12 ++++++------ ro_py/groups.py | 9 +++++---- ro_py/trades.py | 12 ++++++------ ro_py/utilities/pages.py | 12 ++++++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index e2df4f1f..d7a5b7f3 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -42,11 +42,11 @@ def __init__(self, requests, asset_id): self.content_rating_type_id = None self.update() - def update(self): + async def update(self): """ Updates the asset's information. """ - asset_info_req = self.requests.get( + asset_info_req = await self.requests.get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.id @@ -76,12 +76,12 @@ def update(self): self.minimum_membership_level = asset_info["MinimumMembershipLevel"] self.content_rating_type_id = asset_info["ContentRatingTypeId"] - def get_remaining(self): + async def get_remaining(self): """ Gets the remaining amount of this asset. (used for Limited U items) :returns: Amount remaining """ - asset_info_req = self.requests.get( + asset_info_req = await self.requests.get( url=endpoint + "marketplace/productinfo", params={ "assetId": self.asset_id @@ -90,13 +90,13 @@ def get_remaining(self): asset_info = asset_info_req.json() return asset_info["Remaining"] - def get_limited_resale_data(self): + async def get_limited_resale_data(self): """ Gets the limited resale data :returns: LimitedResaleData """ if self.is_limited: - resale_data_req = self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") + resale_data_req = await self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") return LimitedResaleData(resale_data_req.json()) else: raise NotLimitedError("You can only read this information on limited items.") diff --git a/ro_py/groups.py b/ro_py/groups.py index 8d3f925e..1b857d03 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -36,11 +36,11 @@ def __init__(self, requests, group_id): self.update() - def update(self): + async def update(self): """ Updates the group's information. """ - group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] @@ -54,10 +54,11 @@ def update(self): self.shout = None # self.is_locked = group_info["isLocked"] - def update_shout(self, message): - self.requests.patch( + async def update_shout(self, message): + shout_req = await self.requests.patch( url=f"https://groups.roblox.com/v1/groups/{self.id}/status", data={ "message": message } ) + return shout_req.status_code == 200 diff --git a/ro_py/trades.py b/ro_py/trades.py index 3de6f59a..605acf59 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -9,7 +9,7 @@ import iso8601 import enum -endpoint = "https://trades.roblox.com/" +endpoint = "https://trades.roblox.com" def trade_page_handler(requests, this_page): @@ -23,7 +23,7 @@ class Trade: def __init__(self, requests, trade_id): self.requests = requests trade_req = self.requests.get( - url=endpoint + f"v1/trades/{trade_id}" + url=endpoint + f"/v1/trades/{trade_id}" ) trade_data = trade_req.json() self.id = trade_data["id"] @@ -61,16 +61,16 @@ class TradesWrapper: def __init__(self, requests): self.requests = requests - def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10): - trades = Pages( + async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10): + trades = await Pages( requests=self.requests, - url=endpoint + f"/v1/trades/{trade_status_type.value}", + url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, handler=trade_page_handler ) return trades - def send_trade(self): + async def send_trade(self): pass diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 12379c58..97c7890e 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -57,7 +57,7 @@ def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extr self.data = self._get_page() - def _get_page(self, cursor=None): + async def _get_page(self, cursor=None): """ Gets a page at the specified cursor position. """ @@ -65,7 +65,7 @@ def _get_page(self, cursor=None): if cursor: this_parameters["cursor"] = cursor - page_req = self.requests.get( + page_req = await self.requests.get( url=self.url, params=this_parameters ) @@ -75,20 +75,20 @@ def _get_page(self, cursor=None): handler=self.handler ) - def previous(self): + async def previous(self): """ Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = self._get_page(self.data.previous_page_cursor) + self.data = await self._get_page(self.data.previous_page_cursor) else: raise InvalidPageError - def next(self): + async def next(self): """ Moves to the next page. """ if self.data.next_page_cursor: - self.data = self._get_page(self.data.next_page_cursor) + self.data = await self._get_page(self.data.next_page_cursor) else: raise InvalidPageError From c1f3cadb129a9d7673487edcbd3f01d7f9e21fd6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 17:02:47 -0500 Subject: [PATCH 178/518] Updated version identifier + working on cache --- ro_py/utilities/cache.py | 16 +++++++++------- ro_py/utilities/requests.py | 1 + setup.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ro_py/utilities/cache.py b/ro_py/utilities/cache.py index 64bec568..0ad0f765 100644 --- a/ro_py/utilities/cache.py +++ b/ro_py/utilities/cache.py @@ -1,7 +1,9 @@ -cache = { - "users": {}, - "groups": {}, - "games": {}, - "assets": {}, - "badges": {} -} +class Cache: + def __init__(self): + self.cache = { + "users": {}, + "groups": {}, + "games": {}, + "assets": {}, + "badges": {} + } diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 653e856c..cbdda700 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -1,4 +1,5 @@ from ro_py.utilities.errors import ApiError +from ro_py.utilities.cache import cache from json.decoder import JSONDecodeError from cachecontrol import CacheControl import requests_async diff --git a/setup.py b/setup.py index 21c22619..dd55b010 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="0.1.9", + version="2.0.0", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 4e31e7097e468650d355317265ede72741aac0d8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 17:15:31 -0500 Subject: [PATCH 179/518] Update cache.py --- ro_py/utilities/cache.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ro_py/utilities/cache.py b/ro_py/utilities/cache.py index 0ad0f765..083d4ac0 100644 --- a/ro_py/utilities/cache.py +++ b/ro_py/utilities/cache.py @@ -1,3 +1,14 @@ +import enum + + +class CacheType(enum.Enum): + Users = "users" + Groups = "groups" + Games = "games" + Assets = "assets" + Badges = "badges" + + class Cache: def __init__(self): self.cache = { @@ -6,4 +17,13 @@ def __init__(self): "games": {}, "assets": {}, "badges": {} - } + } + + def get(self, cache_type: CacheType, item_id: str): + if item_id in self.cache[cache_type.value]: + return self.cache[cache_type.value][item_id] + else: + return False + + def set(self, cache_type: CacheType, item_id: str, item_obj): + self.cache[cache_type.value][item_id] = item_obj From 44de0cc1f10fc86e641e7cc4e69903d766434b88 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 18:10:34 -0500 Subject: [PATCH 180/518] Fixed class name for cache --- ro_py/utilities/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index cbdda700..ad7aa6a5 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -1,5 +1,5 @@ from ro_py.utilities.errors import ApiError -from ro_py.utilities.cache import cache +from ro_py.utilities.cache import Cache from json.decoder import JSONDecodeError from cachecontrol import CacheControl import requests_async From 4e519e4c5f1d3f41a858cfb7d662658f7ff22b63 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 18:27:47 -0500 Subject: [PATCH 181/518] Cache update Cache was added to Requests and is used across multiple objcets. --- ro_py/client.py | 65 +++++++++++++++++++------------------ ro_py/utilities/requests.py | 7 ++-- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index c49e5f0e..f89c11d5 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -11,7 +11,7 @@ from ro_py.badges import Badge from ro_py.chat import ChatWrapper from ro_py.trades import TradesWrapper -from ro_py.utilities.cache import cache +from ro_py.utilities.cache import CacheType from ro_py.utilities.requests import Requests from ro_py.accountinformation import AccountInformation from ro_py.accountsettings import AccountSettings @@ -33,7 +33,7 @@ class Client: def __init__(self, token: str = None, requests_cache: bool = False): self.requests = Requests( - cache=requests_cache + request_cache=requests_cache ) logging.debug("Initialized requests.") @@ -55,7 +55,7 @@ def __init__(self, token: str = None, requests_cache: bool = False): self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") - auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") + # auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") # self.user = User(self.requests, auth_user_req.json()["id"]) # logging.debug("Initialized authenticated user.") self.chat = ChatWrapper(self.requests) @@ -69,52 +69,53 @@ async def get_user(self, user_id): """ Gets a Roblox user. """ - try: - cache["users"][str(user_id)] - except KeyError: - cache["users"][str(user_id)] = User(self.requests, user_id) - await cache["users"][str(user_id)].update() - return cache["users"][str(user_id)] + user = self.requests.cache.get(CacheType.Users, user_id) + if not user: + user = User(self.requests, user_id) + self.requests.cache.set(CacheType.Users, user_id, user) + await user.update() + return user async def get_group(self, group_id): """ Gets a Roblox group. """ - try: - cache["groups"][str(group_id)] - except KeyError: - cache["groups"][str(group_id)] = Group(self.requests, group_id) - await cache["groups"][str(group_id)].update() - return cache["groups"][str(group_id)] + group = self.requests.cache.get(CacheType.Groups, group_id) + if not group: + group = Group(self.requests, group_id) + self.requests.cache.set(CacheType.Groups, group_id, group) + await group.update() + return group async def get_game(self, game_id): """ Gets a Roblox game. """ - try: - cache["games"][str(game_id)] - except KeyError: - cache["games"][str(game_id)] = Game(self.requests, game_id) - await cache["games"][str(game_id)].update() - return cache["games"][str(game_id)] + game = self.requests.cache.get(CacheType.Games, game_id) + if not game: + game = Game(self.requests, game_id) + self.requests.cache.set(CacheType.Games, game_id, game) + await game.update() + return game async def get_asset(self, asset_id): """ Gets a Roblox asset. """ - try: - cache["assets"][str(asset_id)] - except KeyError: - cache["assets"][str(asset_id)] = Asset(self.requests, asset_id) - await cache["assets"][str(asset_id)].update() - return cache["assets"][str(asset_id)] + asset = self.requests.cache.get(CacheType.Assets, asset_id) + if not asset: + asset = Asset(self.requests, asset_id) + self.requests.cache.set(CacheType.Assets, asset_id, asset) + await asset.update() + return asset async def get_badge(self, badge_id): """ Gets a Roblox badge. """ - try: - cache["badges"][str(badge_id)] - except KeyError: - cache["badges"][str(badge_id)] = Badge(self.requests, badge_id) - return cache["badges"][str(badge_id)] + badge = self.requests.cache.get(CacheType.Assets, badge_id) + if not badge: + badge = Badge(self.requests, badge_id) + self.requests.cache.set(CacheType.Assets, badge_id, badge) + await badge.update() + return badge diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index ad7aa6a5..273dba88 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -6,9 +6,12 @@ class Requests: - def __init__(self, cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): + def __init__(self, request_cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): self.session = requests_async.Session() - if cache: + """Session to use for requests.""" + self.cache = Cache() + """Cache object to use for object storage.""" + if request_cache: self.session = CacheControl(self.session) """ Thank you @nsg for letting me know about this! From 386b66bf2f5449fb053d814968294d5f453bcea5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 20:48:28 -0500 Subject: [PATCH 182/518] Re-ordered imports --- ro_py/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index f89c11d5..3b57512f 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -13,8 +13,8 @@ from ro_py.trades import TradesWrapper from ro_py.utilities.cache import CacheType from ro_py.utilities.requests import Requests -from ro_py.accountinformation import AccountInformation from ro_py.accountsettings import AccountSettings +from ro_py.accountinformation import AccountInformation import logging From 17b7a12a08e5c7e10fcf4133fd96b2db87961412 Mon Sep 17 00:00:00 2001 From: iranathan Date: Wed, 30 Dec 2020 02:53:34 +0100 Subject: [PATCH 183/518] adding expand, accept, decline, new trade class --- ro_py/trades.py | 85 +++++++++++++++++++++++++++++++++++++++++++------ ro_py/users.py | 6 ++-- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 605acf59..fad07768 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -5,6 +5,7 @@ """ from ro_py.utilities.pages import Pages, SortOrder +from ro_py.assets import Asset from ro_py.users import User import iso8601 import enum @@ -15,22 +16,86 @@ def trade_page_handler(requests, this_page): trades_out = [] for raw_trade in this_page: - trades_out.append(Trade(requests, raw_trade["id"])) + trades_out.append(Trade(requests, raw_trade["id"], raw_trade["user"]['id'])) return trades_out class Trade: - def __init__(self, requests, trade_id): + def __init__(self, requests, trade_id, sender, recieve_items, send_items, created, expiration): + self.trade_id = trade_id self.requests = requests - trade_req = self.requests.get( - url=endpoint + f"/v1/trades/{trade_id}" + self.sender = sender + self.recieve_items = recieve_items + self.send_items = send_items + self.created = created + self.experation = expiration + + async def accept(self): + accept_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/accept" ) - trade_data = trade_req.json() - self.id = trade_data["id"] - self.user = User(self.requests, trade_data["user"]["id"]) - self.created = iso8601.parse_date(trade_data["created"]) - self.is_active = trade_data["isActive"] - self.status = trade_data["status"] + return accept_req.status_code == 200 + + async def decline(self): + decline_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/decline" + ) + return decline_req.status_code == 200 + + +class PartialTrade: + def __init__(self, requests, trade_id, user): + self.requests = requests + self.trade_id = trade_id + self.user = user + + async def accept(self): + """ + accepts a trade requests + :returns: true/false + """ + accept_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/accept" + ) + return accept_req.status_code == 200 + + async def decline(self): + """ + decline a trade requests + :returns: true/false + """ + decline_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/decline" + ) + return decline_req.status_code == 200 + + async def expand(self): + """ + gets a more detailed trade request + :return: Trade class + """ + expend_req = await self.requests.get( + url=endpoint + f"/v1/trades/{self.trade_id}" + ) + data = expend_req.json() + + # generate a user class and update it + sender = User(self.requests, data['user']['id']) + await sender.update() + + # load items that will be/have been sent and items that you will/have recieve(d) + recieve_items, send_items = [], [] + for items_0 in data['offers'][0]['userAssets']: + item_0 = Asset(self.requests, items_0['assetId']) + await item_0.update() + recieve_items.append(item_0) + + for items_1 in data['offers'][1]['userAssets']: + item_1 = Asset(self.requests, items_1['assetId']) + await item_1.update() + send_items.append(item_1) + + return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration']) class TradeStatusType(enum.Enum): diff --git a/ro_py/users.py b/ro_py/users.py index 17c38932..4d6c8e39 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -15,11 +15,9 @@ class User: Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. """ - def __init__(self, requests, ui): - + def __init__(self, requests, id): self.requests = requests - self.id = ui - + self.id = id self.description = None self.created = None self.is_banned = None From e685a49dec3056fb1d6ac2ccb8a185648b8bd762 Mon Sep 17 00:00:00 2001 From: iranathan Date: Wed, 30 Dec 2020 03:33:09 +0100 Subject: [PATCH 184/518] add extra arguemets to PartialTrade and time parsing --- ro_py/trades.py | 38 +++++++++++++++++++++++++------------- ro_py/users.py | 4 ++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index fad07768..5b231042 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -13,30 +13,39 @@ endpoint = "https://trades.roblox.com" -def trade_page_handler(requests, this_page): +def trade_page_handler(requests, this_page) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(Trade(requests, raw_trade["id"], raw_trade["user"]['id'])) + trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) return trades_out class Trade: - def __init__(self, requests, trade_id, sender, recieve_items, send_items, created, expiration): + def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender self.recieve_items = recieve_items self.send_items = send_items - self.created = created - self.experation = expiration + self.created = iso8601.parse(created) + self.experation = iso8601.parse(expiration) + self.status = status - async def accept(self): + async def accept(self) -> bool: + """ + accepts a trade requests + :returns: true/false + """ accept_req = await self.requests.post( url=endpoint + f"/v1/trades/{self.trade_id}/accept" ) return accept_req.status_code == 200 - async def decline(self): + async def decline(self) -> bool: + """ + decline a trade requests + :returns: true/false + """ decline_req = await self.requests.post( url=endpoint + f"/v1/trades/{self.trade_id}/decline" ) @@ -44,12 +53,15 @@ async def decline(self): class PartialTrade: - def __init__(self, requests, trade_id, user): + def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool): self.requests = requests self.trade_id = trade_id self.user = user + self.created = iso8601.parse(created) + self.expiration = iso8601.parse(expiration) + self.status = status - async def accept(self): + async def accept(self) -> bool: """ accepts a trade requests :returns: true/false @@ -59,7 +71,7 @@ async def accept(self): ) return accept_req.status_code == 200 - async def decline(self): + async def decline(self) -> bool: """ decline a trade requests :returns: true/false @@ -69,7 +81,7 @@ async def decline(self): ) return decline_req.status_code == 200 - async def expand(self): + async def expand(self) -> Trade: """ gets a more detailed trade request :return: Trade class @@ -95,7 +107,7 @@ async def expand(self): await item_1.update() send_items.append(item_1) - return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration']) + return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status']) class TradeStatusType(enum.Enum): @@ -126,7 +138,7 @@ class TradesWrapper: def __init__(self, requests): self.requests = requests - async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10): + async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = await Pages( requests=self.requests, url=endpoint + f"/v1/trades/{trade_status_type}", diff --git a/ro_py/users.py b/ro_py/users.py index 4d6c8e39..fc3260df 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -15,13 +15,13 @@ class User: Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. """ - def __init__(self, requests, id): + def __init__(self, requests, id, name=None): self.requests = requests self.id = id self.description = None self.created = None self.is_banned = None - self.name = None + self.name = name self.display_name = None async def update(self): From 3adb35b8792261c2a70bdf0257aab0827247d901 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 29 Dec 2020 22:03:00 -0500 Subject: [PATCH 185/518] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index afaccd5e..028ffb13 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Welcome to ro.py ro.py is a Python wrapper for the Roblox web API. ## Documentation -You can view documentation for ro.py at [https://ro.py.jmksite.dev/](https://ro.py.jmksite.dev/) +You can view documentation for ro.py at [ro.py.jmksite.dev](https://ro.py.jmksite.dev/) ## Installation You can install ro.py from pip: @@ -27,7 +27,9 @@ print(f"Status: {user.get_status() or 'None.'}") Find more examples in the examples folder. ## Credits -@mfd-co - helped with endpoints +[@iranathan](https://github.com/iranathan) - maintainer +[@jmkdev](https://github.com/iranathan) - maintainer +[@nsg-mfd](https://github.com/nsg-mfd) - helped with endpoints ## Other Libraries https://github.com/RbxAPI/Pyblox From 3354917533b65fbb0688030d5086be927f00bb2d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 30 Dec 2020 17:20:50 -0500 Subject: [PATCH 186/518] Updated AccountInformation and requests comments --- ro_py/accountinformation.py | 91 ++++++++++++++++--------------------- ro_py/utilities/requests.py | 6 +-- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 79f7dbe6..9ec2cf9e 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -15,13 +15,12 @@ class AccountInformationMetadata: Represents account information metadata. """ def __init__(self, metadata_raw): - self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ - metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] - self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] - self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] - self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] - self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"] + self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] class PromotionChannels: @@ -29,31 +28,11 @@ class PromotionChannels: Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.__dict__["facebook"] = promotion_raw["facebook"] - self.__dict__["twitter"] = promotion_raw["twitter"] - self.__dict__["youtube"] = promotion_raw["youtube"] - self.__dict__["twitch"] = promotion_raw["twitch"] - - @property - def promotion_channels_visibility_privacy(self): - return self.__dict__["promotion_channels_visibility_privacy"] - - @property - def facebook(self): - return self.__dict__["facebook"] - - @property - def twitter(self): - return self.__dict__["twitter"] - - @property - def youtube(self): - return self.__dict__["youtube"] - - @property - def twitch(self): - return self.__dict__["twitch"] + self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + self.facebook = promotion_raw["facebook"] + self.twitter = promotion_raw["twitter"] + self.youtube = promotion_raw["youtube"] + self.twitch = promotion_raw["twitch"] class AccountInformation: @@ -65,49 +44,55 @@ def __init__(self, requests): self.requests = requests self.account_information_metadata = None self.promotion_channels = None - self.update() - def update(self): + async def update(self): """ Updates the account information. - :return: Nothing """ - account_information_req = self.requests.get( + account_information_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/metadata" ) self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.requests.get( + promotion_channels_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/promotion-channels" ) self.promotion_channels = PromotionChannels(promotion_channels_req.json()) - def get_gender(self): + async def get_gender(self): """ Gets the user's gender. - :return: RobloxGender + + Returns + ------- + RobloxGender """ - gender_req = self.requests.get(endpoint + "v1/gender") + gender_req = await self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) - def set_gender(self, gender): + async def set_gender(self, gender): """ Sets the user's gender. - :param gender: RobloxGender - :return: Nothing + + Parameters + ---------- + gender : RobloxGender """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) } ) - def get_birthdate(self): + async def get_birthdate(self): """ - Returns the user's birthdate. - :return: datetime + Grabs the user's birthdate. + + Returns + ------- + datetime.datetime """ - birthdate_req = self.requests.get(endpoint + "v1/birthdate") + birthdate_req = await self.requests.get(endpoint + "v1/birthdate") birthdate_raw = birthdate_req.json() birthdate = datetime( year=birthdate_raw["birthYear"], @@ -116,13 +101,15 @@ def get_birthdate(self): ) return birthdate - def set_birthdate(self, birthdate): + async def set_birthdate(self, birthdate): """ Sets the user's birthdate. - :param birthdate: A datetime object. - :return: Nothing + + Parameters + ---------- + birthdate : datetime.datetime """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 273dba88..ac624925 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -22,7 +22,7 @@ def __init__(self, request_cache=True, jmk_endpoint="https://roblox.jmksite.dev/ async def get(self, *args, **kwargs): """ - Essentially identical to requests.Session.get. + Essentially identical to requests_async.Session.get. """ get_request = await self.session.get(*args, **kwargs) @@ -44,7 +44,7 @@ async def get(self, *args, **kwargs): async def post(self, *args, **kwargs): """ - Essentially identical to requests.Session.post. + Essentially identical to requests_async.Session.post. """ post_request = await self.session.post(*args, **kwargs) @@ -71,7 +71,7 @@ async def post(self, *args, **kwargs): async def patch(self, *args, **kwargs): """ - Essentially identical to requests.Session.patch. + Essentially identical to requests_async.Session.patch. """ patch_request = await self.session.patch(*args, **kwargs) From 0f1f195e5004e3db5e8e4e0db06cdff6f9ff360c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 30 Dec 2020 21:21:07 -0500 Subject: [PATCH 187/518] Commented tons of things! --- ro_py/accountinformation.py | 11 +++++++++++ ro_py/accountsettings.py | 24 +++++++++++++----------- ro_py/client.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 9ec2cf9e..bc57794c 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -16,11 +16,17 @@ class AccountInformationMetadata: """ def __init__(self, metadata_raw): self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + """Unsure what this does.""" self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + """Whether the account settings policy is enabled (unsure exactly what this does)""" self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + """Whether the user's linked phone number is enabled.""" self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + """Maximum length of the user's description.""" self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + """Whether the user's description is enabled.""" self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + """Whether the UserBlock endpoints are updated (unsure exactly what this does)""" class PromotionChannels: @@ -29,10 +35,15 @@ class PromotionChannels: """ def __init__(self, promotion_raw): self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + """Visibility of promotion channels.""" self.facebook = promotion_raw["facebook"] + """Link to the user's Facebook page.""" self.twitter = promotion_raw["twitter"] + """Link to the user's Twitter page.""" self.youtube = promotion_raw["youtube"] + """Link to the user's YouTube page.""" self.twitch = promotion_raw["twitch"] + """Link to the user's Twitch page.""" class AccountInformation: diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 196694c6..1dcd415a 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -33,24 +33,26 @@ class PrivacySettings(enum.Enum): class RobloxEmail: """ Represents an obfuscated version of the email you have set on your account. - """ - def __init__(self, email_data): - self.__dict__["email_address"] = email_data["emailAddress"] - self.__dict__["verified"] = email_data["verified"] - - @property - def email_address(self): - return self.__dict__["email_address"] - @property - def verified(self): - return self.__dict__["verified"] + Parameters + ---------- + email_data : dict + Raw data to parse from. + """ + def __init__(self, email_data: dict): + self.email_address = email_data["emailAddress"] + self.verified = email_data["verified"] class AccountSettings: """ Represents authenticated client account settings (https://accountsettings.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests diff --git a/ro_py/client.py b/ro_py/client.py index 3b57512f..983f3eb9 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -25,9 +25,9 @@ class Client: Parameters ---------- - token: str + token : str Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser. - requests_cache: bool + requests_cache : bool Toggle for cached requests using CacheControl. """ @@ -68,6 +68,11 @@ def __init__(self, token: str = None, requests_cache: bool = False): async def get_user(self, user_id): """ Gets a Roblox user. + + Parameters + ---------- + user_id + ID of the user to generate the object from. """ user = self.requests.cache.get(CacheType.Users, user_id) if not user: @@ -79,6 +84,11 @@ async def get_user(self, user_id): async def get_group(self, group_id): """ Gets a Roblox group. + + Parameters + ---------- + group_id + ID of the group to generate the object from. """ group = self.requests.cache.get(CacheType.Groups, group_id) if not group: @@ -90,6 +100,11 @@ async def get_group(self, group_id): async def get_game(self, game_id): """ Gets a Roblox game. + + Parameters + ---------- + game_id + ID of the game to generate the object from. """ game = self.requests.cache.get(CacheType.Games, game_id) if not game: @@ -101,6 +116,11 @@ async def get_game(self, game_id): async def get_asset(self, asset_id): """ Gets a Roblox asset. + + Parameters + ---------- + asset_id + ID of the asset to generate the object from. """ asset = self.requests.cache.get(CacheType.Assets, asset_id) if not asset: @@ -112,6 +132,11 @@ async def get_asset(self, asset_id): async def get_badge(self, badge_id): """ Gets a Roblox badge. + + Parameters + ---------- + badge_id + ID of the badge to generate the object from. """ badge = self.requests.cache.get(CacheType.Assets, badge_id) if not badge: From 8586211759abedd17633e2527a41e0686595a740 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 30 Dec 2020 22:16:19 -0500 Subject: [PATCH 188/518] Docs are back! Oh, and comments are here! --- docs/accountinformation.html | 400 +++++++++++++++++++---------------- docs/accountsettings.html | 95 ++++----- docs/assets.html | 111 +++++++--- docs/badges.html | 34 ++- docs/catalog.html | 38 +++- docs/chat.html | 130 +++++++++--- docs/client.html | 360 ++++++++++++++++++++----------- docs/groups.html | 37 ++-- docs/index.html | 8 +- docs/thumbnails.html | 83 ++++++-- docs/trades.html | 382 +++++++++++++++++++++++++++++---- docs/users.html | 120 +++++------ docs/utilities/cache.html | 166 ++++++++++++++- docs/utilities/pages.html | 36 ++-- docs/utilities/requests.html | 157 +++++++++----- ro_py/accountinformation.py | 9 +- ro_py/accountsettings.py | 2 +- ro_py/assets.py | 17 +- ro_py/badges.py | 10 + ro_py/catalog.py | 7 + ro_py/chat.py | 39 +++- ro_py/utilities/requests.py | 12 +- 22 files changed, 1586 insertions(+), 667 deletions(-) diff --git a/docs/accountinformation.html b/docs/accountinformation.html index d5312db1..230bcdb5 100644 --- a/docs/accountinformation.html +++ b/docs/accountinformation.html @@ -44,13 +44,18 @@

Module ro_py.accountinformation

Represents account information metadata. """ def __init__(self, metadata_raw): - self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ - metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] - self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] - self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] - self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] - self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"] + self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + """Unsure what this does.""" + self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + """Whether the account settings policy is enabled (unsure exactly what this does)""" + self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + """Whether the user's linked phone number is enabled.""" + self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + """Maximum length of the user's description.""" + self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + """Whether the user's description is enabled.""" + self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + """Whether the UserBlock endpoints are updated (unsure exactly what this does)""" class PromotionChannels: @@ -58,85 +63,81 @@

Module ro_py.accountinformation

Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.__dict__["facebook"] = promotion_raw["facebook"] - self.__dict__["twitter"] = promotion_raw["twitter"] - self.__dict__["youtube"] = promotion_raw["youtube"] - self.__dict__["twitch"] = promotion_raw["twitch"] - - @property - def promotion_channels_visibility_privacy(self): - return self.__dict__["promotion_channels_visibility_privacy"] - - @property - def facebook(self): - return self.__dict__["facebook"] - - @property - def twitter(self): - return self.__dict__["twitter"] - - @property - def youtube(self): - return self.__dict__["youtube"] - - @property - def twitch(self): - return self.__dict__["twitch"] + self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + """Visibility of promotion channels.""" + self.facebook = promotion_raw["facebook"] + """Link to the user's Facebook page.""" + self.twitter = promotion_raw["twitter"] + """Link to the user's Twitter page.""" + self.youtube = promotion_raw["youtube"] + """Link to the user's YouTube page.""" + self.twitch = promotion_raw["twitch"] + """Link to the user's Twitch page.""" class AccountInformation: """ Represents authenticated client account information (https://accountinformation.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests self.account_information_metadata = None self.promotion_channels = None - self.update() - def update(self): + async def update(self): """ Updates the account information. - :return: Nothing """ - account_information_req = self.requests.get( + account_information_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/metadata" ) self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.requests.get( + promotion_channels_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/promotion-channels" ) self.promotion_channels = PromotionChannels(promotion_channels_req.json()) - def get_gender(self): + async def get_gender(self): """ Gets the user's gender. - :return: RobloxGender + + Returns + ------- + ro_py.gender.RobloxGender """ - gender_req = self.requests.get(endpoint + "v1/gender") + gender_req = await self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) - def set_gender(self, gender): + async def set_gender(self, gender): """ Sets the user's gender. - :param gender: RobloxGender - :return: Nothing + + Parameters + ---------- + gender : ro_py.gender.RobloxGender """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) } ) - def get_birthdate(self): + async def get_birthdate(self): """ - Returns the user's birthdate. - :return: datetime + Grabs the user's birthdate. + + Returns + ------- + datetime.datetime """ - birthdate_req = self.requests.get(endpoint + "v1/birthdate") + birthdate_req = await self.requests.get(endpoint + "v1/birthdate") birthdate_raw = birthdate_req.json() birthdate = datetime( year=birthdate_raw["birthYear"], @@ -145,13 +146,15 @@

Module ro_py.accountinformation

) return birthdate - def set_birthdate(self, birthdate): + async def set_birthdate(self, birthdate): """ Sets the user's birthdate. - :param birthdate: A datetime object. - :return: Nothing + + Parameters + ---------- + birthdate : datetime.datetime """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, @@ -176,7 +179,12 @@

Classes

Represents authenticated client account information (https://accountinformation.roblox.com/) -This is only available for authenticated clients as it cannot be accessed otherwise.

+This is only available for authenticated clients as it cannot be accessed otherwise.

+

Parameters

+
+
requests : Requests
+
Requests object to use for API requests.
+
Expand source code @@ -185,54 +193,65 @@

Classes

""" Represents authenticated client account information (https://accountinformation.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests self.account_information_metadata = None self.promotion_channels = None - self.update() - def update(self): + async def update(self): """ Updates the account information. - :return: Nothing """ - account_information_req = self.requests.get( + account_information_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/metadata" ) self.account_information_metadata = AccountInformationMetadata(account_information_req.json()) - promotion_channels_req = self.requests.get( + promotion_channels_req = await self.requests.get( url="https://accountinformation.roblox.com/v1/promotion-channels" ) self.promotion_channels = PromotionChannels(promotion_channels_req.json()) - def get_gender(self): + async def get_gender(self): """ Gets the user's gender. - :return: RobloxGender + + Returns + ------- + ro_py.gender.RobloxGender """ - gender_req = self.requests.get(endpoint + "v1/gender") + gender_req = await self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) - def set_gender(self, gender): + async def set_gender(self, gender): """ Sets the user's gender. - :param gender: RobloxGender - :return: Nothing + + Parameters + ---------- + gender : ro_py.gender.RobloxGender """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/gender", data={ "gender": str(gender.value) } ) - def get_birthdate(self): + async def get_birthdate(self): """ - Returns the user's birthdate. - :return: datetime + Grabs the user's birthdate. + + Returns + ------- + datetime.datetime """ - birthdate_req = self.requests.get(endpoint + "v1/birthdate") + birthdate_req = await self.requests.get(endpoint + "v1/birthdate") birthdate_raw = birthdate_req.json() birthdate = datetime( year=birthdate_raw["birthYear"], @@ -241,13 +260,15 @@

Classes

) return birthdate - def set_birthdate(self, birthdate): + async def set_birthdate(self, birthdate): """ Sets the user's birthdate. - :param birthdate: A datetime object. - :return: Nothing + + Parameters + ---------- + birthdate : datetime.datetime """ - self.requests.post( + await self.requests.post( url=endpoint + "v1/birthdate", data={ "birthMonth": birthdate.month, @@ -259,21 +280,28 @@

Classes

Methods

-def get_birthdate(self) +async def get_birthdate(self)
-

Returns the user's birthdate. -:return: datetime

+

Grabs the user's birthdate.

+

Returns

+
+
datetime.datetime
+
 
+
Expand source code -
def get_birthdate(self):
+
async def get_birthdate(self):
     """
-    Returns the user's birthdate.
-    :return: datetime
+    Grabs the user's birthdate.
+
+    Returns
+    -------
+    datetime.datetime
     """
-    birthdate_req = self.requests.get(endpoint + "v1/birthdate")
+    birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
     birthdate_raw = birthdate_req.json()
     birthdate = datetime(
         year=birthdate_raw["birthYear"],
@@ -284,42 +312,54 @@ 

Methods

-def get_gender(self) +async def get_gender(self)
-

Gets the user's gender. -:return: RobloxGender

+

Gets the user's gender.

+

Returns

+
+
RobloxGender
+
 
+
Expand source code -
def get_gender(self):
+
async def get_gender(self):
     """
     Gets the user's gender.
-    :return: RobloxGender
+
+    Returns
+    -------
+    ro_py.gender.RobloxGender
     """
-    gender_req = self.requests.get(endpoint + "v1/gender")
+    gender_req = await self.requests.get(endpoint + "v1/gender")
     return RobloxGender(gender_req.json()["gender"])
-def set_birthdate(self, birthdate) +async def set_birthdate(self, birthdate)
-

Sets the user's birthdate. -:param birthdate: A datetime object. -:return: Nothing

+

Sets the user's birthdate.

+

Parameters

+
+
birthdate : datetime.datetime
+
 
+
Expand source code -
def set_birthdate(self, birthdate):
+
async def set_birthdate(self, birthdate):
     """
     Sets the user's birthdate.
-    :param birthdate: A datetime object.
-    :return: Nothing
+
+    Parameters
+    ----------
+    birthdate : datetime.datetime
     """
-    self.requests.post(
+    await self.requests.post(
         url=endpoint + "v1/birthdate",
         data={
           "birthMonth": birthdate.month,
@@ -330,23 +370,28 @@ 

Methods

-def set_gender(self, gender) +async def set_gender(self, gender)
-

Sets the user's gender. -:param gender: RobloxGender -:return: Nothing

+

Sets the user's gender.

+

Parameters

+
+
gender : RobloxGender
+
 
+
Expand source code -
def set_gender(self, gender):
+
async def set_gender(self, gender):
     """
     Sets the user's gender.
-    :param gender: RobloxGender
-    :return: Nothing
+
+    Parameters
+    ----------
+    gender : ro_py.gender.RobloxGender
     """
-    self.requests.post(
+    await self.requests.post(
         url=endpoint + "v1/gender",
         data={
             "gender": str(gender.value)
@@ -355,25 +400,23 @@ 

Methods

-def update(self) +async def update(self)
-

Updates the account information. -:return: Nothing

+

Updates the account information.

Expand source code -
def update(self):
+
async def update(self):
     """
     Updates the account information.
-    :return: Nothing
     """
-    account_information_req = self.requests.get(
+    account_information_req = await self.requests.get(
         url="https://accountinformation.roblox.com/v1/metadata"
     )
     self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
-    promotion_channels_req = self.requests.get(
+    promotion_channels_req = await self.requests.get(
         url="https://accountinformation.roblox.com/v1/promotion-channels"
     )
     self.promotion_channels = PromotionChannels(promotion_channels_req.json())
@@ -396,14 +439,46 @@

Methods

Represents account information metadata. """ def __init__(self, metadata_raw): - self.__dict__["is_allowed_notifications_endpoint_disabled"] = \ - metadata_raw["isAllowedNotificationsEndpointDisabled"] - self.__dict__["is_account_settings_policy_enabled"] = metadata_raw["isAccountSettingsPolicyEnabled"] - self.__dict__["is_phone_number_enabled"] = metadata_raw["isPhoneNumberEnabled"] - self.__dict__["max_user_description_length"] = metadata_raw["MaxUserDescriptionLength"] - self.__dict__["is_user_description_enabled"] = metadata_raw["isUserDescriptionEnabled"] - self.__dict__["is_user_block_endpoints_updated"] = metadata_raw["isUserBlockEndpointsUpdated"]
+ self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"] + """Unsure what this does.""" + self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"] + """Whether the account settings policy is enabled (unsure exactly what this does)""" + self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"] + """Whether the user's linked phone number is enabled.""" + self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"] + """Maximum length of the user's description.""" + self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"] + """Whether the user's description is enabled.""" + self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"] + """Whether the UserBlock endpoints are updated (unsure exactly what this does)"""
+

Instance variables

+
+
var is_account_settings_policy_enabled
+
+

Whether the account settings policy is enabled (unsure exactly what this does)

+
+
var is_allowed_notifications_endpoint_disabled
+
+

Unsure what this does.

+
+
var is_phone_number_enabled
+
+

Whether the user's linked phone number is enabled.

+
+
var is_user_block_endpoints_updated
+
+

Whether the UserBlock endpoints are updated (unsure exactly what this does)

+
+
var is_user_description_enabled
+
+

Whether the user's description is enabled.

+
+
var max_user_description_length
+
+

Maximum length of the user's description.

+
+
class PromotionChannels @@ -420,93 +495,38 @@

Methods

Represents account information promotion channels. """ def __init__(self, promotion_raw): - self.__dict__["promotion_channels_visibility_privacy"] = promotion_raw["promotionChannelsVisibilityPrivacy"] - self.__dict__["facebook"] = promotion_raw["facebook"] - self.__dict__["twitter"] = promotion_raw["twitter"] - self.__dict__["youtube"] = promotion_raw["youtube"] - self.__dict__["twitch"] = promotion_raw["twitch"] - - @property - def promotion_channels_visibility_privacy(self): - return self.__dict__["promotion_channels_visibility_privacy"] - - @property - def facebook(self): - return self.__dict__["facebook"] - - @property - def twitter(self): - return self.__dict__["twitter"] - - @property - def youtube(self): - return self.__dict__["youtube"] - - @property - def twitch(self): - return self.__dict__["twitch"]
+ self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"] + """Visibility of promotion channels.""" + self.facebook = promotion_raw["facebook"] + """Link to the user's Facebook page.""" + self.twitter = promotion_raw["twitter"] + """Link to the user's Twitter page.""" + self.youtube = promotion_raw["youtube"] + """Link to the user's YouTube page.""" + self.twitch = promotion_raw["twitch"] + """Link to the user's Twitch page."""

Instance variables

var facebook
-
-
- -Expand source code - -
@property
-def facebook(self):
-    return self.__dict__["facebook"]
-
+

Link to the user's Facebook page.

var promotion_channels_visibility_privacy
-
-
- -Expand source code - -
@property
-def promotion_channels_visibility_privacy(self):
-    return self.__dict__["promotion_channels_visibility_privacy"]
-
+

Visibility of promotion channels.

var twitch
-
-
- -Expand source code - -
@property
-def twitch(self):
-    return self.__dict__["twitch"]
-
+

Link to the user's Twitch page.

var twitter
-
-
- -Expand source code - -
@property
-def twitter(self):
-    return self.__dict__["twitter"]
-
+

Link to the user's Twitter page.

var youtube
-
-
- -Expand source code - -
@property
-def youtube(self):
-    return self.__dict__["youtube"]
-
+

Link to the user's YouTube page.

@@ -538,6 +558,14 @@

AccountInformationMetadata

+
  • PromotionChannels

    diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 0a81a06c..07de4596 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -62,24 +62,26 @@

    Module ro_py.accountsettings

    class RobloxEmail: """ Represents an obfuscated version of the email you have set on your account. - """ - def __init__(self, email_data): - self.__dict__["email_address"] = email_data["emailAddress"] - self.__dict__["verified"] = email_data["verified"] - - @property - def email_address(self): - return self.__dict__["email_address"] - @property - def verified(self): - return self.__dict__["verified"] + Parameters + ---------- + email_data : dict + Raw data to parse from. + """ + def __init__(self, email_data: dict): + self.email_address = email_data["emailAddress"] + self.verified = email_data["verified"] class AccountSettings: """ Represents authenticated client account settings (https://accountsettings.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests @@ -125,7 +127,12 @@

    Classes

    Represents authenticated client account settings (https://accountsettings.roblox.com/) -This is only available for authenticated clients as it cannot be accessed otherwise.

    +This is only available for authenticated clients as it cannot be accessed otherwise.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
  • Expand source code @@ -134,6 +141,11 @@

    Classes

    """ Represents authenticated client account settings (https://accountsettings.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests @@ -295,10 +307,15 @@

    Class variables

    class RobloxEmail -(email_data) +(email_data: dict)
    -

    Represents an obfuscated version of the email you have set on your account.

    +

    Represents an obfuscated version of the email you have set on your account.

    +

    Parameters

    +
    +
    email_data : dict
    +
    Raw data to parse from.
    +
    Expand source code @@ -306,48 +323,18 @@

    Class variables

    class RobloxEmail:
         """
         Represents an obfuscated version of the email you have set on your account.
    -    """
    -    def __init__(self, email_data):
    -        self.__dict__["email_address"] = email_data["emailAddress"]
    -        self.__dict__["verified"] = email_data["verified"]
     
    -    @property
    -    def email_address(self):
    -        return self.__dict__["email_address"]
    -
    -    @property
    -    def verified(self):
    -        return self.__dict__["verified"]
    -
    -

    Instance variables

    -
    -
    var email_address
    -
    -
    -
    - -Expand source code - -
    @property
    -def email_address(self):
    -    return self.__dict__["email_address"]
    -
    -
    -
    var verified
    -
    -
    -
    - -Expand source code - -
    @property
    -def verified(self):
    -    return self.__dict__["verified"]
    + Parameters + ---------- + email_data : dict + Raw data to parse from. + """ + def __init__(self, email_data: dict): + self.email_address = email_data["emailAddress"] + self.verified = email_data["verified"]
    -
    -

    Methods

    -def update(self) +async def update(self)

    Updates the group's information.

    @@ -166,11 +168,11 @@

    Methods

    Expand source code -
    def update(self):
    +
    async def update(self):
         """
         Updates the group's information.
         """
    -    group_info_req = self.requests.get(endpoint + f"v1/groups/{self.id}")
    +    group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}")
         group_info = group_info_req.json()
         self.name = group_info["name"]
         self.description = group_info["description"]
    @@ -185,7 +187,7 @@ 

    Methods

    -def update_shout(self, message) +async def update_shout(self, message)
    @@ -193,13 +195,14 @@

    Methods

    Expand source code -
    def update_shout(self, message):
    -    self.requests.patch(
    +
    async def update_shout(self, message):
    +    shout_req = await self.requests.patch(
             url=f"https://groups.roblox.com/v1/groups/{self.id}/status",
             data={
                 "message": message
             }
    -    )
    + ) + return shout_req.status_code == 200
    diff --git a/docs/index.html b/docs/index.html index 1506cde9..60051955 100644 --- a/docs/index.html +++ b/docs/index.html @@ -28,7 +28,9 @@

    Package ro_py

    Welcome to ro.py! ro.py is a powerful wrapper for the Roblox web API. It can be used to create (almost) anything from chat bots to group management systems. -ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at

    +ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/ +You can view the source code at https://github.com/jmk-developer/ro.py/ +You can also view the documentation at https://ro.py.jmksite.dev/.

    Expand source code @@ -41,7 +43,9 @@

    Package ro_py

    Welcome to ro.py! ro.py is a powerful wrapper for the Roblox web API. It can be used to create (almost) anything from chat bots to group management systems. -ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at +ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/ +You can view the source code at https://github.com/jmk-developer/ro.py/ +You can also view the documentation at https://ro.py.jmksite.dev/. """
    diff --git a/docs/thumbnails.html b/docs/thumbnails.html index b4e23b20..aca96691 100644 --- a/docs/thumbnails.html +++ b/docs/thumbnails.html @@ -79,6 +79,11 @@

    Module ro_py.thumbnails

    class ThumbnailGenerator: """ This object is used to generate thumbnails. + + Parameters + ---------- + requests: Requests + Requests object. """ def __init__(self, requests): self.requests = requests @@ -86,11 +91,17 @@

    Module ro_py.thumbnails

    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ Gets a group's icon. - :param group: The group. - :param size: The thumbnail size, formatted widthxheight. - :param file_format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL + + Parameters + ---------- + group: Group + The group. + size: str + The thumbnail size, formatted WIDTHxHEIGHT. + file_format: str + The thumbnail format. + is_circular: bool + Whether to output a circular version of the thumbnail. """ group_icon_req = self.requests.get( url=endpoint + "v1/groups/icons", @@ -174,7 +185,12 @@

    Classes

    (requests)
    -

    This object is used to generate thumbnails.

    +

    This object is used to generate thumbnails.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object.
    +
    Expand source code @@ -182,6 +198,11 @@

    Classes

    class ThumbnailGenerator:
         """
         This object is used to generate thumbnails.
    +
    +    Parameters
    +    ----------
    +    requests: Requests
    +        Requests object.
         """
         def __init__(self, requests):
             self.requests = requests
    @@ -189,11 +210,17 @@ 

    Classes

    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False): """ Gets a group's icon. - :param group: The group. - :param size: The thumbnail size, formatted widthxheight. - :param file_format: The thumbnail format - :param is_circular: The circle thumbnail output parameter. - :return: Image URL + + Parameters + ---------- + group: Group + The group. + size: str + The thumbnail size, formatted WIDTHxHEIGHT. + file_format: str + The thumbnail format. + is_circular: bool + Whether to output a circular version of the thumbnail. """ group_icon_req = self.requests.get( url=endpoint + "v1/groups/icons", @@ -354,12 +381,18 @@

    Methods

    def get_group_icon(self, group, size='150x150', file_format='Png', is_circular=False)
    -

    Gets a group's icon. -:param group: The group. -:param size: The thumbnail size, formatted widthxheight. -:param file_format: The thumbnail format -:param is_circular: The circle thumbnail output parameter. -:return: Image URL

    +

    Gets a group's icon.

    +

    Parameters

    +
    +
    group : Group
    +
    The group.
    +
    size : str
    +
    The thumbnail size, formatted WIDTHxHEIGHT.
    +
    file_format : str
    +
    The thumbnail format.
    +
    is_circular : bool
    +
    Whether to output a circular version of the thumbnail.
    +
    Expand source code @@ -367,11 +400,17 @@

    Methods

    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
         """
         Gets a group's icon.
    -    :param group: The group.
    -    :param size: The thumbnail size, formatted widthxheight.
    -    :param file_format: The thumbnail format
    -    :param is_circular: The circle thumbnail output parameter.
    -    :return: Image URL
    +
    +    Parameters
    +    ----------
    +    group: Group
    +        The group.
    +    size: str
    +        The thumbnail size, formatted WIDTHxHEIGHT.
    +    file_format: str
    +        The thumbnail format.
    +    is_circular: bool
    +        Whether to output a circular version of the thumbnail.
         """
         group_icon_req = self.requests.get(
             url=endpoint + "v1/groups/icons",
    diff --git a/docs/trades.html b/docs/trades.html
    index c5c92174..64584ae5 100644
    --- a/docs/trades.html
    +++ b/docs/trades.html
    @@ -34,32 +34,109 @@ 

    Module ro_py.trades

    """ from ro_py.utilities.pages import Pages, SortOrder +from ro_py.assets import Asset from ro_py.users import User import iso8601 import enum -endpoint = "https://trades.roblox.com/" +endpoint = "https://trades.roblox.com" -def trade_page_handler(requests, this_page): +def trade_page_handler(requests, this_page) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(Trade(requests, raw_trade["id"])) + trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) return trades_out class Trade: - def __init__(self, requests, trade_id): + def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool): + self.trade_id = trade_id self.requests = requests - trade_req = self.requests.get( - url=endpoint + f"v1/trades/{trade_id}" + self.sender = sender + self.recieve_items = recieve_items + self.send_items = send_items + self.created = iso8601.parse(created) + self.experation = iso8601.parse(expiration) + self.status = status + + async def accept(self) -> bool: + """ + accepts a trade requests + :returns: true/false + """ + accept_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/accept" + ) + return accept_req.status_code == 200 + + async def decline(self) -> bool: + """ + decline a trade requests + :returns: true/false + """ + decline_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/decline" + ) + return decline_req.status_code == 200 + + +class PartialTrade: + def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool): + self.requests = requests + self.trade_id = trade_id + self.user = user + self.created = iso8601.parse(created) + self.expiration = iso8601.parse(expiration) + self.status = status + + async def accept(self) -> bool: + """ + accepts a trade requests + :returns: true/false + """ + accept_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/accept" + ) + return accept_req.status_code == 200 + + async def decline(self) -> bool: + """ + decline a trade requests + :returns: true/false + """ + decline_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/decline" ) - trade_data = trade_req.json() - self.id = trade_data["id"] - self.user = User(self.requests, trade_data["user"]["id"]) - self.created = iso8601.parse_date(trade_data["created"]) - self.is_active = trade_data["isActive"] - self.status = trade_data["status"] + return decline_req.status_code == 200 + + async def expand(self) -> Trade: + """ + gets a more detailed trade request + :return: Trade class + """ + expend_req = await self.requests.get( + url=endpoint + f"/v1/trades/{self.trade_id}" + ) + data = expend_req.json() + + # generate a user class and update it + sender = User(self.requests, data['user']['id']) + await sender.update() + + # load items that will be/have been sent and items that you will/have recieve(d) + recieve_items, send_items = [], [] + for items_0 in data['offers'][0]['userAssets']: + item_0 = Asset(self.requests, items_0['assetId']) + await item_0.update() + recieve_items.append(item_0) + + for items_1 in data['offers'][1]['userAssets']: + item_1 = Asset(self.requests, items_1['assetId']) + await item_1.update() + send_items.append(item_1) + + return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status']) class TradeStatusType(enum.Enum): @@ -90,17 +167,17 @@

    Module ro_py.trades

    def __init__(self, requests): self.requests = requests - def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10): - trades = Pages( + async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: + trades = await Pages( requests=self.requests, - url=endpoint + f"/v1/trades/{trade_status_type.value}", + url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, handler=trade_page_handler ) return trades - def send_trade(self): + async def send_trade(self): pass
    @@ -112,7 +189,7 @@

    Module ro_py.trades

    Functions

    -def trade_page_handler(requests, this_page) +def trade_page_handler(requests, this_page) ‑> list
    @@ -120,10 +197,10 @@

    Functions

    Expand source code -
    def trade_page_handler(requests, this_page):
    +
    def trade_page_handler(requests, this_page) -> list:
         trades_out = []
         for raw_trade in this_page:
    -        trades_out.append(Trade(requests, raw_trade["id"]))
    +        trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
         return trades_out
    @@ -132,9 +209,161 @@

    Functions

    Classes

    +
    +class PartialTrade +(requests, trade_id: int, user: User, created, expiration, status: bool) +
    +
    +
    +
    + +Expand source code + +
    class PartialTrade:
    +    def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool):
    +        self.requests = requests
    +        self.trade_id = trade_id
    +        self.user = user
    +        self.created = iso8601.parse(created)
    +        self.expiration = iso8601.parse(expiration)
    +        self.status = status
    +
    +    async def accept(self) -> bool:
    +        """
    +        accepts a trade requests
    +        :returns: true/false
    +        """
    +        accept_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +        )
    +        return accept_req.status_code == 200
    +
    +    async def decline(self) -> bool:
    +        """
    +        decline a trade requests
    +        :returns: true/false
    +        """
    +        decline_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +        )
    +        return decline_req.status_code == 200
    +
    +    async def expand(self) -> Trade:
    +        """
    +        gets a more detailed trade request
    +        :return: Trade class
    +        """
    +        expend_req = await self.requests.get(
    +            url=endpoint + f"/v1/trades/{self.trade_id}"
    +        )
    +        data = expend_req.json()
    +
    +        # generate a user class and update it
    +        sender = User(self.requests, data['user']['id'])
    +        await sender.update()
    +
    +        # load items that will be/have been sent and items that you will/have recieve(d)
    +        recieve_items, send_items = [], []
    +        for items_0 in data['offers'][0]['userAssets']:
    +            item_0 = Asset(self.requests, items_0['assetId'])
    +            await item_0.update()
    +            recieve_items.append(item_0)
    +
    +        for items_1 in data['offers'][1]['userAssets']:
    +            item_1 = Asset(self.requests, items_1['assetId'])
    +            await item_1.update()
    +            send_items.append(item_1)
    +
    +        return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
    +
    +

    Methods

    +
    +
    +async def accept(self) ‑> bool +
    +
    +

    accepts a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def accept(self) -> bool:
    +    """
    +    accepts a trade requests
    +    :returns: true/false
    +    """
    +    accept_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +    )
    +    return accept_req.status_code == 200
    +
    +
    +
    +async def decline(self) ‑> bool +
    +
    +

    decline a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def decline(self) -> bool:
    +    """
    +    decline a trade requests
    +    :returns: true/false
    +    """
    +    decline_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +    )
    +    return decline_req.status_code == 200
    +
    +
    +
    +async def expand(self) ‑> Trade +
    +
    +

    gets a more detailed trade request +:return: Trade class

    +
    + +Expand source code + +
    async def expand(self) -> Trade:
    +    """
    +    gets a more detailed trade request
    +    :return: Trade class
    +    """
    +    expend_req = await self.requests.get(
    +        url=endpoint + f"/v1/trades/{self.trade_id}"
    +    )
    +    data = expend_req.json()
    +
    +    # generate a user class and update it
    +    sender = User(self.requests, data['user']['id'])
    +    await sender.update()
    +
    +    # load items that will be/have been sent and items that you will/have recieve(d)
    +    recieve_items, send_items = [], []
    +    for items_0 in data['offers'][0]['userAssets']:
    +        item_0 = Asset(self.requests, items_0['assetId'])
    +        await item_0.update()
    +        recieve_items.append(item_0)
    +
    +    for items_1 in data['offers'][1]['userAssets']:
    +        item_1 = Asset(self.requests, items_1['assetId'])
    +        await item_1.update()
    +        send_items.append(item_1)
    +
    +    return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
    +
    +
    +
    +
    class Trade -(requests, trade_id) +(requests, trade_id: int, sender: User, recieve_items: list, send_items: list, created, expiration, status: bool)
    @@ -143,19 +372,82 @@

    Classes

    Expand source code
    class Trade:
    -    def __init__(self, requests, trade_id):
    +    def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool):
    +        self.trade_id = trade_id
             self.requests = requests
    -        trade_req = self.requests.get(
    -            url=endpoint + f"v1/trades/{trade_id}"
    +        self.sender = sender
    +        self.recieve_items = recieve_items
    +        self.send_items = send_items
    +        self.created = iso8601.parse(created)
    +        self.experation = iso8601.parse(expiration)
    +        self.status = status
    +
    +    async def accept(self) -> bool:
    +        """
    +        accepts a trade requests
    +        :returns: true/false
    +        """
    +        accept_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +        )
    +        return accept_req.status_code == 200
    +
    +    async def decline(self) -> bool:
    +        """
    +        decline a trade requests
    +        :returns: true/false
    +        """
    +        decline_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
             )
    -        trade_data = trade_req.json()
    -        self.id = trade_data["id"]
    -        self.user = User(self.requests, trade_data["user"]["id"])
    -        self.created = iso8601.parse_date(trade_data["created"])
    -        self.is_active = trade_data["isActive"]
    -        self.status = trade_data["status"]
    + return decline_req.status_code == 200
    + +

    Methods

    +
    +
    +async def accept(self) ‑> bool +
    +
    +

    accepts a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def accept(self) -> bool:
    +    """
    +    accepts a trade requests
    +    :returns: true/false
    +    """
    +    accept_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +    )
    +    return accept_req.status_code == 200
    +
    +async def decline(self) ‑> bool +
    +
    +

    decline a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def decline(self) -> bool:
    +    """
    +    decline a trade requests
    +    :returns: true/false
    +    """
    +    decline_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +    )
    +    return decline_req.status_code == 200
    +
    +
    +
    +
    class TradeStatusType (value, names=None, *, module=None, qualname=None, type=None, start=1) @@ -237,23 +529,23 @@

    Class variables

    def __init__(self, requests): self.requests = requests - def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10): - trades = Pages( + async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: + trades = await Pages( requests=self.requests, - url=endpoint + f"/v1/trades/{trade_status_type.value}", + url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, handler=trade_page_handler ) return trades - def send_trade(self): + async def send_trade(self): pass

    Methods

    -def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10) +async def get_trades(self, trade_status_type: <TradeStatusType.Inbound: 'Inbound'>, sort_order=SortOrder.Ascending, limit=10) ‑> Pages
    @@ -261,10 +553,10 @@

    Methods

    Expand source code -
    def get_trades(self, trade_status_type: TradeStatusType, sort_order=SortOrder.Ascending, limit=10):
    -    trades = Pages(
    +
    async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
    +    trades = await Pages(
             requests=self.requests,
    -        url=endpoint + f"/v1/trades/{trade_status_type.value}",
    +        url=endpoint + f"/v1/trades/{trade_status_type}",
             sort_order=sort_order,
             limit=limit,
             handler=trade_page_handler
    @@ -273,7 +565,7 @@ 

    Methods

    -def send_trade(self) +async def send_trade(self)
    @@ -281,7 +573,7 @@

    Methods

    Expand source code -
    def send_trade(self):
    +
    async def send_trade(self):
         pass
    @@ -309,7 +601,19 @@

    Index

  • Classes

    • +

      PartialTrade

      + +
    • +
    • Trade

      +
    • TradeStatusType

      diff --git a/docs/users.html b/docs/users.html index e1a25b72..86cea00a 100644 --- a/docs/users.html +++ b/docs/users.html @@ -44,25 +44,21 @@

      Module ro_py.users

      Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. """ - def __init__(self, requests, ui): - + def __init__(self, requests, id, name=None): self.requests = requests - self.id = ui - + self.id = id self.description = None self.created = None self.is_banned = None - self.name = None + self.name = name self.display_name = None - self.update() - - def update(self): + async def update(self): """ Updates some class values. :return: Nothing """ - user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") + user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -72,58 +68,58 @@

      Module ro_py.users

      # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - def get_status(self): + async def get_status(self): """ Gets the user's status. :return: A string """ - status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - def get_roblox_badges(self): + async def get_roblox_badges(self): """ Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - def get_friends_count(self): + async def get_friends_count(self): """ Gets the user's friends count. :return: An integer """ - friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count - def get_followers_count(self): + async def get_followers_count(self): """ Gets the user's followers count. :return: An integer """ - followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count - def get_followings_count(self): + async def get_followings_count(self): """ Gets the user's followings count. :return: An integer """ - followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count - def get_friends(self): + async def get_friends(self): """ Gets the user's friends. :return: A list of User instances. """ - friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: @@ -144,7 +140,7 @@

      Classes

      class User -(requests, ui) +(requests, id, name=None)

      Represents a Roblox user and their profile. @@ -158,25 +154,21 @@

      Classes

      Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. """ - def __init__(self, requests, ui): - + def __init__(self, requests, id, name=None): self.requests = requests - self.id = ui - + self.id = id self.description = None self.created = None self.is_banned = None - self.name = None + self.name = name self.display_name = None - self.update() - - def update(self): + async def update(self): """ Updates some class values. :return: Nothing """ - user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}") + user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() self.description = user_info["description"] self.created = iso8601.parse_date(user_info["created"]) @@ -186,58 +178,58 @@

      Classes

      # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - def get_status(self): + async def get_status(self): """ Gets the user's status. :return: A string """ - status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status") + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] - def get_roblox_badges(self): + async def get_roblox_badges(self): """ Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - def get_friends_count(self): + async def get_friends_count(self): """ Gets the user's friends count. :return: An integer """ - friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] return friends_count - def get_followers_count(self): + async def get_followers_count(self): """ Gets the user's followers count. :return: An integer """ - followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] return followers_count - def get_followings_count(self): + async def get_followings_count(self): """ Gets the user's followings count. :return: An integer """ - followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count - def get_friends(self): + async def get_friends(self): """ Gets the user's friends. :return: A list of User instances. """ - friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: @@ -249,7 +241,7 @@

      Classes

      Methods

      -def get_followers_count(self) +async def get_followers_count(self)

      Gets the user's followers count. @@ -258,18 +250,18 @@

      Methods

      Expand source code -
      def get_followers_count(self):
      +
      async def get_followers_count(self):
           """
           Gets the user's followers count.
           :return: An integer
           """
      -    followers_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
      +    followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
           followers_count = followers_count_req.json()["count"]
           return followers_count
      -def get_followings_count(self) +async def get_followings_count(self)

      Gets the user's followings count. @@ -278,18 +270,18 @@

      Methods

      Expand source code -
      def get_followings_count(self):
      +
      async def get_followings_count(self):
           """
           Gets the user's followings count.
           :return: An integer
           """
      -    followings_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
      +    followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
           followings_count = followings_count_req.json()["count"]
           return followings_count
      -def get_friends(self) +async def get_friends(self)

      Gets the user's friends. @@ -298,12 +290,12 @@

      Methods

      Expand source code -
      def get_friends(self):
      +
      async def get_friends(self):
           """
           Gets the user's friends.
           :return: A list of User instances.
           """
      -    friends_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
      +    friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
           friends_raw = friends_req.json()["data"]
           friends_list = []
           for friend_raw in friends_raw:
      @@ -314,7 +306,7 @@ 

      Methods

      -def get_friends_count(self) +async def get_friends_count(self)

      Gets the user's friends count. @@ -323,18 +315,18 @@

      Methods

      Expand source code -
      def get_friends_count(self):
      +
      async def get_friends_count(self):
           """
           Gets the user's friends count.
           :return: An integer
           """
      -    friends_count_req = self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
      +    friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
           friends_count = friends_count_req.json()["count"]
           return friends_count
      -def get_roblox_badges(self) +async def get_roblox_badges(self)

      Gets the user's roblox badges. @@ -343,12 +335,12 @@

      Methods

      Expand source code -
      def get_roblox_badges(self):
      +
      async def get_roblox_badges(self):
           """
           Gets the user's roblox badges.
           :return: A list of RobloxBadge instances
           """
      -    roblox_badges_req = self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
      +    roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
           roblox_badges = []
           for roblox_badge_data in roblox_badges_req.json():
               roblox_badges.append(RobloxBadge(roblox_badge_data))
      @@ -356,7 +348,7 @@ 

      Methods

      -def get_status(self) +async def get_status(self)

      Gets the user's status. @@ -365,17 +357,17 @@

      Methods

      Expand source code -
      def get_status(self):
      +
      async def get_status(self):
           """
           Gets the user's status.
           :return: A string
           """
      -    status_req = self.requests.get(endpoint + f"v1/users/{self.id}/status")
      +    status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
           return status_req.json()["status"]
      -def update(self) +async def update(self)

      Updates some class values. @@ -384,12 +376,12 @@

      Methods

      Expand source code -
      def update(self):
      +
      async def update(self):
           """
           Updates some class values.
           :return: Nothing
           """
      -    user_info_req = self.requests.get(endpoint + f"v1/users/{self.id}")
      +    user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
           user_info = user_info_req.json()
           self.description = user_info["description"]
           self.created = iso8601.parse_date(user_info["created"])
      diff --git a/docs/utilities/cache.html b/docs/utilities/cache.html
      index f4cd45e7..0a07b3b7 100644
      --- a/docs/utilities/cache.html
      +++ b/docs/utilities/cache.html
      @@ -26,13 +26,35 @@ 

      Module ro_py.utilities.cache

      Expand source code -
      cache = {
      -    "users": {},
      -    "groups": {},
      -    "games": {},
      -    "assets": {},
      -    "badges": {}
      -}
      +
      import enum
      +
      +
      +class CacheType(enum.Enum):
      +    Users = "users"
      +    Groups = "groups"
      +    Games = "games"
      +    Assets = "assets"
      +    Badges = "badges"
      +
      +
      +class Cache:
      +    def __init__(self):
      +        self.cache = {
      +            "users": {},
      +            "groups": {},
      +            "games": {},
      +            "assets": {},
      +            "badges": {}
      +        }
      +
      +    def get(self, cache_type: CacheType, item_id: str):
      +        if item_id in self.cache[cache_type.value]:
      +            return self.cache[cache_type.value][item_id]
      +        else:
      +            return False
      +
      +    def set(self, cache_type: CacheType, item_id: str, item_obj):
      +        self.cache[cache_type.value][item_id] = item_obj
  • @@ -42,6 +64,115 @@

    Module ro_py.utilities.cache

    +

    Classes

    +
    +
    +class Cache +
    +
    +
    +
    + +Expand source code + +
    class Cache:
    +    def __init__(self):
    +        self.cache = {
    +            "users": {},
    +            "groups": {},
    +            "games": {},
    +            "assets": {},
    +            "badges": {}
    +        }
    +
    +    def get(self, cache_type: CacheType, item_id: str):
    +        if item_id in self.cache[cache_type.value]:
    +            return self.cache[cache_type.value][item_id]
    +        else:
    +            return False
    +
    +    def set(self, cache_type: CacheType, item_id: str, item_obj):
    +        self.cache[cache_type.value][item_id] = item_obj
    +
    +

    Methods

    +
    +
    +def get(self, cache_type: CacheType, item_id: str) +
    +
    +
    +
    + +Expand source code + +
    def get(self, cache_type: CacheType, item_id: str):
    +    if item_id in self.cache[cache_type.value]:
    +        return self.cache[cache_type.value][item_id]
    +    else:
    +        return False
    +
    +
    +
    +def set(self, cache_type: CacheType, item_id: str, item_obj) +
    +
    +
    +
    + +Expand source code + +
    def set(self, cache_type: CacheType, item_id: str, item_obj):
    +    self.cache[cache_type.value][item_id] = item_obj
    +
    +
    +
    +
    +
    +class CacheType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    An enumeration.

    +
    + +Expand source code + +
    class CacheType(enum.Enum):
    +    Users = "users"
    +    Groups = "groups"
    +    Games = "games"
    +    Assets = "assets"
    +    Badges = "badges"
    +
    +

    Ancestors

    +
      +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var Assets
    +
    +
    +
    +
    var Badges
    +
    +
    +
    +
    var Games
    +
    +
    +
    +
    var Groups
    +
    +
    +
    +
    var Users
    +
    +
    +
    +
    +
    +
    diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index 4d39e4f0..d1f4a6c8 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -85,7 +85,7 @@

    Module ro_py.utilities.pages

    self.data = self._get_page() - def _get_page(self, cursor=None): + async def _get_page(self, cursor=None): """ Gets a page at the specified cursor position. """ @@ -93,7 +93,7 @@

    Module ro_py.utilities.pages

    if cursor: this_parameters["cursor"] = cursor - page_req = self.requests.get( + page_req = await self.requests.get( url=self.url, params=this_parameters ) @@ -103,21 +103,21 @@

    Module ro_py.utilities.pages

    handler=self.handler ) - def previous(self): + async def previous(self): """ Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = self._get_page(self.data.previous_page_cursor) + self.data = await self._get_page(self.data.previous_page_cursor) else: raise InvalidPageError - def next(self): + async def next(self): """ Moves to the next page. """ if self.data.next_page_cursor: - self.data = self._get_page(self.data.next_page_cursor) + self.data = await self._get_page(self.data.next_page_cursor) else: raise InvalidPageError
    @@ -219,7 +219,7 @@

    Instance variables

    self.data = self._get_page() - def _get_page(self, cursor=None): + async def _get_page(self, cursor=None): """ Gets a page at the specified cursor position. """ @@ -227,7 +227,7 @@

    Instance variables

    if cursor: this_parameters["cursor"] = cursor - page_req = self.requests.get( + page_req = await self.requests.get( url=self.url, params=this_parameters ) @@ -237,21 +237,21 @@

    Instance variables

    handler=self.handler ) - def previous(self): + async def previous(self): """ Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = self._get_page(self.data.previous_page_cursor) + self.data = await self._get_page(self.data.previous_page_cursor) else: raise InvalidPageError - def next(self): + async def next(self): """ Moves to the next page. """ if self.data.next_page_cursor: - self.data = self._get_page(self.data.next_page_cursor) + self.data = await self._get_page(self.data.next_page_cursor) else: raise InvalidPageError
    @@ -281,7 +281,7 @@

    Instance variables

    Methods

    -def next(self) +async def next(self)

    Moves to the next page.

    @@ -289,18 +289,18 @@

    Methods

    Expand source code -
    def next(self):
    +
    async def next(self):
         """
         Moves to the next page.
         """
         if self.data.next_page_cursor:
    -        self.data = self._get_page(self.data.next_page_cursor)
    +        self.data = await self._get_page(self.data.next_page_cursor)
         else:
             raise InvalidPageError
    -def previous(self) +async def previous(self)

    Moves to the previous page.

    @@ -308,12 +308,12 @@

    Methods

    Expand source code -
    def previous(self):
    +
    async def previous(self):
         """
         Moves to the previous page.
         """
         if self.data.previous_page_cursor:
    -        self.data = self._get_page(self.data.previous_page_cursor)
    +        self.data = await self._get_page(self.data.previous_page_cursor)
         else:
             raise InvalidPageError
    diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 5eece9ef..71b579dd 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -27,15 +27,29 @@

    Module ro_py.utilities.requests

    Expand source code
    from ro_py.utilities.errors import ApiError
    +from ro_py.utilities.cache import Cache
     from json.decoder import JSONDecodeError
     from cachecontrol import CacheControl
    -import requests
    +import requests_async
     
     
     class Requests:
    -    def __init__(self, cache=True):
    -        self.session = requests.Session()
    -        if cache:
    +    """
    +    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
    +
    +    Parameters
    +    ----------
    +    request_cache: bool
    +        Enable this to wrap the session in a CacheControl object. Untested.
    +    jmk_endpoint: str
    +        Not currently in use.
    +    """
    +    def __init__(self, request_cache: bool = True, jmk_endpoint="https://roblox.jmksite.dev/"):
    +        self.session = requests_async.Session()
    +        """Session to use for requests."""
    +        self.cache = Cache()
    +        """Cache object to use for object storage."""
    +        if request_cache:
                 self.session = CacheControl(self.session)
             """
             Thank you @nsg for letting me know about this!
    @@ -44,12 +58,12 @@ 

    Module ro_py.utilities.requests

    """ self.session.headers["User-Agent"] = "Roblox/WinInet" - def get(self, *args, **kwargs): + async def get(self, *args, **kwargs): """ - Essentially identical to requests.Session.get. + Essentially identical to requests_async.Session.get. """ - get_request = self.session.get(*args, **kwargs) + get_request = await self.session.get(*args, **kwargs) try: get_request_json = get_request.json() @@ -66,17 +80,17 @@

    Module ro_py.utilities.requests

    raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") - def post(self, *args, **kwargs): + async def post(self, *args, **kwargs): """ - Essentially identical to requests.Session.post. + Essentially identical to requests_async.Session.post. """ - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -85,23 +99,25 @@

    Module ro_py.utilities.requests

    if isinstance(post_request_json, dict): try: - post_request_json["errors"] + post_request_error = post_request_json["errors"] except KeyError: return post_request else: return post_request - def patch(self, *args, **kwargs): + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + + async def patch(self, *args, **kwargs): """ - Essentially identical to requests.Session.patch. + Essentially identical to requests_async.Session.patch. """ - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) patch_request_json = patch_request.json() @@ -127,18 +143,38 @@

    Classes

    class Requests -(cache=True) +(request_cache: bool = True, jmk_endpoint='https://roblox.jmksite.dev/')
    -
    +

    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.

    +

    Parameters

    +
    +
    request_cache : bool
    +
    Enable this to wrap the session in a CacheControl object. Untested.
    +
    jmk_endpoint : str
    +
    Not currently in use.
    +
    Expand source code
    class Requests:
    -    def __init__(self, cache=True):
    -        self.session = requests.Session()
    -        if cache:
    +    """
    +    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
    +
    +    Parameters
    +    ----------
    +    request_cache: bool
    +        Enable this to wrap the session in a CacheControl object. Untested.
    +    jmk_endpoint: str
    +        Not currently in use.
    +    """
    +    def __init__(self, request_cache: bool = True, jmk_endpoint="https://roblox.jmksite.dev/"):
    +        self.session = requests_async.Session()
    +        """Session to use for requests."""
    +        self.cache = Cache()
    +        """Cache object to use for object storage."""
    +        if request_cache:
                 self.session = CacheControl(self.session)
             """
             Thank you @nsg for letting me know about this!
    @@ -147,12 +183,12 @@ 

    Classes

    """ self.session.headers["User-Agent"] = "Roblox/WinInet" - def get(self, *args, **kwargs): + async def get(self, *args, **kwargs): """ - Essentially identical to requests.Session.get. + Essentially identical to requests_async.Session.get. """ - get_request = self.session.get(*args, **kwargs) + get_request = await self.session.get(*args, **kwargs) try: get_request_json = get_request.json() @@ -169,17 +205,17 @@

    Classes

    raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") - def post(self, *args, **kwargs): + async def post(self, *args, **kwargs): """ - Essentially identical to requests.Session.post. + Essentially identical to requests_async.Session.post. """ - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) if post_request.status_code == 403: if "X-CSRF-TOKEN" in post_request.headers: self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = self.session.post(*args, **kwargs) + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -188,23 +224,25 @@

    Classes

    if isinstance(post_request_json, dict): try: - post_request_json["errors"] + post_request_error = post_request_json["errors"] except KeyError: return post_request else: return post_request - def patch(self, *args, **kwargs): + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + + async def patch(self, *args, **kwargs): """ - Essentially identical to requests.Session.patch. + Essentially identical to requests_async.Session.patch. """ - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) if patch_request.status_code == 403: if "X-CSRF-TOKEN" in patch_request.headers: self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = self.session.patch(*args, **kwargs) + patch_request = await self.session.patch(*args, **kwargs) patch_request_json = patch_request.json() @@ -218,23 +256,34 @@

    Classes

    raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
    +

    Instance variables

    +
    +
    var cache
    +
    +

    Cache object to use for object storage.

    +
    +
    var session
    +
    +

    Session to use for requests.

    +
    +

    Methods

    -def get(self, *args, **kwargs) +async def get(self, *args, **kwargs)
    -

    Essentially identical to requests.Session.get.

    +

    Essentially identical to requests_async.Session.get.

    Expand source code -
    def get(self, *args, **kwargs):
    +
    async def get(self, *args, **kwargs):
         """
    -    Essentially identical to requests.Session.get.
    +    Essentially identical to requests_async.Session.get.
         """
     
    -    get_request = self.session.get(*args, **kwargs)
    +    get_request = await self.session.get(*args, **kwargs)
     
         try:
             get_request_json = get_request.json()
    @@ -253,25 +302,25 @@ 

    Methods

    -def patch(self, *args, **kwargs) +async def patch(self, *args, **kwargs)
    -

    Essentially identical to requests.Session.patch.

    +

    Essentially identical to requests_async.Session.patch.

    Expand source code -
    def patch(self, *args, **kwargs):
    +
    async def patch(self, *args, **kwargs):
         """
    -    Essentially identical to requests.Session.patch.
    +    Essentially identical to requests_async.Session.patch.
         """
     
    -    patch_request = self.session.patch(*args, **kwargs)
    +    patch_request = await self.session.patch(*args, **kwargs)
     
         if patch_request.status_code == 403:
             if "X-CSRF-TOKEN" in patch_request.headers:
                 self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
    -            patch_request = self.session.patch(*args, **kwargs)
    +            patch_request = await self.session.patch(*args, **kwargs)
     
         patch_request_json = patch_request.json()
     
    @@ -287,25 +336,25 @@ 

    Methods

    -def post(self, *args, **kwargs) +async def post(self, *args, **kwargs)
    -

    Essentially identical to requests.Session.post.

    +

    Essentially identical to requests_async.Session.post.

    Expand source code -
    def post(self, *args, **kwargs):
    +
    async def post(self, *args, **kwargs):
         """
    -    Essentially identical to requests.Session.post.
    +    Essentially identical to requests_async.Session.post.
         """
     
    -    post_request = self.session.post(*args, **kwargs)
    +    post_request = await self.session.post(*args, **kwargs)
     
         if post_request.status_code == 403:
             if "X-CSRF-TOKEN" in post_request.headers:
                 self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
    -            post_request = self.session.post(*args, **kwargs)
    +            post_request = await self.session.post(*args, **kwargs)
     
         try:
             post_request_json = post_request.json()
    @@ -314,11 +363,13 @@ 

    Methods

    if isinstance(post_request_json, dict): try: - post_request_json["errors"] + post_request_error = post_request_json["errors"] except KeyError: return post_request else: - return post_request
    + return post_request + + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}")
    @@ -342,9 +393,11 @@

    Index

  • Requests

  • diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index bc57794c..6ba8a1e0 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -50,6 +50,11 @@ class AccountInformation: """ Represents authenticated client account information (https://accountinformation.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. """ def __init__(self, requests): self.requests = requests @@ -75,7 +80,7 @@ async def get_gender(self): Returns ------- - RobloxGender + ro_py.gender.RobloxGender """ gender_req = await self.requests.get(endpoint + "v1/gender") return RobloxGender(gender_req.json()["gender"]) @@ -86,7 +91,7 @@ async def set_gender(self, gender): Parameters ---------- - gender : RobloxGender + gender : ro_py.gender.RobloxGender """ await self.requests.post( url=endpoint + "v1/gender", diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 1dcd415a..83e94e19 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -51,7 +51,7 @@ class AccountSettings: Parameters ---------- - requests : requests.Requests + requests : ro_py.utilities.requests.Requests Requests object to use for API requests. """ def __init__(self, requests): diff --git a/ro_py/assets.py b/ro_py/assets.py index d7a5b7f3..52055e7d 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -17,6 +17,13 @@ class Asset: """ Represents an asset. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + asset_id + ID of the asset. """ def __init__(self, requests, asset_id): self.id = asset_id @@ -79,7 +86,10 @@ async def update(self): async def get_remaining(self): """ Gets the remaining amount of this asset. (used for Limited U items) - :returns: Amount remaining + + Returns + ------- + int """ asset_info_req = await self.requests.get( url=endpoint + "marketplace/productinfo", @@ -93,7 +103,10 @@ async def get_remaining(self): async def get_limited_resale_data(self): """ Gets the limited resale data - :returns: LimitedResaleData + + Returns + ------- + LimitedResaleData """ if self.is_limited: resale_data_req = await self.requests.get(f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data") diff --git a/ro_py/badges.py b/ro_py/badges.py index bfec4a1c..36978f2a 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -20,6 +20,13 @@ def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage): class Badge: """ Represents a game-awarded badge. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + badge_id + ID of the badge. """ def __init__(self, requests, badge_id): self.id = badge_id @@ -33,6 +40,9 @@ def __init__(self, requests, badge_id): self.update() def update(self): + """ + Updates the badge's information. + """ badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] diff --git a/ro_py/catalog.py b/ro_py/catalog.py index ad087128..05a2c6ea 100644 --- a/ro_py/catalog.py +++ b/ro_py/catalog.py @@ -8,7 +8,14 @@ class AppStore(enum.Enum): + """ + Represents an app store that the Roblox app is downloadable on. + """ google_play = "GooglePlay" + android = "GooglePlay" amazon = "Amazon" + fire = "Amazon" ios = "iOS" + iphone = "iOS" + idevice = "iOS" xbox = "Xbox" diff --git a/ro_py/chat.py b/ro_py/chat.py index ff921243..008cf496 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -82,6 +82,18 @@ def send_message(self, content): class Message: + """ + Represents a single message in a chat conversation. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + message_id + ID of the message. + conversation_id + ID of the conversation that contains the message. + """ def __init__(self, requests, message_id, conversation_id): self.requests = requests self.id = message_id @@ -91,10 +103,11 @@ def __init__(self, requests, message_id, conversation_id): self.sender = None self.read = None - self.update() - - def update(self): - message_req = self.requests.get( + async def update(self): + """ + Updates the message with new data. + """ + message_req = await self.requests.get( url="https://chat.roblox.com/v2/get-messages", params={ "conversationId": self.conversation_id, @@ -110,18 +123,28 @@ def update(self): class ChatWrapper: + """ + Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right + of the Roblox web client. + """ def __init__(self, requests): self.requests = requests - def get_conversation(self, conversation_id): + async def get_conversation(self, conversation_id): """ Gets a conversation by the conversation ID. + + Parameters + ---------- + conversation_id + ID of the conversation. """ - return Conversation(self.requests, conversation_id) + conversation = Conversation(self.requests, conversation_id) + await conversation.update() - def get_conversations(self, page_number=1, page_size=10): + async def get_conversations(self, page_number=1, page_size=10): """ - Gets the list of conversations. This will be updated soon to use the new Pages object. + Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented. """ conversations_req = self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index ac624925..deb256f9 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -6,7 +6,17 @@ class Requests: - def __init__(self, request_cache=True, jmk_endpoint="https://roblox.jmksite.dev/"): + """ + This wrapper functions similarly to requests_async.Session, but made specifically for Roblox. + + Parameters + ---------- + request_cache: bool + Enable this to wrap the session in a CacheControl object. Untested. + jmk_endpoint: str + Not currently in use. + """ + def __init__(self, request_cache: bool = True, jmk_endpoint="https://roblox.jmksite.dev/"): self.session = requests_async.Session() """Session to use for requests.""" self.cache = Cache() From 1dcbe75b55b5ae033841aa28601b084b3973ef89 Mon Sep 17 00:00:00 2001 From: iranathan Date: Thu, 31 Dec 2020 23:45:07 +0100 Subject: [PATCH 189/518] add get_roles, role class + more --- ro_py/groups.py | 16 ++++++++++++--- ro_py/roles.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 ro_py/roles.py diff --git a/ro_py/groups.py b/ro_py/groups.py index 1b857d03..0c47a049 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -5,8 +5,9 @@ """ from ro_py.users import User +from ro_py.roles import Role -endpoint = "https://groups.roblox.com/" +endpoint = "https://groups.roblox.com" class Shout: @@ -40,7 +41,7 @@ async def update(self): """ Updates the group's information. """ - group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] @@ -56,9 +57,18 @@ async def update(self): async def update_shout(self, message): shout_req = await self.requests.patch( - url=f"https://groups.roblox.com/v1/groups/{self.id}/status", + url=endpoint+ f"/v1/groups/{self.id}/status", data={ "message": message } ) return shout_req.status_code == 200 + + async def get_roles(self): + role_req = await self.requests.get( + url=endpoint + f"/v1/groups/{self.id}/roles" + ) + roles = [] + for role in role_req.json()['roles']: + roles.append(Role(self.requests, self, role)) + return roles diff --git a/ro_py/roles.py b/ro_py/roles.py new file mode 100644 index 00000000..9b799fe9 --- /dev/null +++ b/ro_py/roles.py @@ -0,0 +1,53 @@ +endpoint = "https://groups.roblox.com" + + +class Role: + """ + Represents a role + This is only available for authenticated clients as it cannot be accessed otherwise. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + group : ro_py.groups.Group + Group the role belongs to. + role_data : dict + Dictionary containing role information. + """ + def __init__(self, requests, group, role_data): + self.requests = requests + self.group = group + self.id = role_data['id'] + self.name = role_data['name'] + self.description = role_data['description'] + self.rank = role_data['rank'] + self.member_count = role_data['memberCount'] + + async def update(self): + update_req = await self.requests.get( + url=endpoint + f"/v1/groups/{self.group.id}/roles" + ) + data = update_req.json() + for role in data['roles']: + if role['id'] == self.id: + self.name = role['name'] + self.description = role['description'] + self.rank = role['rank'] + self.member_count = role['memberCount'] + break + + async def edit(self, name=None, description=None, rank=None): + edit_req = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", + data={ + "description": description if description else self.description + "name": name if name else self.name, + "rank": rank if rank else self.rank + } + ) + return edit_req.status_code == 200 + + # TODO: + async def edit_permissions(self): + pass From 5dda22ac1c5cee68b9c59e37bd3803b812d6eeb3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 31 Dec 2020 18:04:50 -0500 Subject: [PATCH 190/518] Fixed ira's roles.py --- ro_py/roles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/roles.py b/ro_py/roles.py index 9b799fe9..336c6f3f 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -41,7 +41,7 @@ async def edit(self, name=None, description=None, rank=None): edit_req = await self.requests.patch( url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", data={ - "description": description if description else self.description + "description": description if description else self.description, "name": name if name else self.name, "rank": rank if rank else self.rank } From 11b32f778b935da5c08bb2c2b8428106923d3077 Mon Sep 17 00:00:00 2001 From: iranathan Date: Fri, 1 Jan 2021 01:06:38 +0100 Subject: [PATCH 191/518] add role permission functions --- ro_py/roles.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/ro_py/roles.py b/ro_py/roles.py index 336c6f3f..51abd9a6 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -1,6 +1,47 @@ endpoint = "https://groups.roblox.com" +class RolePermissions(enum.Enum): + """ + Represents role permissions. + """ + view_wall = None + post_to_wall = None + delete_from_wall = None + view_status = None + post_to_status = None + change_rank = None + invite_members = None + remove_members = None + manage_relationships = None + view_audit_logs = None + spend_group_funds = None + advertise_group = None + create_items = None + manage_items = None + manage_group_games = None + + +def get_rp_names(rp): + return { + "viewWall": rp.view_wall, + "PostToWall": rp.post_to_wall, + "deleteFromWall": rp.delete_from_wall, + "viewStatus": rp.view_status, + "postToStatus": rp.post_to_status, + "changeRank": rp.change_rank, + "inviteMembers": rp.invite_members, + "removeMembers": rp.remove_members, + "manageRelationships": rp.manage_relationships, + "viewAuditLogs": rp.view_audit_logs, + "spendGroupFunds": rp.spend_group_funds, + "advertiseGroup": rp.advertise_group, + "createItems": rp.create_items, + "manageItems": rp.manage_items, + "manageGroupGames": rp.manage_group_games + } + + class Role: """ Represents a role @@ -48,6 +89,18 @@ async def edit(self, name=None, description=None, rank=None): ) return edit_req.status_code == 200 - # TODO: - async def edit_permissions(self): - pass + async def edit_permissions(self, role_permissions): + data = { + "permissions": {} + } + + for key, value in get_rp_names(role_permissions): + if value is True or False: + data['permissions'][key] = value + + edit_req = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions", + data=data + ) + + return edit_req.status_code == 200 From ef796380871f6aa094a3b530f25bb9b930c00d69 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 31 Dec 2020 20:47:36 -0500 Subject: [PATCH 192/518] Fixed all asynchronous issues --- ro_py/assets.py | 1 - ro_py/badges.py | 5 ++--- ro_py/games.py | 15 +++++++-------- ro_py/groups.py | 2 -- ro_py/roles.py | 9 +++++++++ 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index 52055e7d..cc8052be 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -47,7 +47,6 @@ def __init__(self, requests, asset_id): self.is_limited_unique = None self.minimum_membership_level = None self.content_rating_type_id = None - self.update() async def update(self): """ diff --git a/ro_py/badges.py b/ro_py/badges.py index 36978f2a..0840e24f 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -37,13 +37,12 @@ def __init__(self, requests, badge_id): self.display_description = None self.enabled = None self.statistics = None - self.update() - def update(self): + async def update(self): """ Updates the badge's information. """ - badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") + badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] diff --git a/ro_py/games.py b/ro_py/games.py index 34f50cf2..4d61796f 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -37,13 +37,12 @@ def __init__(self, requests, universe_id): self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None - self.update() - def update(self): + async def update(self): """ Updates the game's information. """ - game_info_req = self.requests.get( + game_info_req = await self.requests.get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -64,11 +63,11 @@ def update(self): self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - def get_votes(self): + async def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = self.requests.get( + votes_info_req = await self.requests.get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -79,12 +78,12 @@ def get_votes(self): votes = Votes(votes_info) return votes - def get_badges(self): + async def get_badges(self): """ Gets the game's badges. This will be updated soon to use the new Page object. """ - badges_req = self.requests.get( + badges_req = await self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -122,4 +121,4 @@ def game_from_place_id(place_id): :return: Instace of Game \""" return Game(self.requests, place_id_to_universe_id(place_id)) -""" \ No newline at end of file +""" diff --git a/ro_py/groups.py b/ro_py/groups.py index 0c47a049..a8a7bc3b 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -35,8 +35,6 @@ def __init__(self, requests, group_id): self.public_entry_allowed = None self.shout = None - self.update() - async def update(self): """ Updates the group's information. diff --git a/ro_py/roles.py b/ro_py/roles.py index 51abd9a6..b80d6f60 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -1,3 +1,12 @@ +""" + +IRA PUT THINGS HERE + +""" + + +import enum + endpoint = "https://groups.roblox.com" From 1271a755dd4408dd31cbf0aa0234e40ebae579ea Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 31 Dec 2020 20:49:04 -0500 Subject: [PATCH 193/518] Updated docs with new async fixes --- docs/assets.html | 2 - docs/badges.html | 16 +- docs/games.html | 44 ++--- docs/groups.html | 61 ++++-- docs/index.html | 5 + docs/roles.html | 501 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 582 insertions(+), 47 deletions(-) create mode 100644 docs/roles.html diff --git a/docs/assets.html b/docs/assets.html index e0cb171d..7bfe04a0 100644 --- a/docs/assets.html +++ b/docs/assets.html @@ -76,7 +76,6 @@

    Module ro_py.assets

    self.is_limited_unique = None self.minimum_membership_level = None self.content_rating_type_id = None - self.update() async def update(self): """ @@ -203,7 +202,6 @@

    Parameters

    self.is_limited_unique = None self.minimum_membership_level = None self.content_rating_type_id = None - self.update() async def update(self): """ diff --git a/docs/badges.html b/docs/badges.html index 17c961e3..c692c0a3 100644 --- a/docs/badges.html +++ b/docs/badges.html @@ -66,13 +66,12 @@

    Module ro_py.badges

    self.display_description = None self.enabled = None self.statistics = None - self.update() - def update(self): + async def update(self): """ Updates the badge's information. """ - badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") + badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] @@ -133,13 +132,12 @@

    Parameters

    self.display_description = None self.enabled = None self.statistics = None - self.update() - def update(self): + async def update(self): """ Updates the badge's information. """ - badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}") + badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}") badge_info = badge_info_req.json() self.name = badge_info["name"] self.description = badge_info["description"] @@ -156,7 +154,7 @@

    Parameters

    Methods

    -def update(self) +async def update(self)

    Updates the badge's information.

    @@ -164,11 +162,11 @@

    Methods

    Expand source code -
    def update(self):
    +
    async def update(self):
         """
         Updates the badge's information.
         """
    -    badge_info_req = self.requests.get(endpoint + f"v1/badges/{self.id}")
    +    badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}")
         badge_info = badge_info_req.json()
         self.name = badge_info["name"]
         self.description = badge_info["description"]
    diff --git a/docs/games.html b/docs/games.html
    index 10b2bf7b..e5850f0c 100644
    --- a/docs/games.html
    +++ b/docs/games.html
    @@ -66,13 +66,12 @@ 

    Module ro_py.games

    self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None - self.update() - def update(self): + async def update(self): """ Updates the game's information. """ - game_info_req = self.requests.get( + game_info_req = await self.requests.get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -93,11 +92,11 @@

    Module ro_py.games

    self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - def get_votes(self): + async def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = self.requests.get( + votes_info_req = await self.requests.get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -108,12 +107,12 @@

    Module ro_py.games

    votes = Votes(votes_info) return votes - def get_badges(self): + async def get_badges(self): """ Gets the game's badges. This will be updated soon to use the new Page object. """ - badges_req = self.requests.get( + badges_req = await self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -191,13 +190,12 @@

    Classes

    self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None - self.update() - def update(self): + async def update(self): """ Updates the game's information. """ - game_info_req = self.requests.get( + game_info_req = await self.requests.get( url=endpoint + "v1/games", params={ "universeIds": str(self.id) @@ -218,11 +216,11 @@

    Classes

    self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] - def get_votes(self): + async def get_votes(self): """ :return: An instance of Votes """ - votes_info_req = self.requests.get( + votes_info_req = await self.requests.get( url=endpoint + "v1/games/votes", params={ "universeIds": str(self.id) @@ -233,12 +231,12 @@

    Classes

    votes = Votes(votes_info) return votes - def get_badges(self): + async def get_badges(self): """ Gets the game's badges. This will be updated soon to use the new Page object. """ - badges_req = self.requests.get( + badges_req = await self.requests.get( url=f"https://badges.roblox.com/v1/universes/{self.id}/badges", params={ "limit": 100, @@ -254,7 +252,7 @@

    Classes

    Methods

    -def get_badges(self) +async def get_badges(self)

    Gets the game's badges. @@ -263,12 +261,12 @@

    Methods

    Expand source code -
    def get_badges(self):
    +
    async def get_badges(self):
         """
         Gets the game's badges.
         This will be updated soon to use the new Page object.
         """
    -    badges_req = self.requests.get(
    +    badges_req = await self.requests.get(
             url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
             params={
                 "limit": 100,
    @@ -283,7 +281,7 @@ 

    Methods

    -def get_votes(self) +async def get_votes(self)

    :return: An instance of Votes

    @@ -291,11 +289,11 @@

    Methods

    Expand source code -
    def get_votes(self):
    +
    async def get_votes(self):
         """
         :return: An instance of Votes
         """
    -    votes_info_req = self.requests.get(
    +    votes_info_req = await self.requests.get(
             url=endpoint + "v1/games/votes",
             params={
                 "universeIds": str(self.id)
    @@ -308,7 +306,7 @@ 

    Methods

    -def update(self) +async def update(self)

    Updates the game's information.

    @@ -316,11 +314,11 @@

    Methods

    Expand source code -
    def update(self):
    +
    async def update(self):
         """
         Updates the game's information.
         """
    -    game_info_req = self.requests.get(
    +    game_info_req = await self.requests.get(
             url=endpoint + "v1/games",
             params={
                 "universeIds": str(self.id)
    diff --git a/docs/groups.html b/docs/groups.html
    index 26241bf0..963e622d 100644
    --- a/docs/groups.html
    +++ b/docs/groups.html
    @@ -34,8 +34,9 @@ 

    Module ro_py.groups

    """ from ro_py.users import User +from ro_py.roles import Role -endpoint = "https://groups.roblox.com/" +endpoint = "https://groups.roblox.com" class Shout: @@ -63,13 +64,11 @@

    Module ro_py.groups

    self.public_entry_allowed = None self.shout = None - self.update() - async def update(self): """ Updates the group's information. """ - group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] @@ -85,12 +84,21 @@

    Module ro_py.groups

    async def update_shout(self, message): shout_req = await self.requests.patch( - url=f"https://groups.roblox.com/v1/groups/{self.id}/status", + url=endpoint+ f"/v1/groups/{self.id}/status", data={ "message": message } ) - return shout_req.status_code == 200
    + return shout_req.status_code == 200 + + async def get_roles(self): + role_req = await self.requests.get( + url=endpoint + f"/v1/groups/{self.id}/roles" + ) + roles = [] + for role in role_req.json()['roles']: + roles.append(Role(self.requests, self, role)) + return roles
    @@ -128,13 +136,11 @@

    Classes

    self.public_entry_allowed = None self.shout = None - self.update() - async def update(self): """ Updates the group's information. """ - group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] @@ -150,15 +156,43 @@

    Classes

    async def update_shout(self, message): shout_req = await self.requests.patch( - url=f"https://groups.roblox.com/v1/groups/{self.id}/status", + url=endpoint+ f"/v1/groups/{self.id}/status", data={ "message": message } ) - return shout_req.status_code == 200
    + return shout_req.status_code == 200 + + async def get_roles(self): + role_req = await self.requests.get( + url=endpoint + f"/v1/groups/{self.id}/roles" + ) + roles = [] + for role in role_req.json()['roles']: + roles.append(Role(self.requests, self, role)) + return roles

    Methods

    +
    +async def get_roles(self) +
    +
    +
    +
    + +Expand source code + +
    async def get_roles(self):
    +    role_req = await self.requests.get(
    +        url=endpoint + f"/v1/groups/{self.id}/roles"
    +    )
    +    roles = []
    +    for role in role_req.json()['roles']:
    +        roles.append(Role(self.requests, self, role))
    +    return roles
    +
    +
    async def update(self)
    @@ -172,7 +206,7 @@

    Methods

    """ Updates the group's information. """ - group_info_req = await self.requests.get(endpoint + f"v1/groups/{self.id}") + group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}") group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] @@ -197,7 +231,7 @@

    Methods

    async def update_shout(self, message):
         shout_req = await self.requests.patch(
    -        url=f"https://groups.roblox.com/v1/groups/{self.id}/status",
    +        url=endpoint+ f"/v1/groups/{self.id}/status",
             data={
                 "message": message
             }
    @@ -245,6 +279,7 @@ 

    Index

  • Group

    diff --git a/docs/index.html b/docs/index.html index 60051955..8a3272e3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -112,6 +112,10 @@

    Sub-modules

    This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) I don't know if this is really that useful, but I …

    +
    ro_py.roles
    +
    +

    IRA PUT THINGS HERE

    +
    ro_py.thumbnails

    This file houses functions and classes that pertain to Roblox icons and thumbnails.

    @@ -159,6 +163,7 @@

    Index

  • ro_py.notifications
  • ro_py.robloxbadges
  • ro_py.robloxstatus
  • +
  • ro_py.roles
  • ro_py.thumbnails
  • ro_py.trades
  • ro_py.users
  • diff --git a/docs/roles.html b/docs/roles.html new file mode 100644 index 00000000..ab65d3ba --- /dev/null +++ b/docs/roles.html @@ -0,0 +1,501 @@ + + + + + + +ro_py.roles API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.roles

    +
    +
    +

    IRA PUT THINGS HERE

    +
    + +Expand source code + +
    """
    +
    +IRA PUT THINGS HERE
    +
    +"""
    +
    +
    +import enum
    +
    +endpoint = "https://groups.roblox.com"
    +
    +
    +class RolePermissions(enum.Enum):
    +    """
    +    Represents role permissions.
    +    """
    +    view_wall = None
    +    post_to_wall = None
    +    delete_from_wall = None
    +    view_status = None
    +    post_to_status = None
    +    change_rank = None
    +    invite_members = None
    +    remove_members = None
    +    manage_relationships = None
    +    view_audit_logs = None
    +    spend_group_funds = None
    +    advertise_group = None
    +    create_items = None
    +    manage_items = None
    +    manage_group_games = None
    +
    +
    +def get_rp_names(rp):
    +    return {
    +        "viewWall": rp.view_wall,
    +        "PostToWall": rp.post_to_wall,
    +        "deleteFromWall": rp.delete_from_wall,
    +        "viewStatus": rp.view_status,
    +        "postToStatus": rp.post_to_status,
    +        "changeRank": rp.change_rank,
    +        "inviteMembers": rp.invite_members,
    +        "removeMembers": rp.remove_members,
    +        "manageRelationships": rp.manage_relationships,
    +        "viewAuditLogs": rp.view_audit_logs,
    +        "spendGroupFunds": rp.spend_group_funds,
    +        "advertiseGroup": rp.advertise_group,
    +        "createItems": rp.create_items,
    +        "manageItems": rp.manage_items,
    +        "manageGroupGames": rp.manage_group_games
    +    }
    +
    +
    +class Role:
    +    """
    +    Represents a role
    +    This is only available for authenticated clients as it cannot be accessed otherwise.
    +
    +    Parameters
    +    ----------
    +    requests : ro_py.utilities.requests.Requests
    +            Requests object to use for API requests.
    +    group : ro_py.groups.Group
    +            Group the role belongs to.
    +    role_data : dict
    +            Dictionary containing role information.
    +    """
    +    def __init__(self, requests, group, role_data):
    +        self.requests = requests
    +        self.group = group
    +        self.id = role_data['id']
    +        self.name = role_data['name']
    +        self.description = role_data['description']
    +        self.rank = role_data['rank']
    +        self.member_count = role_data['memberCount']
    +
    +    async def update(self):
    +        update_req = await self.requests.get(
    +            url=endpoint + f"/v1/groups/{self.group.id}/roles"
    +        )
    +        data = update_req.json()
    +        for role in data['roles']:
    +            if role['id'] == self.id:
    +                self.name = role['name']
    +                self.description = role['description']
    +                self.rank = role['rank']
    +                self.member_count = role['memberCount']
    +                break
    +
    +    async def edit(self, name=None, description=None, rank=None):
    +        edit_req = await self.requests.patch(
    +            url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
    +            data={
    +                "description": description if description else self.description,
    +                "name": name if name else self.name,
    +                "rank": rank if rank else self.rank
    +            }
    +        )
    +        return edit_req.status_code == 200
    +
    +    async def edit_permissions(self, role_permissions):
    +        data = {
    +            "permissions": {}
    +        }
    +
    +        for key, value in get_rp_names(role_permissions):
    +            if value is True or False:
    +                data['permissions'][key] = value
    +
    +        edit_req = await self.requests.patch(
    +            url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
    +            data=data
    +        )
    +
    +        return edit_req.status_code == 200
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def get_rp_names(rp) +
    +
    +
    +
    + +Expand source code + +
    def get_rp_names(rp):
    +    return {
    +        "viewWall": rp.view_wall,
    +        "PostToWall": rp.post_to_wall,
    +        "deleteFromWall": rp.delete_from_wall,
    +        "viewStatus": rp.view_status,
    +        "postToStatus": rp.post_to_status,
    +        "changeRank": rp.change_rank,
    +        "inviteMembers": rp.invite_members,
    +        "removeMembers": rp.remove_members,
    +        "manageRelationships": rp.manage_relationships,
    +        "viewAuditLogs": rp.view_audit_logs,
    +        "spendGroupFunds": rp.spend_group_funds,
    +        "advertiseGroup": rp.advertise_group,
    +        "createItems": rp.create_items,
    +        "manageItems": rp.manage_items,
    +        "manageGroupGames": rp.manage_group_games
    +    }
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Role +(requests, group, role_data) +
    +
    +

    Represents a role +This is only available for authenticated clients as it cannot be accessed otherwise.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
    group : Group
    +
    Group the role belongs to.
    +
    role_data : dict
    +
    Dictionary containing role information.
    +
    +
    + +Expand source code + +
    class Role:
    +    """
    +    Represents a role
    +    This is only available for authenticated clients as it cannot be accessed otherwise.
    +
    +    Parameters
    +    ----------
    +    requests : ro_py.utilities.requests.Requests
    +            Requests object to use for API requests.
    +    group : ro_py.groups.Group
    +            Group the role belongs to.
    +    role_data : dict
    +            Dictionary containing role information.
    +    """
    +    def __init__(self, requests, group, role_data):
    +        self.requests = requests
    +        self.group = group
    +        self.id = role_data['id']
    +        self.name = role_data['name']
    +        self.description = role_data['description']
    +        self.rank = role_data['rank']
    +        self.member_count = role_data['memberCount']
    +
    +    async def update(self):
    +        update_req = await self.requests.get(
    +            url=endpoint + f"/v1/groups/{self.group.id}/roles"
    +        )
    +        data = update_req.json()
    +        for role in data['roles']:
    +            if role['id'] == self.id:
    +                self.name = role['name']
    +                self.description = role['description']
    +                self.rank = role['rank']
    +                self.member_count = role['memberCount']
    +                break
    +
    +    async def edit(self, name=None, description=None, rank=None):
    +        edit_req = await self.requests.patch(
    +            url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
    +            data={
    +                "description": description if description else self.description,
    +                "name": name if name else self.name,
    +                "rank": rank if rank else self.rank
    +            }
    +        )
    +        return edit_req.status_code == 200
    +
    +    async def edit_permissions(self, role_permissions):
    +        data = {
    +            "permissions": {}
    +        }
    +
    +        for key, value in get_rp_names(role_permissions):
    +            if value is True or False:
    +                data['permissions'][key] = value
    +
    +        edit_req = await self.requests.patch(
    +            url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
    +            data=data
    +        )
    +
    +        return edit_req.status_code == 200
    +
    +

    Methods

    +
    +
    +async def edit(self, name=None, description=None, rank=None) +
    +
    +
    +
    + +Expand source code + +
    async def edit(self, name=None, description=None, rank=None):
    +    edit_req = await self.requests.patch(
    +        url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
    +        data={
    +            "description": description if description else self.description,
    +            "name": name if name else self.name,
    +            "rank": rank if rank else self.rank
    +        }
    +    )
    +    return edit_req.status_code == 200
    +
    +
    +
    +async def edit_permissions(self, role_permissions) +
    +
    +
    +
    + +Expand source code + +
    async def edit_permissions(self, role_permissions):
    +    data = {
    +        "permissions": {}
    +    }
    +
    +    for key, value in get_rp_names(role_permissions):
    +        if value is True or False:
    +            data['permissions'][key] = value
    +
    +    edit_req = await self.requests.patch(
    +        url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
    +        data=data
    +    )
    +
    +    return edit_req.status_code == 200
    +
    +
    +
    +async def update(self) +
    +
    +
    +
    + +Expand source code + +
    async def update(self):
    +    update_req = await self.requests.get(
    +        url=endpoint + f"/v1/groups/{self.group.id}/roles"
    +    )
    +    data = update_req.json()
    +    for role in data['roles']:
    +        if role['id'] == self.id:
    +            self.name = role['name']
    +            self.description = role['description']
    +            self.rank = role['rank']
    +            self.member_count = role['memberCount']
    +            break
    +
    +
    +
    +
    +
    +class RolePermissions +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    Represents role permissions.

    +
    + +Expand source code + +
    class RolePermissions(enum.Enum):
    +    """
    +    Represents role permissions.
    +    """
    +    view_wall = None
    +    post_to_wall = None
    +    delete_from_wall = None
    +    view_status = None
    +    post_to_status = None
    +    change_rank = None
    +    invite_members = None
    +    remove_members = None
    +    manage_relationships = None
    +    view_audit_logs = None
    +    spend_group_funds = None
    +    advertise_group = None
    +    create_items = None
    +    manage_items = None
    +    manage_group_games = None
    +
    +

    Ancestors

    +
      +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var advertise_group
    +
    +
    +
    +
    var change_rank
    +
    +
    +
    +
    var create_items
    +
    +
    +
    +
    var delete_from_wall
    +
    +
    +
    +
    var invite_members
    +
    +
    +
    +
    var manage_group_games
    +
    +
    +
    +
    var manage_items
    +
    +
    +
    +
    var manage_relationships
    +
    +
    +
    +
    var post_to_status
    +
    +
    +
    +
    var post_to_wall
    +
    +
    +
    +
    var remove_members
    +
    +
    +
    +
    var spend_group_funds
    +
    +
    +
    +
    var view_audit_logs
    +
    +
    +
    +
    var view_status
    +
    +
    +
    +
    var view_wall
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file From 38d33e1b5110a009e2bc9e43c4e4a570753f8072 Mon Sep 17 00:00:00 2001 From: mfd-co Date: Fri, 1 Jan 2021 03:12:40 +0000 Subject: [PATCH 194/518] This represents the gamepersistence Api, named the class DataStore, as GamePersistence isn't as catchy --- ro_py/gamepersistence.py | 296 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 ro_py/gamepersistence.py diff --git a/ro_py/gamepersistence.py b/ro_py/gamepersistence.py new file mode 100644 index 00000000..1a774b79 --- /dev/null +++ b/ro_py/gamepersistence.py @@ -0,0 +1,296 @@ +""" + +This file houses functions used for tampering with Roblox Datastores + +""" + +from urllib.parse import quote +from math import floor +import re + +endpoint = "http://gamepersistence.roblox.com/" + +class DataStore: + """ + Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com). + This is only available for authenticated clients, and games that they own. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + placeId : int + PlaceId to modify the DataStores for, + if the currently authenticated user doesn't have sufficient permissions, + it will raise a NotAuthorizedToModifyPlaceDataStores exception + name : str + The name of the DataStore, + as in the Second Parameter of + `std::shared_ptr DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")` + scope : str, optional + The scope of the DataStore, + as on the Second Parameter of + `std::shared_ptr DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")` + legacy : bool, optional + Describes whether or not this will use the legacy endpoints, + over the new v1 endpoints (Does not apply to getSortedValues) + legacyNamingScheme : bool, optional + Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), + there will be no qkeys[idx].target (normally the key that is passed into each method), + and the qkeys[idx].key will match the key passed into each method. + """ + def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False): + self.requests = requests + self.placeId = placeId + self.legacy = legacy + self.legacyNamingScheme = legacyNamingScheme + self.name = name + self.scope = scope if scope != None else "global" + + async def get(self, key): + """ + Represents a get request to a data store, + using legacy works the same + + Parameters + ---------- + key : str + The key of the value you wish to get, + as in the Second Parameter of + `void DataStore::getAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + + Returns + ------- + typing.Any + """ + if self.legacy == True: + data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" + r = await self.requests.post( + url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': 'application/x-www-form-urlencoded' + }, data=data) + if len(r.json()['data']) == 0: + return None + else: + return r.json()['data'][0]['Value'] + else: + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + r = await self.requests.get( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId) + }) + if r.status_code == 204: + return None + else: + return r.text; + + async def set(self, key, value): + """ + Represents a set request to a data store, + using legacy works the same + + Parameters + ---------- + key : str + The key of the value you wish to get, + as in the Second Parameter of + `void DataStore::getAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + value + The value to set for the key, + as in the 3rd parameter of + `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function resumeFunction, boost::function errorFunction)` + + Returns + ------- + typing.Any + """ + if self.legacy == True: + data = f"value={quote(str(value))}" + url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': 'application/x-www-form-urlencoded' + }, data=data) + if len(r.json()['data']) == 0: + return None + else: + return r.json()['data'] + else: + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': '*/*', + 'Content-Length': str(len(str(value))) + }, data=quote(str(value))) + if r.status_code == 200: + return value; + + async def setIfValue(self, key, value, expectedValue): + """ + Represents a conditional set request to a data store, + only supports legacy + + Parameters + ---------- + key : str + The key of the value you wish to get, + as in the Second Parameter of + `void DataStore::getAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + value + The value to set for the key, + as in the 3rd parameter of + `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function resumeFunction, boost::function errorFunction)` + expectedValue + The expectedValue for that key, if you know the key doesn't exist, then set this as None + + Returns + ------- + typing.Any + """ + data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}" + url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': 'application/x-www-form-urlencoded' + }, data=data) + try: + if r.json()['data'] != 0: + return r.json()['data'] + except KeyError: + return r.json()['error'] + + async def setIfIdx(self, key, value, idx): + """ + Represents a conditional set request to a data store, + only supports new endpoints, + + Parameters + ---------- + key : str + The key of the value you wish to get, + as in the Second Parameter of + `void DataStore::getAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + value + The value to set for the key, + as in the 3rd parameter of + `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function resumeFunction, boost::function errorFunction)` + idx : int + The expectedidx, there + + Returns + ------- + typing.Any + """ + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': '*/*', + 'Content-Length': str(len(str(value))) + }, data=quote(str(value))) + if r.status_code == 409: + usn = r.headers['roblox-usn'] + split = usn.split('.') + msn_hash = split[0] + current_value = split[1] + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" + r2 = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': '*/*', + 'Content-Length': str(len(str(value))) + }, data=quote(str(value))) + if r2.status_code == 409: + return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16))) + else: + return value + + async def increment(self, key, delta = 0): + """ + Represents a conditional set request to a data store, + only supports legacy + + Parameters + ---------- + key : str + The key of the value you wish to get, + as in the Second Parameter of + `void DataStore::getAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + delta : int, optional + The value to set for the key, + as in the 3rd parameter of + `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function resumeFunction, boost::function errorFunction)` + + Returns + ------- + typing.Any + """ + data = "" + url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" + + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': 'application/x-www-form-urlencoded' + }, data=data) + try: + if r.json()['data'] != 0: + return r.json()['data'] + except KeyError: + cap = re.search("\(.+\)", r.json()['error']) + reason = cap.group(0).replace("(", "").replace(")", "") + if reason == "ExistingValueNotNumeric": + return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double" + + async def remove(self, key): + """ + Represents a get request to a data store, + using legacy works the same + + Parameters + ---------- + key : str + The key of the value you wish to remove, + as in the Second Parameter of + `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function resumeFunction, boost::function errorFunction)` + + Returns + ------- + typing.Any + """ + if self.legacy == True: + data = "" + url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId), + 'Content-Type': 'application/x-www-form-urlencoded' + }, data=data) + if r.json()['data'] == None: + return None + else: + return r.json()['data'] + else: + url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + r = await self.requests.post( + url=url, + headers={ + 'Roblox-Place-Id': str(self.placeId) + }) + if r.status_code == 204: + return None + else: + return r.text; \ No newline at end of file From 28b2e24d440f16c5572c2a79e0542a916e76db1a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 1 Jan 2021 00:46:47 -0500 Subject: [PATCH 195/518] Docs update --- docs/gamepersistence.html | 1103 +++++++++++++++++++++++++++++++++++++ docs/index.html | 5 + 2 files changed, 1108 insertions(+) create mode 100644 docs/gamepersistence.html diff --git a/docs/gamepersistence.html b/docs/gamepersistence.html new file mode 100644 index 00000000..b357b16d --- /dev/null +++ b/docs/gamepersistence.html @@ -0,0 +1,1103 @@ + + + + + + +ro_py.gamepersistence API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.gamepersistence

    +
    +
    +

    This file houses functions used for tampering with Roblox Datastores

    +
    + +Expand source code + +
    """
    +
    +This file houses functions used for tampering with Roblox Datastores
    +
    +"""
    +
    +from urllib.parse import quote
    +from math import floor
    +import re
    +
    +endpoint = "http://gamepersistence.roblox.com/"
    +
    +class DataStore:
    +    """
    +    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com).
    +    This is only available for authenticated clients, and games that they own.
    +
    +    Parameters
    +    ----------
    +    requests : ro_py.utilities.requests.Requests
    +        Requests object to use for API requests.
    +    placeId : int
    +        PlaceId to modify the DataStores for, 
    +        if the currently authenticated user doesn't have sufficient permissions, 
    +        it will raise a NotAuthorizedToModifyPlaceDataStores exception
    +    name : str
    +        The name of the DataStore, 
    +        as in the Second Parameter of 
    +        `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
    +    scope : str, optional
    +        The scope of the DataStore,
    +        as on the Second Parameter of
    +         `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
    +    legacy : bool, optional
    +        Describes whether or not this will use the legacy endpoints, 
    +        over the new v1 endpoints (Does not apply to getSortedValues)
    +    legacyNamingScheme : bool, optional
    +        Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), 
    +        there will be no qkeys[idx].target (normally the key that is passed into each method), 
    +        and the qkeys[idx].key will match the key passed into each method.
    +    """
    +    def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False):
    +       self.requests = requests
    +       self.placeId = placeId
    +       self.legacy = legacy
    +       self.legacyNamingScheme = legacyNamingScheme
    +       self.name = name
    +       self.scope = scope if scope != None else "global"
    +
    +    async def get(self, key):
    +        """
    +        Represents a get request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
    +            r = await self.requests.post(
    +                url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if len(r.json()['data']) == 0:
    +                return None
    +            else:
    +                return r.json()['data'][0]['Value']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.get(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId)
    +            })
    +            if r.status_code == 204:
    +                return None
    +            else:
    +                return r.text;
    +            
    +    async def set(self, key, value):
    +        """
    +        Represents a set request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = f"value={quote(str(value))}"
    +            url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
    +            r = await self.requests.post(
    +                url=url, 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if len(r.json()['data']) == 0:
    +                return None
    +            else:
    +                return r.json()['data']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': '*/*',
    +                    'Content-Length': str(len(str(value)))
    +            }, data=quote(str(value)))
    +            if r.status_code == 200:
    +                return value;
    +
    +    async def setIfValue(self, key, value, expectedValue):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports legacy
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        expectedValue
    +            The expectedValue for that key, if you know the key doesn't exist, then set this as None
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}"
    +        url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}"
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        try:
    +            if r.json()['data'] != 0:
    +                return r.json()['data']
    +        except KeyError:
    +            return r.json()['error']
    +
    +    async def setIfIdx(self, key, value, idx):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports new endpoints,
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        idx : int
    +            The expectedidx, there
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
    +        r = await self.requests.post(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': '*/*',
    +                'Content-Length': str(len(str(value)))
    +        }, data=quote(str(value)))
    +        if r.status_code == 409:
    +            usn = r.headers['roblox-usn']
    +            split = usn.split('.')
    +            msn_hash = split[0]
    +            current_value = split[1]
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
    +            r2 = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': '*/*',
    +                    'Content-Length': str(len(str(value)))
    +            }, data=quote(str(value)))
    +            if r2.status_code == 409:
    +                return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
    +            else:
    +                return value
    +
    +    async def increment(self, key, delta = 0):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports legacy
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        delta : int, optional
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        data = ""
    +        url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
    +
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        try:
    +            if r.json()['data'] != 0:
    +                return r.json()['data']
    +        except KeyError:
    +            cap = re.search("\(.+\)", r.json()['error'])
    +            reason = cap.group(0).replace("(", "").replace(")", "")
    +            if reason == "ExistingValueNotNumeric":
    +                return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
    +    
    +    async def remove(self, key):
    +        """
    +        Represents a get request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to remove, 
    +            as in the Second Parameter of 
    +            `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = ""
    +            url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url, 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if r.json()['data'] == None:
    +                return None
    +            else:
    +                return r.json()['data']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId)
    +            })
    +            if r.status_code == 204:
    +                return None
    +            else:
    +                return r.text;
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class DataStore +(requests, placeId, name, scope, legacy=True, legacyNamingScheme=False) +
    +
    +

    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com). +This is only available for authenticated clients, and games that they own.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
    placeId : int
    +
    PlaceId to modify the DataStores for, +if the currently authenticated user doesn't have sufficient permissions, +it will raise a NotAuthorizedToModifyPlaceDataStores exception
    +
    name : str
    +
    The name of the DataStore, +as in the Second Parameter of +std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")
    +
    scope : str, optional
    +
    The scope of the DataStore, +as on the Second Parameter of +std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")
    +
    legacy : bool, optional
    +
    Describes whether or not this will use the legacy endpoints, +over the new v1 endpoints (Does not apply to getSortedValues)
    +
    legacyNamingScheme : bool, optional
    +
    Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), +there will be no qkeys[idx].target (normally the key that is passed into each method), +and the qkeys[idx].key will match the key passed into each method.
    +
    +
    + +Expand source code + +
    class DataStore:
    +    """
    +    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com).
    +    This is only available for authenticated clients, and games that they own.
    +
    +    Parameters
    +    ----------
    +    requests : ro_py.utilities.requests.Requests
    +        Requests object to use for API requests.
    +    placeId : int
    +        PlaceId to modify the DataStores for, 
    +        if the currently authenticated user doesn't have sufficient permissions, 
    +        it will raise a NotAuthorizedToModifyPlaceDataStores exception
    +    name : str
    +        The name of the DataStore, 
    +        as in the Second Parameter of 
    +        `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
    +    scope : str, optional
    +        The scope of the DataStore,
    +        as on the Second Parameter of
    +         `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
    +    legacy : bool, optional
    +        Describes whether or not this will use the legacy endpoints, 
    +        over the new v1 endpoints (Does not apply to getSortedValues)
    +    legacyNamingScheme : bool, optional
    +        Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), 
    +        there will be no qkeys[idx].target (normally the key that is passed into each method), 
    +        and the qkeys[idx].key will match the key passed into each method.
    +    """
    +    def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False):
    +       self.requests = requests
    +       self.placeId = placeId
    +       self.legacy = legacy
    +       self.legacyNamingScheme = legacyNamingScheme
    +       self.name = name
    +       self.scope = scope if scope != None else "global"
    +
    +    async def get(self, key):
    +        """
    +        Represents a get request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
    +            r = await self.requests.post(
    +                url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if len(r.json()['data']) == 0:
    +                return None
    +            else:
    +                return r.json()['data'][0]['Value']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.get(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId)
    +            })
    +            if r.status_code == 204:
    +                return None
    +            else:
    +                return r.text;
    +            
    +    async def set(self, key, value):
    +        """
    +        Represents a set request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = f"value={quote(str(value))}"
    +            url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
    +            r = await self.requests.post(
    +                url=url, 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if len(r.json()['data']) == 0:
    +                return None
    +            else:
    +                return r.json()['data']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': '*/*',
    +                    'Content-Length': str(len(str(value)))
    +            }, data=quote(str(value)))
    +            if r.status_code == 200:
    +                return value;
    +
    +    async def setIfValue(self, key, value, expectedValue):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports legacy
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        expectedValue
    +            The expectedValue for that key, if you know the key doesn't exist, then set this as None
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}"
    +        url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}"
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        try:
    +            if r.json()['data'] != 0:
    +                return r.json()['data']
    +        except KeyError:
    +            return r.json()['error']
    +
    +    async def setIfIdx(self, key, value, idx):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports new endpoints,
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        value
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        idx : int
    +            The expectedidx, there
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
    +        r = await self.requests.post(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': '*/*',
    +                'Content-Length': str(len(str(value)))
    +        }, data=quote(str(value)))
    +        if r.status_code == 409:
    +            usn = r.headers['roblox-usn']
    +            split = usn.split('.')
    +            msn_hash = split[0]
    +            current_value = split[1]
    +            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
    +            r2 = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': '*/*',
    +                    'Content-Length': str(len(str(value)))
    +            }, data=quote(str(value)))
    +            if r2.status_code == 409:
    +                return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
    +            else:
    +                return value
    +
    +    async def increment(self, key, delta = 0):
    +        """
    +        Represents a conditional set request to a data store,
    +        only supports legacy
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to get, 
    +            as in the Second Parameter of 
    +            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        delta : int, optional
    +            The value to set for the key,
    +            as in the 3rd parameter of
    +            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +        
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        data = ""
    +        url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
    +
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        try:
    +            if r.json()['data'] != 0:
    +                return r.json()['data']
    +        except KeyError:
    +            cap = re.search("\(.+\)", r.json()['error'])
    +            reason = cap.group(0).replace("(", "").replace(")", "")
    +            if reason == "ExistingValueNotNumeric":
    +                return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
    +    
    +    async def remove(self, key):
    +        """
    +        Represents a get request to a data store,
    +        using legacy works the same
    +
    +        Parameters
    +        ----------
    +        key : str
    +            The key of the value you wish to remove, 
    +            as in the Second Parameter of 
    +            `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +
    +        Returns
    +        -------
    +        typing.Any
    +        """
    +        if self.legacy == True:
    +            data = ""
    +            url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url, 
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId),
    +                    'Content-Type': 'application/x-www-form-urlencoded'
    +            }, data=data)
    +            if r.json()['data'] == None:
    +                return None
    +            else:
    +                return r.json()['data']
    +        else:
    +            url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +            r = await self.requests.post(
    +                url=url,
    +                headers={
    +                    'Roblox-Place-Id': str(self.placeId)
    +            })
    +            if r.status_code == 204:
    +                return None
    +            else:
    +                return r.text;
    +
    +

    Methods

    +
    +
    +async def get(self, key) +
    +
    +

    Represents a get request to a data store, +using legacy works the same

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to get, +as in the Second Parameter of +void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def get(self, key):
    +    """
    +    Represents a get request to a data store,
    +    using legacy works the same
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to get, 
    +        as in the Second Parameter of 
    +        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    if self.legacy == True:
    +        data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
    +        r = await self.requests.post(
    +            url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        if len(r.json()['data']) == 0:
    +            return None
    +        else:
    +            return r.json()['data'][0]['Value']
    +    else:
    +        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +        r = await self.requests.get(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId)
    +        })
    +        if r.status_code == 204:
    +            return None
    +        else:
    +            return r.text;
    +
    +
    +
    +async def increment(self, key, delta=0) +
    +
    +

    Represents a conditional set request to a data store, +only supports legacy

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to get, +as in the Second Parameter of +void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    delta : int, optional
    +
    The value to set for the key, +as in the 3rd parameter of +void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def increment(self, key, delta = 0):
    +    """
    +    Represents a conditional set request to a data store,
    +    only supports legacy
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to get, 
    +        as in the Second Parameter of 
    +        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    delta : int, optional
    +        The value to set for the key,
    +        as in the 3rd parameter of
    +        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    data = ""
    +    url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
    +
    +    r = await self.requests.post(
    +        url=url, 
    +        headers={
    +            'Roblox-Place-Id': str(self.placeId),
    +            'Content-Type': 'application/x-www-form-urlencoded'
    +    }, data=data)
    +    try:
    +        if r.json()['data'] != 0:
    +            return r.json()['data']
    +    except KeyError:
    +        cap = re.search("\(.+\)", r.json()['error'])
    +        reason = cap.group(0).replace("(", "").replace(")", "")
    +        if reason == "ExistingValueNotNumeric":
    +            return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
    +
    +
    +
    +async def remove(self, key) +
    +
    +

    Represents a get request to a data store, +using legacy works the same

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to remove, +as in the Second Parameter of +void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def remove(self, key):
    +    """
    +    Represents a get request to a data store,
    +    using legacy works the same
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to remove, 
    +        as in the Second Parameter of 
    +        `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    if self.legacy == True:
    +        data = ""
    +        url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        if r.json()['data'] == None:
    +            return None
    +        else:
    +            return r.json()['data']
    +    else:
    +        url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +        r = await self.requests.post(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId)
    +        })
    +        if r.status_code == 204:
    +            return None
    +        else:
    +            return r.text;
    +
    +
    +
    +async def set(self, key, value) +
    +
    +

    Represents a set request to a data store, +using legacy works the same

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to get, +as in the Second Parameter of +void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    value
    +
    The value to set for the key, +as in the 3rd parameter of +void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def set(self, key, value):
    +    """
    +    Represents a set request to a data store,
    +    using legacy works the same
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to get, 
    +        as in the Second Parameter of 
    +        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    value
    +        The value to set for the key,
    +        as in the 3rd parameter of
    +        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    if self.legacy == True:
    +        data = f"value={quote(str(value))}"
    +        url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
    +        r = await self.requests.post(
    +            url=url, 
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': 'application/x-www-form-urlencoded'
    +        }, data=data)
    +        if len(r.json()['data']) == 0:
    +            return None
    +        else:
    +            return r.json()['data']
    +    else:
    +        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
    +        r = await self.requests.post(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': '*/*',
    +                'Content-Length': str(len(str(value)))
    +        }, data=quote(str(value)))
    +        if r.status_code == 200:
    +            return value;
    +
    +
    +
    +async def setIfIdx(self, key, value, idx) +
    +
    +

    Represents a conditional set request to a data store, +only supports new endpoints,

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to get, +as in the Second Parameter of +void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    value
    +
    The value to set for the key, +as in the 3rd parameter of +void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    idx : int
    +
    The expectedidx, there
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def setIfIdx(self, key, value, idx):
    +    """
    +    Represents a conditional set request to a data store,
    +    only supports new endpoints,
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to get, 
    +        as in the Second Parameter of 
    +        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    value
    +        The value to set for the key,
    +        as in the 3rd parameter of
    +        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    idx : int
    +        The expectedidx, there
    +
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
    +    r = await self.requests.post(
    +        url=url,
    +        headers={
    +            'Roblox-Place-Id': str(self.placeId),
    +            'Content-Type': '*/*',
    +            'Content-Length': str(len(str(value)))
    +    }, data=quote(str(value)))
    +    if r.status_code == 409:
    +        usn = r.headers['roblox-usn']
    +        split = usn.split('.')
    +        msn_hash = split[0]
    +        current_value = split[1]
    +        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
    +        r2 = await self.requests.post(
    +            url=url,
    +            headers={
    +                'Roblox-Place-Id': str(self.placeId),
    +                'Content-Type': '*/*',
    +                'Content-Length': str(len(str(value)))
    +        }, data=quote(str(value)))
    +        if r2.status_code == 409:
    +            return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
    +        else:
    +            return value
    +
    +
    +
    +async def setIfValue(self, key, value, expectedValue) +
    +
    +

    Represents a conditional set request to a data store, +only supports legacy

    +

    Parameters

    +
    +
    key : str
    +
    The key of the value you wish to get, +as in the Second Parameter of +void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    value
    +
    The value to set for the key, +as in the 3rd parameter of +void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
    +
    expectedValue
    +
    The expectedValue for that key, if you know the key doesn't exist, then set this as None
    +
    +

    Returns

    +
    +
    typing.Any
    +
     
    +
    +
    + +Expand source code + +
    async def setIfValue(self, key, value, expectedValue):
    +    """
    +    Represents a conditional set request to a data store,
    +    only supports legacy
    +
    +    Parameters
    +    ----------
    +    key : str
    +        The key of the value you wish to get, 
    +        as in the Second Parameter of 
    +        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    value
    +        The value to set for the key,
    +        as in the 3rd parameter of
    +        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
    +    expectedValue
    +        The expectedValue for that key, if you know the key doesn't exist, then set this as None
    +
    +    Returns
    +    -------
    +    typing.Any
    +    """
    +    data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}"
    +    url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}"
    +    r = await self.requests.post(
    +        url=url, 
    +        headers={
    +            'Roblox-Place-Id': str(self.placeId),
    +            'Content-Type': 'application/x-www-form-urlencoded'
    +    }, data=data)
    +    try:
    +        if r.json()['data'] != 0:
    +            return r.json()['data']
    +    except KeyError:
    +        return r.json()['error']
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index 8a3272e3..30bf1f62 100644 --- a/docs/index.html +++ b/docs/index.html @@ -85,6 +85,10 @@

    Sub-modules

    This file houses functions and classes that pertain to the Roblox economy endpoints.

    +
    ro_py.gamepersistence
    +
    +

    This file houses functions used for tampering with Roblox Datastores

    +
    ro_py.games

    This file houses functions and classes that pertain to Roblox universes and places.

    @@ -157,6 +161,7 @@

    Index

  • ro_py.chat
  • ro_py.client
  • ro_py.economy
  • +
  • ro_py.gamepersistence
  • ro_py.games
  • ro_py.gender
  • ro_py.groups
  • From 9e80035da9991b5a82ce1ce8732da2173d905fce Mon Sep 17 00:00:00 2001 From: iranathan Date: Fri, 1 Jan 2021 14:30:07 +0100 Subject: [PATCH 196/518] add docs --- ro_py/groups.py | 19 +++++++++++++++++++ ro_py/roles.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index a8a7bc3b..1f385657 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -54,6 +54,18 @@ async def update(self): # self.is_locked = group_info["isLocked"] async def update_shout(self, message): + """ + Changes the shout in the group. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + + Returns + ------- + int + """ shout_req = await self.requests.patch( url=endpoint+ f"/v1/groups/{self.id}/status", data={ @@ -63,6 +75,13 @@ async def update_shout(self, message): return shout_req.status_code == 200 async def get_roles(self): + """ + Gets all roles of the group. + + Returns + ------- + list + """ role_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.id}/roles" ) diff --git a/ro_py/roles.py b/ro_py/roles.py index b80d6f60..51c7c9d8 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -1,6 +1,6 @@ """ -IRA PUT THINGS HERE +This file contains classes and functions related to Roblox roles. """ @@ -32,6 +32,17 @@ class RolePermissions(enum.Enum): def get_rp_names(rp): + """ + Converts permissions into something Roblox can read. + + Parameters + ---------- + rp : ro_py.roles.RolePermissions + + Returns + ------- + dict + """ return { "viewWall": rp.view_wall, "PostToWall": rp.post_to_wall, @@ -75,6 +86,9 @@ def __init__(self, requests, group, role_data): self.member_count = role_data['memberCount'] async def update(self): + """ + Updates information of the role. + """ update_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.group.id}/roles" ) @@ -88,6 +102,22 @@ async def update(self): break async def edit(self, name=None, description=None, rank=None): + """ + Edits the name, description or rank of a role + + Parameters + ---------- + name : str, optional + New name for the role. + description : str, optional + New description for the role. + rank : int, optional + Number from 1-254 that determains the new rank number for the role. + + Returns + ------- + int + """ edit_req = await self.requests.patch( url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", data={ @@ -99,6 +129,18 @@ async def edit(self, name=None, description=None, rank=None): return edit_req.status_code == 200 async def edit_permissions(self, role_permissions): + """ + Edits the permissions of a role. + + Parameters + ---------- + role_permissions : ro_py.roles.RolePermissions + New permissions that will overwrite the old ones. + + Returns + ------- + int + """ data = { "permissions": {} } From d7b6cfdce894e1bced0889fb5c5cd4bcf14859e5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 1 Jan 2021 20:06:58 -0500 Subject: [PATCH 197/518] Fixed typo in setup and edited gitignore --- .gitignore | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cc34eb8e..4c8aa7f3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ ro_py_old/ other/ build.bat chat.py +ro_py/robloxdocs.py diff --git a/setup.py b/setup.py index dd55b010..8239b36a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="2.0.0", + version="1.0.0", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 34b6d72235bbfa8990ccb17dd07a23a8e017b004 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 2 Jan 2021 23:05:15 -0500 Subject: [PATCH 198/518] Update README.md --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index 028ffb13..925a5ed9 100644 --- a/README.md +++ b/README.md @@ -10,21 +10,7 @@ pip3 install ro-py ``` ## Examples -Using the client: -```python -from ro_py.client import Client -client = Client("Token goes here") # Token is optional, but allows for authentication! -``` -Viewing a user's info: -```python -from ro_py.client import Client -client = Client() -user_id = 576059883 -user = client.get_user(user_id) -print(f"Username: {user.name}") -print(f"Status: {user.get_status() or 'None.'}") -``` -Find more examples in the examples folder. +(Update: I'm writing new examples. For now, look in the examples folder.) ## Credits [@iranathan](https://github.com/iranathan) - maintainer From ed498e7545de323157e237ff0b8ac4aadca6ff93 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 12:07:23 -0500 Subject: [PATCH 199/518] Hopefully pushed a fix --- ro_py/trades.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 5b231042..4c7294a2 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -21,7 +21,7 @@ def trade_page_handler(requests, this_page) -> list: class Trade: - def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: User, recieve_items, send_items, created, expiration, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender From da29b862f2446015c69ddb8a7c41fae78438fdb0 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 12:07:58 -0500 Subject: [PATCH 200/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8239b36a..3fc37b30 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="1.0.0", + version="1.0.1", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From a89544e7fc50aca90c062a6815531215ec60abe9 Mon Sep 17 00:00:00 2001 From: iranathan Date: Sun, 3 Jan 2021 20:27:31 +0100 Subject: [PATCH 201/518] fix for older versions & wall post functions --- ro_py/gamepersistence.py | 120 ++++++++++++++++++------------------ ro_py/groups.py | 46 +++++++++++++- ro_py/roles.py | 2 +- ro_py/trades.py | 5 +- ro_py/utilities/pages.py | 9 +-- ro_py/utilities/requests.py | 24 ++++++++ 6 files changed, 137 insertions(+), 69 deletions(-) diff --git a/ro_py/gamepersistence.py b/ro_py/gamepersistence.py index 1a774b79..edec6098 100644 --- a/ro_py/gamepersistence.py +++ b/ro_py/gamepersistence.py @@ -10,6 +10,7 @@ endpoint = "http://gamepersistence.roblox.com/" + class DataStore: """ Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com). @@ -19,7 +20,7 @@ class DataStore: ---------- requests : ro_py.utilities.requests.Requests Requests object to use for API requests. - placeId : int + place_id : int PlaceId to modify the DataStores for, if the currently authenticated user doesn't have sufficient permissions, it will raise a NotAuthorizedToModifyPlaceDataStores exception @@ -34,18 +35,19 @@ class DataStore: legacy : bool, optional Describes whether or not this will use the legacy endpoints, over the new v1 endpoints (Does not apply to getSortedValues) - legacyNamingScheme : bool, optional + legacy_naming_scheme : bool, optional Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), there will be no qkeys[idx].target (normally the key that is passed into each method), and the qkeys[idx].key will match the key passed into each method. """ - def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False): - self.requests = requests - self.placeId = placeId - self.legacy = legacy - self.legacyNamingScheme = legacyNamingScheme - self.name = name - self.scope = scope if scope != None else "global" + + def __init__(self, requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False): + self.requests = requests + self.place_id = place_id + self.legacy = legacy + self.legacy_naming_scheme = legacy_naming_scheme + self.name = name + self.scope = scope if scope is not None else "global" async def get(self, key): """ @@ -63,30 +65,30 @@ async def get(self, key): ------- typing.Any """ - if self.legacy == True: - data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" + if self.legacy: + data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" r = await self.requests.post( - url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", + url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}", headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'][0]['Value'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.get( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; - + return r.text + async def set(self, key, value): """ Represents a set request to a data store, @@ -107,32 +109,32 @@ async def set(self, key, value): ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = f"value={quote(str(value))}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" + url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 200: - return value; + return value - async def setIfValue(self, key, value, expectedValue): + async def set_if_value(self, key, value, expected_value): """ Represents a conditional set request to a data store, only supports legacy @@ -147,28 +149,28 @@ async def setIfValue(self, key, value, expectedValue): The value to set for the key, as in the 3rd parameter of `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function resumeFunction, boost::function errorFunction)` - expectedValue - The expectedValue for that key, if you know the key doesn't exist, then set this as None + expected_value + The expected_value for that key, if you know the key doesn't exist, then set this as None Returns ------- typing.Any """ - data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" + data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}" + url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] except KeyError: return r.json()['error'] - async def setIfIdx(self, key, value, idx): + async def set_if_idx(self, key, value, idx): """ Represents a conditional set request to a data store, only supports new endpoints, @@ -190,33 +192,33 @@ async def setIfIdx(self, key, value, idx): ------- typing.Any """ - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 409: usn = r.headers['roblox-usn'] split = usn.split('.') msn_hash = split[0] current_value = split[1] - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" r2 = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r2.status_code == 409: return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16))) else: return value - async def increment(self, key, delta = 0): + async def increment(self, key, delta=0): """ Represents a conditional set request to a data store, only supports legacy @@ -237,14 +239,14 @@ async def increment(self, key, delta = 0): typing.Any """ data = "" - url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" + url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] @@ -253,7 +255,7 @@ async def increment(self, key, delta = 0): reason = cap.group(0).replace("(", "").replace(")", "") if reason == "ExistingValueNotNumeric": return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double" - + async def remove(self, key): """ Represents a get request to a data store, @@ -270,27 +272,27 @@ async def remove(self, key): ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = "" - url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) - if r.json()['data'] == None: + }, data=data) + if r.json()['data'] is None: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; \ No newline at end of file + return r.text diff --git a/ro_py/groups.py b/ro_py/groups.py index 1f385657..8d74fa49 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -3,9 +3,11 @@ This file houses functions and classes that pertain to Roblox groups. """ - +import iso8601 +from typing import List from ro_py.users import User from ro_py.roles import Role +from ro_py.utilities.pages import Pages, SortOrder endpoint = "https://groups.roblox.com" @@ -19,6 +21,33 @@ def __init__(self, requests, shout_data): self.poster = User(requests, shout_data["poster"]["userId"]) +class WallPost: + """ + Represents a roblox wall post. + """ + def __init__(self, requests, wall_data, group): + self.requests = requests + self.group = group + self.id = wall_data['id'] + self.body = wall_data['body'] + self.created = iso8601.parse(wall_data['created']) + self.updated = iso8601.parse(wall_data['updated']) + self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username']) + + async def delete(self): + wall_req = await self.requests.delete( + url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}" + ) + return wall_req.status == 200 + + +def wall_post_handeler(requests, this_page, args) -> List[WallPost]: + wall_posts = [] + for wall_post in this_page: + wall_posts.append(WallPost(requests, wall_post, args)) + return wall_posts + + class Group: """ Represents a group. @@ -55,7 +84,7 @@ async def update(self): async def update_shout(self, message): """ - Changes the shout in the group. + Updates the shout of the group. Parameters ---------- @@ -67,7 +96,7 @@ async def update_shout(self, message): int """ shout_req = await self.requests.patch( - url=endpoint+ f"/v1/groups/{self.id}/status", + url=endpoint + f"/v1/groups/{self.id}/status", data={ "message": message } @@ -89,3 +118,14 @@ async def get_roles(self): for role in role_req.json()['roles']: roles.append(Role(self.requests, self, role)) return roles + + async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100): + wall_req = await Pages( + requests=self.requests, + url=endpoint + f"/v2/groups/{self.id}/wall/posts", + sort_order=sort_order, + limit=limit, + handler=wall_post_handeler, + handler_args=self + ) + return wall_req diff --git a/ro_py/roles.py b/ro_py/roles.py index 51c7c9d8..ef0f689d 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -81,7 +81,7 @@ def __init__(self, requests, group, role_data): self.group = group self.id = role_data['id'] self.name = role_data['name'] - self.description = role_data['description'] + self.description = role_data.get('description') self.rank = role_data['rank'] self.member_count = role_data['memberCount'] diff --git a/ro_py/trades.py b/ro_py/trades.py index 5b231042..ce9ac45c 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -7,13 +7,14 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset from ro_py.users import User +from typing import List import iso8601 import enum endpoint = "https://trades.roblox.com" -def trade_page_handler(requests, this_page) -> list: +def trade_page_handler(requests, this_page, args) -> list: trades_out = [] for raw_trade in this_page: trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) @@ -21,7 +22,7 @@ def trade_page_handler(requests, this_page) -> list: class Trade: - def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: User, recieve_items: List[Asset], send_items: List[Asset], created, expiration, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 97c7890e..06233c2a 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -14,7 +14,7 @@ class Page: """ Represents a single page from a Pages object. """ - def __init__(self, requests, data, handler=None): + def __init__(self, requests, data, handler=None, handler_args=None): self.previous_page_cursor = data["previousPageCursor"] """Cursor to navigate to the previous page.""" self.next_page_cursor = data["nextPageCursor"] @@ -24,7 +24,7 @@ def __init__(self, requests, data, handler=None): """Raw data from this page.""" if handler: - self.data = handler(requests, self.data) + self.data = handler(requests, self.data, handler_args) class Pages: @@ -36,7 +36,7 @@ class Pages: Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required. """ - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None): + def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): if extra_parameters is None: extra_parameters = {} @@ -72,7 +72,8 @@ async def _get_page(self, cursor=None): return Page( requests=self.requests, data=page_req.json(), - handler=self.handler + handler=self.handler, + handler_args=handler_args ) async def previous(self): diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index deb256f9..ece61eda 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -102,3 +102,27 @@ async def patch(self, *args, **kwargs): return patch_request raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + + async def delete(self, *args, **kwargs): + """ + Essentially identical to requests_async.Session.delete. + """ + + delete_request = await self.session.delete(*args, **kwargs) + + if delete_request.status_code == 403: + if "X-CSRF-TOKEN" in delete_request.headers: + self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] + delete_request = await self.session.delete(*args, **kwargs) + + delete_request_json = delete_request.json() + + if isinstance(delete_request_json, dict): + try: + delete_request_error = delete_request_json["errors"] + except KeyError: + return delete_request + else: + return delete_request + + raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") From 37167f398aa6f142cf48eb59259b7e7e14f3e6e5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:06:44 -0500 Subject: [PATCH 202/518] added dep --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3fc37b30..1c0fa4f3 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ install_requires=[ "iso8601", "signalrcore", - "cachecontrol" + "cachecontrol", + "requests-async" ] ) From 06ed24c7cb7cd057ddad6055a19cc7a23000cb9a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:14:46 -0500 Subject: [PATCH 203/518] Removed warning --- ro_py/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 983f3eb9..0c454e35 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -62,8 +62,6 @@ def __init__(self, token: str = None, requests_cache: bool = False): logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") - else: - logging.warning("The active client is not authenticated, so some features will not be enabled.") async def get_user(self, user_id): """ From dd277562d1b46d90d9cbf4b48f1ef3c95b91a412 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:16:04 -0500 Subject: [PATCH 204/518] removed robloxdocs from ignore --- .gitignore | 1 - ro_py/robloxdocs.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 ro_py/robloxdocs.py diff --git a/.gitignore b/.gitignore index 4c8aa7f3..cc34eb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,3 @@ ro_py_old/ other/ build.bat chat.py -ro_py/robloxdocs.py diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py new file mode 100644 index 00000000..25dbf5f0 --- /dev/null +++ b/ro_py/robloxdocs.py @@ -0,0 +1,19 @@ +""" + +This file houses functions and classes that pertain to the Roblox API documentation pages. +I don't know if this is really that useful, but it might be useful for an API browser program, or for accessing +endpoints that aren't supported directly by ro.py yet. + +""" + +from lxml import html +from io import StringIO + + +class EndpointDocs: + def __init__(self, requests, docs_url): + self.requests = requests + self.url = docs_url + + async def get_versions(self): + docs_req = self.requests.get(self.url + "/docs") From c3fd035f700f0782c0407dc39fcf90d8d9541f86 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:23:38 -0500 Subject: [PATCH 205/518] working on RobloxDocs --- ro_py/robloxdocs.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index 25dbf5f0..fded63d0 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -10,6 +10,20 @@ from io import StringIO +class EndpointDocsDataInfo: + def __init__(self, data): + self.version = data["version"] + self.title = data["title"] + + +class EndpointDocsData: + def __init__(self, data): + self.swagger_version = data["swagger"] + self.info = EndpointDocsDataInfo(data["info"]) + self.host = data["host"] + self.schemes = data["schemes"] + + class EndpointDocs: def __init__(self, requests, docs_url): self.requests = requests @@ -17,3 +31,14 @@ def __init__(self, requests, docs_url): async def get_versions(self): docs_req = self.requests.get(self.url + "/docs") + root = html.parse(StringIO(docs_req.text)).getroot() + try: + vs_element = root.get_element_by_id("version-selector") + return vs_element.value_options + except KeyError: + return ["v1"] + + async def get_data_for_version(self, version): + data_req = self.requests.get(self.url + "/docs/json/" + version) + version_data = data_req.json() + return EndpointDocsData(version_data) From 56b1e92fa002a8e355d0250fa97c91057623255a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:30:53 -0500 Subject: [PATCH 206/518] added docspath --- ro_py/robloxdocs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index fded63d0..9a0c5b78 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -10,6 +10,11 @@ from io import StringIO +class EndpointDocsPath: + def __init__(self, data): + self.data = data + + class EndpointDocsDataInfo: def __init__(self, data): self.version = data["version"] @@ -22,6 +27,9 @@ def __init__(self, data): self.info = EndpointDocsDataInfo(data["info"]) self.host = data["host"] self.schemes = data["schemes"] + self.paths = {} + for path_k, path_v in data["paths"].items(): + self.paths[path_k] = EndpointDocsPath(path_v) class EndpointDocs: From f1cea104303aa9dc9bc28f51732a493e89e0a963 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:39:26 -0500 Subject: [PATCH 207/518] Added pathrequesttype and parameters --- ro_py/robloxdocs.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index 9a0c5b78..91a150c5 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -10,9 +10,25 @@ from io import StringIO +class EndpointDocsPathRequestTypeParameters: + def __init__(self): + pass + + +class EndpointDocsPathRequestType: + def __init__(self, data): + self.tags = data["tags"] + self.summary = data["summary"] + self.description = data["description"] + self.consumes = data["consumes"] + self.produces = data["produces"] + + class EndpointDocsPath: def __init__(self, data): self.data = data + for type_k, type_v in self.data: + setattr(self, type_k, EndpointDocsPathRequestType(type_v)) class EndpointDocsDataInfo: From f836d2aebe1e3897869845415eaf16861407fe2d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:49:11 -0500 Subject: [PATCH 208/518] Added Properties, Response, and renamed parameters --- ro_py/robloxdocs.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index 91a150c5..e8a5d6c3 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -10,9 +10,26 @@ from io import StringIO -class EndpointDocsPathRequestTypeParameters: - def __init__(self): - pass +class EndpointDocsPathRequestTypeProperties: + def __init__(self, data): + self.internal = data["internal"] + self.metric_ids = data["metricIds"] + + +class EndpointDocsPathRequestTypeResponse: + def __init__(self, data): + self.description = data["description"] + self.schema = data["schema"] + + +class EndpointDocsPathRequestTypeParameter: + def __init__(self, data): + self.name = data["name"] + self.iin = data["in"] # I can't make this say "in" so this is close enough + self.description = data["description"] + self.required = data["required"] + self.type = data["type"] # TODO: actually convert this to python types + self.format = data["format"] class EndpointDocsPathRequestType: @@ -22,6 +39,13 @@ def __init__(self, data): self.description = data["description"] self.consumes = data["consumes"] self.produces = data["produces"] + self.parameters = [] + self.responses = {} + self.properties = EndpointDocsPathRequestTypeProperties(data["properties"]) + for raw_parameter in data["parameters"]: + self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter)) + for rr_k, rr_v in data["responses"]: + self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v) class EndpointDocsPath: From 522f616fd064882fd649be1070b9092dbff29912 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 16:57:39 -0500 Subject: [PATCH 209/518] Fixed some issues --- ro_py/robloxdocs.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index e8a5d6c3..bec80513 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -18,25 +18,43 @@ def __init__(self, data): class EndpointDocsPathRequestTypeResponse: def __init__(self, data): - self.description = data["description"] - self.schema = data["schema"] + self.description = None + self.schema = None + if "description" in data: + self.description = data["description"] + if "schema" in data: + self.schema = data["schema"] class EndpointDocsPathRequestTypeParameter: def __init__(self, data): self.name = data["name"] self.iin = data["in"] # I can't make this say "in" so this is close enough - self.description = data["description"] + + if "description" in data: + self.description = data["description"] + else: + self.description = None + self.required = data["required"] self.type = data["type"] # TODO: actually convert this to python types - self.format = data["format"] + + if "format" in data: + self.format = data["format"] + else: + self.format = None class EndpointDocsPathRequestType: def __init__(self, data): self.tags = data["tags"] self.summary = data["summary"] - self.description = data["description"] + + if "description" in data: + self.description = data["description"] + else: + self.description = None + self.consumes = data["consumes"] self.produces = data["produces"] self.parameters = [] @@ -44,14 +62,14 @@ def __init__(self, data): self.properties = EndpointDocsPathRequestTypeProperties(data["properties"]) for raw_parameter in data["parameters"]: self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter)) - for rr_k, rr_v in data["responses"]: + for rr_k, rr_v in data["responses"].items(): self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v) class EndpointDocsPath: def __init__(self, data): self.data = data - for type_k, type_v in self.data: + for type_k, type_v in self.data.items(): setattr(self, type_k, EndpointDocsPathRequestType(type_v)) @@ -78,7 +96,7 @@ def __init__(self, requests, docs_url): self.url = docs_url async def get_versions(self): - docs_req = self.requests.get(self.url + "/docs") + docs_req = await self.requests.get(self.url + "/docs") root = html.parse(StringIO(docs_req.text)).getroot() try: vs_element = root.get_element_by_id("version-selector") @@ -87,6 +105,6 @@ async def get_versions(self): return ["v1"] async def get_data_for_version(self, version): - data_req = self.requests.get(self.url + "/docs/json/" + version) + data_req = await self.requests.get(self.url + "/docs/json/" + version) version_data = data_req.json() return EndpointDocsData(version_data) From c37200645a5643b763a74d44116e1f69a7aca776 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 3 Jan 2021 21:30:15 -0500 Subject: [PATCH 210/518] Added get_user_by_username + more --- examples/username.py | 26 ++++++++++++++++++++++++++ ro_py/client.py | 33 +++++++++++++++++++++++++++++++++ ro_py/robloxdocs.py | 19 ++++++++++++------- ro_py/utilities/errors.py | 4 ++++ 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 examples/username.py diff --git a/examples/username.py b/examples/username.py new file mode 100644 index 00000000..131c2fd5 --- /dev/null +++ b/examples/username.py @@ -0,0 +1,26 @@ +from ro_py.client import Client +import asyncio + +client = Client() + +user_name = "JMK_RBXDev" + + +async def grab_info(): + print(f"Loading user {user_name}...") + user = await client.get_user_by_username(user_name) + print("Loaded user.") + + print(f"Username: {user.name}") + print(f"Display Name: {user.display_name}") + print(f"Description: {user.description}") + print(f"Status: {await user.get_status() or 'None.'}") + + +def main(): + loop = asyncio.get_event_loop() + loop.run_until_complete(grab_info()) + + +if __name__ == '__main__': + main() diff --git a/ro_py/client.py b/ro_py/client.py index 0c454e35..162b25db 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -15,6 +15,7 @@ from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.accountinformation import AccountInformation +from ro_py.utilities.errors import UserDoesNotExistError import logging @@ -79,6 +80,38 @@ async def get_user(self, user_id): await user.update() return user + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + """ + Gets a Roblox user by their username.. + + Parameters + ---------- + user_name : str + Name of the user to generate the object from. + exclude_banned_users : bool + Whether to exclude banned users in the request. + """ + username_req = await self.requests.post( + url="https://users.roblox.com/v1/usernames/users", + data={ + "usernames": [ + user_name + ], + "excludeBannedUsers": exclude_banned_users + } + ) + username_data = username_req.json() + if len(username_data["data"]) > 0: + user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser + user = self.requests.cache.get(CacheType.Users, user_id) + if not user: + user = User(self.requests, user_id) + self.requests.cache.set(CacheType.Users, user_id, user) + await user.update() + return user + else: + raise UserDoesNotExistError + async def get_group(self, group_id): """ Gets a Roblox group. diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py index bec80513..249a7815 100644 --- a/ro_py/robloxdocs.py +++ b/ro_py/robloxdocs.py @@ -37,7 +37,10 @@ def __init__(self, data): self.description = None self.required = data["required"] - self.type = data["type"] # TODO: actually convert this to python types + self.type = None + + if "type" in data: + self.type = data["type"] if "format" in data: self.format = data["format"] @@ -48,12 +51,14 @@ def __init__(self, data): class EndpointDocsPathRequestType: def __init__(self, data): self.tags = data["tags"] - self.summary = data["summary"] + self.description = None + self.summary = None + + if "summary" in data: + self.summary = data["summary"] if "description" in data: self.description = data["description"] - else: - self.description = None self.consumes = data["consumes"] self.produces = data["produces"] @@ -68,9 +73,9 @@ def __init__(self, data): class EndpointDocsPath: def __init__(self, data): - self.data = data - for type_k, type_v in self.data.items(): - setattr(self, type_k, EndpointDocsPathRequestType(type_v)) + self.data = {} + for type_k, type_v in data.items(): + self.data[type_k] = EndpointDocsPathRequestType(type_v) class EndpointDocsDataInfo: diff --git a/ro_py/utilities/errors.py b/ro_py/utilities/errors.py index 91ad4d3a..9229bf56 100644 --- a/ro_py/utilities/errors.py +++ b/ro_py/utilities/errors.py @@ -33,3 +33,7 @@ class ChatError(Exception): class InvalidPageError(Exception): """Called when an invalid page is requested.""" + + +class UserDoesNotExistError(Exception): + """Called when a user does not exist.""" From e0671570805bf8e8fcec57d07473873d1e312886 Mon Sep 17 00:00:00 2001 From: mfd-co Date: Mon, 4 Jan 2021 18:52:26 +0000 Subject: [PATCH 211/518] Stash --- .vscode/settings.json | 3 +++ ro_py/roles.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..97f8994c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.pythonPath": "C:\\Users\\minig\\AppData\\Local\\Programs\\Python\\Python39\\python.exe" +} diff --git a/ro_py/roles.py b/ro_py/roles.py index 9b799fe9..336c6f3f 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -41,7 +41,7 @@ async def edit(self, name=None, description=None, rank=None): edit_req = await self.requests.patch( url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", data={ - "description": description if description else self.description + "description": description if description else self.description, "name": name if name else self.name, "rank": rank if rank else self.rank } From e9d6f447e093cc3e1ddc468ebb96e1ce5a15518e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 4 Jan 2021 17:16:15 -0500 Subject: [PATCH 212/518] Updated version identifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1c0fa4f3..b4627687 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="1.0.1", + version="1.0.2", author="jmkdev", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 5fbed8762658d23b9e90b6f7fb7527c873a595c1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 5 Jan 2021 16:05:56 -0500 Subject: [PATCH 213/518] Modified chat+notifications to work properly with async --- ro_py/chat.py | 8 ++++---- ro_py/notifications.py | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 008cf496..23ed45cd 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -63,11 +63,11 @@ def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): self.typing = ConversationTyping(self.requests, conversation_id) - def get_message(self, message_id): + async def get_message(self, message_id): return Message(self.requests, message_id, self.id) - def send_message(self, content): - send_message_req = self.requests.post( + async def send_message(self, content): + send_message_req = await self.requests.post( url=endpoint + "v2/send-message", data={ "message": content, @@ -146,7 +146,7 @@ async def get_conversations(self, page_number=1, page_size=10): """ Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented. """ - conversations_req = self.requests.get( + conversations_req = await self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 4f17c972..05602319 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -14,7 +14,7 @@ from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json -import logging +import asyncio class Notification: @@ -68,7 +68,15 @@ def __init__(self, requests, on_open, on_close, on_error, on_notification): self.on_notification = on_notification self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.negotiate_request = self.requests.get( + self.connection = None + + self.negotiate_request = None + self.wss_url = None + + async def initialize(self): + loop = asyncio.new_event_loop() + + self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", @@ -100,12 +108,7 @@ def on_message(_self, raw_notification): return if len(notification_json) > 0: notification = Notification(notification_json) - self.on_notification(notification) - logging.debug( - f"""Notification: -Type: {notification.type} -Data: {notification.data}""" - ) + loop.run_until_complete(self.on_notification(notification)) else: return @@ -126,7 +129,7 @@ def on_message(_self, raw_notification): self.connection.start() - def close(self): + async def close(self): """ Closes the connection and stops receiving notifications. """ From 5a27dd1df6635b062220a4b9ca497af4e69a86ab Mon Sep 17 00:00:00 2001 From: iranathan Date: Tue, 5 Jan 2021 23:39:03 +0100 Subject: [PATCH 214/518] add PartialUser and Member --- ro_py/groups.py | 56 +++++++++++++++++++++++++++++++++++++++ ro_py/roles.py | 2 +- ro_py/trades.py | 8 +++--- ro_py/users.py | 21 +++++++++++++-- ro_py/utilities/errors.py | 4 +++ 5 files changed, 84 insertions(+), 7 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 8d74fa49..8064d950 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -7,6 +7,7 @@ from typing import List from ro_py.users import User from ro_py.roles import Role +from ro_py.utilities.errors import NotFound from ro_py.utilities.pages import Pages, SortOrder endpoint = "https://groups.roblox.com" @@ -129,3 +130,58 @@ async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100): handler_args=self ) return wall_req + + async def get_member_by_id(self, roblox_id): + # Get list of group user is in. + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + + # Create data to return. + role = Role(self.requests, self, group_data['role']) + member = Member(self.requests, roblox_id, None, self, role) + return await member.update() + + +class Member(User): + """ + Represents a user in a group. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. + group : ro_py.groups.Group + The group the user is in. + role : ro_py.roles.Role + The role the user has is the group. + """ + def __init__(self, requests, roblox_id, name=None, group=None, role=None): + super().__init__(requests, roblox_id, name) + self.role = role + self.group = group + + async def promote(self): + pass + + async def demote(self): + pass + + async def setrank(self): + pass diff --git a/ro_py/roles.py b/ro_py/roles.py index ef0f689d..8645929d 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -83,7 +83,7 @@ def __init__(self, requests, group, role_data): self.name = role_data['name'] self.description = role_data.get('description') self.rank = role_data['rank'] - self.member_count = role_data['memberCount'] + self.member_count = role_data.get('memberCount') async def update(self): """ diff --git a/ro_py/trades.py b/ro_py/trades.py index 4c7294a2..f670938b 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -6,7 +6,7 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset -from ro_py.users import User +from ro_py.users import User, PartialUser import iso8601 import enum @@ -16,12 +16,12 @@ def trade_page_handler(requests, this_page) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) + trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) return trades_out class Trade: - def __init__(self, requests, trade_id: int, sender: User, recieve_items, send_items, created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender @@ -53,7 +53,7 @@ async def decline(self) -> bool: class PartialTrade: - def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool): + def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool): self.requests = requests self.trade_id = trade_id self.user = user diff --git a/ro_py/users.py b/ro_py/users.py index fc3260df..5da138e7 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -14,10 +14,19 @@ class User: """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. """ - def __init__(self, requests, id, name=None): + def __init__(self, requests, roblox_id, name=None): self.requests = requests - self.id = id + self.id = roblox_id self.description = None self.created = None self.is_banned = None @@ -38,6 +47,7 @@ async def update(self): self.display_name = user_info["displayName"] # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req + return self async def get_status(self): """ @@ -98,3 +108,10 @@ async def get_friends(self): User(self.requests, friend_raw["id"]) ) return friends_list + + +class PartialUser(User): + """ + Represents a user with less information then the normal User class. + """ + pass diff --git a/ro_py/utilities/errors.py b/ro_py/utilities/errors.py index 91ad4d3a..c599feca 100644 --- a/ro_py/utilities/errors.py +++ b/ro_py/utilities/errors.py @@ -33,3 +33,7 @@ class ChatError(Exception): class InvalidPageError(Exception): """Called when an invalid page is requested.""" + + +class NotFound(Exception): + """Called when something is not found.""" From 9f952d57c7c036298cdb2045b196f39438c92ee5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 09:01:20 -0500 Subject: [PATCH 215/518] Modified notifications + some auth modifications --- ro_py/client.py | 15 ++++++++++++++- ro_py/notifications.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 162b25db..082cfb43 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -51,7 +51,7 @@ def __init__(self, token: str = None, requests_cache: bool = False): """TradesWrapper object. Only available for authenticated clients.""" if token: - self.requests.session.cookies[".ROBLOSECURITY"] = token + self.token_login(token) logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) @@ -64,6 +64,19 @@ def __init__(self, token: str = None, requests_cache: bool = False): self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") + def token_login(self, token): + self.requests.session.cookies[".ROBLOSECURITY"] = token + + async def user_login(self, username, password): + login_req = self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password + } + ) + async def get_user(self, user_id): """ Gets a Roblox user. diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 05602319..0acf5c68 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -11,9 +11,10 @@ from ro_py.utilities.caseconvert import to_snake_case -from signalrcore.hub_connection_builder import HubConnectionBuilder +from signalrcore_async.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json +import time import asyncio @@ -74,8 +75,6 @@ def __init__(self, requests, on_open, on_close, on_error, on_notification): self.wss_url = None async def initialize(self): - loop = asyncio.new_event_loop() - self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" @@ -98,7 +97,7 @@ async def initialize(self): } ) - def on_message(_self, raw_notification): + async def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -108,11 +107,27 @@ def on_message(_self, raw_notification): return if len(notification_json) > 0: notification = Notification(notification_json) - loop.run_until_complete(self.on_notification(notification)) + await self.on_notification(notification) else: return - self.connection.with_automatic_reconnect({ + def _internal_send(_self, message, protocol=None): + + _self.logger.debug("Sending message {0}".format(message)) + + try: + protocol = _self.protocol if protocol is None else protocol + + _self._ws.send(protocol.encode(message)) + _self.connection_checker.last_message = time.time() + + if _self.reconnection_handler is not None: + _self.reconnection_handler.reset() + + except Exception as ex: + raise ex + + self.connection = self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, @@ -125,9 +140,10 @@ def on_message(_self, raw_notification): self.connection.on_close(self.on_close) if self.on_error: self.connection.on_error(self.on_error) - self.connection.hub.on_message = on_message + self.connection.on_message = on_message + self.connection._internal_send = _internal_send - self.connection.start() + await self.connection.start() async def close(self): """ From b142870b2e9abd932e9b14f1325f4cd228ec74e6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 13:46:11 -0500 Subject: [PATCH 216/518] Captcha, requests + client changes --- ro_py/captcha.py | 14 ++++++++++++++ ro_py/client.py | 23 ++++++++++++++++++++--- ro_py/utilities/requests.py | 10 ++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 ro_py/captcha.py diff --git a/ro_py/captcha.py b/ro_py/captcha.py new file mode 100644 index 00000000..7884d358 --- /dev/null +++ b/ro_py/captcha.py @@ -0,0 +1,14 @@ +""" + +Will put something here. -jmkdev + +""" + + +class UnsolvedCaptcha: + def __init__(self, data, pkey): + self.token = data["token"] + self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/?pkey={pkey}&session={self.token.split('|')[0]}&lang=en-gb" + self.challenge_url = data["challenge_url"] + self.challenge_url_cdn = data["challenge_url_cdn"] + self.noscript = data["noscript"] diff --git a/ro_py/client.py b/ro_py/client.py index 082cfb43..7afb2d18 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -11,11 +11,12 @@ from ro_py.badges import Badge from ro_py.chat import ChatWrapper from ro_py.trades import TradesWrapper +from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.cache import CacheType from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.accountinformation import AccountInformation -from ro_py.utilities.errors import UserDoesNotExistError +from ro_py.utilities.errors import UserDoesNotExistError, ApiError import logging @@ -68,14 +69,30 @@ def token_login(self, token): self.requests.session.cookies[".ROBLOSECURITY"] = token async def user_login(self, username, password): - login_req = self.requests.post( + login_req = await self.requests.post( url="https://auth.roblox.com/v2/login", json={ "ctype": "Username", "cvalue": username, "password": password - } + }, + quickreturn=True ) + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index ece61eda..578cb8ab 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -35,6 +35,8 @@ async def get(self, *args, **kwargs): Essentially identical to requests_async.Session.get. """ + quickreturn = kwargs.pop("quickreturn", False) + get_request = await self.session.get(*args, **kwargs) try: @@ -50,6 +52,9 @@ async def get(self, *args, **kwargs): else: return get_request + if quickreturn: + return get_request + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") async def post(self, *args, **kwargs): @@ -57,6 +62,8 @@ async def post(self, *args, **kwargs): Essentially identical to requests_async.Session.post. """ + quickreturn = kwargs.pop("quickreturn", False) + post_request = await self.session.post(*args, **kwargs) if post_request.status_code == 403: @@ -77,6 +84,9 @@ async def post(self, *args, **kwargs): else: return post_request + if quickreturn: + return post_request + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") async def patch(self, *args, **kwargs): From e28ec8814942e6aae43be00b8284918ee263517e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 19:06:52 -0500 Subject: [PATCH 217/518] Fixed captcha login --- ro_py/client.py | 58 +++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 7afb2d18..3160ca34 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -68,31 +68,43 @@ def __init__(self, token: str = None, requests_cache: bool = False): def token_login(self, token): self.requests.session.cookies[".ROBLOSECURITY"] = token - async def user_login(self, username, password): - login_req = await self.requests.post( - url="https://auth.roblox.com/v2/login", - json={ - "ctype": "Username", - "cvalue": username, - "password": password - }, - quickreturn=True - ) - if login_req.status_code == 200: - # If we're here, no captcha is required and we're already logged in, so we can return. - return - elif login_req.status_code == 403: - # A captcha is required, so we need to return the captcha to solve. - field_data = login_req.json()["errors"][0]["fieldData"] - captcha_req = await self.requests.post( - url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", - headers={ - "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + async def user_login(self, username, password, token=None): + if token: + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "cvalue": username, + "ctype": "Username", + "password": password, + "captchaToken": token, + "captchaProvider": "PROVIDER_ARKOSE_LABS" + } + ) + else: + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password }, - data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + quickreturn=True ) - captcha_json = captcha_req.json() - return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ From d77bfb522aba18ec77c6fa43a0beae7c5e63ec09 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 22:50:22 -0500 Subject: [PATCH 218/518] XCSRF "fix" --- ro_py/client.py | 3 ++- ro_py/utilities/requests.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 3160ca34..f38cb21f 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -78,7 +78,8 @@ async def user_login(self, username, password, token=None): "password": password, "captchaToken": token, "captchaProvider": "PROVIDER_ARKOSE_LABS" - } + }, + doxcsrf=False ) else: login_req = await self.requests.post( diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 578cb8ab..9f145984 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -63,13 +63,15 @@ async def post(self, *args, **kwargs): """ quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) post_request = await self.session.post(*args, **kwargs) - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) + if doxcsrf: + if post_request.status_code == 403: + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() From e99fc6f188cfef91da87b564773a209504652943 Mon Sep 17 00:00:00 2001 From: mfd-co Date: Fri, 8 Jan 2021 03:56:20 +0000 Subject: [PATCH 219/518] Change some things, like using no asycn requests fixes the socket hangup? lol this is eternally fucking with my mind --- ro_py/client.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 3160ca34..34a7e419 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -17,6 +17,7 @@ from ro_py.accountsettings import AccountSettings from ro_py.accountinformation import AccountInformation from ro_py.utilities.errors import UserDoesNotExistError, ApiError +import requests import logging @@ -69,17 +70,21 @@ def token_login(self, token): self.requests.session.cookies[".ROBLOSECURITY"] = token async def user_login(self, username, password, token=None): + print(token) if token: - login_req = await self.requests.post( + # TVF + login_req2 = requests.post( url="https://auth.roblox.com/v2/login", - json={ - "cvalue": username, - "ctype": "Username", - "password": password, - "captchaToken": token, - "captchaProvider": "PROVIDER_ARKOSE_LABS" - } + headers={"Content-Type":"application/json"}, + data=f"{{\"cvalue\": \"{username}\", \"ctype\":\"Username\",\"password\":\"{password}\",\"captchaToken\":\"{str(token)}\", \"captchaProvider\":\"PROVIDER_ARKOSE_LABS\"}}" + ) + # TVF_RESOL + login_req3 = requests.post( + url="https://auth.roblox.com/v2/login", + headers={"Content-Type":"application/json", "x-csrf-token": login_req2.headers.get('x-csrf-token')}, + data=f"{{\"cvalue\": \"{username}\", \"ctype\":\"Username\",\"password\":\"{password}\",\"captchaToken\":\"{str(token)}\", \"captchaProvider\":\"PROVIDER_ARKOSE_LABS\"}}" ) + return login_req3.json() else: login_req = await self.requests.post( url="https://auth.roblox.com/v2/login", @@ -90,10 +95,11 @@ async def user_login(self, username, password, token=None): }, quickreturn=True ) - if login_req.status_code == 200: - # If we're here, no captcha is required and we're already logged in, so we can return. - return - elif login_req.status_code == 403: + try: + print(login_req.json(), login_req2.json()) + except: + print(); + if login_req.status_code == 403 and login_req.json()["errors"][0]['message'] != "Token Validation Failed": # A captcha is required, so we need to return the captcha to solve. field_data = login_req.json()["errors"][0]["fieldData"] captcha_req = await self.requests.post( From 55302ca49a17dcce388f0c465c29aca839bb8d34 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 23:08:33 -0500 Subject: [PATCH 220/518] Delete settings.json --- .vscode/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 97f8994c..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "C:\\Users\\minig\\AppData\\Local\\Programs\\Python\\Python39\\python.exe" -} From d611a1dc4c6d73406125c82542f19cb1fb19fd93 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 23:17:19 -0500 Subject: [PATCH 221/518] i have no idea what is going on --- ro_py/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index f38cb21f..59b06e15 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -78,8 +78,12 @@ async def user_login(self, username, password, token=None): "password": password, "captchaToken": token, "captchaProvider": "PROVIDER_ARKOSE_LABS" +<<<<<<< HEAD }, doxcsrf=False +======= + } +>>>>>>> parent of e505e9e... Merge pull request #3 from nsg-mfd/main ) else: login_req = await self.requests.post( From 208107c886045f05b5670e03275ca3d762632df7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 23:17:44 -0500 Subject: [PATCH 222/518] i have no idea what is going on --- ro_py/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index 59b06e15..4b0aa1fd 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -78,12 +78,16 @@ async def user_login(self, username, password, token=None): "password": password, "captchaToken": token, "captchaProvider": "PROVIDER_ARKOSE_LABS" +<<<<<<< HEAD <<<<<<< HEAD }, doxcsrf=False ======= } >>>>>>> parent of e505e9e... Merge pull request #3 from nsg-mfd/main +======= + } +>>>>>>> parent of e99fc6f... Change some things, like using no asycn requests fixes the socket hangup? lol this is eternally fucking with my mind ) else: login_req = await self.requests.post( From 222a31cc9bafcdb0522e5e08a16a9fde8b1ce0c4 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 23:19:59 -0500 Subject: [PATCH 223/518] hopefully this fixed whatever the hell happened before --- ro_py/client.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 4b0aa1fd..3160ca34 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -78,16 +78,7 @@ async def user_login(self, username, password, token=None): "password": password, "captchaToken": token, "captchaProvider": "PROVIDER_ARKOSE_LABS" -<<<<<<< HEAD -<<<<<<< HEAD - }, - doxcsrf=False -======= - } ->>>>>>> parent of e505e9e... Merge pull request #3 from nsg-mfd/main -======= } ->>>>>>> parent of e99fc6f... Change some things, like using no asycn requests fixes the socket hangup? lol this is eternally fucking with my mind ) else: login_req = await self.requests.post( From e64b475562133da41daa4b203777caa9540c6aff Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 7 Jan 2021 23:51:34 -0500 Subject: [PATCH 224/518] =?UTF-8?q?help=20i=20can't=20=F0=9F=85=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/client.py | 3 ++- ro_py/utilities/requests.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index 3160ca34..b19dc168 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -70,7 +70,7 @@ def token_login(self, token): async def user_login(self, username, password, token=None): if token: - login_req = await self.requests.post( + login_req = self.requests.back_post( url="https://auth.roblox.com/v2/login", json={ "cvalue": username, @@ -80,6 +80,7 @@ async def user_login(self, username, password, token=None): "captchaProvider": "PROVIDER_ARKOSE_LABS" } ) + return login_req else: login_req = await self.requests.post( url="https://auth.roblox.com/v2/login", diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 9f145984..0c4e8bb9 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -3,6 +3,7 @@ from json.decoder import JSONDecodeError from cachecontrol import CacheControl import requests_async +import requests class Requests: @@ -57,6 +58,12 @@ async def get(self, *args, **kwargs): raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + def back_post(self, *args, **kwargs): + kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + post_request = requests.post(*args, **kwargs) + self.session.cookies = post_request.cookies + return post_request + async def post(self, *args, **kwargs): """ Essentially identical to requests_async.Session.post. From 7a4359cb441aa17aa3a0eb6fc42a29f66890f6a3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 00:08:05 -0500 Subject: [PATCH 225/518] Small captcha modifications + added uilogin --- examples/uilogin.py | 149 ++++++++++++++++++++++++++++++++++++ ro_py/client.py | 2 +- ro_py/utilities/requests.py | 7 ++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 examples/uilogin.py diff --git a/examples/uilogin.py b/examples/uilogin.py new file mode 100644 index 00000000..581057cb --- /dev/null +++ b/examples/uilogin.py @@ -0,0 +1,149 @@ +import wx +import asyncio +import pytweening +from ro_py.client import Client +import wx.html2 + +roblox = Client() + + +async def user_login(username, password, key=None): + if key: + return await roblox.user_login(username, password, key) + else: + return await roblox.user_login(username, password) + + +class RbxLogin(wx.Frame): + def __init__(self, *args, **kwds): + kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + self.SetSize((512, 512)) + self.SetTitle("Login with Roblox") + self.SetBackgroundColour(wx.Colour(255, 255, 255)) + + self.username = None + self.password = None + + root_sizer = wx.BoxSizer(wx.VERTICAL) + + self.inner_panel = wx.Panel(self, wx.ID_ANY) + root_sizer.Add(self.inner_panel, 1, wx.ALL | wx.EXPAND, 100) + + inner_sizer = wx.BoxSizer(wx.VERTICAL) + + inner_sizer.Add((0, 20), 0, 0, 0) + + login_label = wx.StaticText(self.inner_panel, wx.ID_ANY, "Please log in with your username and password.", + style=wx.ALIGN_CENTER_HORIZONTAL) + inner_sizer.Add(login_label, 1, 0, 0) + + self.username_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "\n") + self.username_entry.SetFont( + wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI")) + self.username_entry.SetFocus() + inner_sizer.Add(self.username_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4) + + self.password_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "", style=wx.TE_PASSWORD) + self.password_entry.SetFont( + wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI")) + inner_sizer.Add(self.password_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4) + + self.log_in_button = wx.Button(self.inner_panel, wx.ID_ANY, "Login") + inner_sizer.Add(self.log_in_button, 1, wx.ALL | wx.EXPAND, 0) + + inner_sizer.Add((0, 20), 0, 0, 0) + + self.web_view = wx.html2.WebView.New(self, wx.ID_ANY) + self.web_view.Hide() + self.web_view.EnableAccessToDevTools(False) + self.web_view.EnableContextMenu(False) + + root_sizer.Add(self.web_view, 1, wx.EXPAND, 0) + + self.inner_panel.SetSizer(inner_sizer) + + self.SetSizer(root_sizer) + + self.Layout() + + self.Bind(wx.EVT_BUTTON, self.login_click, self.log_in_button) + self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) + + def login_load(self, event): + _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}") + if token == "undefined": + token = False + if token: + self.web_view.Hide() + """ + for i in range(0, 600): + self.web_view.SetPosition((0, int(0 + pytweening.easeOutQuad(i / 600) * 600))) + """ + asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password, token)) + print(roblox.requests.session.cookies) + if ".ROBLOSECURITY" in roblox.requests.session.cookies: + self.Close() + else: + wx.MessageBox(f"Failed to log in.", "Error", wx.OK | wx.ICON_ERROR) + self.Close() + + def login_click(self, event): + self.username = self.username_entry.GetValue() + self.password = self.password_entry.GetValue() + self.username.strip("\n") + self.password.strip("\n") + + if not (self.username and self.password): + # If either the username or password is missing, return + return + + if len(self.username) < 3: + # If the username is shorter than 3, return + return + + # Disable the entries to stop people from typing in them. + self.username_entry.Disable() + self.password_entry.Disable() + self.log_in_button.Disable() + + # Get the position of the inner_panel + old_pos = self.inner_panel.GetPosition() + start_point = old_pos[0] + + # Move the panel over to the right. + for i in range(0, 512): + wx.Yield() + self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1])) + + # Hide the panel. The panel is already on the right so it's not visible anyways. + self.inner_panel.Hide() + self.web_view.SetSize((512, 600)) + + # Expand the window. + for i in range(0, 88): + self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88))) + + # Runs the user_login function. + fd = asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password)) + + # Load the captcha URL. + if fd: + self.web_view.LoadURL(fd.url) + self.web_view.Show() + else: + # No captcha needed. + self.Close() + + +class MyApp(wx.App): + def OnInit(self): + self.rbx_login = RbxLogin(None, wx.ID_ANY, "") + self.SetTopWindow(self.rbx_login) + self.rbx_login.Show() + return True + + +if __name__ == "__main__": + app = MyApp(0) + app.MainLoop() diff --git a/ro_py/client.py b/ro_py/client.py index b19dc168..75d29521 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -73,8 +73,8 @@ async def user_login(self, username, password, token=None): login_req = self.requests.back_post( url="https://auth.roblox.com/v2/login", json={ - "cvalue": username, "ctype": "Username", + "cvalue": username, "password": password, "captchaToken": token, "captchaProvider": "PROVIDER_ARKOSE_LABS" diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 0c4e8bb9..837d4597 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -60,7 +60,14 @@ async def get(self, *args, **kwargs): def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + kwargs["headers"] = kwargs.pop("headers", self.session.headers) + post_request = requests.post(*args, **kwargs) + + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) + self.session.cookies = post_request.cookies return post_request From d80f80f9f5152b84d88998ca1c0c7a37f2dca678 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 09:07:15 -0500 Subject: [PATCH 226/518] Updated UiLogin --- examples/uilogin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/uilogin.py b/examples/uilogin.py index 581057cb..5c5977b8 100644 --- a/examples/uilogin.py +++ b/examples/uilogin.py @@ -80,12 +80,12 @@ def login_load(self, event): for i in range(0, 600): self.web_view.SetPosition((0, int(0 + pytweening.easeOutQuad(i / 600) * 600))) """ - asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password, token)) - print(roblox.requests.session.cookies) + lr = asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password, token)) if ".ROBLOSECURITY" in roblox.requests.session.cookies: self.Close() else: - wx.MessageBox(f"Failed to log in.", "Error", wx.OK | wx.ICON_ERROR) + wx.MessageBox(f"Failed to log in.\n" + f"Detailed information from server: {lr.json()['errors'][0]['message']}", "Error", wx.OK | wx.ICON_ERROR) self.Close() def login_click(self, event): From 416394cf659785ee17bac5df3243460499aa994b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 09:09:43 -0500 Subject: [PATCH 227/518] Added comments to the examples --- examples/uilogin.py | 12 ++++++++++++ examples/user.py | 10 ++++++++++ examples/username.py | 10 ++++++++++ 3 files changed, 32 insertions(+) diff --git a/examples/uilogin.py b/examples/uilogin.py index 5c5977b8..2d95a86f 100644 --- a/examples/uilogin.py +++ b/examples/uilogin.py @@ -1,3 +1,15 @@ +""" + +ro.py +UILogin Example + +This example uses wxPython to log in to an account with a username, password and captcha. +At the time of writing, Roblox doesn't have any OAuth support, so the best we can do +is a dialog like this. + +""" + + import wx import asyncio import pytweening diff --git a/examples/user.py b/examples/user.py index 43ee553a..ee9720d7 100644 --- a/examples/user.py +++ b/examples/user.py @@ -1,3 +1,13 @@ +""" + +ro.py +User Example + +This example loads a user from their user ID. + +""" + + from ro_py.client import Client import asyncio diff --git a/examples/username.py b/examples/username.py index 131c2fd5..9427db38 100644 --- a/examples/username.py +++ b/examples/username.py @@ -1,3 +1,13 @@ +""" + +ro.py +Username Example + +This example loads a User from their username. + +""" + + from ro_py.client import Client import asyncio From 68d624fad97f720c218e8d974ebe3136b428c7e6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 10:02:59 -0500 Subject: [PATCH 228/518] =?UTF-8?q?Extensions=20=F0=9F=A7=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/extensions/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ro_py/extensions/__init__.py diff --git a/ro_py/extensions/__init__.py b/ro_py/extensions/__init__.py new file mode 100644 index 00000000..5ad02ba6 --- /dev/null +++ b/ro_py/extensions/__init__.py @@ -0,0 +1,5 @@ +""" + +This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement. + +""" From 3876688c7b5e25896ef3cd1724dfd40047beba1f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 10:10:17 -0500 Subject: [PATCH 229/518] Bots extension --- ro_py/extensions/bots.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 ro_py/extensions/bots.py diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py new file mode 100644 index 00000000..b356dd68 --- /dev/null +++ b/ro_py/extensions/bots.py @@ -0,0 +1,5 @@ +""" + +This extension houses functions that allow generation of Bot objects, which interpret commands. + +""" From dbdeec79c543e9f2032508e173cf6208b90e59d1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 10:31:22 -0500 Subject: [PATCH 230/518] Working on bots object --- ro_py/extensions/bots.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index b356dd68..3b96a34e 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -3,3 +3,25 @@ This extension houses functions that allow generation of Bot objects, which interpret commands. """ + + +from ro_py.client import Client + + +class Bot(Client): + def __init__(self): + super().__init__() + + +class Command: + def __init__(self): + pass + + +def command(function): + def decorator(func): + if isinstance(func, Command): + raise TypeError('Callback is already a command.') + return Command(func, name=name, **attrs) + + return decorator From 14314e30ecc6b0ed300ffeeda3059cab607e46bb Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 10:34:25 -0500 Subject: [PATCH 231/518] Updated bots with initializer --- ro_py/extensions/bots.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 3b96a34e..44442999 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -6,6 +6,7 @@ from ro_py.client import Client +import asyncio class Bot(Client): @@ -14,14 +15,15 @@ def __init__(self): class Command: - def __init__(self): - pass + def __init__(self, func, **kwargs): + if not asyncio.iscoroutinefunction(func): + raise TypeError('Callback must be a coroutine.') -def command(function): +def command(function, **attrs): def decorator(func): if isinstance(func, Command): raise TypeError('Callback is already a command.') - return Command(func, name=name, **attrs) + return Command(func, **attrs) return decorator From 072cf27d82046d515d31221d61443b37bd221729 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 10:56:38 -0500 Subject: [PATCH 232/518] Updated __call__ --- ro_py/extensions/bots.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 44442999..e3018bc3 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -18,9 +18,17 @@ class Command: def __init__(self, func, **kwargs): if not asyncio.iscoroutinefunction(func): raise TypeError('Callback must be a coroutine.') + self._callback = func + @property + def callback(self): + return self._callback -def command(function, **attrs): + async def __call__(self, *args, **kwargs): + return await self.callback(*args, **kwargs) + + +def command(**attrs): def decorator(func): if isinstance(func, Command): raise TypeError('Callback is already a command.') From 9ca88d121a963db72988e0660723d674ce3f8795 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 13:47:31 -0500 Subject: [PATCH 233/518] small groups fix --- ro_py/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 8064d950..800c3059 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -121,7 +121,7 @@ async def get_roles(self): return roles async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100): - wall_req = await Pages( + wall_req = Pages( requests=self.requests, url=endpoint + f"/v2/groups/{self.id}/wall/posts", sort_order=sort_order, From 1ac6e2352e22263c2dd5c21eaff70ea4c8aae89e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 18:35:27 -0500 Subject: [PATCH 234/518] Prompt extension + guilogin example --- examples/guilogin.py | 22 +++++++++ ro_py/extensions/appicon.png | Bin 0 -> 2245 bytes ro_py/extensions/appicon_large.png | Bin 0 -> 12208 bytes .../uilogin.py => ro_py/extensions/prompt.py | 43 ++++++++++-------- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 examples/guilogin.py create mode 100644 ro_py/extensions/appicon.png create mode 100644 ro_py/extensions/appicon_large.png rename examples/uilogin.py => ro_py/extensions/prompt.py (83%) diff --git a/examples/guilogin.py b/examples/guilogin.py new file mode 100644 index 00000000..61247d40 --- /dev/null +++ b/examples/guilogin.py @@ -0,0 +1,22 @@ +""" + +ro.py +GUI Login Example + +This example uses the prompt extension to login with a GUI dialog. + +""" + + +from ro_py.client import Client +from ro_py.extensions.prompt import authenticate_prompt + +client = Client() + + +def main(): + authenticate_prompt(client) + + +if __name__ == '__main__': + main() diff --git a/ro_py/extensions/appicon.png b/ro_py/extensions/appicon.png new file mode 100644 index 0000000000000000000000000000000000000000..399c59492cb6a5a2259f168211b8597c93485180 GIT binary patch literal 2245 zcmV;$2s-zPP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2wF)*K~!i%#hH0* zR8;+M6g4qKV?zHBx5Q|~C5meV#Td6};({7uV$>KV zCI*ec0HR>E6vaXf=kMG*GfrRMtnJhsuoZ~ET(&iU@S%NQvE zLA%V8D!E*IS;OT9%2#HB?6WujClp1Xe$lE6_H~b1x#fz}Yq;DfBV~egN|OKnj_Z#o z>u7JJ{uV+>>LEix0vJD>YAfZ2)<@>9BnoFsTvii>n0WNYDFLNh5@?$ORS?Axm}tD%r-9$QnadfFNI>Eh8757b{2%m5N+w3}DLt}>zRziWm?E^9==?%G zs8AmkLcNK_#f#TWrG2IAk|lV2w>35mjY1DaNp({ULW@hi)W|KoWhJDx1|71NvLx6l zvndDjtpTH$2)v5%^8-IALE!()M=eqKnJC;X!{wf$hp>`O7bWP+PrSX2)_Up>slOmX ze^Zx)yaX_QKdKJn#m6cEuWFe1J&9!yxDEmba&Hq^=>+TfQlM{5j~{J!5kDh4Xnjmk zLYm>eflwE9UKAoQMU2@*Glu8+w&Bl9Fdk+?q74%B8K|#Xj}8QCg+@jRDY|%~k#A|W zQm>=_hcm3w@`~CYlh`F-G=- zP%Y8EfqDhwr-LNWi~{b;83DXMg6cC;=&?w~b*7+MDdFE4h+Vc5rJc2(a@HMIT1;*=u)+y?G*CY4gtzT`}hXQJn^Xck*1ZH^gYD@}gD1{)& z$Ti+gMCy1mnUomF{bZz}gUfu)`3Ru@XyX;k2@L3}Q8bsj9(j%^Y(b}$hekRnfAHBJ zNdT`NKy_MJ`(gM*fiW_l@6RR*Is)z_yng=NHVAN1?I7yE=8C|{U7Yspc4(x4RyB zlmWeyxuNh>St^`-+HsVUhN!q`VT?VD*tyi4w%Hl|3G~=lB+jU@3qkwMoBN; z1_DRG%104JK=sdS3hO@`npRD`O6SKVbvLaMNW3ZEOC4{ROvo=`Ts)7obUl;34Jr*v z-Yp!P?I(yLK>jby`x!XSK~-v1Y352Vi>_^ym(4^rLq^CO@?hEHL#^#)#@7^@LLUNz z{~)Rb!QcjvC{pFGo)3^Z@} z@;dXug=HmD9Xpuflr@E}reX}m|z;Ff!x!*jd62*HRMB$l|=K-}+ z&ymEQ^!0%y@Z*?~hf+<_mk%l+lhc^%0p**qM))}?0V!0X4tPI55=7R^$=G#LXl9bSCFflnS%cG$VB99%^0w+93gu^ z?8Po83e!^BzRTSS!3kp%-}%S3B9O@eEW(Z^-__Eb&73_GUyX;*2ts4MX4go0PU=d9 z7q8aL^llKC#?}Rm_)p$uKsVac=rnX$n2?1Kc*=G3M`RLfxOFY=501y+Ixf%R>Gjkn zc(H1$R@Jea^ae&qBhJ8XVhO&S&CXOL)50T%*{ts@<3&;j0k@WoKqP@Mhz^DaC-Sfc zE{TvOb{t;xG%7Aar4vdW({H0W4HCN{@g%FKh4zqA<&(6(Lr;XjUg}5`MIa0ajlkHG zu@|XXLbWWRk21&tCieKf*tDGb-R&L-EQP>mEZR&Yj<5t0vK-@^+&8097J)DzGzyD# z!90s{CWP$Tiupy8zl%@~LaFSxoBJAtZIHMY5iPSf6LK{Kp0({97!(3wKxi};>k&#i zA)M-Y@sZ-8xQK+Hna}U(S`L9G2vVGtd36^v#4Oy^QFoYrIG zHT%btiB8?H*KfFTZh~}r9>%|=|C0|xP9O{jA(`MLZ8E#fApyKV6y9)m3qj;xbu0-} T5S8z)00000NkvXXu0mjf1B@-I literal 0 HcmV?d00001 diff --git a/ro_py/extensions/appicon_large.png b/ro_py/extensions/appicon_large.png new file mode 100644 index 0000000000000000000000000000000000000000..1f530eb11ba28518ef51adf489aa28cd7840d33c GIT binary patch literal 12208 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 Ib)5141Ngx$4*&oF literal 0 HcmV?d00001 diff --git a/examples/uilogin.py b/ro_py/extensions/prompt.py similarity index 83% rename from examples/uilogin.py rename to ro_py/extensions/prompt.py index 2d95a86f..e663edf4 100644 --- a/examples/uilogin.py +++ b/ro_py/extensions/prompt.py @@ -11,19 +11,17 @@ import wx +from wx import html2 +import os import asyncio import pytweening -from ro_py.client import Client -import wx.html2 -roblox = Client() - -async def user_login(username, password, key=None): +async def user_login(client, username, password, key=None): if key: - return await roblox.user_login(username, password, key) + return await client.user_login(username, password, key) else: - return await roblox.user_login(username, password) + return await client.user_login(username, password) class RbxLogin(wx.Frame): @@ -33,9 +31,12 @@ def __init__(self, *args, **kwds): self.SetSize((512, 512)) self.SetTitle("Login with Roblox") self.SetBackgroundColour(wx.Colour(255, 255, 255)) + self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png"))) self.username = None self.password = None + self.client = None + self.status = False root_sizer = wx.BoxSizer(wx.VERTICAL) @@ -88,16 +89,20 @@ def login_load(self, event): token = False if token: self.web_view.Hide() - """ - for i in range(0, 600): - self.web_view.SetPosition((0, int(0 + pytweening.easeOutQuad(i / 600) * 600))) - """ - lr = asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password, token)) - if ".ROBLOSECURITY" in roblox.requests.session.cookies: + lr = asyncio.get_event_loop().run_until_complete(user_login( + self.client, + self.username, + self.password, + token + )) + if ".ROBLOSECURITY" in self.client.requests.session.cookies: + self.status = True self.Close() else: + self.status = False wx.MessageBox(f"Failed to log in.\n" - f"Detailed information from server: {lr.json()['errors'][0]['message']}", "Error", wx.OK | wx.ICON_ERROR) + f"Detailed information from server: {lr.json()['errors'][0]['message']}", + "Error", wx.OK | wx.ICON_ERROR) self.Close() def login_click(self, event): @@ -137,7 +142,7 @@ def login_click(self, event): self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88))) # Runs the user_login function. - fd = asyncio.get_event_loop().run_until_complete(user_login(self.username, self.password)) + fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password)) # Load the captcha URL. if fd: @@ -148,7 +153,7 @@ def login_click(self, event): self.Close() -class MyApp(wx.App): +class AuthApp(wx.App): def OnInit(self): self.rbx_login = RbxLogin(None, wx.ID_ANY, "") self.SetTopWindow(self.rbx_login) @@ -156,6 +161,8 @@ def OnInit(self): return True -if __name__ == "__main__": - app = MyApp(0) +def authenticate_prompt(client): + app = AuthApp(0) + app.rbx_login.client = client app.MainLoop() + return app.rbx_login.status From 97b054dff76a6f5f9cf0678e96716138ce0c45fa Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 8 Jan 2021 23:06:11 -0500 Subject: [PATCH 235/518] New docs + updated version identifier --- docs/accountsettings.html | 6 +- docs/captcha.html | 102 ++++++ docs/chat.html | 28 +- docs/client.html | 274 ++++++++++++++- docs/extensions/bots.html | 219 ++++++++++++ docs/extensions/index.html | 81 +++++ docs/extensions/prompt.html | 630 +++++++++++++++++++++++++++++++++++ docs/gamepersistence.html | 357 ++++++++++---------- docs/groups.html | 476 +++++++++++++++++++++++++- docs/index.html | 20 +- docs/notifications.html | 175 ++++++++-- docs/robloxdocs.html | 434 ++++++++++++++++++++++++ docs/roles.html | 171 +++++++++- docs/trades.html | 18 +- docs/users.html | 98 +++++- docs/utilities/errors.html | 54 ++- docs/utilities/pages.html | 22 +- docs/utilities/requests.html | 200 ++++++++++- ro_py/accountsettings.py | 2 +- ro_py/captcha.py | 2 +- ro_py/extensions/prompt.py | 29 +- setup.py | 2 +- 22 files changed, 3100 insertions(+), 300 deletions(-) create mode 100644 docs/captcha.html create mode 100644 docs/extensions/bots.html create mode 100644 docs/extensions/index.html create mode 100644 docs/extensions/prompt.html create mode 100644 docs/robloxdocs.html diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 07de4596..4b224421 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -5,7 +5,7 @@ ro_py.accountsettings API documentation - + @@ -22,14 +22,14 @@

    Module ro_py.accountsettings

    -

    This file houses functions and classes that pertain to Roblox client .

    +

    This file houses functions and classes that pertain to Roblox client settings.

    Expand source code
    """
     
    -This file houses functions and classes that pertain to Roblox client .
    +This file houses functions and classes that pertain to Roblox client settings.
     
     """
     
    diff --git a/docs/captcha.html b/docs/captcha.html
    new file mode 100644
    index 00000000..02169a07
    --- /dev/null
    +++ b/docs/captcha.html
    @@ -0,0 +1,102 @@
    +
    +
    +
    +
    +
    +
    +ro_py.captcha API documentation
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Module ro_py.captcha

    +
    +
    +

    This file houses functions and classes that pertain to the Roblox captcha.

    +
    + +Expand source code + +
    """
    +
    +This file houses functions and classes that pertain to the Roblox captcha.
    +
    +"""
    +
    +
    +class UnsolvedCaptcha:
    +    def __init__(self, data, pkey):
    +        self.token = data["token"]
    +        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/?pkey={pkey}&session={self.token.split('|')[0]}&lang=en-gb"
    +        self.challenge_url = data["challenge_url"]
    +        self.challenge_url_cdn = data["challenge_url_cdn"]
    +        self.noscript = data["noscript"]
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class UnsolvedCaptcha +(data, pkey) +
    +
    +
    +
    + +Expand source code + +
    class UnsolvedCaptcha:
    +    def __init__(self, data, pkey):
    +        self.token = data["token"]
    +        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/?pkey={pkey}&session={self.token.split('|')[0]}&lang=en-gb"
    +        self.challenge_url = data["challenge_url"]
    +        self.challenge_url_cdn = data["challenge_url_cdn"]
    +        self.noscript = data["noscript"]
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/chat.html b/docs/chat.html index 2da79c4c..0a250613 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -92,11 +92,11 @@

    Module ro_py.chat

    self.typing = ConversationTyping(self.requests, conversation_id) - def get_message(self, message_id): + async def get_message(self, message_id): return Message(self.requests, message_id, self.id) - def send_message(self, content): - send_message_req = self.requests.post( + async def send_message(self, content): + send_message_req = await self.requests.post( url=endpoint + "v2/send-message", data={ "message": content, @@ -175,7 +175,7 @@

    Module ro_py.chat

    """ Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented. """ - conversations_req = self.requests.get( + conversations_req = await self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, @@ -253,7 +253,7 @@

    Classes

    """ Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented. """ - conversations_req = self.requests.get( + conversations_req = await self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, @@ -312,7 +312,7 @@

    Parameters

    """ Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented. """ - conversations_req = self.requests.get( + conversations_req = await self.requests.get( url="https://chat.roblox.com/v2/get-user-conversations", params={ "pageNumber": page_number, @@ -365,11 +365,11 @@

    Parameters

    self.typing = ConversationTyping(self.requests, conversation_id) - def get_message(self, message_id): + async def get_message(self, message_id): return Message(self.requests, message_id, self.id) - def send_message(self, content): - send_message_req = self.requests.post( + async def send_message(self, content): + send_message_req = await self.requests.post( url=endpoint + "v2/send-message", data={ "message": content, @@ -385,7 +385,7 @@

    Parameters

    Methods

    -def get_message(self, message_id) +async def get_message(self, message_id)
    @@ -393,12 +393,12 @@

    Methods

    Expand source code -
    def get_message(self, message_id):
    +
    async def get_message(self, message_id):
         return Message(self.requests, message_id, self.id)
    -def send_message(self, content) +async def send_message(self, content)
    @@ -406,8 +406,8 @@

    Methods

    Expand source code -
    def send_message(self, content):
    -    send_message_req = self.requests.post(
    +
    async def send_message(self, content):
    +    send_message_req = await self.requests.post(
             url=endpoint + "v2/send-message",
             data={
                 "message": content,
    diff --git a/docs/client.html b/docs/client.html
    index 0ad4c33a..377d5e61 100644
    --- a/docs/client.html
    +++ b/docs/client.html
    @@ -40,10 +40,12 @@ 

    Module ro_py.client

    from ro_py.badges import Badge from ro_py.chat import ChatWrapper from ro_py.trades import TradesWrapper +from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.cache import CacheType from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.accountinformation import AccountInformation +from ro_py.utilities.errors import UserDoesNotExistError, ApiError import logging @@ -79,7 +81,7 @@

    Module ro_py.client

    """TradesWrapper object. Only available for authenticated clients.""" if token: - self.requests.session.cookies[".ROBLOSECURITY"] = token + self.token_login(token) logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) @@ -91,8 +93,48 @@

    Module ro_py.client

    logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") + + def token_login(self, token): + self.requests.session.cookies[".ROBLOSECURITY"] = token + + async def user_login(self, username, password, token=None): + if token: + login_req = self.requests.back_post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password, + "captchaToken": token, + "captchaProvider": "PROVIDER_ARKOSE_LABS" + } + ) + return login_req else: - logging.warning("The active client is not authenticated, so some features will not be enabled.") + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password + }, + quickreturn=True + ) + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ @@ -110,6 +152,38 @@

    Module ro_py.client

    await user.update() return user + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + """ + Gets a Roblox user by their username.. + + Parameters + ---------- + user_name : str + Name of the user to generate the object from. + exclude_banned_users : bool + Whether to exclude banned users in the request. + """ + username_req = await self.requests.post( + url="https://users.roblox.com/v1/usernames/users", + data={ + "usernames": [ + user_name + ], + "excludeBannedUsers": exclude_banned_users + } + ) + username_data = username_req.json() + if len(username_data["data"]) > 0: + user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser + user = self.requests.cache.get(CacheType.Users, user_id) + if not user: + user = User(self.requests, user_id) + self.requests.cache.set(CacheType.Users, user_id, user) + await user.update() + return user + else: + raise UserDoesNotExistError + async def get_group(self, group_id): """ Gets a Roblox group. @@ -232,7 +306,7 @@

    Parameters

    """TradesWrapper object. Only available for authenticated clients.""" if token: - self.requests.session.cookies[".ROBLOSECURITY"] = token + self.token_login(token) logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) @@ -244,8 +318,48 @@

    Parameters

    logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") + + def token_login(self, token): + self.requests.session.cookies[".ROBLOSECURITY"] = token + + async def user_login(self, username, password, token=None): + if token: + login_req = self.requests.back_post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password, + "captchaToken": token, + "captchaProvider": "PROVIDER_ARKOSE_LABS" + } + ) + return login_req else: - logging.warning("The active client is not authenticated, so some features will not be enabled.") + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password + }, + quickreturn=True + ) + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ @@ -263,6 +377,38 @@

    Parameters

    await user.update() return user + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + """ + Gets a Roblox user by their username.. + + Parameters + ---------- + user_name : str + Name of the user to generate the object from. + exclude_banned_users : bool + Whether to exclude banned users in the request. + """ + username_req = await self.requests.post( + url="https://users.roblox.com/v1/usernames/users", + data={ + "usernames": [ + user_name + ], + "excludeBannedUsers": exclude_banned_users + } + ) + username_data = username_req.json() + if len(username_data["data"]) > 0: + user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser + user = self.requests.cache.get(CacheType.Users, user_id) + if not user: + user = User(self.requests, user_id) + self.requests.cache.set(CacheType.Users, user_id, user) + await user.update() + return user + else: + raise UserDoesNotExistError + async def get_group(self, group_id): """ Gets a Roblox group. @@ -327,6 +473,10 @@

    Parameters

    await badge.update() return badge
    +

    Subclasses

    +

    Instance variables

    var accountinformation
    @@ -503,6 +653,117 @@

    Parameters

    return user
    +
    +async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False) +
    +
    +

    Gets a Roblox user by their username..

    +

    Parameters

    +
    +
    user_name : str
    +
    Name of the user to generate the object from.
    +
    exclude_banned_users : bool
    +
    Whether to exclude banned users in the request.
    +
    +
    + +Expand source code + +
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
    +    """
    +    Gets a Roblox user by their username..
    +
    +    Parameters
    +    ----------
    +    user_name : str
    +        Name of the user to generate the object from.
    +    exclude_banned_users : bool
    +        Whether to exclude banned users in the request.
    +    """
    +    username_req = await self.requests.post(
    +        url="https://users.roblox.com/v1/usernames/users",
    +        data={
    +            "usernames": [
    +                user_name
    +            ],
    +            "excludeBannedUsers": exclude_banned_users
    +        }
    +    )
    +    username_data = username_req.json()
    +    if len(username_data["data"]) > 0:
    +        user_id = username_req.json()["data"][0]["id"]  # TODO: make this a partialuser
    +        user = self.requests.cache.get(CacheType.Users, user_id)
    +        if not user:
    +            user = User(self.requests, user_id)
    +            self.requests.cache.set(CacheType.Users, user_id, user)
    +            await user.update()
    +        return user
    +    else:
    +        raise UserDoesNotExistError
    +
    +
    +
    +def token_login(self, token) +
    +
    +
    +
    + +Expand source code + +
    def token_login(self, token):
    +    self.requests.session.cookies[".ROBLOSECURITY"] = token
    +
    +
    +
    +async def user_login(self, username, password, token=None) +
    +
    +
    +
    + +Expand source code + +
    async def user_login(self, username, password, token=None):
    +    if token:
    +        login_req = self.requests.back_post(
    +            url="https://auth.roblox.com/v2/login",
    +            json={
    +                "ctype": "Username",
    +                "cvalue": username,
    +                "password": password,
    +                "captchaToken": token,
    +                "captchaProvider": "PROVIDER_ARKOSE_LABS"
    +            }
    +        )
    +        return login_req
    +    else:
    +        login_req = await self.requests.post(
    +            url="https://auth.roblox.com/v2/login",
    +            json={
    +                "ctype": "Username",
    +                "cvalue": username,
    +                "password": password
    +            },
    +            quickreturn=True
    +        )
    +        if login_req.status_code == 200:
    +            # If we're here, no captcha is required and we're already logged in, so we can return.
    +            return
    +        elif login_req.status_code == 403:
    +            # A captcha is required, so we need to return the captcha to solve.
    +            field_data = login_req.json()["errors"][0]["fieldData"]
    +            captcha_req = await self.requests.post(
    +                url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81",
    +                headers={
    +                    "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    +                },
    +                data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}"
    +            )
    +            captcha_json = captcha_req.json()
    +            return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    +
    +
  • @@ -523,7 +784,7 @@

    Index

    • Client

      - diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html new file mode 100644 index 00000000..0ef268ff --- /dev/null +++ b/docs/extensions/bots.html @@ -0,0 +1,219 @@ + + + + + + +ro_py.extensions.bots API documentation + + + + + + + + + + + +
      +
      +
      +

      Module ro_py.extensions.bots

      +
      +
      +

      This extension houses functions that allow generation of Bot objects, which interpret commands.

      +
      + +Expand source code + +
      """
      +
      +This extension houses functions that allow generation of Bot objects, which interpret commands.
      +
      +"""
      +
      +
      +from ro_py.client import Client
      +import asyncio
      +
      +
      +class Bot(Client):
      +    def __init__(self):
      +        super().__init__()
      +
      +
      +class Command:
      +    def __init__(self, func, **kwargs):
      +        if not asyncio.iscoroutinefunction(func):
      +            raise TypeError('Callback must be a coroutine.')
      +        self._callback = func
      +
      +    @property
      +    def callback(self):
      +        return self._callback
      +
      +    async def __call__(self, *args, **kwargs):
      +        return await self.callback(*args, **kwargs)
      +
      +
      +def command(**attrs):
      +    def decorator(func):
      +        if isinstance(func, Command):
      +            raise TypeError('Callback is already a command.')
      +        return Command(func, **attrs)
      +
      +    return decorator
      +
      +
      +
      +
      +
      +
      +
      +

      Functions

      +
      +
      +def command(**attrs) +
      +
      +
      +
      + +Expand source code + +
      def command(**attrs):
      +    def decorator(func):
      +        if isinstance(func, Command):
      +            raise TypeError('Callback is already a command.')
      +        return Command(func, **attrs)
      +
      +    return decorator
      +
      +
      +
      +
      +
      +

      Classes

      +
      +
      +class Bot +
      +
      +

      Represents an authenticated Roblox client.

      +

      Parameters

      +
      +
      token : str
      +
      Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
      +
      requests_cache : bool
      +
      Toggle for cached requests using CacheControl.
      +
      +
      + +Expand source code + +
      class Bot(Client):
      +    def __init__(self):
      +        super().__init__()
      +
      +

      Ancestors

      + +

      Inherited members

      + +
      +
      +class Command +(func, **kwargs) +
      +
      +
      +
      + +Expand source code + +
      class Command:
      +    def __init__(self, func, **kwargs):
      +        if not asyncio.iscoroutinefunction(func):
      +            raise TypeError('Callback must be a coroutine.')
      +        self._callback = func
      +
      +    @property
      +    def callback(self):
      +        return self._callback
      +
      +    async def __call__(self, *args, **kwargs):
      +        return await self.callback(*args, **kwargs)
      +
      +

      Instance variables

      +
      +
      var callback
      +
      +
      +
      + +Expand source code + +
      @property
      +def callback(self):
      +    return self._callback
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/extensions/index.html b/docs/extensions/index.html new file mode 100644 index 00000000..c1cdf452 --- /dev/null +++ b/docs/extensions/index.html @@ -0,0 +1,81 @@ + + + + + + +ro_py.extensions API documentation + + + + + + + + + + + +
      +
      +
      +

      Module ro_py.extensions

      +
      +
      +

      This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.

      +
      + +Expand source code + +
      """
      +
      +This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.
      +
      +"""
      +
      +
      +
      +

      Sub-modules

      +
      +
      ro_py.extensions.bots
      +
      +

      This extension houses functions that allow generation of Bot objects, which interpret commands.

      +
      +
      ro_py.extensions.prompt
      +
      +

      This extension houses functions that allow human verification prompts for interactive applications.

      +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html new file mode 100644 index 00000000..7a8493ac --- /dev/null +++ b/docs/extensions/prompt.html @@ -0,0 +1,630 @@ + + + + + + +ro_py.extensions.prompt API documentation + + + + + + + + + + + +
      +
      +
      +

      Module ro_py.extensions.prompt

      +
      +
      +

      This extension houses functions that allow human verification prompts for interactive applications.

      +
      + +Expand source code + +
      """
      +
      +This extension houses functions that allow human verification prompts for interactive applications.
      +
      +"""
      +
      +
      +import wx
      +from wx import html2
      +import os
      +import asyncio
      +import pytweening
      +
      +
      +async def user_login(client, username, password, key=None):
      +    if key:
      +        return await client.user_login(username, password, key)
      +    else:
      +        return await client.user_login(username, password)
      +
      +
      +class RbxLogin(wx.Frame):
      +    """
      +    wx.Frame wrapper for Roblox authentication.
      +    """
      +    def __init__(self, *args, **kwds):
      +        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
      +        wx.Frame.__init__(self, *args, **kwds)
      +        self.SetSize((512, 512))
      +        self.SetTitle("Login with Roblox")
      +        self.SetBackgroundColour(wx.Colour(255, 255, 255))
      +        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
      +
      +        self.username = None
      +        self.password = None
      +        self.client = None
      +        self.status = False
      +
      +        root_sizer = wx.BoxSizer(wx.VERTICAL)
      +
      +        self.inner_panel = wx.Panel(self, wx.ID_ANY)
      +        root_sizer.Add(self.inner_panel, 1, wx.ALL | wx.EXPAND, 100)
      +
      +        inner_sizer = wx.BoxSizer(wx.VERTICAL)
      +
      +        inner_sizer.Add((0, 20), 0, 0, 0)
      +
      +        login_label = wx.StaticText(self.inner_panel, wx.ID_ANY, "Please log in with your username and password.",
      +                                    style=wx.ALIGN_CENTER_HORIZONTAL)
      +        inner_sizer.Add(login_label, 1, 0, 0)
      +
      +        self.username_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "\n")
      +        self.username_entry.SetFont(
      +            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      +        self.username_entry.SetFocus()
      +        inner_sizer.Add(self.username_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      +
      +        self.password_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "", style=wx.TE_PASSWORD)
      +        self.password_entry.SetFont(
      +            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      +        inner_sizer.Add(self.password_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      +
      +        self.log_in_button = wx.Button(self.inner_panel, wx.ID_ANY, "Login")
      +        inner_sizer.Add(self.log_in_button, 1, wx.ALL | wx.EXPAND, 0)
      +
      +        inner_sizer.Add((0, 20), 0, 0, 0)
      +
      +        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
      +        self.web_view.Hide()
      +        self.web_view.EnableAccessToDevTools(False)
      +        self.web_view.EnableContextMenu(False)
      +
      +        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
      +
      +        self.inner_panel.SetSizer(inner_sizer)
      +
      +        self.SetSizer(root_sizer)
      +
      +        self.Layout()
      +
      +        self.Bind(wx.EVT_BUTTON, self.login_click, self.log_in_button)
      +        self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
      +
      +    def login_load(self, event):
      +        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
      +        if token == "undefined":
      +            token = False
      +        if token:
      +            self.web_view.Hide()
      +            lr = asyncio.get_event_loop().run_until_complete(user_login(
      +                self.client,
      +                self.username,
      +                self.password,
      +                token
      +            ))
      +            if ".ROBLOSECURITY" in self.client.requests.session.cookies:
      +                self.status = True
      +                self.Close()
      +            else:
      +                self.status = False
      +                wx.MessageBox(f"Failed to log in.\n"
      +                              f"Detailed information from server: {lr.json()['errors'][0]['message']}",
      +                              "Error", wx.OK | wx.ICON_ERROR)
      +                self.Close()
      +
      +    def login_click(self, event):
      +        self.username = self.username_entry.GetValue()
      +        self.password = self.password_entry.GetValue()
      +        self.username.strip("\n")
      +        self.password.strip("\n")
      +
      +        if not (self.username and self.password):
      +            # If either the username or password is missing, return
      +            return
      +
      +        if len(self.username) < 3:
      +            # If the username is shorter than 3, return
      +            return
      +
      +        # Disable the entries to stop people from typing in them.
      +        self.username_entry.Disable()
      +        self.password_entry.Disable()
      +        self.log_in_button.Disable()
      +
      +        # Get the position of the inner_panel
      +        old_pos = self.inner_panel.GetPosition()
      +        start_point = old_pos[0]
      +
      +        # Move the panel over to the right.
      +        for i in range(0, 512):
      +            wx.Yield()
      +            self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
      +
      +        # Hide the panel. The panel is already on the right so it's not visible anyways.
      +        self.inner_panel.Hide()
      +        self.web_view.SetSize((512, 600))
      +
      +        # Expand the window.
      +        for i in range(0, 88):
      +            self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
      +
      +        # Runs the user_login function.
      +        fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password))
      +
      +        # Load the captcha URL.
      +        if fd:
      +            self.web_view.LoadURL(fd.url)
      +            self.web_view.Show()
      +        else:
      +            # No captcha needed.
      +            self.Close()
      +
      +
      +class AuthApp(wx.App):
      +    """
      +    wx.App wrapper for Roblox authentication.
      +    """
      +    def __init__(self):
      +        """"""
      +
      +    def OnInit(self):
      +        self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
      +        self.SetTopWindow(self.rbx_login)
      +        self.rbx_login.Show()
      +        return True
      +
      +
      +def authenticate_prompt(client):
      +    """
      +    Prompts a login screen.
      +    Returns True if the user has sucessfully been authenticated and False if they have not.
      +
      +    Parameters
      +    ----------
      +    client : ro_py.client.Client
      +        Client object to authenticate.
      +
      +    Returns
      +    ------
      +    bool
      +    """
      +    app = AuthApp(0)
      +    app.rbx_login.client = client
      +    app.MainLoop()
      +    return app.rbx_login.status
      +
      +
      +
      +
      +
      +
      +
      +

      Functions

      +
      +
      +def authenticate_prompt(client) +
      +
      +

      Prompts a login screen. +Returns True if the user has sucessfully been authenticated and False if they have not.

      +

      Parameters

      +
      +
      client : Client
      +
      Client object to authenticate.
      +
      +

      Returns

      +
      +
      bool
      +
       
      +
      +
      + +Expand source code + +
      def authenticate_prompt(client):
      +    """
      +    Prompts a login screen.
      +    Returns True if the user has sucessfully been authenticated and False if they have not.
      +
      +    Parameters
      +    ----------
      +    client : ro_py.client.Client
      +        Client object to authenticate.
      +
      +    Returns
      +    ------
      +    bool
      +    """
      +    app = AuthApp(0)
      +    app.rbx_login.client = client
      +    app.MainLoop()
      +    return app.rbx_login.status
      +
      +
      +
      +async def user_login(client, username, password, key=None) +
      +
      +
      +
      + +Expand source code + +
      async def user_login(client, username, password, key=None):
      +    if key:
      +        return await client.user_login(username, password, key)
      +    else:
      +        return await client.user_login(username, password)
      +
      +
      +
      +
      +
      +

      Classes

      +
      +
      +class AuthApp +
      +
      +

      wx.App wrapper for Roblox authentication.

      +
      + +Expand source code + +
      class AuthApp(wx.App):
      +    """
      +    wx.App wrapper for Roblox authentication.
      +    """
      +    def __init__(self):
      +        """"""
      +
      +    def OnInit(self):
      +        self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
      +        self.SetTopWindow(self.rbx_login)
      +        self.rbx_login.Show()
      +        return True
      +
      +

      Ancestors

      +
        +
      • wx.core.App
      • +
      • wx._core.PyApp
      • +
      • wx._core.AppConsole
      • +
      • wx._core.EvtHandler
      • +
      • wx._core.Object
      • +
      • wx._core.Trackable
      • +
      • wx._core.EventFilter
      • +
      • sip.wrapper
      • +
      • sip.simplewrapper
      • +
      +

      Methods

      +
      +
      +def OnInit(self) +
      +
      +

      OnInit(self) -> bool

      +
      + +Expand source code + +
      def OnInit(self):
      +    self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
      +    self.SetTopWindow(self.rbx_login)
      +    self.rbx_login.Show()
      +    return True
      +
      +
      +
      +
      +
      +class RbxLogin +(*args, **kwds) +
      +
      +

      wx.Frame wrapper for Roblox authentication.

      +
      + +Expand source code + +
      class RbxLogin(wx.Frame):
      +    """
      +    wx.Frame wrapper for Roblox authentication.
      +    """
      +    def __init__(self, *args, **kwds):
      +        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
      +        wx.Frame.__init__(self, *args, **kwds)
      +        self.SetSize((512, 512))
      +        self.SetTitle("Login with Roblox")
      +        self.SetBackgroundColour(wx.Colour(255, 255, 255))
      +        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
      +
      +        self.username = None
      +        self.password = None
      +        self.client = None
      +        self.status = False
      +
      +        root_sizer = wx.BoxSizer(wx.VERTICAL)
      +
      +        self.inner_panel = wx.Panel(self, wx.ID_ANY)
      +        root_sizer.Add(self.inner_panel, 1, wx.ALL | wx.EXPAND, 100)
      +
      +        inner_sizer = wx.BoxSizer(wx.VERTICAL)
      +
      +        inner_sizer.Add((0, 20), 0, 0, 0)
      +
      +        login_label = wx.StaticText(self.inner_panel, wx.ID_ANY, "Please log in with your username and password.",
      +                                    style=wx.ALIGN_CENTER_HORIZONTAL)
      +        inner_sizer.Add(login_label, 1, 0, 0)
      +
      +        self.username_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "\n")
      +        self.username_entry.SetFont(
      +            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      +        self.username_entry.SetFocus()
      +        inner_sizer.Add(self.username_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      +
      +        self.password_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "", style=wx.TE_PASSWORD)
      +        self.password_entry.SetFont(
      +            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      +        inner_sizer.Add(self.password_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      +
      +        self.log_in_button = wx.Button(self.inner_panel, wx.ID_ANY, "Login")
      +        inner_sizer.Add(self.log_in_button, 1, wx.ALL | wx.EXPAND, 0)
      +
      +        inner_sizer.Add((0, 20), 0, 0, 0)
      +
      +        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
      +        self.web_view.Hide()
      +        self.web_view.EnableAccessToDevTools(False)
      +        self.web_view.EnableContextMenu(False)
      +
      +        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
      +
      +        self.inner_panel.SetSizer(inner_sizer)
      +
      +        self.SetSizer(root_sizer)
      +
      +        self.Layout()
      +
      +        self.Bind(wx.EVT_BUTTON, self.login_click, self.log_in_button)
      +        self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
      +
      +    def login_load(self, event):
      +        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
      +        if token == "undefined":
      +            token = False
      +        if token:
      +            self.web_view.Hide()
      +            lr = asyncio.get_event_loop().run_until_complete(user_login(
      +                self.client,
      +                self.username,
      +                self.password,
      +                token
      +            ))
      +            if ".ROBLOSECURITY" in self.client.requests.session.cookies:
      +                self.status = True
      +                self.Close()
      +            else:
      +                self.status = False
      +                wx.MessageBox(f"Failed to log in.\n"
      +                              f"Detailed information from server: {lr.json()['errors'][0]['message']}",
      +                              "Error", wx.OK | wx.ICON_ERROR)
      +                self.Close()
      +
      +    def login_click(self, event):
      +        self.username = self.username_entry.GetValue()
      +        self.password = self.password_entry.GetValue()
      +        self.username.strip("\n")
      +        self.password.strip("\n")
      +
      +        if not (self.username and self.password):
      +            # If either the username or password is missing, return
      +            return
      +
      +        if len(self.username) < 3:
      +            # If the username is shorter than 3, return
      +            return
      +
      +        # Disable the entries to stop people from typing in them.
      +        self.username_entry.Disable()
      +        self.password_entry.Disable()
      +        self.log_in_button.Disable()
      +
      +        # Get the position of the inner_panel
      +        old_pos = self.inner_panel.GetPosition()
      +        start_point = old_pos[0]
      +
      +        # Move the panel over to the right.
      +        for i in range(0, 512):
      +            wx.Yield()
      +            self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
      +
      +        # Hide the panel. The panel is already on the right so it's not visible anyways.
      +        self.inner_panel.Hide()
      +        self.web_view.SetSize((512, 600))
      +
      +        # Expand the window.
      +        for i in range(0, 88):
      +            self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
      +
      +        # Runs the user_login function.
      +        fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password))
      +
      +        # Load the captcha URL.
      +        if fd:
      +            self.web_view.LoadURL(fd.url)
      +            self.web_view.Show()
      +        else:
      +            # No captcha needed.
      +            self.Close()
      +
      +

      Ancestors

      +
        +
      • wx._core.Frame
      • +
      • wx._core.TopLevelWindow
      • +
      • wx._core.NonOwnedWindow
      • +
      • wx._core.Window
      • +
      • wx._core.WindowBase
      • +
      • wx._core.EvtHandler
      • +
      • wx._core.Object
      • +
      • wx._core.Trackable
      • +
      • sip.wrapper
      • +
      • sip.simplewrapper
      • +
      +

      Methods

      +
      +
      +def login_click(self, event) +
      +
      +
      +
      + +Expand source code + +
      def login_click(self, event):
      +    self.username = self.username_entry.GetValue()
      +    self.password = self.password_entry.GetValue()
      +    self.username.strip("\n")
      +    self.password.strip("\n")
      +
      +    if not (self.username and self.password):
      +        # If either the username or password is missing, return
      +        return
      +
      +    if len(self.username) < 3:
      +        # If the username is shorter than 3, return
      +        return
      +
      +    # Disable the entries to stop people from typing in them.
      +    self.username_entry.Disable()
      +    self.password_entry.Disable()
      +    self.log_in_button.Disable()
      +
      +    # Get the position of the inner_panel
      +    old_pos = self.inner_panel.GetPosition()
      +    start_point = old_pos[0]
      +
      +    # Move the panel over to the right.
      +    for i in range(0, 512):
      +        wx.Yield()
      +        self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
      +
      +    # Hide the panel. The panel is already on the right so it's not visible anyways.
      +    self.inner_panel.Hide()
      +    self.web_view.SetSize((512, 600))
      +
      +    # Expand the window.
      +    for i in range(0, 88):
      +        self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
      +
      +    # Runs the user_login function.
      +    fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password))
      +
      +    # Load the captcha URL.
      +    if fd:
      +        self.web_view.LoadURL(fd.url)
      +        self.web_view.Show()
      +    else:
      +        # No captcha needed.
      +        self.Close()
      +
      +
      +
      +def login_load(self, event) +
      +
      +
      +
      + +Expand source code + +
      def login_load(self, event):
      +    _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
      +    if token == "undefined":
      +        token = False
      +    if token:
      +        self.web_view.Hide()
      +        lr = asyncio.get_event_loop().run_until_complete(user_login(
      +            self.client,
      +            self.username,
      +            self.password,
      +            token
      +        ))
      +        if ".ROBLOSECURITY" in self.client.requests.session.cookies:
      +            self.status = True
      +            self.Close()
      +        else:
      +            self.status = False
      +            wx.MessageBox(f"Failed to log in.\n"
      +                          f"Detailed information from server: {lr.json()['errors'][0]['message']}",
      +                          "Error", wx.OK | wx.ICON_ERROR)
      +            self.Close()
      +
      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/gamepersistence.html b/docs/gamepersistence.html index b357b16d..a06f491d 100644 --- a/docs/gamepersistence.html +++ b/docs/gamepersistence.html @@ -39,6 +39,7 @@

      Module ro_py.gamepersistence

      endpoint = "http://gamepersistence.roblox.com/" + class DataStore: """ Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com). @@ -48,7 +49,7 @@

      Module ro_py.gamepersistence

      ---------- requests : ro_py.utilities.requests.Requests Requests object to use for API requests. - placeId : int + place_id : int PlaceId to modify the DataStores for, if the currently authenticated user doesn't have sufficient permissions, it will raise a NotAuthorizedToModifyPlaceDataStores exception @@ -63,18 +64,19 @@

      Module ro_py.gamepersistence

      legacy : bool, optional Describes whether or not this will use the legacy endpoints, over the new v1 endpoints (Does not apply to getSortedValues) - legacyNamingScheme : bool, optional + legacy_naming_scheme : bool, optional Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), there will be no qkeys[idx].target (normally the key that is passed into each method), and the qkeys[idx].key will match the key passed into each method. """ - def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False): - self.requests = requests - self.placeId = placeId - self.legacy = legacy - self.legacyNamingScheme = legacyNamingScheme - self.name = name - self.scope = scope if scope != None else "global" + + def __init__(self, requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False): + self.requests = requests + self.place_id = place_id + self.legacy = legacy + self.legacy_naming_scheme = legacy_naming_scheme + self.name = name + self.scope = scope if scope is not None else "global" async def get(self, key): """ @@ -92,30 +94,30 @@

      Module ro_py.gamepersistence

      ------- typing.Any """ - if self.legacy == True: - data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" + if self.legacy: + data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" r = await self.requests.post( - url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", + url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}", headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'][0]['Value'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.get( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; - + return r.text + async def set(self, key, value): """ Represents a set request to a data store, @@ -136,32 +138,32 @@

      Module ro_py.gamepersistence

      ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = f"value={quote(str(value))}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" + url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 200: - return value; + return value - async def setIfValue(self, key, value, expectedValue): + async def set_if_value(self, key, value, expected_value): """ Represents a conditional set request to a data store, only supports legacy @@ -176,28 +178,28 @@

      Module ro_py.gamepersistence

      The value to set for the key, as in the 3rd parameter of `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)` - expectedValue - The expectedValue for that key, if you know the key doesn't exist, then set this as None + expected_value + The expected_value for that key, if you know the key doesn't exist, then set this as None Returns ------- typing.Any """ - data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" + data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}" + url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] except KeyError: return r.json()['error'] - async def setIfIdx(self, key, value, idx): + async def set_if_idx(self, key, value, idx): """ Represents a conditional set request to a data store, only supports new endpoints, @@ -219,33 +221,33 @@

      Module ro_py.gamepersistence

      ------- typing.Any """ - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 409: usn = r.headers['roblox-usn'] split = usn.split('.') msn_hash = split[0] current_value = split[1] - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" r2 = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r2.status_code == 409: return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16))) else: return value - async def increment(self, key, delta = 0): + async def increment(self, key, delta=0): """ Represents a conditional set request to a data store, only supports legacy @@ -266,14 +268,14 @@

      Module ro_py.gamepersistence

      typing.Any """ data = "" - url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" + url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] @@ -282,7 +284,7 @@

      Module ro_py.gamepersistence

      reason = cap.group(0).replace("(", "").replace(")", "") if reason == "ExistingValueNotNumeric": return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double" - + async def remove(self, key): """ Represents a get request to a data store, @@ -299,30 +301,30 @@

      Module ro_py.gamepersistence

      ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = "" - url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) - if r.json()['data'] == None: + }, data=data) + if r.json()['data'] is None: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text;
      + return r.text
    @@ -336,7 +338,7 @@

    Classes

    class DataStore -(requests, placeId, name, scope, legacy=True, legacyNamingScheme=False) +(requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False)

    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com). @@ -345,7 +347,7 @@

    Parameters

    requests : Requests
    Requests object to use for API requests.
    -
    placeId : int
    +
    place_id : int
    PlaceId to modify the DataStores for, if the currently authenticated user doesn't have sufficient permissions, it will raise a NotAuthorizedToModifyPlaceDataStores exception
    @@ -360,7 +362,7 @@

    Parameters

    legacy : bool, optional
    Describes whether or not this will use the legacy endpoints, over the new v1 endpoints (Does not apply to getSortedValues)
    -
    legacyNamingScheme : bool, optional
    +
    legacy_naming_scheme : bool, optional
    Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), there will be no qkeys[idx].target (normally the key that is passed into each method), and the qkeys[idx].key will match the key passed into each method.
    @@ -378,7 +380,7 @@

    Parameters

    ---------- requests : ro_py.utilities.requests.Requests Requests object to use for API requests. - placeId : int + place_id : int PlaceId to modify the DataStores for, if the currently authenticated user doesn't have sufficient permissions, it will raise a NotAuthorizedToModifyPlaceDataStores exception @@ -393,18 +395,19 @@

    Parameters

    legacy : bool, optional Describes whether or not this will use the legacy endpoints, over the new v1 endpoints (Does not apply to getSortedValues) - legacyNamingScheme : bool, optional + legacy_naming_scheme : bool, optional Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), there will be no qkeys[idx].target (normally the key that is passed into each method), and the qkeys[idx].key will match the key passed into each method. """ - def __init__(self, requests, placeId, name, scope, legacy = True, legacyNamingScheme = False): - self.requests = requests - self.placeId = placeId - self.legacy = legacy - self.legacyNamingScheme = legacyNamingScheme - self.name = name - self.scope = scope if scope != None else "global" + + def __init__(self, requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False): + self.requests = requests + self.place_id = place_id + self.legacy = legacy + self.legacy_naming_scheme = legacy_naming_scheme + self.name = name + self.scope = scope if scope is not None else "global" async def get(self, key): """ @@ -422,30 +425,30 @@

    Parameters

    ------- typing.Any """ - if self.legacy == True: - data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" + if self.legacy: + data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" r = await self.requests.post( - url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", + url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}", headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'][0]['Value'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.get( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; - + return r.text + async def set(self, key, value): """ Represents a set request to a data store, @@ -466,32 +469,32 @@

    Parameters

    ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = f"value={quote(str(value))}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" + url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 200: - return value; + return value - async def setIfValue(self, key, value, expectedValue): + async def set_if_value(self, key, value, expected_value): """ Represents a conditional set request to a data store, only supports legacy @@ -506,28 +509,28 @@

    Parameters

    The value to set for the key, as in the 3rd parameter of `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)` - expectedValue - The expectedValue for that key, if you know the key doesn't exist, then set this as None + expected_value + The expected_value for that key, if you know the key doesn't exist, then set this as None Returns ------- typing.Any """ - data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" + data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}" + url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] except KeyError: return r.json()['error'] - async def setIfIdx(self, key, value, idx): + async def set_if_idx(self, key, value, idx): """ Represents a conditional set request to a data store, only supports new endpoints, @@ -549,33 +552,33 @@

    Parameters

    ------- typing.Any """ - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 409: usn = r.headers['roblox-usn'] split = usn.split('.') msn_hash = split[0] current_value = split[1] - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" r2 = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r2.status_code == 409: return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16))) else: return value - async def increment(self, key, delta = 0): + async def increment(self, key, delta=0): """ Represents a conditional set request to a data store, only supports legacy @@ -596,14 +599,14 @@

    Parameters

    typing.Any """ data = "" - url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" + url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] @@ -612,7 +615,7 @@

    Parameters

    reason = cap.group(0).replace("(", "").replace(")", "") if reason == "ExistingValueNotNumeric": return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double" - + async def remove(self, key): """ Represents a get request to a data store, @@ -629,30 +632,30 @@

    Parameters

    ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = "" - url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) - if r.json()['data'] == None: + }, data=data) + if r.json()['data'] is None: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; + return r.text

    Methods

    @@ -694,29 +697,29 @@

    Returns

    ------- typing.Any """ - if self.legacy == True: - data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacyNamingScheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" + if self.legacy: + data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}" r = await self.requests.post( - url=endpoint + f"persistence/getV2?placeId={str(self.placeId)}&type=standard&scope={quote(self.scope)}", + url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}", headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'][0]['Value'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.get( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text; + return r.text
    @@ -745,7 +748,7 @@

    Returns

    Expand source code -
    async def increment(self, key, delta = 0):
    +
    async def increment(self, key, delta=0):
         """
         Represents a conditional set request to a data store,
         only supports legacy
    @@ -766,14 +769,14 @@ 

    Returns

    typing.Any """ data = "" - url = endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacyNamingScheme else endpoint + f"persistence/increment?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" + url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] @@ -822,30 +825,30 @@

    Returns

    ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = "" - url = endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacyNamingScheme else endpoint + f"persistence/remove?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) - if r.json()['data'] == None: + }, data=data) + if r.json()['data'] is None: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId) - }) + 'Roblox-Place-Id': str(self.place_id) + }) if r.status_code == 204: return None else: - return r.text;
    + return r.text
    @@ -894,34 +897,34 @@

    Returns

    ------- typing.Any """ - if self.legacy == True: + if self.legacy: data = f"value={quote(str(value))}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" + url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) if len(r.json()['data']) == 0: return None else: return r.json()['data'] else: - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 200: - return value;
    + return value
    -
    -async def setIfIdx(self, key, value, idx) +
    +async def set_if_idx(self, key, value, idx)

    Represents a conditional set request to a data store, @@ -948,7 +951,7 @@

    Returns

    Expand source code -
    async def setIfIdx(self, key, value, idx):
    +
    async def set_if_idx(self, key, value, idx):
         """
         Represents a conditional set request to a data store,
         only supports new endpoints,
    @@ -970,35 +973,35 @@ 

    Returns

    ------- typing.Any """ - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0" r = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r.status_code == 409: usn = r.headers['roblox-usn'] split = usn.split('.') msn_hash = split[0] current_value = split[1] - url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacyNamingScheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" + url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}" r2 = await self.requests.post( url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': '*/*', 'Content-Length': str(len(str(value))) - }, data=quote(str(value))) + }, data=quote(str(value))) if r2.status_code == 409: return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16))) else: return value
    -
    -async def setIfValue(self, key, value, expectedValue) +
    +async def set_if_value(self, key, value, expected_value)

    Represents a conditional set request to a data store, @@ -1013,8 +1016,8 @@

    Parameters

    The value to set for the key, as in the 3rd parameter of void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)
    -
    expectedValue
    -
    The expectedValue for that key, if you know the key doesn't exist, then set this as None
    +
    expected_value
    +
    The expected_value for that key, if you know the key doesn't exist, then set this as None

    Returns

    @@ -1025,7 +1028,7 @@

    Returns

    Expand source code -
    async def setIfValue(self, key, value, expectedValue):
    +
    async def set_if_value(self, key, value, expected_value):
         """
         Represents a conditional set request to a data store,
         only supports legacy
    @@ -1040,21 +1043,21 @@ 

    Returns

    The value to set for the key, as in the 3rd parameter of `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)` - expectedValue - The expectedValue for that key, if you know the key doesn't exist, then set this as None + expected_value + The expected_value for that key, if you know the key doesn't exist, then set this as None Returns ------- typing.Any """ - data = f"value={quote(str(value))}&expectedValue={quote(str(expectedValue)) if expectedValue != None else ''}" - url = endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" if self.legacyNamingScheme == True else endpoint + f"persistence/set?placeId={str(self.placeId)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expectedValue))) if expectedValue != None else str(0)}" + data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}" + url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" r = await self.requests.post( - url=url, + url=url, headers={ - 'Roblox-Place-Id': str(self.placeId), + 'Roblox-Place-Id': str(self.place_id), 'Content-Type': 'application/x-www-form-urlencoded' - }, data=data) + }, data=data) try: if r.json()['data'] != 0: return r.json()['data'] @@ -1087,8 +1090,8 @@

    increment
  • remove
  • set
  • -
  • setIfIdx
  • -
  • setIfValue
  • +
  • set_if_idx
  • +
  • set_if_value
  • diff --git a/docs/groups.html b/docs/groups.html index 963e622d..1d8d6c32 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -32,9 +32,12 @@

    Module ro_py.groups

    This file houses functions and classes that pertain to Roblox groups. """ - +import iso8601 +from typing import List from ro_py.users import User from ro_py.roles import Role +from ro_py.utilities.errors import NotFound +from ro_py.utilities.pages import Pages, SortOrder endpoint = "https://groups.roblox.com" @@ -48,6 +51,33 @@

    Module ro_py.groups

    self.poster = User(requests, shout_data["poster"]["userId"]) +class WallPost: + """ + Represents a roblox wall post. + """ + def __init__(self, requests, wall_data, group): + self.requests = requests + self.group = group + self.id = wall_data['id'] + self.body = wall_data['body'] + self.created = iso8601.parse(wall_data['created']) + self.updated = iso8601.parse(wall_data['updated']) + self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username']) + + async def delete(self): + wall_req = await self.requests.delete( + url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}" + ) + return wall_req.status == 200 + + +def wall_post_handeler(requests, this_page, args) -> List[WallPost]: + wall_posts = [] + for wall_post in this_page: + wall_posts.append(WallPost(requests, wall_post, args)) + return wall_posts + + class Group: """ Represents a group. @@ -83,8 +113,20 @@

    Module ro_py.groups

    # self.is_locked = group_info["isLocked"] async def update_shout(self, message): + """ + Updates the shout of the group. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + + Returns + ------- + int + """ shout_req = await self.requests.patch( - url=endpoint+ f"/v1/groups/{self.id}/status", + url=endpoint + f"/v1/groups/{self.id}/status", data={ "message": message } @@ -92,13 +134,86 @@

    Module ro_py.groups

    return shout_req.status_code == 200 async def get_roles(self): + """ + Gets all roles of the group. + + Returns + ------- + list + """ role_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.id}/roles" ) roles = [] for role in role_req.json()['roles']: roles.append(Role(self.requests, self, role)) - return roles
    + return roles + + async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100): + wall_req = Pages( + requests=self.requests, + url=endpoint + f"/v2/groups/{self.id}/wall/posts", + sort_order=sort_order, + limit=limit, + handler=wall_post_handeler, + handler_args=self + ) + return wall_req + + async def get_member_by_id(self, roblox_id): + # Get list of group user is in. + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + + # Create data to return. + role = Role(self.requests, self, group_data['role']) + member = Member(self.requests, roblox_id, None, self, role) + return await member.update() + + +class Member(User): + """ + Represents a user in a group. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. + group : ro_py.groups.Group + The group the user is in. + role : ro_py.roles.Role + The role the user has is the group. + """ + def __init__(self, requests, roblox_id, name=None, group=None, role=None): + super().__init__(requests, roblox_id, name) + self.role = role + self.group = group + + async def promote(self): + pass + + async def demote(self): + pass + + async def setrank(self): + pass
    @@ -106,6 +221,25 @@

    Module ro_py.groups

    +

    Functions

    +
    +
    +def wall_post_handeler(requests, this_page, args) ‑> List[WallPost] +
    +
    +
    +
    + +Expand source code + +
    def wall_post_handeler(requests, this_page, args) -> List[WallPost]:
    +    wall_posts = []
    +    for wall_post in this_page:
    +        wall_posts.append(WallPost(requests, wall_post, args))
    +    return wall_posts
    +
    +
    +

    Classes

    @@ -155,8 +289,20 @@

    Classes

    # self.is_locked = group_info["isLocked"] async def update_shout(self, message): + """ + Updates the shout of the group. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + + Returns + ------- + int + """ shout_req = await self.requests.patch( - url=endpoint+ f"/v1/groups/{self.id}/status", + url=endpoint + f"/v1/groups/{self.id}/status", data={ "message": message } @@ -164,26 +310,112 @@

    Classes

    return shout_req.status_code == 200 async def get_roles(self): + """ + Gets all roles of the group. + + Returns + ------- + list + """ role_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.id}/roles" ) roles = [] for role in role_req.json()['roles']: roles.append(Role(self.requests, self, role)) - return roles
    + return roles + + async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100): + wall_req = Pages( + requests=self.requests, + url=endpoint + f"/v2/groups/{self.id}/wall/posts", + sort_order=sort_order, + limit=limit, + handler=wall_post_handeler, + handler_args=self + ) + return wall_req + + async def get_member_by_id(self, roblox_id): + # Get list of group user is in. + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + + # Create data to return. + role = Role(self.requests, self, group_data['role']) + member = Member(self.requests, roblox_id, None, self, role) + return await member.update()

    Methods

    +
    +async def get_member_by_id(self, roblox_id) +
    +
    +
    +
    + +Expand source code + +
    async def get_member_by_id(self, roblox_id):
    +    # Get list of group user is in.
    +    member_req = await self.requests.get(
    +        url=endpoint + f"/v2/users/{roblox_id}/groups/roles"
    +    )
    +    data = member_req.json()
    +
    +    # Find group in list.
    +    group_data = None
    +    for group in data['data']:
    +        if group['group']['id'] == self.id:
    +            group_data = group
    +            break
    +
    +    # Check if user is in group.
    +    if not group_data:
    +        raise NotFound(f"The user {roblox_id} was not found in group {self.id}")
    +
    +    # Create data to return.
    +    role = Role(self.requests, self, group_data['role'])
    +    member = Member(self.requests, roblox_id, None, self, role)
    +    return await member.update()
    +
    +
    async def get_roles(self)
    -
    +

    Gets all roles of the group.

    +

    Returns

    +
    +
    list
    +
     
    +
    Expand source code
    async def get_roles(self):
    +    """
    +    Gets all roles of the group.
    +
    +    Returns
    +    -------
    +    list
    +    """
         role_req = await self.requests.get(
             url=endpoint + f"/v1/groups/{self.id}/roles"
         )
    @@ -193,6 +425,27 @@ 

    Methods

    return roles
    +
    +async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100) +
    +
    +
    +
    + +Expand source code + +
    async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100):
    +    wall_req = Pages(
    +        requests=self.requests,
    +        url=endpoint + f"/v2/groups/{self.id}/wall/posts",
    +        sort_order=sort_order,
    +        limit=limit,
    +        handler=wall_post_handeler,
    +        handler_args=self
    +    )
    +    return wall_req
    +
    +
    async def update(self)
    @@ -224,14 +477,36 @@

    Methods

    async def update_shout(self, message)
    -
    +

    Updates the shout of the group.

    +

    Parameters

    +
    +
    message : str
    +
    Message that will overwrite the current shout of a group.
    +
    +

    Returns

    +
    +
    int
    +
     
    +
    Expand source code
    async def update_shout(self, message):
    +    """
    +    Updates the shout of the group.
    +
    +    Parameters
    +    ----------
    +    message : str
    +        Message that will overwrite the current shout of a group.
    +
    +    Returns
    +    -------
    +    int
    +    """
         shout_req = await self.requests.patch(
    -        url=endpoint+ f"/v1/groups/{self.id}/status",
    +        url=endpoint + f"/v1/groups/{self.id}/status",
             data={
                 "message": message
             }
    @@ -241,6 +516,121 @@ 

    Methods

    +
    +class Member +(requests, roblox_id, name=None, group=None, role=None) +
    +
    +

    Represents a user in a group.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
    roblox_id : int
    +
    The id of a user.
    +
    name : str
    +
    The name of the user.
    +
    group : Group
    +
    The group the user is in.
    +
    role : Role
    +
    The role the user has is the group.
    +
    +
    + +Expand source code + +
    class Member(User):
    +    """
    +    Represents a user in a group.
    +
    +    Parameters
    +    ----------
    +    requests : ro_py.utilities.requests.Requests
    +            Requests object to use for API requests.
    +    roblox_id : int
    +            The id of a user.
    +    name : str
    +            The name of the user.
    +    group : ro_py.groups.Group
    +            The group the user is in.
    +    role : ro_py.roles.Role
    +            The role the user has is the group.
    +    """
    +    def __init__(self, requests, roblox_id, name=None, group=None, role=None):
    +        super().__init__(requests, roblox_id, name)
    +        self.role = role
    +        self.group = group
    +
    +    async def promote(self):
    +        pass
    +
    +    async def demote(self):
    +        pass
    +
    +    async def setrank(self):
    +        pass
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +async def demote(self) +
    +
    +
    +
    + +Expand source code + +
    async def demote(self):
    +    pass
    +
    +
    +
    +async def promote(self) +
    +
    +
    +
    + +Expand source code + +
    async def promote(self):
    +    pass
    +
    +
    +
    +async def setrank(self) +
    +
    +
    +
    + +Expand source code + +
    async def setrank(self):
    +    pass
    +
    +
    +
    +

    Inherited members

    + +
    class Shout (requests, shout_data) @@ -260,6 +650,55 @@

    Methods

    self.poster = User(requests, shout_data["poster"]["userId"])
    +
    +class WallPost +(requests, wall_data, group) +
    +
    +

    Represents a roblox wall post.

    +
    + +Expand source code + +
    class WallPost:
    +    """
    +    Represents a roblox wall post.
    +    """
    +    def __init__(self, requests, wall_data, group):
    +        self.requests = requests
    +        self.group = group
    +        self.id = wall_data['id']
    +        self.body = wall_data['body']
    +        self.created = iso8601.parse(wall_data['created'])
    +        self.updated = iso8601.parse(wall_data['updated'])
    +        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
    +
    +    async def delete(self):
    +        wall_req = await self.requests.delete(
    +            url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
    +        )
    +        return wall_req.status == 200
    +
    +

    Methods

    +
    +
    +async def delete(self) +
    +
    +
    +
    + +Expand source code + +
    async def delete(self):
    +    wall_req = await self.requests.delete(
    +        url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
    +    )
    +    return wall_req.status == 200
    +
    +
    +
    +
    @@ -274,19 +713,40 @@

    Index

  • ro_py
  • +
  • Functions

    + +
  • Classes

  • diff --git a/docs/index.html b/docs/index.html index 30bf1f62..634bf1aa 100644 --- a/docs/index.html +++ b/docs/index.html @@ -59,7 +59,7 @@

    Sub-modules

    ro_py.accountsettings
    -

    This file houses functions and classes that pertain to Roblox client .

    +

    This file houses functions and classes that pertain to Roblox client settings.

    ro_py.assets
    @@ -69,6 +69,10 @@

    Sub-modules

    This file houses functions and classes that pertain to game-awarded badges.

    +
    ro_py.captcha
    +
    +

    This file houses functions and classes that pertain to the Roblox captcha.

    +
    ro_py.catalog

    This file houses functions and classes that pertain to the Roblox catalog.

    @@ -85,6 +89,10 @@

    Sub-modules

    This file houses functions and classes that pertain to the Roblox economy endpoints.

    +
    ro_py.extensions
    +
    +

    This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.

    +
    ro_py.gamepersistence

    This file houses functions used for tampering with Roblox Datastores

    @@ -111,6 +119,11 @@

    Sub-modules

    This file houses functions and classes that pertain to Roblox-awarded badges.

    +
    ro_py.robloxdocs
    +
    +

    This file houses functions and classes that pertain to the Roblox API documentation pages. +I don't know if this is really that useful, but it might be …

    +
    ro_py.robloxstatus

    This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) @@ -118,7 +131,7 @@

    Sub-modules

    ro_py.roles
    -

    IRA PUT THINGS HERE

    +

    This file contains classes and functions related to Roblox roles.

    ro_py.thumbnails
    @@ -157,16 +170,19 @@

    Index

  • ro_py.accountsettings
  • ro_py.assets
  • ro_py.badges
  • +
  • ro_py.captcha
  • ro_py.catalog
  • ro_py.chat
  • ro_py.client
  • ro_py.economy
  • +
  • ro_py.extensions
  • ro_py.gamepersistence
  • ro_py.games
  • ro_py.gender
  • ro_py.groups
  • ro_py.notifications
  • ro_py.robloxbadges
  • +
  • ro_py.robloxdocs
  • ro_py.robloxstatus
  • ro_py.roles
  • ro_py.thumbnails
  • diff --git a/docs/notifications.html b/docs/notifications.html index 20f01d68..a690b049 100644 --- a/docs/notifications.html +++ b/docs/notifications.html @@ -48,10 +48,11 @@

    Module ro_py.notifications

    from ro_py.utilities.caseconvert import to_snake_case -from signalrcore.hub_connection_builder import HubConnectionBuilder +from signalrcore_async.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json -import logging +import time +import asyncio class Notification: @@ -105,7 +106,13 @@

    Module ro_py.notifications

    self.on_notification = on_notification self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.negotiate_request = self.requests.get( + self.connection = None + + self.negotiate_request = None + self.wss_url = None + + async def initialize(self): + self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", @@ -127,7 +134,7 @@

    Module ro_py.notifications

    } ) - def on_message(_self, raw_notification): + async def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -137,16 +144,27 @@

    Module ro_py.notifications

    return if len(notification_json) > 0: notification = Notification(notification_json) - self.on_notification(notification) - logging.debug( - f"""Notification: -Type: {notification.type} -Data: {notification.data}""" - ) + await self.on_notification(notification) else: return - self.connection.with_automatic_reconnect({ + def _internal_send(_self, message, protocol=None): + + _self.logger.debug("Sending message {0}".format(message)) + + try: + protocol = _self.protocol if protocol is None else protocol + + _self._ws.send(protocol.encode(message)) + _self.connection_checker.last_message = time.time() + + if _self.reconnection_handler is not None: + _self.reconnection_handler.reset() + + except Exception as ex: + raise ex + + self.connection = self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, @@ -159,11 +177,12 @@

    Module ro_py.notifications

    self.connection.on_close(self.on_close) if self.on_error: self.connection.on_error(self.on_error) - self.connection.hub.on_message = on_message + self.connection.on_message = on_message + self.connection._internal_send = _internal_send - self.connection.start() + await self.connection.start() - def close(self): + async def close(self): """ Closes the connection and stops receiving notifications. """ @@ -251,7 +270,13 @@

    Classes

    self.on_notification = on_notification self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.negotiate_request = self.requests.get( + self.connection = None + + self.negotiate_request = None + self.wss_url = None + + async def initialize(self): + self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", @@ -273,7 +298,7 @@

    Classes

    } ) - def on_message(_self, raw_notification): + async def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -283,16 +308,27 @@

    Classes

    return if len(notification_json) > 0: notification = Notification(notification_json) - self.on_notification(notification) - logging.debug( - f"""Notification: -Type: {notification.type} -Data: {notification.data}""" - ) + await self.on_notification(notification) else: return - self.connection.with_automatic_reconnect({ + def _internal_send(_self, message, protocol=None): + + _self.logger.debug("Sending message {0}".format(message)) + + try: + protocol = _self.protocol if protocol is None else protocol + + _self._ws.send(protocol.encode(message)) + _self.connection_checker.last_message = time.time() + + if _self.reconnection_handler is not None: + _self.reconnection_handler.reset() + + except Exception as ex: + raise ex + + self.connection = self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, @@ -305,11 +341,12 @@

    Classes

    self.connection.on_close(self.on_close) if self.on_error: self.connection.on_error(self.on_error) - self.connection.hub.on_message = on_message + self.connection.on_message = on_message + self.connection._internal_send = _internal_send - self.connection.start() + await self.connection.start() - def close(self): + async def close(self): """ Closes the connection and stops receiving notifications. """ @@ -318,7 +355,7 @@

    Classes

    Methods

    -def close(self) +async def close(self)

    Closes the connection and stops receiving notifications.

    @@ -326,13 +363,94 @@

    Methods

    Expand source code -
    def close(self):
    +
    async def close(self):
         """
         Closes the connection and stops receiving notifications.
         """
         self.connection.stop()
    +
    +async def initialize(self) +
    +
    +
    +
    + +Expand source code + +
    async def initialize(self):
    +    self.negotiate_request = await self.requests.get(
    +        url="https://realtime.roblox.com/notifications/negotiate"
    +            "?clientProtocol=1.5"
    +            "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
    +        cookies={
    +            ".ROBLOSECURITY": self.roblosecurity
    +        }
    +    )
    +    self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
    +                   f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
    +                   f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
    +    self.connection = HubConnectionBuilder()
    +    self.connection.with_url(
    +        self.wss_url,
    +        options={
    +            "headers": {
    +                "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
    +            },
    +            "skip_negotiation": False
    +        }
    +    )
    +
    +    async def on_message(_self, raw_notification):
    +        """
    +        Internal callback when a message is received.
    +        """
    +        try:
    +            notification_json = json.loads(raw_notification)
    +        except json.decoder.JSONDecodeError:
    +            return
    +        if len(notification_json) > 0:
    +            notification = Notification(notification_json)
    +            await self.on_notification(notification)
    +        else:
    +            return
    +
    +    def _internal_send(_self, message, protocol=None):
    +
    +        _self.logger.debug("Sending message {0}".format(message))
    +
    +        try:
    +            protocol = _self.protocol if protocol is None else protocol
    +
    +            _self._ws.send(protocol.encode(message))
    +            _self.connection_checker.last_message = time.time()
    +
    +            if _self.reconnection_handler is not None:
    +                _self.reconnection_handler.reset()
    +
    +        except Exception as ex:
    +            raise ex
    +
    +    self.connection = self.connection.with_automatic_reconnect({
    +        "type": "raw",
    +        "keep_alive_interval": 10,
    +        "reconnect_interval": 5,
    +        "max_attempts": 5
    +    }).build()
    +
    +    if self.on_open:
    +        self.connection.on_open(self.on_open)
    +    if self.on_close:
    +        self.connection.on_close(self.on_close)
    +    if self.on_error:
    +        self.connection.on_error(self.on_error)
    +    self.connection.on_message = on_message
    +    self.connection._internal_send = _internal_send
    +
    +    await self.connection.start()
    +
    +
    @@ -358,6 +476,7 @@

    NotificationReceiver

    diff --git a/docs/robloxdocs.html b/docs/robloxdocs.html new file mode 100644 index 00000000..7a1d9467 --- /dev/null +++ b/docs/robloxdocs.html @@ -0,0 +1,434 @@ + + + + + + +ro_py.robloxdocs API documentation + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.robloxdocs

    +
    +
    +

    This file houses functions and classes that pertain to the Roblox API documentation pages. +I don't know if this is really that useful, but it might be useful for an API browser program, or for accessing +endpoints that aren't supported directly by ro.py yet.

    +
    + +Expand source code + +
    """
    +
    +This file houses functions and classes that pertain to the Roblox API documentation pages.
    +I don't know if this is really that useful, but it might be useful for an API browser program, or for accessing
    +endpoints that aren't supported directly by ro.py yet.
    +
    +"""
    +
    +from lxml import html
    +from io import StringIO
    +
    +
    +class EndpointDocsPathRequestTypeProperties:
    +    def __init__(self, data):
    +        self.internal = data["internal"]
    +        self.metric_ids = data["metricIds"]
    +
    +
    +class EndpointDocsPathRequestTypeResponse:
    +    def __init__(self, data):
    +        self.description = None
    +        self.schema = None
    +        if "description" in data:
    +            self.description = data["description"]
    +        if "schema" in data:
    +            self.schema = data["schema"]
    +
    +
    +class EndpointDocsPathRequestTypeParameter:
    +    def __init__(self, data):
    +        self.name = data["name"]
    +        self.iin = data["in"]  # I can't make this say "in" so this is close enough
    +
    +        if "description" in data:
    +            self.description = data["description"]
    +        else:
    +            self.description = None
    +
    +        self.required = data["required"]
    +        self.type = None
    +
    +        if "type" in data:
    +            self.type = data["type"]
    +
    +        if "format" in data:
    +            self.format = data["format"]
    +        else:
    +            self.format = None
    +
    +
    +class EndpointDocsPathRequestType:
    +    def __init__(self, data):
    +        self.tags = data["tags"]
    +        self.description = None
    +        self.summary = None
    +
    +        if "summary" in data:
    +            self.summary = data["summary"]
    +
    +        if "description" in data:
    +            self.description = data["description"]
    +
    +        self.consumes = data["consumes"]
    +        self.produces = data["produces"]
    +        self.parameters = []
    +        self.responses = {}
    +        self.properties = EndpointDocsPathRequestTypeProperties(data["properties"])
    +        for raw_parameter in data["parameters"]:
    +            self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter))
    +        for rr_k, rr_v in data["responses"].items():
    +            self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v)
    +
    +
    +class EndpointDocsPath:
    +    def __init__(self, data):
    +        self.data = {}
    +        for type_k, type_v in data.items():
    +            self.data[type_k] = EndpointDocsPathRequestType(type_v)
    +
    +
    +class EndpointDocsDataInfo:
    +    def __init__(self, data):
    +        self.version = data["version"]
    +        self.title = data["title"]
    +
    +
    +class EndpointDocsData:
    +    def __init__(self, data):
    +        self.swagger_version = data["swagger"]
    +        self.info = EndpointDocsDataInfo(data["info"])
    +        self.host = data["host"]
    +        self.schemes = data["schemes"]
    +        self.paths = {}
    +        for path_k, path_v in data["paths"].items():
    +            self.paths[path_k] = EndpointDocsPath(path_v)
    +
    +
    +class EndpointDocs:
    +    def __init__(self, requests, docs_url):
    +        self.requests = requests
    +        self.url = docs_url
    +
    +    async def get_versions(self):
    +        docs_req = await self.requests.get(self.url + "/docs")
    +        root = html.parse(StringIO(docs_req.text)).getroot()
    +        try:
    +            vs_element = root.get_element_by_id("version-selector")
    +            return vs_element.value_options
    +        except KeyError:
    +            return ["v1"]
    +
    +    async def get_data_for_version(self, version):
    +        data_req = await self.requests.get(self.url + "/docs/json/" + version)
    +        version_data = data_req.json()
    +        return EndpointDocsData(version_data)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class EndpointDocs +(requests, docs_url) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocs:
    +    def __init__(self, requests, docs_url):
    +        self.requests = requests
    +        self.url = docs_url
    +
    +    async def get_versions(self):
    +        docs_req = await self.requests.get(self.url + "/docs")
    +        root = html.parse(StringIO(docs_req.text)).getroot()
    +        try:
    +            vs_element = root.get_element_by_id("version-selector")
    +            return vs_element.value_options
    +        except KeyError:
    +            return ["v1"]
    +
    +    async def get_data_for_version(self, version):
    +        data_req = await self.requests.get(self.url + "/docs/json/" + version)
    +        version_data = data_req.json()
    +        return EndpointDocsData(version_data)
    +
    +

    Methods

    +
    +
    +async def get_data_for_version(self, version) +
    +
    +
    +
    + +Expand source code + +
    async def get_data_for_version(self, version):
    +    data_req = await self.requests.get(self.url + "/docs/json/" + version)
    +    version_data = data_req.json()
    +    return EndpointDocsData(version_data)
    +
    +
    +
    +async def get_versions(self) +
    +
    +
    +
    + +Expand source code + +
    async def get_versions(self):
    +    docs_req = await self.requests.get(self.url + "/docs")
    +    root = html.parse(StringIO(docs_req.text)).getroot()
    +    try:
    +        vs_element = root.get_element_by_id("version-selector")
    +        return vs_element.value_options
    +    except KeyError:
    +        return ["v1"]
    +
    +
    +
    +
    +
    +class EndpointDocsData +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsData:
    +    def __init__(self, data):
    +        self.swagger_version = data["swagger"]
    +        self.info = EndpointDocsDataInfo(data["info"])
    +        self.host = data["host"]
    +        self.schemes = data["schemes"]
    +        self.paths = {}
    +        for path_k, path_v in data["paths"].items():
    +            self.paths[path_k] = EndpointDocsPath(path_v)
    +
    +
    +
    +class EndpointDocsDataInfo +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsDataInfo:
    +    def __init__(self, data):
    +        self.version = data["version"]
    +        self.title = data["title"]
    +
    +
    +
    +class EndpointDocsPath +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsPath:
    +    def __init__(self, data):
    +        self.data = {}
    +        for type_k, type_v in data.items():
    +            self.data[type_k] = EndpointDocsPathRequestType(type_v)
    +
    +
    +
    +class EndpointDocsPathRequestType +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsPathRequestType:
    +    def __init__(self, data):
    +        self.tags = data["tags"]
    +        self.description = None
    +        self.summary = None
    +
    +        if "summary" in data:
    +            self.summary = data["summary"]
    +
    +        if "description" in data:
    +            self.description = data["description"]
    +
    +        self.consumes = data["consumes"]
    +        self.produces = data["produces"]
    +        self.parameters = []
    +        self.responses = {}
    +        self.properties = EndpointDocsPathRequestTypeProperties(data["properties"])
    +        for raw_parameter in data["parameters"]:
    +            self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter))
    +        for rr_k, rr_v in data["responses"].items():
    +            self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v)
    +
    +
    +
    +class EndpointDocsPathRequestTypeParameter +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsPathRequestTypeParameter:
    +    def __init__(self, data):
    +        self.name = data["name"]
    +        self.iin = data["in"]  # I can't make this say "in" so this is close enough
    +
    +        if "description" in data:
    +            self.description = data["description"]
    +        else:
    +            self.description = None
    +
    +        self.required = data["required"]
    +        self.type = None
    +
    +        if "type" in data:
    +            self.type = data["type"]
    +
    +        if "format" in data:
    +            self.format = data["format"]
    +        else:
    +            self.format = None
    +
    +
    +
    +class EndpointDocsPathRequestTypeProperties +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsPathRequestTypeProperties:
    +    def __init__(self, data):
    +        self.internal = data["internal"]
    +        self.metric_ids = data["metricIds"]
    +
    +
    +
    +class EndpointDocsPathRequestTypeResponse +(data) +
    +
    +
    +
    + +Expand source code + +
    class EndpointDocsPathRequestTypeResponse:
    +    def __init__(self, data):
    +        self.description = None
    +        self.schema = None
    +        if "description" in data:
    +            self.description = data["description"]
    +        if "schema" in data:
    +            self.schema = data["schema"]
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/roles.html b/docs/roles.html index ab65d3ba..067f239a 100644 --- a/docs/roles.html +++ b/docs/roles.html @@ -5,7 +5,7 @@ ro_py.roles API documentation - + @@ -22,14 +22,14 @@

    Module ro_py.roles

    -

    IRA PUT THINGS HERE

    +

    This file contains classes and functions related to Roblox roles.

    Expand source code
    """
     
    -IRA PUT THINGS HERE
    +This file contains classes and functions related to Roblox roles.
     
     """
     
    @@ -61,6 +61,17 @@ 

    Module ro_py.roles

    def get_rp_names(rp): + """ + Converts permissions into something Roblox can read. + + Parameters + ---------- + rp : ro_py.roles.RolePermissions + + Returns + ------- + dict + """ return { "viewWall": rp.view_wall, "PostToWall": rp.post_to_wall, @@ -99,11 +110,14 @@

    Module ro_py.roles

    self.group = group self.id = role_data['id'] self.name = role_data['name'] - self.description = role_data['description'] + self.description = role_data.get('description') self.rank = role_data['rank'] - self.member_count = role_data['memberCount'] + self.member_count = role_data.get('memberCount') async def update(self): + """ + Updates information of the role. + """ update_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.group.id}/roles" ) @@ -117,6 +131,22 @@

    Module ro_py.roles

    break async def edit(self, name=None, description=None, rank=None): + """ + Edits the name, description or rank of a role + + Parameters + ---------- + name : str, optional + New name for the role. + description : str, optional + New description for the role. + rank : int, optional + Number from 1-254 that determains the new rank number for the role. + + Returns + ------- + int + """ edit_req = await self.requests.patch( url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", data={ @@ -128,6 +158,18 @@

    Module ro_py.roles

    return edit_req.status_code == 200 async def edit_permissions(self, role_permissions): + """ + Edits the permissions of a role. + + Parameters + ---------- + role_permissions : ro_py.roles.RolePermissions + New permissions that will overwrite the old ones. + + Returns + ------- + int + """ data = { "permissions": {} } @@ -155,12 +197,33 @@

    Functions

    def get_rp_names(rp)
    -
    +

    Converts permissions into something Roblox can read.

    +

    Parameters

    +
    +
    rp : RolePermissions
    +
     
    +
    +

    Returns

    +
    +
    dict
    +
     
    +
    Expand source code
    def get_rp_names(rp):
    +    """
    +    Converts permissions into something Roblox can read.
    +
    +    Parameters
    +    ----------
    +    rp : ro_py.roles.RolePermissions
    +
    +    Returns
    +    -------
    +    dict
    +    """
         return {
             "viewWall": rp.view_wall,
             "PostToWall": rp.post_to_wall,
    @@ -224,11 +287,14 @@ 

    Parameters

    self.group = group self.id = role_data['id'] self.name = role_data['name'] - self.description = role_data['description'] + self.description = role_data.get('description') self.rank = role_data['rank'] - self.member_count = role_data['memberCount'] + self.member_count = role_data.get('memberCount') async def update(self): + """ + Updates information of the role. + """ update_req = await self.requests.get( url=endpoint + f"/v1/groups/{self.group.id}/roles" ) @@ -242,6 +308,22 @@

    Parameters

    break async def edit(self, name=None, description=None, rank=None): + """ + Edits the name, description or rank of a role + + Parameters + ---------- + name : str, optional + New name for the role. + description : str, optional + New description for the role. + rank : int, optional + Number from 1-254 that determains the new rank number for the role. + + Returns + ------- + int + """ edit_req = await self.requests.patch( url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}", data={ @@ -253,6 +335,18 @@

    Parameters

    return edit_req.status_code == 200 async def edit_permissions(self, role_permissions): + """ + Edits the permissions of a role. + + Parameters + ---------- + role_permissions : ro_py.roles.RolePermissions + New permissions that will overwrite the old ones. + + Returns + ------- + int + """ data = { "permissions": {} } @@ -274,12 +368,42 @@

    Methods

    async def edit(self, name=None, description=None, rank=None)
    -
    +

    Edits the name, description or rank of a role

    +

    Parameters

    +
    +
    name : str, optional
    +
    New name for the role.
    +
    description : str, optional
    +
    New description for the role.
    +
    rank : int, optional
    +
    Number from 1-254 that determains the new rank number for the role.
    +
    +

    Returns

    +
    +
    int
    +
     
    +
    Expand source code
    async def edit(self, name=None, description=None, rank=None):
    +    """
    +    Edits the name, description or rank of a role
    +
    +    Parameters
    +    ----------
    +    name : str, optional
    +        New name for the role.
    +    description : str, optional
    +        New description for the role.
    +    rank : int, optional
    +        Number from 1-254 that determains the new rank number for the role.
    +
    +    Returns
    +    -------
    +    int
    +    """
         edit_req = await self.requests.patch(
             url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
             data={
    @@ -295,12 +419,34 @@ 

    Methods

    async def edit_permissions(self, role_permissions)
    -
    +

    Edits the permissions of a role.

    +

    Parameters

    +
    +
    role_permissions : RolePermissions
    +
    New permissions that will overwrite the old ones.
    +
    +

    Returns

    +
    +
    int
    +
     
    +
    Expand source code
    async def edit_permissions(self, role_permissions):
    +    """
    +    Edits the permissions of a role.
    +
    +    Parameters
    +    ----------
    +    role_permissions : ro_py.roles.RolePermissions
    +        New permissions that will overwrite the old ones.
    +
    +    Returns
    +    -------
    +    int
    +    """
         data = {
             "permissions": {}
         }
    @@ -321,12 +467,15 @@ 

    Methods

    async def update(self)
    -
    +

    Updates information of the role.

    Expand source code
    async def update(self):
    +    """
    +    Updates information of the role.
    +    """
         update_req = await self.requests.get(
             url=endpoint + f"/v1/groups/{self.group.id}/roles"
         )
    diff --git a/docs/trades.html b/docs/trades.html
    index 64584ae5..b7f1a141 100644
    --- a/docs/trades.html
    +++ b/docs/trades.html
    @@ -35,7 +35,7 @@ 

    Module ro_py.trades

    from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset -from ro_py.users import User +from ro_py.users import User, PartialUser import iso8601 import enum @@ -45,12 +45,12 @@

    Module ro_py.trades

    def trade_page_handler(requests, this_page) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) + trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) return trades_out class Trade: - def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender @@ -82,7 +82,7 @@

    Module ro_py.trades

    class PartialTrade: - def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool): + def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool): self.requests = requests self.trade_id = trade_id self.user = user @@ -200,7 +200,7 @@

    Functions

    def trade_page_handler(requests, this_page) -> list:
         trades_out = []
         for raw_trade in this_page:
    -        trades_out.append(Trade(requests, raw_trade["id"], User(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
    +        trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
         return trades_out
    @@ -211,7 +211,7 @@

    Classes

    class PartialTrade -(requests, trade_id: int, user: User, created, expiration, status: bool) +(requests, trade_id: int, user: PartialUser, created, expiration, status: bool)
    @@ -220,7 +220,7 @@

    Classes

    Expand source code
    class PartialTrade:
    -    def __init__(self, requests, trade_id: int, user: User, created, expiration, status: bool):
    +    def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool):
             self.requests = requests
             self.trade_id = trade_id
             self.user = user
    @@ -363,7 +363,7 @@ 

    Methods

    class Trade -(requests, trade_id: int, sender: User, recieve_items: list, send_items: list, created, expiration, status: bool) +(requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool)
    @@ -372,7 +372,7 @@

    Methods

    Expand source code
    class Trade:
    -    def __init__(self, requests, trade_id: int, sender: User, recieve_items: list[Asset], send_items: list[Asset], created, expiration, status: bool):
    +    def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool):
             self.trade_id = trade_id
             self.requests = requests
             self.sender = sender
    diff --git a/docs/users.html b/docs/users.html
    index 86cea00a..b824b3fd 100644
    --- a/docs/users.html
    +++ b/docs/users.html
    @@ -43,10 +43,19 @@ 

    Module ro_py.users

    """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. """ - def __init__(self, requests, id, name=None): + def __init__(self, requests, roblox_id, name=None): self.requests = requests - self.id = id + self.id = roblox_id self.description = None self.created = None self.is_banned = None @@ -67,6 +76,7 @@

    Module ro_py.users

    self.display_name = user_info["displayName"] # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req + return self async def get_status(self): """ @@ -126,7 +136,14 @@

    Module ro_py.users

    friends_list.append( User(self.requests, friend_raw["id"]) ) - return friends_list
    + return friends_list + + +class PartialUser(User): + """ + Represents a user with less information then the normal User class. + """ + pass
    @@ -138,13 +155,57 @@

    Module ro_py.users

    Classes

    +
    +class PartialUser +(requests, roblox_id, name=None) +
    +
    +

    Represents a user with less information then the normal User class.

    +
    + +Expand source code + +
    class PartialUser(User):
    +    """
    +    Represents a user with less information then the normal User class.
    +    """
    +    pass
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    class User -(requests, id, name=None) +(requests, roblox_id, name=None)

    Represents a Roblox user and their profile. -Can be initialized with either a user ID or a username.

    +Can be initialized with either a user ID or a username.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
    roblox_id : int
    +
    The id of a user.
    +
    name : str
    +
    The name of the user.
    +
    Expand source code @@ -153,10 +214,19 @@

    Classes

    """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. + + Parameters + ---------- + requests : ro_py.utilities.requests.Requests + Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. """ - def __init__(self, requests, id, name=None): + def __init__(self, requests, roblox_id, name=None): self.requests = requests - self.id = id + self.id = roblox_id self.description = None self.created = None self.is_banned = None @@ -177,6 +247,7 @@

    Classes

    self.display_name = user_info["displayName"] # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req + return self async def get_status(self): """ @@ -238,6 +309,11 @@

    Classes

    ) return friends_list
    +

    Subclasses

    +

    Methods

    @@ -387,7 +463,10 @@

    Methods

    self.created = iso8601.parse_date(user_info["created"]) self.is_banned = user_info["isBanned"] self.name = user_info["name"] - self.display_name = user_info["displayName"]
    + self.display_name = user_info["displayName"] + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req + return self
    @@ -409,6 +488,9 @@

    Index

  • Classes

    • +

      PartialUser

      +
    • +
    • User

      • get_followers_count
      • diff --git a/docs/utilities/errors.html b/docs/utilities/errors.html index b8da4ed1..43ea1b20 100644 --- a/docs/utilities/errors.html +++ b/docs/utilities/errors.html @@ -62,7 +62,15 @@

        Module ro_py.utilities.errors

        class InvalidPageError(Exception): - """Called when an invalid page is requested.""" + """Called when an invalid page is requested.""" + + +class NotFound(Exception): + """Called when something is not found.""" + + +class UserDoesNotExistError(Exception): + """Called when a user does not exist."""
  • @@ -172,6 +180,25 @@

    Ancestors

  • builtins.BaseException
  • +
    +class NotFound +(*args, **kwargs) +
    +
    +

    Called when something is not found.

    +
    + +Expand source code + +
    class NotFound(Exception):
    +    """Called when something is not found."""
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    class NotLimitedError (*args, **kwargs) @@ -192,6 +219,25 @@

    Ancestors

  • builtins.BaseException
  • +
    +class UserDoesNotExistError +(*args, **kwargs) +
    +
    +

    Called when a user does not exist.

    +
    + +Expand source code + +
    class UserDoesNotExistError(Exception):
    +    """Called when a user does not exist."""
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    @@ -224,8 +270,14 @@

    InvalidShotTypeError

  • +

    NotFound

    +
  • +
  • NotLimitedError

  • +
  • +

    UserDoesNotExistError

    +
  • diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index d1f4a6c8..cd94dbd0 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -42,7 +42,7 @@

    Module ro_py.utilities.pages

    """ Represents a single page from a Pages object. """ - def __init__(self, requests, data, handler=None): + def __init__(self, requests, data, handler=None, handler_args=None): self.previous_page_cursor = data["previousPageCursor"] """Cursor to navigate to the previous page.""" self.next_page_cursor = data["nextPageCursor"] @@ -52,7 +52,7 @@

    Module ro_py.utilities.pages

    """Raw data from this page.""" if handler: - self.data = handler(requests, self.data) + self.data = handler(requests, self.data, handler_args) class Pages: @@ -64,7 +64,7 @@

    Module ro_py.utilities.pages

    Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required. """ - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None): + def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): if extra_parameters is None: extra_parameters = {} @@ -100,7 +100,8 @@

    Module ro_py.utilities.pages

    return Page( requests=self.requests, data=page_req.json(), - handler=self.handler + handler=self.handler, + handler_args=handler_args ) async def previous(self): @@ -133,7 +134,7 @@

    Classes

    class Page -(requests, data, handler=None) +(requests, data, handler=None, handler_args=None)

    Represents a single page from a Pages object.

    @@ -145,7 +146,7 @@

    Classes

    """ Represents a single page from a Pages object. """ - def __init__(self, requests, data, handler=None): + def __init__(self, requests, data, handler=None, handler_args=None): self.previous_page_cursor = data["previousPageCursor"] """Cursor to navigate to the previous page.""" self.next_page_cursor = data["nextPageCursor"] @@ -155,7 +156,7 @@

    Classes

    """Raw data from this page.""" if handler: - self.data = handler(requests, self.data)
    + self.data = handler(requests, self.data, handler_args)

    Instance variables

    @@ -175,7 +176,7 @@

    Instance variables

    class Pages -(requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None) +(requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None)

    Represents a paged object.

    @@ -198,7 +199,7 @@

    Instance variables

    Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required. """ - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None): + def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): if extra_parameters is None: extra_parameters = {} @@ -234,7 +235,8 @@

    Instance variables

    return Page( requests=self.requests, data=page_req.json(), - handler=self.handler + handler=self.handler, + handler_args=handler_args ) async def previous(self): diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 71b579dd..209add62 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -31,6 +31,7 @@

    Module ro_py.utilities.requests

    from json.decoder import JSONDecodeError from cachecontrol import CacheControl import requests_async +import requests class Requests: @@ -63,6 +64,8 @@

    Module ro_py.utilities.requests

    Essentially identical to requests_async.Session.get. """ + quickreturn = kwargs.pop("quickreturn", False) + get_request = await self.session.get(*args, **kwargs) try: @@ -78,19 +81,39 @@

    Module ro_py.utilities.requests

    else: return get_request + if quickreturn: + return get_request + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + def back_post(self, *args, **kwargs): + kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + kwargs["headers"] = kwargs.pop("headers", self.session.headers) + + post_request = requests.post(*args, **kwargs) + + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) + + self.session.cookies = post_request.cookies + return post_request + async def post(self, *args, **kwargs): """ Essentially identical to requests_async.Session.post. """ + quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) + post_request = await self.session.post(*args, **kwargs) - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) + if doxcsrf: + if post_request.status_code == 403: + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -105,6 +128,9 @@

    Module ro_py.utilities.requests

    else: return post_request + if quickreturn: + return post_request + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") async def patch(self, *args, **kwargs): @@ -129,7 +155,31 @@

    Module ro_py.utilities.requests

    else: return patch_request - raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + + async def delete(self, *args, **kwargs): + """ + Essentially identical to requests_async.Session.delete. + """ + + delete_request = await self.session.delete(*args, **kwargs) + + if delete_request.status_code == 403: + if "X-CSRF-TOKEN" in delete_request.headers: + self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] + delete_request = await self.session.delete(*args, **kwargs) + + delete_request_json = delete_request.json() + + if isinstance(delete_request_json, dict): + try: + delete_request_error = delete_request_json["errors"] + except KeyError: + return delete_request + else: + return delete_request + + raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
    @@ -188,6 +238,8 @@

    Parameters

    Essentially identical to requests_async.Session.get. """ + quickreturn = kwargs.pop("quickreturn", False) + get_request = await self.session.get(*args, **kwargs) try: @@ -203,19 +255,39 @@

    Parameters

    else: return get_request + if quickreturn: + return get_request + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + def back_post(self, *args, **kwargs): + kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + kwargs["headers"] = kwargs.pop("headers", self.session.headers) + + post_request = requests.post(*args, **kwargs) + + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) + + self.session.cookies = post_request.cookies + return post_request + async def post(self, *args, **kwargs): """ Essentially identical to requests_async.Session.post. """ + quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) + post_request = await self.session.post(*args, **kwargs) - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) + if doxcsrf: + if post_request.status_code == 403: + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -230,6 +302,9 @@

    Parameters

    else: return post_request + if quickreturn: + return post_request + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") async def patch(self, *args, **kwargs): @@ -254,7 +329,31 @@

    Parameters

    else: return patch_request - raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + + async def delete(self, *args, **kwargs): + """ + Essentially identical to requests_async.Session.delete. + """ + + delete_request = await self.session.delete(*args, **kwargs) + + if delete_request.status_code == 403: + if "X-CSRF-TOKEN" in delete_request.headers: + self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] + delete_request = await self.session.delete(*args, **kwargs) + + delete_request_json = delete_request.json() + + if isinstance(delete_request_json, dict): + try: + delete_request_error = delete_request_json["errors"] + except KeyError: + return delete_request + else: + return delete_request + + raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")

    Instance variables

    @@ -269,6 +368,63 @@

    Instance variables

    Methods

    +
    +def back_post(self, *args, **kwargs) +
    +
    +
    +
    + +Expand source code + +
    def back_post(self, *args, **kwargs):
    +    kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies)
    +    kwargs["headers"] = kwargs.pop("headers", self.session.headers)
    +
    +    post_request = requests.post(*args, **kwargs)
    +
    +    if "X-CSRF-TOKEN" in post_request.headers:
    +        self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
    +        post_request = requests.post(*args, **kwargs)
    +
    +    self.session.cookies = post_request.cookies
    +    return post_request
    +
    +
    +
    +async def delete(self, *args, **kwargs) +
    +
    +

    Essentially identical to requests_async.Session.delete.

    +
    + +Expand source code + +
    async def delete(self, *args, **kwargs):
    +    """
    +    Essentially identical to requests_async.Session.delete.
    +    """
    +
    +    delete_request = await self.session.delete(*args, **kwargs)
    +
    +    if delete_request.status_code == 403:
    +        if "X-CSRF-TOKEN" in delete_request.headers:
    +            self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"]
    +            delete_request = await self.session.delete(*args, **kwargs)
    +
    +    delete_request_json = delete_request.json()
    +
    +    if isinstance(delete_request_json, dict):
    +        try:
    +            delete_request_error = delete_request_json["errors"]
    +        except KeyError:
    +            return delete_request
    +    else:
    +        return delete_request
    +
    +    raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
    +
    +
    async def get(self, *args, **kwargs)
    @@ -283,6 +439,8 @@

    Methods

    Essentially identical to requests_async.Session.get. """ + quickreturn = kwargs.pop("quickreturn", False) + get_request = await self.session.get(*args, **kwargs) try: @@ -298,6 +456,9 @@

    Methods

    else: return get_request + if quickreturn: + return get_request + raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") @@ -349,12 +510,16 @@

    Methods

    Essentially identical to requests_async.Session.post. """ + quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) + post_request = await self.session.post(*args, **kwargs) - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) + if doxcsrf: + if post_request.status_code == 403: + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = await self.session.post(*args, **kwargs) try: post_request_json = post_request.json() @@ -369,6 +534,9 @@

    Methods

    else: return post_request + if quickreturn: + return post_request + raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") @@ -392,8 +560,10 @@

    Index

    @@ -52,9 +67,24 @@

    Module ro_py.captcha

    Classes

    +
    +class CaptchaMetadata +(data) +
    +
    +
    +
    + +Expand source code + +
    class CaptchaMetadata:
    +    def __init__(self, data):
    +        self.fun_captcha_public_keys = data["funCaptchaPublicKeys"]
    +
    +
    class UnsolvedCaptcha -(data, pkey) +(pkey)
    @@ -63,9 +93,29 @@

    Classes

    Expand source code
    class UnsolvedCaptcha:
    +    def __init__(self, pkey):
    +        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    +                   f"?pkey={pkey}" \
    +                   f"&lang=en"
    + +
    +
    +class UnsolvedLoginCaptcha +(data, pkey) +
    +
    +
    +
    + +Expand source code + +
    class UnsolvedLoginCaptcha:
         def __init__(self, data, pkey):
             self.token = data["token"]
    -        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/?pkey={pkey}&session={self.token.split('|')[0]}&lang=en-gb"
    +        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    +                   f"?pkey={pkey}" \
    +                   f"&session={self.token.split('|')[0]}" \
    +                   f"&lang=en"
             self.challenge_url = data["challenge_url"]
             self.challenge_url_cdn = data["challenge_url_cdn"]
             self.noscript = data["noscript"]
    @@ -88,8 +138,14 @@

    Index

  • Classes

  • diff --git a/docs/chat.html b/docs/chat.html index 0a250613..3a636faa 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -50,8 +50,8 @@

    Module ro_py.chat

    self.requests = requests self.id = conversation_id - def __enter__(self): - self.requests.post( + async def __aenter__(self): + await self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -59,8 +59,8 @@

    Module ro_py.chat

    } ) - def __exit__(self, *args, **kwargs): - self.requests.post( + async def __aexit__(self, *args, **kwargs): + await self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -72,24 +72,33 @@

    Module ro_py.chat

    class Conversation: def __init__(self, requests, conversation_id=None, raw=False, raw_data=None): self.requests = requests + self.raw = raw + self.id = None + self.title = None + self.initiator = None + self.type = None + self.typing = ConversationTyping(self.requests, conversation_id) - if raw: + if self.raw: data = raw_data self.id = data["id"] - else: - self.id = conversation_id - conversation_req = requests.get( - url="https://chat.roblox.com/v2/get-conversations", - params={ - "conversationIds": self.id - } - ) - data = conversation_req.json()[0] + self.title = data["title"] + self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.type = data["conversationType"] + self.typing = ConversationTyping(self.requests, conversation_id) + async def update(self): + conversation_req = await self.requests.get( + url="https://chat.roblox.com/v2/get-conversations", + params={ + "conversationIds": self.id + } + ) + data = conversation_req.json()[0] + self.id = data["id"] self.title = data["title"] self.initiator = User(self.requests, data["initiator"]["targetId"]) self.type = data["conversationType"] - self.typing = ConversationTyping(self.requests, conversation_id) async def get_message(self, message_id): @@ -345,24 +354,33 @@

    Parameters

    class Conversation:
         def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
             self.requests = requests
    +        self.raw = raw
    +        self.id = None
    +        self.title = None
    +        self.initiator = None
    +        self.type = None
    +        self.typing = ConversationTyping(self.requests, conversation_id)
     
    -        if raw:
    +        if self.raw:
                 data = raw_data
                 self.id = data["id"]
    -        else:
    -            self.id = conversation_id
    -            conversation_req = requests.get(
    -                url="https://chat.roblox.com/v2/get-conversations",
    -                params={
    -                    "conversationIds": self.id
    -                }
    -            )
    -            data = conversation_req.json()[0]
    +            self.title = data["title"]
    +            self.initiator = User(self.requests, data["initiator"]["targetId"])
    +            self.type = data["conversationType"]
    +            self.typing = ConversationTyping(self.requests, conversation_id)
     
    +    async def update(self):
    +        conversation_req = await self.requests.get(
    +            url="https://chat.roblox.com/v2/get-conversations",
    +            params={
    +                "conversationIds": self.id
    +            }
    +        )
    +        data = conversation_req.json()[0]
    +        self.id = data["id"]
             self.title = data["title"]
             self.initiator = User(self.requests, data["initiator"]["targetId"])
             self.type = data["conversationType"]
    -
             self.typing = ConversationTyping(self.requests, conversation_id)
     
         async def get_message(self, message_id):
    @@ -421,6 +439,30 @@ 

    Methods

    raise ChatError(send_message_json["statusMessage"])
    +
    +async def update(self) +
    +
    +
    +
    + +Expand source code + +
    async def update(self):
    +    conversation_req = await self.requests.get(
    +        url="https://chat.roblox.com/v2/get-conversations",
    +        params={
    +            "conversationIds": self.id
    +        }
    +    )
    +    data = conversation_req.json()[0]
    +    self.id = data["id"]
    +    self.title = data["title"]
    +    self.initiator = User(self.requests, data["initiator"]["targetId"])
    +    self.type = data["conversationType"]
    +    self.typing = ConversationTyping(self.requests, conversation_id)
    +
    +
    @@ -438,8 +480,8 @@

    Methods

    self.requests = requests self.id = conversation_id - def __enter__(self): - self.requests.post( + async def __aenter__(self): + await self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -447,8 +489,8 @@

    Methods

    } ) - def __exit__(self, *args, **kwargs): - self.requests.post( + async def __aexit__(self, *args, **kwargs): + await self.requests.post( url=endpoint + "v2/update-user-typing-status", data={ "conversationId": self.id, @@ -579,6 +621,7 @@

    Con
  • diff --git a/docs/client.html b/docs/client.html index 377d5e61..84e12262 100644 --- a/docs/client.html +++ b/docs/client.html @@ -40,12 +40,12 @@

    Module ro_py.client

    from ro_py.badges import Badge from ro_py.chat import ChatWrapper from ro_py.trades import TradesWrapper -from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.cache import CacheType from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.accountinformation import AccountInformation -from ro_py.utilities.errors import UserDoesNotExistError, ApiError +from ro_py.utilities.errors import UserDoesNotExistError +from ro_py.captcha import UnsolvedLoginCaptcha import logging @@ -73,8 +73,6 @@

    Module ro_py.client

    """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None """AccountSettings object. Only available for authenticated clients.""" - # self.user = None - # """User object. Only available for authenticated clients.""" self.chat = None """ChatWrapper object. Only available for authenticated clients.""" self.trade = None @@ -86,18 +84,39 @@

    Module ro_py.client

    self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") - # auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") - # self.user = User(self.requests, auth_user_req.json()["id"]) - # logging.debug("Initialized authenticated user.") self.chat = ChatWrapper(self.requests) logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") def token_login(self, token): + """ + Authenticates the client with a ROBLOSECURITY token. + + Parameters + ---------- + token : str + .ROBLOSECURITY token to authenticate with. + """ self.requests.session.cookies[".ROBLOSECURITY"] = token async def user_login(self, username, password, token=None): + """ + Authenticates the client with a username and password. + + Parameters + ---------- + username : str + Username to log in with. + password : str + Password to log in with. + token : str, optional + If you have already solved the captcha, pass it here. + + Returns + ------- + ro_py.captcha.UnsolvedCaptcha or request + """ if token: login_req = self.requests.back_post( url="https://auth.roblox.com/v2/login", @@ -134,7 +153,7 @@

    Module ro_py.client

    data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" ) captcha_json = captcha_req.json() - return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ @@ -298,8 +317,6 @@

    Parameters

    """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None """AccountSettings object. Only available for authenticated clients.""" - # self.user = None - # """User object. Only available for authenticated clients.""" self.chat = None """ChatWrapper object. Only available for authenticated clients.""" self.trade = None @@ -311,18 +328,39 @@

    Parameters

    self.accountinformation = AccountInformation(self.requests) self.accountsettings = AccountSettings(self.requests) logging.debug("Initialized AccountInformation and AccountSettings.") - # auth_user_req = self.requests.get("https://users.roblox.com/v1/users/authenticated") - # self.user = User(self.requests, auth_user_req.json()["id"]) - # logging.debug("Initialized authenticated user.") self.chat = ChatWrapper(self.requests) logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.requests) logging.debug("Initialized trade wrapper.") def token_login(self, token): + """ + Authenticates the client with a ROBLOSECURITY token. + + Parameters + ---------- + token : str + .ROBLOSECURITY token to authenticate with. + """ self.requests.session.cookies[".ROBLOSECURITY"] = token async def user_login(self, username, password, token=None): + """ + Authenticates the client with a username and password. + + Parameters + ---------- + username : str + Username to log in with. + password : str + Password to log in with. + token : str, optional + If you have already solved the captcha, pass it here. + + Returns + ------- + ro_py.captcha.UnsolvedCaptcha or request + """ if token: login_req = self.requests.back_post( url="https://auth.roblox.com/v2/login", @@ -359,7 +397,7 @@

    Parameters

    data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" ) captcha_json = captcha_req.json() - return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") async def get_user(self, user_id): """ @@ -706,12 +744,25 @@

    Parameters

    def token_login(self, token)
  • -
    +

    Authenticates the client with a ROBLOSECURITY token.

    +

    Parameters

    +
    +
    token : str
    +
    .ROBLOSECURITY token to authenticate with.
    +
    Expand source code
    def token_login(self, token):
    +    """
    +    Authenticates the client with a ROBLOSECURITY token.
    +
    +    Parameters
    +    ----------
    +    token : str
    +        .ROBLOSECURITY token to authenticate with.
    +    """
         self.requests.session.cookies[".ROBLOSECURITY"] = token
    @@ -719,12 +770,42 @@

    Parameters

    async def user_login(self, username, password, token=None)
    -
    +

    Authenticates the client with a username and password.

    +

    Parameters

    +
    +
    username : str
    +
    Username to log in with.
    +
    password : str
    +
    Password to log in with.
    +
    token : str, optional
    +
    If you have already solved the captcha, pass it here.
    +
    +

    Returns

    +
    +
    UnsolvedCaptcha or request
    +
     
    +
    Expand source code
    async def user_login(self, username, password, token=None):
    +    """
    +    Authenticates the client with a username and password.
    +
    +    Parameters
    +    ----------
    +    username : str
    +        Username to log in with.
    +    password : str
    +        Password to log in with.
    +    token : str, optional
    +        If you have already solved the captcha, pass it here.
    +
    +    Returns
    +    -------
    +    ro_py.captcha.UnsolvedCaptcha or request
    +    """
         if token:
             login_req = self.requests.back_post(
                 url="https://auth.roblox.com/v2/login",
    @@ -761,7 +842,7 @@ 

    Parameters

    data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" ) captcha_json = captcha_req.json() - return UnsolvedCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    + return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index 0ef268ff..178fc0a1 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -133,7 +133,9 @@

    Inherited members

  • get_group
  • get_user
  • get_user_by_username
  • +
  • token_login
  • trade
  • +
  • user_login
  • diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html index 7a8493ac..6e80283d 100644 --- a/docs/extensions/prompt.html +++ b/docs/extensions/prompt.html @@ -35,9 +35,9 @@

    Module ro_py.extensions.prompt

    import wx +import wxasync from wx import html2 import os -import asyncio import pytweening @@ -107,21 +107,21 @@

    Module ro_py.extensions.prompt

    self.Layout() - self.Bind(wx.EVT_BUTTON, self.login_click, self.log_in_button) - self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) + wxasync.AsyncBind(wx.EVT_BUTTON, self.login_click, self.log_in_button) + wxasync.AsyncBind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) - def login_load(self, event): + async def login_load(self, event): _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}") if token == "undefined": token = False if token: self.web_view.Hide() - lr = asyncio.get_event_loop().run_until_complete(user_login( + lr = await user_login( self.client, self.username, self.password, token - )) + ) if ".ROBLOSECURITY" in self.client.requests.session.cookies: self.status = True self.Close() @@ -132,7 +132,7 @@

    Module ro_py.extensions.prompt

    "Error", wx.OK | wx.ICON_ERROR) self.Close() - def login_click(self, event): + async def login_click(self, event): self.username = self.username_entry.GetValue() self.password = self.password_entry.GetValue() self.username.strip("\n") @@ -169,7 +169,7 @@

    Module ro_py.extensions.prompt

    self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88))) # Runs the user_login function. - fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password)) + fd = await user_login(self.client, self.username, self.password) # Load the captcha URL. if fd: @@ -180,12 +180,52 @@

    Module ro_py.extensions.prompt

    self.Close() -class AuthApp(wx.App): +class RbxCaptcha(wx.Frame): + """ + wx.Frame wrapper for Roblox authentication. + """ + def __init__(self, *args, **kwds): + kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE + wx.Frame.__init__(self, *args, **kwds) + self.SetSize((512, 600)) + self.SetTitle("Roblox Captcha (ro.py)") + self.SetBackgroundColour(wx.Colour(255, 255, 255)) + self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png"))) + + self.status = False + self.token = None + + root_sizer = wx.BoxSizer(wx.VERTICAL) + + self.web_view = wx.html2.WebView.New(self, wx.ID_ANY) + self.web_view.SetSize((512, 600)) + self.web_view.Show() + self.web_view.EnableAccessToDevTools(False) + self.web_view.EnableContextMenu(False) + + root_sizer.Add(self.web_view, 1, wx.EXPAND, 0) + + self.SetSizer(root_sizer) + + self.Layout() + + self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) + + def login_load(self, event): + _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}") + if token == "undefined": + token = False + if token: + self.web_view.Hide() + self.status = True + self.token = token + self.Close() + + +class AuthApp(wxasync.WxAsyncApp): """ wx.App wrapper for Roblox authentication. """ - def __init__(self): - """""" def OnInit(self): self.rbx_login = RbxLogin(None, wx.ID_ANY, "") @@ -194,11 +234,28 @@

    Module ro_py.extensions.prompt

    return True -def authenticate_prompt(client): +class CaptchaApp(wxasync.WxAsyncApp): + """ + wx.App wrapper for Roblox captcha. + """ + + def OnInit(self): + self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "") + self.SetTopWindow(self.rbx_captcha) + self.rbx_captcha.Show() + return True + + +async def authenticate_prompt(client): """ Prompts a login screen. Returns True if the user has sucessfully been authenticated and False if they have not. + Login prompts look like this: + .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_prompt.png + They also display a captcha, which looks very similar to captcha_prompt(): + .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_captcha_prompt.png + Parameters ---------- client : ro_py.client.Client @@ -210,8 +267,30 @@

    Module ro_py.extensions.prompt

    """ app = AuthApp(0) app.rbx_login.client = client - app.MainLoop() - return app.rbx_login.status + await app.MainLoop() + return app.rbx_login.status + + +async def captcha_prompt(unsolved_captcha): + """ + Prompts a captcha solve screen. + First item in tuple is True if the solve was sucessful, and the second item is the token. + + Image will be placed here soon. + + Parameters + ---------- + unsolved_captcha : ro_py.captcha.UnsolvedCaptcha + Captcha to solve. + + Returns + ------ + tuple of bool and str + """ + app = CaptchaApp(0) + app.rbx_captcha.web_view.LoadURL(unsolved_captcha.url) + await app.MainLoop() + return app.rbx_captcha.status, app.rbx_captcha.token
    @@ -222,11 +301,15 @@

    Module ro_py.extensions.prompt

    Functions

    -def authenticate_prompt(client) +async def authenticate_prompt(client)

    Prompts a login screen. Returns True if the user has sucessfully been authenticated and False if they have not.

    +

    Login prompts look like this: +

    +

    They also display a captcha, which looks very similar to captcha_prompt(): +

    Parameters

    client : Client
    @@ -241,11 +324,16 @@

    Returns

    Expand source code -
    def authenticate_prompt(client):
    +
    async def authenticate_prompt(client):
         """
         Prompts a login screen.
         Returns True if the user has sucessfully been authenticated and False if they have not.
     
    +    Login prompts look like this:
    +    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_prompt.png
    +    They also display a captcha, which looks very similar to captcha_prompt():
    +    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_captcha_prompt.png
    +
         Parameters
         ----------
         client : ro_py.client.Client
    @@ -257,10 +345,53 @@ 

    Returns

    """ app = AuthApp(0) app.rbx_login.client = client - app.MainLoop() + await app.MainLoop() return app.rbx_login.status
    +
    +async def captcha_prompt(unsolved_captcha) +
    +
    +

    Prompts a captcha solve screen. +First item in tuple is True if the solve was sucessful, and the second item is the token.

    +

    Image will be placed here soon.

    +

    Parameters

    +
    +
    unsolved_captcha : UnsolvedCaptcha
    +
    Captcha to solve.
    +
    +

    Returns

    +
    +
    tuple of bool and str
    +
     
    +
    +
    + +Expand source code + +
    async def captcha_prompt(unsolved_captcha):
    +    """
    +    Prompts a captcha solve screen.
    +    First item in tuple is True if the solve was sucessful, and the second item is the token.
    +
    +    Image will be placed here soon.
    +
    +    Parameters
    +    ----------
    +    unsolved_captcha : ro_py.captcha.UnsolvedCaptcha
    +        Captcha to solve.
    +
    +    Returns
    +    ------
    +    tuple of bool and str
    +    """
    +    app = CaptchaApp(0)
    +    app.rbx_captcha.web_view.LoadURL(unsolved_captcha.url)
    +    await app.MainLoop()
    +    return app.rbx_captcha.status, app.rbx_captcha.token
    +
    +
    async def user_login(client, username, password, key=None)
    @@ -284,19 +415,43 @@

    Classes

    class AuthApp +(warn_on_cancel_callback=False, loop=None)
    -

    wx.App wrapper for Roblox authentication.

    +

    wx.App wrapper for Roblox authentication.

    +

    Construct a wx.App object.

    +

    :param redirect: Should sys.stdout and sys.stderr be +redirected? +Defaults to False. If filename is None +then output will be redirected to a window that pops up +as needed. +(You can control what kind of window is created +for the output by resetting the class variable +outputWindowClass to a class of your choosing.)

    +

    :param filename: The name of a file to redirect output to, if +redirect is True.

    +

    :param useBestVisual: Should the app try to use the best +available visual provided by the system (only relevant on +systems that have more than one visual.) +This parameter +must be used instead of calling SetUseBestVisual later +on because it must be set before the underlying GUI +toolkit is initialized.

    +

    :param clearSigInt: Should SIGINT be cleared? +This allows the +app to terminate upon a Ctrl-C in the console like other +GUI apps will.

    +

    :note: You should override OnInit to do application +initialization to ensure that the system, toolkit and +wxWidgets are fully initialized.

    Expand source code -
    class AuthApp(wx.App):
    +
    class AuthApp(wxasync.WxAsyncApp):
         """
         wx.App wrapper for Roblox authentication.
         """
    -    def __init__(self):
    -        """"""
     
         def OnInit(self):
             self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
    @@ -306,6 +461,7 @@ 

    Classes

    Ancestors

      +
    • wxasync.WxAsyncApp
    • wx.core.App
    • wx._core.PyApp
    • wx._core.AppConsole
    • @@ -336,6 +492,173 @@

      Methods

    +
    +class CaptchaApp +(warn_on_cancel_callback=False, loop=None) +
    +
    +

    wx.App wrapper for Roblox captcha.

    +

    Construct a wx.App object.

    +

    :param redirect: Should sys.stdout and sys.stderr be +redirected? +Defaults to False. If filename is None +then output will be redirected to a window that pops up +as needed. +(You can control what kind of window is created +for the output by resetting the class variable +outputWindowClass to a class of your choosing.)

    +

    :param filename: The name of a file to redirect output to, if +redirect is True.

    +

    :param useBestVisual: Should the app try to use the best +available visual provided by the system (only relevant on +systems that have more than one visual.) +This parameter +must be used instead of calling SetUseBestVisual later +on because it must be set before the underlying GUI +toolkit is initialized.

    +

    :param clearSigInt: Should SIGINT be cleared? +This allows the +app to terminate upon a Ctrl-C in the console like other +GUI apps will.

    +

    :note: You should override OnInit to do application +initialization to ensure that the system, toolkit and +wxWidgets are fully initialized.

    +
    + +Expand source code + +
    class CaptchaApp(wxasync.WxAsyncApp):
    +    """
    +    wx.App wrapper for Roblox captcha.
    +    """
    +
    +    def OnInit(self):
    +        self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "")
    +        self.SetTopWindow(self.rbx_captcha)
    +        self.rbx_captcha.Show()
    +        return True
    +
    +

    Ancestors

    +
      +
    • wxasync.WxAsyncApp
    • +
    • wx.core.App
    • +
    • wx._core.PyApp
    • +
    • wx._core.AppConsole
    • +
    • wx._core.EvtHandler
    • +
    • wx._core.Object
    • +
    • wx._core.Trackable
    • +
    • wx._core.EventFilter
    • +
    • sip.wrapper
    • +
    • sip.simplewrapper
    • +
    +

    Methods

    +
    +
    +def OnInit(self) +
    +
    +

    OnInit(self) -> bool

    +
    + +Expand source code + +
    def OnInit(self):
    +    self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "")
    +    self.SetTopWindow(self.rbx_captcha)
    +    self.rbx_captcha.Show()
    +    return True
    +
    +
    +
    +
    +
    +class RbxCaptcha +(*args, **kwds) +
    +
    +

    wx.Frame wrapper for Roblox authentication.

    +
    + +Expand source code + +
    class RbxCaptcha(wx.Frame):
    +    """
    +    wx.Frame wrapper for Roblox authentication.
    +    """
    +    def __init__(self, *args, **kwds):
    +        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
    +        wx.Frame.__init__(self, *args, **kwds)
    +        self.SetSize((512, 600))
    +        self.SetTitle("Roblox Captcha (ro.py)")
    +        self.SetBackgroundColour(wx.Colour(255, 255, 255))
    +        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
    +
    +        self.status = False
    +        self.token = None
    +
    +        root_sizer = wx.BoxSizer(wx.VERTICAL)
    +
    +        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
    +        self.web_view.SetSize((512, 600))
    +        self.web_view.Show()
    +        self.web_view.EnableAccessToDevTools(False)
    +        self.web_view.EnableContextMenu(False)
    +
    +        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
    +
    +        self.SetSizer(root_sizer)
    +
    +        self.Layout()
    +
    +        self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
    +
    +    def login_load(self, event):
    +        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
    +        if token == "undefined":
    +            token = False
    +        if token:
    +            self.web_view.Hide()
    +            self.status = True
    +            self.token = token
    +            self.Close()
    +
    +

    Ancestors

    +
      +
    • wx._core.Frame
    • +
    • wx._core.TopLevelWindow
    • +
    • wx._core.NonOwnedWindow
    • +
    • wx._core.Window
    • +
    • wx._core.WindowBase
    • +
    • wx._core.EvtHandler
    • +
    • wx._core.Object
    • +
    • wx._core.Trackable
    • +
    • sip.wrapper
    • +
    • sip.simplewrapper
    • +
    +

    Methods

    +
    +
    +def login_load(self, event) +
    +
    +
    +
    + +Expand source code + +
    def login_load(self, event):
    +    _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
    +    if token == "undefined":
    +        token = False
    +    if token:
    +        self.web_view.Hide()
    +        self.status = True
    +        self.token = token
    +        self.Close()
    +
    +
    +
    +
    class RbxLogin (*args, **kwds) @@ -405,21 +728,21 @@

    Methods

    self.Layout() - self.Bind(wx.EVT_BUTTON, self.login_click, self.log_in_button) - self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) + wxasync.AsyncBind(wx.EVT_BUTTON, self.login_click, self.log_in_button) + wxasync.AsyncBind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view) - def login_load(self, event): + async def login_load(self, event): _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}") if token == "undefined": token = False if token: self.web_view.Hide() - lr = asyncio.get_event_loop().run_until_complete(user_login( + lr = await user_login( self.client, self.username, self.password, token - )) + ) if ".ROBLOSECURITY" in self.client.requests.session.cookies: self.status = True self.Close() @@ -430,7 +753,7 @@

    Methods

    "Error", wx.OK | wx.ICON_ERROR) self.Close() - def login_click(self, event): + async def login_click(self, event): self.username = self.username_entry.GetValue() self.password = self.password_entry.GetValue() self.username.strip("\n") @@ -467,7 +790,7 @@

    Methods

    self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88))) # Runs the user_login function. - fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password)) + fd = await user_login(self.client, self.username, self.password) # Load the captcha URL. if fd: @@ -493,7 +816,7 @@

    Ancestors

    Methods

    -def login_click(self, event) +async def login_click(self, event)
    @@ -501,7 +824,7 @@

    Methods

    Expand source code -
    def login_click(self, event):
    +
    async def login_click(self, event):
         self.username = self.username_entry.GetValue()
         self.password = self.password_entry.GetValue()
         self.username.strip("\n")
    @@ -538,7 +861,7 @@ 

    Methods

    self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88))) # Runs the user_login function. - fd = asyncio.get_event_loop().run_until_complete(user_login(self.client, self.username, self.password)) + fd = await user_login(self.client, self.username, self.password) # Load the captcha URL. if fd: @@ -550,7 +873,7 @@

    Methods

    -def login_load(self, event) +async def login_load(self, event)
    @@ -558,18 +881,18 @@

    Methods

    Expand source code -
    def login_load(self, event):
    +
    async def login_load(self, event):
         _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
         if token == "undefined":
             token = False
         if token:
             self.web_view.Hide()
    -        lr = asyncio.get_event_loop().run_until_complete(user_login(
    +        lr = await user_login(
                 self.client,
                 self.username,
                 self.password,
                 token
    -        ))
    +        )
             if ".ROBLOSECURITY" in self.client.requests.session.cookies:
                 self.status = True
                 self.Close()
    @@ -600,6 +923,7 @@ 

    Index

  • Functions

  • @@ -612,6 +936,18 @@

    CaptchaApp

    + + +
  • +

    RbxCaptcha

    + +
  • +
  • RbxLogin

    • login_click
    • diff --git a/docs/groups.html b/docs/groups.html index 1d8d6c32..c1a0c31f 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -48,7 +48,7 @@

      Module ro_py.groups

      """ def __init__(self, requests, shout_data): self.body = shout_data["body"] - self.poster = User(requests, shout_data["poster"]["userId"]) + self.poster = None # User(requests, shout_data["poster"]["userId"]) class WallPost: @@ -184,6 +184,10 @@

      Module ro_py.groups

      return await member.update() +class PartialGroup(Group): + pass + + class Member(User): """ Represents a user in a group. @@ -206,14 +210,109 @@

      Module ro_py.groups

      self.role = role self.group = group + async def update_role(self, user): + """ + Updates the role information of the user. + + Returns + ------- + ro_py.roles.Role + """ + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + for role in data['data']: + if role['group']['id'] == self.group.id: + self.role = Role(self.requests, self.group, role['role']) + break + return self.role + + async def change_rank(self, num): + """ + Changes the users rank specified by a number. + If num is 1 the users role will go up by 1. + If num is -1 the users role will go down by 1. + + Parameters + ---------- + num : int + How much to change the rank by. + """ + await self.update_role() + roles = await self.group.get_roles() + role_counter = -1 + for group_role in roles: + role_counter += 1 + if group_role.id == self.role.id: + break + if not roles: + raise NotFound(f"User {self.id} is not in group {self.group.id}") + return await self.setrank(roles[role_counter + num].id) + async def promote(self): - pass + """ + Promotes the user. + + Returns + ------- + int + """ + return await self.change_rank(1) async def demote(self): - pass + """ + Demotes the user. + + Returns + ------- + int + """ + return await self.change_rank(-1) + + async def setrank(self, rank): + """ + Sets the users role to specified role using rank id. + + Parameters + ---------- + rank : int + Rank id + + Returns + ------- + bool + """ + rank_request = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}", + data={ + "roleId": rank + } + ) + return rank_request.status == 200 + + async def setrole(self, role_num): + """ + Sets the users role to specified role using role number (1-255). + + Parameters + ---------- + role_num : int + Role number (1-255) - async def setrank(self): - pass
  • + Returns + ------- + bool + """ + roles = await self.group.get_roles() + rank_role = None + for role in roles: + if role.role == role_num: + rank_role = role + break + if not rank_role: + raise NotFound(f"Role {role_num} not found") + return await self.setrank(rank_role.id)
    @@ -359,6 +458,10 @@

    Classes

    member = Member(self.requests, roblox_id, None, self, role) return await member.update()
    +

    Subclasses

    +

    Methods

    @@ -561,14 +664,109 @@

    Parameters

    self.role = role self.group = group + async def update_role(self, user): + """ + Updates the role information of the user. + + Returns + ------- + ro_py.roles.Role + """ + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + for role in data['data']: + if role['group']['id'] == self.group.id: + self.role = Role(self.requests, self.group, role['role']) + break + return self.role + + async def change_rank(self, num): + """ + Changes the users rank specified by a number. + If num is 1 the users role will go up by 1. + If num is -1 the users role will go down by 1. + + Parameters + ---------- + num : int + How much to change the rank by. + """ + await self.update_role() + roles = await self.group.get_roles() + role_counter = -1 + for group_role in roles: + role_counter += 1 + if group_role.id == self.role.id: + break + if not roles: + raise NotFound(f"User {self.id} is not in group {self.group.id}") + return await self.setrank(roles[role_counter + num].id) + async def promote(self): - pass + """ + Promotes the user. + + Returns + ------- + int + """ + return await self.change_rank(1) async def demote(self): - pass + """ + Demotes the user. + + Returns + ------- + int + """ + return await self.change_rank(-1) + + async def setrank(self, rank): + """ + Sets the users role to specified role using rank id. + + Parameters + ---------- + rank : int + Rank id + + Returns + ------- + bool + """ + rank_request = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}", + data={ + "roleId": rank + } + ) + return rank_request.status == 200 + + async def setrole(self, role_num): + """ + Sets the users role to specified role using role number (1-255). + + Parameters + ---------- + role_num : int + Role number (1-255) - async def setrank(self): - pass
    + Returns + ------- + bool + """ + roles = await self.group.get_roles() + rank_role = None + for role in roles: + if role.role == role_num: + rank_role = role + break + if not rank_role: + raise NotFound(f"Role {role_num} not found") + return await self.setrank(rank_role.id)

    Ancestors

      @@ -576,43 +774,210 @@

      Ancestors

    Methods

    +
    +async def change_rank(self, num) +
    +
    +

    Changes the users rank specified by a number. +If num is 1 the users role will go up by 1. +If num is -1 the users role will go down by 1.

    +

    Parameters

    +
    +
    num : int
    +
    How much to change the rank by.
    +
    +
    + +Expand source code + +
    async def change_rank(self, num):
    +    """
    +    Changes the users rank specified by a number.
    +    If num is 1 the users role will go up by 1.
    +    If num is -1 the users role will go down by 1.
    +
    +    Parameters
    +    ----------
    +    num : int
    +            How much to change the rank by.
    +    """
    +    await self.update_role()
    +    roles = await self.group.get_roles()
    +    role_counter = -1
    +    for group_role in roles:
    +        role_counter += 1
    +        if group_role.id == self.role.id:
    +            break
    +    if not roles:
    +        raise NotFound(f"User {self.id} is not in group {self.group.id}")
    +    return await self.setrank(roles[role_counter + num].id)
    +
    +
    async def demote(self)
    -
    +

    Demotes the user.

    +

    Returns

    +
    +
    int
    +
     
    +
    Expand source code
    async def demote(self):
    -    pass
    + """ + Demotes the user. + + Returns + ------- + int + """ + return await self.change_rank(-1)
    async def promote(self)
    -
    +

    Promotes the user.

    +

    Returns

    +
    +
    int
    +
     
    +
    Expand source code
    async def promote(self):
    -    pass
    + """ + Promotes the user. + + Returns + ------- + int + """ + return await self.change_rank(1)
    -async def setrank(self) +async def setrank(self, rank)
    -
    +

    Sets the users role to specified role using rank id.

    +

    Parameters

    +
    +
    rank : int
    +
    Rank id
    +
    +

    Returns

    +
    +
    bool
    +
     
    +
    Expand source code -
    async def setrank(self):
    -    pass
    +
    async def setrank(self, rank):
    +    """
    +    Sets the users role to specified role using rank id.
    +
    +    Parameters
    +    ----------
    +    rank : int
    +            Rank id
    +
    +    Returns
    +    -------
    +    bool
    +    """
    +    rank_request = await self.requests.patch(
    +        url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}",
    +        data={
    +            "roleId": rank
    +        }
    +    )
    +    return rank_request.status == 200
    +
    +
    +
    +async def setrole(self, role_num) +
    +
    +

    Sets the users role to specified role using role number (1-255).

    +

    Parameters

    +
    +
    role_num : int
    +
    Role number (1-255)
    +
    +

    Returns

    +
    +
    bool
    +
     
    +
    +
    + +Expand source code + +
    async def setrole(self, role_num):
    +    """
    +     Sets the users role to specified role using role number (1-255).
    +
    +     Parameters
    +     ----------
    +     role_num : int
    +            Role number (1-255)
    +
    +     Returns
    +     -------
    +     bool
    +     """
    +    roles = await self.group.get_roles()
    +    rank_role = None
    +    for role in roles:
    +        if role.role == role_num:
    +            rank_role = role
    +            break
    +    if not rank_role:
    +        raise NotFound(f"Role {role_num} not found")
    +    return await self.setrank(rank_role.id)
    +
    +
    +
    +async def update_role(self, user) +
    +
    +

    Updates the role information of the user.

    +

    Returns

    +
    +
    Role
    +
     
    +
    +
    + +Expand source code + +
    async def update_role(self, user):
    +    """
    +    Updates the role information of the user.
    +
    +    Returns
    +    -------
    +    ro_py.roles.Role
    +    """
    +    member_req = await self.requests.get(
    +        url=endpoint + f"/v2/users/{user.id}/groups/roles"
    +    )
    +    data = member_req.json()
    +    for role in data['data']:
    +        if role['group']['id'] == self.group.id:
    +            self.role = Role(self.requests, self.group, role['role'])
    +            break
    +    return self.role
    @@ -631,6 +996,34 @@

    Inherited members

    +
    +class PartialGroup +(requests, group_id) +
    +
    +

    Represents a group.

    +
    + +Expand source code + +
    class PartialGroup(Group):
    +    pass
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    class Shout (requests, shout_data) @@ -647,7 +1040,7 @@

    Inherited members

    """ def __init__(self, requests, shout_data): self.body = shout_data["body"] - self.poster = User(requests, shout_data["poster"]["userId"])
    + self.poster = None # User(requests, shout_data["poster"]["userId"])
    @@ -732,13 +1125,19 @@

    Group
  • Member

    -
      +
    • +

      PartialGroup

      +
    • +
    • Shout

    • diff --git a/docs/roles.html b/docs/roles.html index 067f239a..4b9995f1 100644 --- a/docs/roles.html +++ b/docs/roles.html @@ -94,7 +94,6 @@

      Module ro_py.roles

      class Role: """ Represents a role - This is only available for authenticated clients as it cannot be accessed otherwise. Parameters ---------- @@ -253,8 +252,7 @@

      Classes

      (requests, group, role_data)
  • -

    Represents a role -This is only available for authenticated clients as it cannot be accessed otherwise.

    +

    Represents a role

    Parameters

    requests : Requests
    @@ -271,7 +269,6 @@

    Parameters

    class Role:
         """
         Represents a role
    -    This is only available for authenticated clients as it cannot be accessed otherwise.
     
         Parameters
         ----------
    diff --git a/docs/users.html b/docs/users.html
    index b824b3fd..6ccf1b9b 100644
    --- a/docs/users.html
    +++ b/docs/users.html
    @@ -34,6 +34,7 @@ 

    Module ro_py.users

    """ from ro_py.robloxbadges import RobloxBadge +# from ro_py.groups import PartialGroup import iso8601 endpoint = "https://users.roblox.com/" @@ -138,6 +139,19 @@

    Module ro_py.users

    ) return friends_list + """ + async def get_groups(self): + member_req = await self.requests.get( + url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" + ) + data = member_req.json() + groups = [] + for group in data['data']: + group = group['group'] + groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount'])) + return groups + """ + class PartialUser(User): """ @@ -307,7 +321,20 @@

    Parameters

    friends_list.append( User(self.requests, friend_raw["id"]) ) - return friends_list
    + return friends_list + + """ + async def get_groups(self): + member_req = await self.requests.get( + url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" + ) + data = member_req.json() + groups = [] + for group in data['data']: + group = group['group'] + groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount'])) + return groups + """

    Subclasses

      diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 209add62..44a66c45 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -28,6 +28,7 @@

      Module ro_py.utilities.requests

      from ro_py.utilities.errors import ApiError
       from ro_py.utilities.cache import Cache
      +from ro_py.captcha import CaptchaMetadata
       from json.decoder import JSONDecodeError
       from cachecontrol import CacheControl
       import requests_async
      @@ -179,7 +180,14 @@ 

      Module ro_py.utilities.requests

      else: return delete_request - raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
      + raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") + + async def get_captcha_metadata(self): + captcha_meta_req = await self.get( + url="https://apis.roblox.com/captcha/v1/metadata" + ) + captcha_meta_raw = captcha_meta_req.json() + return CaptchaMetadata(captcha_meta_raw)
    @@ -353,7 +361,14 @@

    Parameters

    else: return delete_request - raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") + raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") + + async def get_captcha_metadata(self): + captcha_meta_req = await self.get( + url="https://apis.roblox.com/captcha/v1/metadata" + ) + captcha_meta_raw = captcha_meta_req.json() + return CaptchaMetadata(captcha_meta_raw)

    Instance variables

    @@ -462,6 +477,23 @@

    Methods

    raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") +
    +async def get_captcha_metadata(self) +
    +
    +
    +
    + +Expand source code + +
    async def get_captcha_metadata(self):
    +    captcha_meta_req = await self.get(
    +        url="https://apis.roblox.com/captcha/v1/metadata"
    +    )
    +    captcha_meta_raw = captcha_meta_req.json()
    +    return CaptchaMetadata(captcha_meta_raw)
    +
    +
    async def patch(self, *args, **kwargs)
    @@ -560,11 +592,12 @@

    Index

    -
    - -Expand source code - -
    class AccountInformation:
    -    """
    -    Represents authenticated client account information (https://accountinformation.roblox.com/)
    -    This is only available for authenticated clients as it cannot be accessed otherwise.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    """
    -    def __init__(self, requests):
    -        self.requests = requests
    -        self.account_information_metadata = None
    -        self.promotion_channels = None
    -
    -    async def update(self):
    -        """
    -        Updates the account information.
    -        """
    -        account_information_req = await self.requests.get(
    -            url="https://accountinformation.roblox.com/v1/metadata"
    -        )
    -        self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
    -        promotion_channels_req = await self.requests.get(
    -            url="https://accountinformation.roblox.com/v1/promotion-channels"
    -        )
    -        self.promotion_channels = PromotionChannels(promotion_channels_req.json())
    -
    -    async def get_gender(self):
    -        """
    -        Gets the user's gender.
    -
    -        Returns
    -        -------
    -        ro_py.gender.RobloxGender
    -        """
    -        gender_req = await self.requests.get(endpoint + "v1/gender")
    -        return RobloxGender(gender_req.json()["gender"])
    -
    -    async def set_gender(self, gender):
    -        """
    -        Sets the user's gender.
    -
    -        Parameters
    -        ----------
    -        gender : ro_py.gender.RobloxGender
    -        """
    -        await self.requests.post(
    -            url=endpoint + "v1/gender",
    -            data={
    -                "gender": str(gender.value)
    -            }
    -        )
    -
    -    async def get_birthdate(self):
    -        """
    -        Grabs the user's birthdate.
    -
    -        Returns
    -        -------
    -        datetime.datetime
    -        """
    -        birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
    -        birthdate_raw = birthdate_req.json()
    -        birthdate = datetime(
    -            year=birthdate_raw["birthYear"],
    -            month=birthdate_raw["birthMonth"],
    -            day=birthdate_raw["birthDay"]
    -        )
    -        return birthdate
    -
    -    async def set_birthdate(self, birthdate):
    -        """
    -        Sets the user's birthdate.
    -
    -        Parameters
    -        ----------
    -        birthdate : datetime.datetime
    -        """
    -        await self.requests.post(
    -            url=endpoint + "v1/birthdate",
    -            data={
    -              "birthMonth": birthdate.month,
    -              "birthDay": birthdate.day,
    -              "birthYear": birthdate.year
    -            }
    -        )
    -

    Methods

    @@ -289,27 +57,6 @@

    Returns

    datetime.datetime
     
    -
    - -Expand source code - -
    async def get_birthdate(self):
    -    """
    -    Grabs the user's birthdate.
    -
    -    Returns
    -    -------
    -    datetime.datetime
    -    """
    -    birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
    -    birthdate_raw = birthdate_req.json()
    -    birthdate = datetime(
    -        year=birthdate_raw["birthYear"],
    -        month=birthdate_raw["birthMonth"],
    -        day=birthdate_raw["birthDay"]
    -    )
    -    return birthdate
    -
    async def get_gender(self) @@ -321,21 +68,6 @@

    Returns

    RobloxGender
     
    -
    - -Expand source code - -
    async def get_gender(self):
    -    """
    -    Gets the user's gender.
    -
    -    Returns
    -    -------
    -    ro_py.gender.RobloxGender
    -    """
    -    gender_req = await self.requests.get(endpoint + "v1/gender")
    -    return RobloxGender(gender_req.json()["gender"])
    -
    async def set_birthdate(self, birthdate) @@ -347,27 +79,6 @@

    Parameters

    birthdate : datetime.datetime
     
    -
    - -Expand source code - -
    async def set_birthdate(self, birthdate):
    -    """
    -    Sets the user's birthdate.
    -
    -    Parameters
    -    ----------
    -    birthdate : datetime.datetime
    -    """
    -    await self.requests.post(
    -        url=endpoint + "v1/birthdate",
    -        data={
    -          "birthMonth": birthdate.month,
    -          "birthDay": birthdate.day,
    -          "birthYear": birthdate.year
    -        }
    -    )
    -
    async def set_gender(self, gender) @@ -379,48 +90,12 @@

    Parameters

    gender : RobloxGender
     
    -
    - -Expand source code - -
    async def set_gender(self, gender):
    -    """
    -    Sets the user's gender.
    -
    -    Parameters
    -    ----------
    -    gender : ro_py.gender.RobloxGender
    -    """
    -    await self.requests.post(
    -        url=endpoint + "v1/gender",
    -        data={
    -            "gender": str(gender.value)
    -        }
    -    )
    -
    async def update(self)

    Updates the account information.

    -
    - -Expand source code - -
    async def update(self):
    -    """
    -    Updates the account information.
    -    """
    -    account_information_req = await self.requests.get(
    -        url="https://accountinformation.roblox.com/v1/metadata"
    -    )
    -    self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
    -    promotion_channels_req = await self.requests.get(
    -        url="https://accountinformation.roblox.com/v1/promotion-channels"
    -    )
    -    self.promotion_channels = PromotionChannels(promotion_channels_req.json())
    -
    @@ -430,28 +105,6 @@

    Parameters

    Represents account information metadata.

    -
    - -Expand source code - -
    class AccountInformationMetadata:
    -    """
    -    Represents account information metadata.
    -    """
    -    def __init__(self, metadata_raw):
    -        self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"]
    -        """Unsure what this does."""
    -        self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"]
    -        """Whether the account settings policy is enabled (unsure exactly what this does)"""
    -        self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"]
    -        """Whether the user's linked phone number is enabled."""
    -        self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"]
    -        """Maximum length of the user's description."""
    -        self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"]
    -        """Whether the user's description is enabled."""
    -        self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"]
    -        """Whether the UserBlock endpoints are updated (unsure exactly what this does)"""
    -

    Instance variables

    var is_account_settings_policy_enabled
    @@ -486,26 +139,6 @@

    Instance variables

    Represents account information promotion channels.

    -
    - -Expand source code - -
    class PromotionChannels:
    -    """
    -    Represents account information promotion channels.
    -    """
    -    def __init__(self, promotion_raw):
    -        self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"]
    -        """Visibility of promotion channels."""
    -        self.facebook = promotion_raw["facebook"]
    -        """Link to the user's Facebook page."""
    -        self.twitter = promotion_raw["twitter"]
    -        """Link to the user's Twitter page."""
    -        self.youtube = promotion_raw["youtube"]
    -        """Link to the user's YouTube page."""
    -        self.twitch = promotion_raw["twitch"]
    -        """Link to the user's Twitch page."""
    -

    Instance variables

    var facebook
    diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 4b224421..e2f0fcb9 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -23,94 +23,6 @@

    Module ro_py.accountsettings

    This file houses functions and classes that pertain to Roblox client settings.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to Roblox client settings.
    -
    -"""
    -
    -import enum
    -
    -endpoint = "https://accountsettings.roblox.com/"
    -
    -
    -class PrivacyLevel(enum.Enum):
    -    """
    -    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.
    -    """
    -    no_one = "NoOne"
    -    friends = "Friends"
    -    everyone = "AllUsers"
    -
    -
    -class PrivacySettings(enum.Enum):
    -    """
    -    Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.
    -    """
    -    app_chat_privacy = 0
    -    game_chat_privacy = 1
    -    inventory_privacy = 2
    -    phone_discovery = 3
    -    phone_discovery_enabled = 4
    -    private_message_privacy = 5
    -
    -
    -class RobloxEmail:
    -    """
    -    Represents an obfuscated version of the email you have set on your account.
    -
    -    Parameters
    -    ----------
    -    email_data : dict
    -        Raw data to parse from.
    -    """
    -    def __init__(self, email_data: dict):
    -        self.email_address = email_data["emailAddress"]
    -        self.verified = email_data["verified"]
    -
    -
    -class AccountSettings:
    -    """
    -    Represents authenticated client account settings (https://accountsettings.roblox.com/)
    -    This is only available for authenticated clients as it cannot be accessed otherwise.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    """
    -    def __init__(self, requests):
    -        self.requests = requests
    -
    -    def get_privacy_setting(self, privacy_setting):
    -        """
    -        Gets the value of a privacy setting.
    -        """
    -        privacy_setting = privacy_setting.value
    -        privacy_endpoint = [
    -            "app-chat-privacy",
    -            "game-chat-privacy",
    -            "inventory-privacy",
    -            "privacy",
    -            "privacy/info",
    -            "private-message-privacy"
    -        ][privacy_setting]
    -        privacy_key = [
    -            "appChatPrivacy",
    -            "gameChatPrivacy",
    -            "inventoryPrivacy",
    -            "phoneDiscovery",
    -            "isPhoneDiscoveryEnabled",
    -            "privateMessagePrivacy"
    -        ][privacy_setting]
    -        privacy_endpoint = endpoint + "v1/" + privacy_endpoint
    -        privacy_req = self.requests.get(privacy_endpoint)
    -        return privacy_req.json()[privacy_key]
    -
    @@ -133,48 +45,6 @@

    Parameters

    requests : Requests
    Requests object to use for API requests.
    -
    - -Expand source code - -
    class AccountSettings:
    -    """
    -    Represents authenticated client account settings (https://accountsettings.roblox.com/)
    -    This is only available for authenticated clients as it cannot be accessed otherwise.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    """
    -    def __init__(self, requests):
    -        self.requests = requests
    -
    -    def get_privacy_setting(self, privacy_setting):
    -        """
    -        Gets the value of a privacy setting.
    -        """
    -        privacy_setting = privacy_setting.value
    -        privacy_endpoint = [
    -            "app-chat-privacy",
    -            "game-chat-privacy",
    -            "inventory-privacy",
    -            "privacy",
    -            "privacy/info",
    -            "private-message-privacy"
    -        ][privacy_setting]
    -        privacy_key = [
    -            "appChatPrivacy",
    -            "gameChatPrivacy",
    -            "inventoryPrivacy",
    -            "phoneDiscovery",
    -            "isPhoneDiscoveryEnabled",
    -            "privateMessagePrivacy"
    -        ][privacy_setting]
    -        privacy_endpoint = endpoint + "v1/" + privacy_endpoint
    -        privacy_req = self.requests.get(privacy_endpoint)
    -        return privacy_req.json()[privacy_key]
    -

    Methods

    @@ -182,35 +52,6 @@

    Methods

    Gets the value of a privacy setting.

    -
    - -Expand source code - -
    def get_privacy_setting(self, privacy_setting):
    -    """
    -    Gets the value of a privacy setting.
    -    """
    -    privacy_setting = privacy_setting.value
    -    privacy_endpoint = [
    -        "app-chat-privacy",
    -        "game-chat-privacy",
    -        "inventory-privacy",
    -        "privacy",
    -        "privacy/info",
    -        "private-message-privacy"
    -    ][privacy_setting]
    -    privacy_key = [
    -        "appChatPrivacy",
    -        "gameChatPrivacy",
    -        "inventoryPrivacy",
    -        "phoneDiscovery",
    -        "isPhoneDiscoveryEnabled",
    -        "privateMessagePrivacy"
    -    ][privacy_setting]
    -    privacy_endpoint = endpoint + "v1/" + privacy_endpoint
    -    privacy_req = self.requests.get(privacy_endpoint)
    -    return privacy_req.json()[privacy_key]
    -
    @@ -220,18 +61,6 @@

    Methods

    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.

    -
    - -Expand source code - -
    class PrivacyLevel(enum.Enum):
    -    """
    -    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.
    -    """
    -    no_one = "NoOne"
    -    friends = "Friends"
    -    everyone = "AllUsers"
    -

    Ancestors

    • enum.Enum
    • @@ -258,21 +87,6 @@

      Class variables

      Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.

      -
      - -Expand source code - -
      class PrivacySettings(enum.Enum):
      -    """
      -    Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.
      -    """
      -    app_chat_privacy = 0
      -    game_chat_privacy = 1
      -    inventory_privacy = 2
      -    phone_discovery = 3
      -    phone_discovery_enabled = 4
      -    private_message_privacy = 5
      -

      Ancestors

      • enum.Enum
      • @@ -316,23 +130,6 @@

        Parameters

        email_data : dict
        Raw data to parse from.
    -
    - -Expand source code - -
    class RobloxEmail:
    -    """
    -    Represents an obfuscated version of the email you have set on your account.
    -
    -    Parameters
    -    ----------
    -    email_data : dict
    -        Raw data to parse from.
    -    """
    -    def __init__(self, email_data: dict):
    -        self.email_address = email_data["emailAddress"]
    -        self.verified = email_data["verified"]
    -
    diff --git a/docs/assets.html b/docs/assets.html index d9bba1d9..68209195 100644 --- a/docs/assets.html +++ b/docs/assets.html @@ -23,127 +23,6 @@

    Module ro_py.assets

    This file houses functions and classes that pertain to Roblox assets.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to Roblox assets.
    -
    -"""
    -
    -# from ro_py.users import User
    -# from ro_py.groups import Group
    -from ro_py.utilities.errors import NotLimitedError
    -from ro_py.economy import LimitedResaleData
    -from ro_py.utilities.asset_type import asset_types
    -import iso8601
    -
    -endpoint = "https://api.roblox.com/"
    -
    -
    -class Asset:
    -    """
    -    Represents an asset.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    asset_id
    -        ID of the asset.
    -    """
    -
    -    def __init__(self, requests, asset_id):
    -        self.id = asset_id
    -        self.requests = requests
    -        self.target_id = None
    -        self.product_type = None
    -        self.asset_id = None
    -        self.product_id = None
    -        self.name = None
    -        self.description = None
    -        self.asset_type_id = None
    -        self.asset_type_name = None
    -        self.creator = None
    -        self.created = None
    -        self.updated = None
    -        self.price = None
    -        self.is_new = None
    -        self.is_for_sale = None
    -        self.is_public_domain = None
    -        self.is_limited = None
    -        self.is_limited_unique = None
    -        self.minimum_membership_level = None
    -        self.content_rating_type_id = None
    -
    -    async def update(self):
    -        """
    -        Updates the asset's information.
    -        """
    -        asset_info_req = await self.requests.get(
    -            url=endpoint + "marketplace/productinfo",
    -            params={
    -                "assetId": self.id
    -            }
    -        )
    -        asset_info = asset_info_req.json()
    -        self.target_id = asset_info["TargetId"]
    -        self.product_type = asset_info["ProductType"]
    -        self.asset_id = asset_info["AssetId"]
    -        self.product_id = asset_info["ProductId"]
    -        self.name = asset_info["Name"]
    -        self.description = asset_info["Description"]
    -        self.asset_type_id = asset_info["AssetTypeId"]
    -        self.asset_type_name = asset_types[self.asset_type_id]
    -        # if asset_info["Creator"]["CreatorType"] == "User":
    -        #     self.creator = User(self.requests, asset_info["Creator"]["Id"])
    -        # elif asset_info["Creator"]["CreatorType"] == "Group":
    -        #     self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
    -        self.created = iso8601.parse_date(asset_info["Created"])
    -        self.updated = iso8601.parse_date(asset_info["Updated"])
    -        self.price = asset_info["PriceInRobux"]
    -        self.is_new = asset_info["IsNew"]
    -        self.is_for_sale = asset_info["IsForSale"]
    -        self.is_public_domain = asset_info["IsPublicDomain"]
    -        self.is_limited = asset_info["IsLimited"]
    -        self.is_limited_unique = asset_info["IsLimitedUnique"]
    -        self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
    -        self.content_rating_type_id = asset_info["ContentRatingTypeId"]
    -
    -    async def get_remaining(self):
    -        """
    -        Gets the remaining amount of this asset. (used for Limited U items)
    -
    -        Returns
    -        -------
    -        int
    -        """
    -        asset_info_req = await self.requests.get(
    -            url=endpoint + "marketplace/productinfo",
    -            params={
    -                "assetId": self.asset_id
    -            }
    -        )
    -        asset_info = asset_info_req.json()
    -        return asset_info["Remaining"]
    -
    -    async def get_limited_resale_data(self):
    -        """
    -        Gets the limited resale data
    -
    -        Returns
    -        -------
    -        LimitedResaleData
    -        """
    -        if self.is_limited:
    -            resale_data_req = await self.requests.get(
    -                f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
    -            return LimitedResaleData(resale_data_req.json())
    -        else:
    -            raise NotLimitedError("You can only read this information on limited items.")
    -
    @@ -167,111 +46,10 @@

    Parameters

    asset_id
    ID of the asset.
    -
    - -Expand source code - -
    class Asset:
    -    """
    -    Represents an asset.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    asset_id
    -        ID of the asset.
    -    """
    -
    -    def __init__(self, requests, asset_id):
    -        self.id = asset_id
    -        self.requests = requests
    -        self.target_id = None
    -        self.product_type = None
    -        self.asset_id = None
    -        self.product_id = None
    -        self.name = None
    -        self.description = None
    -        self.asset_type_id = None
    -        self.asset_type_name = None
    -        self.creator = None
    -        self.created = None
    -        self.updated = None
    -        self.price = None
    -        self.is_new = None
    -        self.is_for_sale = None
    -        self.is_public_domain = None
    -        self.is_limited = None
    -        self.is_limited_unique = None
    -        self.minimum_membership_level = None
    -        self.content_rating_type_id = None
    -
    -    async def update(self):
    -        """
    -        Updates the asset's information.
    -        """
    -        asset_info_req = await self.requests.get(
    -            url=endpoint + "marketplace/productinfo",
    -            params={
    -                "assetId": self.id
    -            }
    -        )
    -        asset_info = asset_info_req.json()
    -        self.target_id = asset_info["TargetId"]
    -        self.product_type = asset_info["ProductType"]
    -        self.asset_id = asset_info["AssetId"]
    -        self.product_id = asset_info["ProductId"]
    -        self.name = asset_info["Name"]
    -        self.description = asset_info["Description"]
    -        self.asset_type_id = asset_info["AssetTypeId"]
    -        self.asset_type_name = asset_types[self.asset_type_id]
    -        # if asset_info["Creator"]["CreatorType"] == "User":
    -        #     self.creator = User(self.requests, asset_info["Creator"]["Id"])
    -        # elif asset_info["Creator"]["CreatorType"] == "Group":
    -        #     self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
    -        self.created = iso8601.parse_date(asset_info["Created"])
    -        self.updated = iso8601.parse_date(asset_info["Updated"])
    -        self.price = asset_info["PriceInRobux"]
    -        self.is_new = asset_info["IsNew"]
    -        self.is_for_sale = asset_info["IsForSale"]
    -        self.is_public_domain = asset_info["IsPublicDomain"]
    -        self.is_limited = asset_info["IsLimited"]
    -        self.is_limited_unique = asset_info["IsLimitedUnique"]
    -        self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
    -        self.content_rating_type_id = asset_info["ContentRatingTypeId"]
    -
    -    async def get_remaining(self):
    -        """
    -        Gets the remaining amount of this asset. (used for Limited U items)
    -
    -        Returns
    -        -------
    -        int
    -        """
    -        asset_info_req = await self.requests.get(
    -            url=endpoint + "marketplace/productinfo",
    -            params={
    -                "assetId": self.asset_id
    -            }
    -        )
    -        asset_info = asset_info_req.json()
    -        return asset_info["Remaining"]
    -
    -    async def get_limited_resale_data(self):
    -        """
    -        Gets the limited resale data
    -
    -        Returns
    -        -------
    -        LimitedResaleData
    -        """
    -        if self.is_limited:
    -            resale_data_req = await self.requests.get(
    -                f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
    -            return LimitedResaleData(resale_data_req.json())
    -        else:
    -            raise NotLimitedError("You can only read this information on limited items.")
    -
    +

    Subclasses

    +

    Methods

    @@ -284,25 +62,6 @@

    Returns

    LimitedResaleData
     
    -
    - -Expand source code - -
    async def get_limited_resale_data(self):
    -    """
    -    Gets the limited resale data
    -
    -    Returns
    -    -------
    -    LimitedResaleData
    -    """
    -    if self.is_limited:
    -        resale_data_req = await self.requests.get(
    -            f"https://economy.roblox.com/v1/assets/{self.asset_id}/resale-data")
    -        return LimitedResaleData(resale_data_req.json())
    -    else:
    -        raise NotLimitedError("You can only read this information on limited items.")
    -
    async def get_remaining(self) @@ -314,74 +73,43 @@

    Returns

    int
     
    -
    - -Expand source code - -
    async def get_remaining(self):
    -    """
    -    Gets the remaining amount of this asset. (used for Limited U items)
    -
    -    Returns
    -    -------
    -    int
    -    """
    -    asset_info_req = await self.requests.get(
    -        url=endpoint + "marketplace/productinfo",
    -        params={
    -            "assetId": self.asset_id
    -        }
    -    )
    -    asset_info = asset_info_req.json()
    -    return asset_info["Remaining"]
    -
    async def update(self)

    Updates the asset's information.

    -
    - -Expand source code - -
    async def update(self):
    -    """
    -    Updates the asset's information.
    -    """
    -    asset_info_req = await self.requests.get(
    -        url=endpoint + "marketplace/productinfo",
    -        params={
    -            "assetId": self.id
    -        }
    -    )
    -    asset_info = asset_info_req.json()
    -    self.target_id = asset_info["TargetId"]
    -    self.product_type = asset_info["ProductType"]
    -    self.asset_id = asset_info["AssetId"]
    -    self.product_id = asset_info["ProductId"]
    -    self.name = asset_info["Name"]
    -    self.description = asset_info["Description"]
    -    self.asset_type_id = asset_info["AssetTypeId"]
    -    self.asset_type_name = asset_types[self.asset_type_id]
    -    # if asset_info["Creator"]["CreatorType"] == "User":
    -    #     self.creator = User(self.requests, asset_info["Creator"]["Id"])
    -    # elif asset_info["Creator"]["CreatorType"] == "Group":
    -    #     self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
    -    self.created = iso8601.parse_date(asset_info["Created"])
    -    self.updated = iso8601.parse_date(asset_info["Updated"])
    -    self.price = asset_info["PriceInRobux"]
    -    self.is_new = asset_info["IsNew"]
    -    self.is_for_sale = asset_info["IsForSale"]
    -    self.is_public_domain = asset_info["IsPublicDomain"]
    -    self.is_limited = asset_info["IsLimited"]
    -    self.is_limited_unique = asset_info["IsLimitedUnique"]
    -    self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
    -    self.content_rating_type_id = asset_info["ContentRatingTypeId"]
    -
    +
    +class UserAsset +(requests, asset_id, user_asset_id) +
    +
    +

    Represents an asset.

    +

    Parameters

    +
    +
    requests : Requests
    +
    Requests object to use for API requests.
    +
    asset_id
    +
    ID of the asset.
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    @@ -406,6 +134,9 @@

    Assetupdate +
  • +

    UserAsset

    +
  • diff --git a/docs/badges.html b/docs/badges.html index c692c0a3..5ec6209e 100644 --- a/docs/badges.html +++ b/docs/badges.html @@ -23,68 +23,6 @@

    Module ro_py.badges

    This file houses functions and classes that pertain to game-awarded badges.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to game-awarded badges.
    -
    -"""
    -
    -endpoint = "https://badges.roblox.com/"
    -
    -
    -class BadgeStatistics:
    -    """
    -    Represents a badge's statistics.
    -    """
    -    def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage):
    -        self.past_date_awarded_count = past_date_awarded_count
    -        self.awarded_count = awarded_count
    -        self.win_rate_percentage = win_rate_percentage
    -
    -
    -class Badge:
    -    """
    -    Represents a game-awarded badge.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    badge_id
    -        ID of the badge.
    -    """
    -    def __init__(self, requests, badge_id):
    -        self.id = badge_id
    -        self.requests = requests
    -        self.name = None
    -        self.description = None
    -        self.display_name = None
    -        self.display_description = None
    -        self.enabled = None
    -        self.statistics = None
    -
    -    async def update(self):
    -        """
    -        Updates the badge's information.
    -        """
    -        badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}")
    -        badge_info = badge_info_req.json()
    -        self.name = badge_info["name"]
    -        self.description = badge_info["description"]
    -        self.display_name = badge_info["displayName"]
    -        self.display_description = badge_info["displayDescription"]
    -        self.enabled = badge_info["enabled"]
    -        statistics_info = badge_info["statistics"]
    -        self.statistics = BadgeStatistics(
    -            statistics_info["pastDayAwardedCount"],
    -            statistics_info["awardedCount"],
    -            statistics_info["winRatePercentage"]
    -        )
    -
    @@ -108,49 +46,6 @@

    Parameters

    badge_id
    ID of the badge.
    -
    - -Expand source code - -
    class Badge:
    -    """
    -    Represents a game-awarded badge.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    badge_id
    -        ID of the badge.
    -    """
    -    def __init__(self, requests, badge_id):
    -        self.id = badge_id
    -        self.requests = requests
    -        self.name = None
    -        self.description = None
    -        self.display_name = None
    -        self.display_description = None
    -        self.enabled = None
    -        self.statistics = None
    -
    -    async def update(self):
    -        """
    -        Updates the badge's information.
    -        """
    -        badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}")
    -        badge_info = badge_info_req.json()
    -        self.name = badge_info["name"]
    -        self.description = badge_info["description"]
    -        self.display_name = badge_info["displayName"]
    -        self.display_description = badge_info["displayDescription"]
    -        self.enabled = badge_info["enabled"]
    -        statistics_info = badge_info["statistics"]
    -        self.statistics = BadgeStatistics(
    -            statistics_info["pastDayAwardedCount"],
    -            statistics_info["awardedCount"],
    -            statistics_info["winRatePercentage"]
    -        )
    -

    Methods

    @@ -158,28 +53,6 @@

    Methods

    Updates the badge's information.

    -
    - -Expand source code - -
    async def update(self):
    -    """
    -    Updates the badge's information.
    -    """
    -    badge_info_req = await self.requests.get(endpoint + f"v1/badges/{self.id}")
    -    badge_info = badge_info_req.json()
    -    self.name = badge_info["name"]
    -    self.description = badge_info["description"]
    -    self.display_name = badge_info["displayName"]
    -    self.display_description = badge_info["displayDescription"]
    -    self.enabled = badge_info["enabled"]
    -    statistics_info = badge_info["statistics"]
    -    self.statistics = BadgeStatistics(
    -        statistics_info["pastDayAwardedCount"],
    -        statistics_info["awardedCount"],
    -        statistics_info["winRatePercentage"]
    -    )
    -
    @@ -189,19 +62,6 @@

    Methods

    Represents a badge's statistics.

    -
    - -Expand source code - -
    class BadgeStatistics:
    -    """
    -    Represents a badge's statistics.
    -    """
    -    def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage):
    -        self.past_date_awarded_count = past_date_awarded_count
    -        self.awarded_count = awarded_count
    -        self.win_rate_percentage = win_rate_percentage
    -

    diff --git a/docs/captcha.html b/docs/captcha.html index d4ebb497..965a62ea 100644 --- a/docs/captcha.html +++ b/docs/captcha.html @@ -23,40 +23,6 @@

    Module ro_py.captcha

    This file houses functions and classes that pertain to the Roblox captcha.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to the Roblox captcha.
    -
    -"""
    -
    -
    -class UnsolvedLoginCaptcha:
    -    def __init__(self, data, pkey):
    -        self.token = data["token"]
    -        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    -                   f"?pkey={pkey}" \
    -                   f"&session={self.token.split('|')[0]}" \
    -                   f"&lang=en"
    -        self.challenge_url = data["challenge_url"]
    -        self.challenge_url_cdn = data["challenge_url_cdn"]
    -        self.noscript = data["noscript"]
    -
    -
    -class UnsolvedCaptcha:
    -    def __init__(self, pkey):
    -        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    -                   f"?pkey={pkey}" \
    -                   f"&lang=en"
    -
    -
    -class CaptchaMetadata:
    -    def __init__(self, data):
    -        self.fun_captcha_public_keys = data["funCaptchaPublicKeys"]
    -
    @@ -73,14 +39,6 @@

    Classes

    -
    - -Expand source code - -
    class CaptchaMetadata:
    -    def __init__(self, data):
    -        self.fun_captcha_public_keys = data["funCaptchaPublicKeys"]
    -
    class UnsolvedCaptcha @@ -88,16 +46,6 @@

    Classes

    -
    - -Expand source code - -
    class UnsolvedCaptcha:
    -    def __init__(self, pkey):
    -        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    -                   f"?pkey={pkey}" \
    -                   f"&lang=en"
    -
    class UnsolvedLoginCaptcha @@ -105,21 +53,6 @@

    Classes

    -
    - -Expand source code - -
    class UnsolvedLoginCaptcha:
    -    def __init__(self, data, pkey):
    -        self.token = data["token"]
    -        self.url = f"https://roblox-api.arkoselabs.com/fc/api/nojs/" \
    -                   f"?pkey={pkey}" \
    -                   f"&session={self.token.split('|')[0]}" \
    -                   f"&lang=en"
    -        self.challenge_url = data["challenge_url"]
    -        self.challenge_url_cdn = data["challenge_url_cdn"]
    -        self.noscript = data["noscript"]
    -
    diff --git a/docs/catalog.html b/docs/catalog.html index 6e7b1237..430404d0 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -23,32 +23,6 @@

    Module ro_py.catalog

    This file houses functions and classes that pertain to the Roblox catalog.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to the Roblox catalog.
    -
    -"""
    -
    -import enum
    -
    -
    -class AppStore(enum.Enum):
    -    """
    -    Represents an app store that the Roblox app is downloadable on.
    -    """
    -    google_play = "GooglePlay"
    -    android = "GooglePlay"
    -    amazon = "Amazon"
    -    fire = "Amazon"
    -    ios = "iOS"
    -    iphone = "iOS"
    -    idevice = "iOS"
    -    xbox = "Xbox"
    -
    @@ -65,23 +39,6 @@

    Classes

    Represents an app store that the Roblox app is downloadable on.

    -
    - -Expand source code - -
    class AppStore(enum.Enum):
    -    """
    -    Represents an app store that the Roblox app is downloadable on.
    -    """
    -    google_play = "GooglePlay"
    -    android = "GooglePlay"
    -    amazon = "Amazon"
    -    fire = "Amazon"
    -    ios = "iOS"
    -    iphone = "iOS"
    -    idevice = "iOS"
    -    xbox = "Xbox"
    -

    Ancestors

    • enum.Enum
    • diff --git a/docs/chat.html b/docs/chat.html index 3a636faa..d7a3101e 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -23,184 +23,6 @@

      Module ro_py.chat

      This file houses functions and classes that pertain to chatting and messaging.

      -
      - -Expand source code - -
      """
      -
      -This file houses functions and classes that pertain to chatting and messaging.
      -
      -"""
      -
      -from ro_py.utilities.errors import ChatError
      -from ro_py.users import User
      -
      -endpoint = "https://chat.roblox.com/"
      -
      -
      -class ChatSettings:
      -    def __init__(self, settings_data):
      -        self.enabled = settings_data["chatEnabled"]
      -        self.is_active_chat_user = settings_data["isActiveChatUser"]
      -
      -
      -class ConversationTyping:
      -    def __init__(self, requests, conversation_id):
      -        self.requests = requests
      -        self.id = conversation_id
      -
      -    async def __aenter__(self):
      -        await self.requests.post(
      -            url=endpoint + "v2/update-user-typing-status",
      -            data={
      -                "conversationId": self.id,
      -                "isTyping": "true"
      -            }
      -        )
      -
      -    async def __aexit__(self, *args, **kwargs):
      -        await self.requests.post(
      -            url=endpoint + "v2/update-user-typing-status",
      -            data={
      -                "conversationId": self.id,
      -                "isTyping": "false"
      -            }
      -        )
      -
      -
      -class Conversation:
      -    def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
      -        self.requests = requests
      -        self.raw = raw
      -        self.id = None
      -        self.title = None
      -        self.initiator = None
      -        self.type = None
      -        self.typing = ConversationTyping(self.requests, conversation_id)
      -
      -        if self.raw:
      -            data = raw_data
      -            self.id = data["id"]
      -            self.title = data["title"]
      -            self.initiator = User(self.requests, data["initiator"]["targetId"])
      -            self.type = data["conversationType"]
      -            self.typing = ConversationTyping(self.requests, conversation_id)
      -
      -    async def update(self):
      -        conversation_req = await self.requests.get(
      -            url="https://chat.roblox.com/v2/get-conversations",
      -            params={
      -                "conversationIds": self.id
      -            }
      -        )
      -        data = conversation_req.json()[0]
      -        self.id = data["id"]
      -        self.title = data["title"]
      -        self.initiator = User(self.requests, data["initiator"]["targetId"])
      -        self.type = data["conversationType"]
      -        self.typing = ConversationTyping(self.requests, conversation_id)
      -
      -    async def get_message(self, message_id):
      -        return Message(self.requests, message_id, self.id)
      -
      -    async def send_message(self, content):
      -        send_message_req = await self.requests.post(
      -            url=endpoint + "v2/send-message",
      -            data={
      -                "message": content,
      -                "conversationId": self.id
      -            }
      -        )
      -        send_message_json = send_message_req.json()
      -        if send_message_json["sent"]:
      -            return Message(self.requests, send_message_json["messageId"], self.id)
      -        else:
      -            raise ChatError(send_message_json["statusMessage"])
      -
      -
      -class Message:
      -    """
      -    Represents a single message in a chat conversation.
      -
      -    Parameters
      -    ----------
      -    requests : ro_py.utilities.requests.Requests
      -        Requests object to use for API requests.
      -    message_id
      -        ID of the message.
      -    conversation_id
      -        ID of the conversation that contains the message.
      -    """
      -    def __init__(self, requests, message_id, conversation_id):
      -        self.requests = requests
      -        self.id = message_id
      -        self.conversation_id = conversation_id
      -
      -        self.content = None
      -        self.sender = None
      -        self.read = None
      -
      -    async def update(self):
      -        """
      -        Updates the message with new data.
      -        """
      -        message_req = await self.requests.get(
      -            url="https://chat.roblox.com/v2/get-messages",
      -            params={
      -                "conversationId": self.conversation_id,
      -                "pageSize": 1,
      -                "exclusiveStartMessageId": self.id
      -            }
      -        )
      -
      -        message_json = message_req.json()[0]
      -        self.content = message_json["content"]
      -        self.sender = User(self.requests, message_json["senderTargetId"])
      -        self.read = message_json["read"]
      -
      -
      -class ChatWrapper:
      -    """
      -    Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right
      -    of the Roblox web client.
      -    """
      -    def __init__(self, requests):
      -        self.requests = requests
      -
      -    async def get_conversation(self, conversation_id):
      -        """
      -        Gets a conversation by the conversation ID.
      -
      -        Parameters
      -        ----------
      -        conversation_id
      -            ID of the conversation.
      -        """
      -        conversation = Conversation(self.requests, conversation_id)
      -        await conversation.update()
      -
      -    async def get_conversations(self, page_number=1, page_size=10):
      -        """
      -        Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented.
      -        """
      -        conversations_req = await self.requests.get(
      -            url="https://chat.roblox.com/v2/get-user-conversations",
      -            params={
      -                "pageNumber": page_number,
      -                "pageSize": page_size
      -            }
      -        )
      -        conversations_json = conversations_req.json()
      -        conversations = []
      -        for conversation_raw in conversations_json:
      -            conversations.append(Conversation(
      -                requests=self.requests,
      -                raw=True,
      -                raw_data=conversation_raw
      -            ))
      -        return conversations
      -
      @@ -217,15 +39,6 @@

      Classes

      -
      - -Expand source code - -
      class ChatSettings:
      -    def __init__(self, settings_data):
      -        self.enabled = settings_data["chatEnabled"]
      -        self.is_active_chat_user = settings_data["isActiveChatUser"]
      -
      class ChatWrapper @@ -234,51 +47,6 @@

      Classes

      Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right of the Roblox web client.

      -
      - -Expand source code - -
      class ChatWrapper:
      -    """
      -    Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right
      -    of the Roblox web client.
      -    """
      -    def __init__(self, requests):
      -        self.requests = requests
      -
      -    async def get_conversation(self, conversation_id):
      -        """
      -        Gets a conversation by the conversation ID.
      -
      -        Parameters
      -        ----------
      -        conversation_id
      -            ID of the conversation.
      -        """
      -        conversation = Conversation(self.requests, conversation_id)
      -        await conversation.update()
      -
      -    async def get_conversations(self, page_number=1, page_size=10):
      -        """
      -        Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented.
      -        """
      -        conversations_req = await self.requests.get(
      -            url="https://chat.roblox.com/v2/get-user-conversations",
      -            params={
      -                "pageNumber": page_number,
      -                "pageSize": page_size
      -            }
      -        )
      -        conversations_json = conversations_req.json()
      -        conversations = []
      -        for conversation_raw in conversations_json:
      -            conversations.append(Conversation(
      -                requests=self.requests,
      -                raw=True,
      -                raw_data=conversation_raw
      -            ))
      -        return conversations
      -

      Methods

      @@ -291,53 +59,12 @@

      Parameters

      conversation_id
      ID of the conversation.
      -
      - -Expand source code - -
      async def get_conversation(self, conversation_id):
      -    """
      -    Gets a conversation by the conversation ID.
      -
      -    Parameters
      -    ----------
      -    conversation_id
      -        ID of the conversation.
      -    """
      -    conversation = Conversation(self.requests, conversation_id)
      -    await conversation.update()
      -
      async def get_conversations(self, page_number=1, page_size=10)

      Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented.

      -
      - -Expand source code - -
      async def get_conversations(self, page_number=1, page_size=10):
      -    """
      -    Gets the list of conversations. This will be updated soon to use the new Pages object, so it is not documented.
      -    """
      -    conversations_req = await self.requests.get(
      -        url="https://chat.roblox.com/v2/get-user-conversations",
      -        params={
      -            "pageNumber": page_number,
      -            "pageSize": page_size
      -        }
      -    )
      -    conversations_json = conversations_req.json()
      -    conversations = []
      -    for conversation_raw in conversations_json:
      -        conversations.append(Conversation(
      -            requests=self.requests,
      -            raw=True,
      -            raw_data=conversation_raw
      -        ))
      -    return conversations
      -
    @@ -347,59 +74,6 @@

    Parameters

    -
    - -Expand source code - -
    class Conversation:
    -    def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
    -        self.requests = requests
    -        self.raw = raw
    -        self.id = None
    -        self.title = None
    -        self.initiator = None
    -        self.type = None
    -        self.typing = ConversationTyping(self.requests, conversation_id)
    -
    -        if self.raw:
    -            data = raw_data
    -            self.id = data["id"]
    -            self.title = data["title"]
    -            self.initiator = User(self.requests, data["initiator"]["targetId"])
    -            self.type = data["conversationType"]
    -            self.typing = ConversationTyping(self.requests, conversation_id)
    -
    -    async def update(self):
    -        conversation_req = await self.requests.get(
    -            url="https://chat.roblox.com/v2/get-conversations",
    -            params={
    -                "conversationIds": self.id
    -            }
    -        )
    -        data = conversation_req.json()[0]
    -        self.id = data["id"]
    -        self.title = data["title"]
    -        self.initiator = User(self.requests, data["initiator"]["targetId"])
    -        self.type = data["conversationType"]
    -        self.typing = ConversationTyping(self.requests, conversation_id)
    -
    -    async def get_message(self, message_id):
    -        return Message(self.requests, message_id, self.id)
    -
    -    async def send_message(self, content):
    -        send_message_req = await self.requests.post(
    -            url=endpoint + "v2/send-message",
    -            data={
    -                "message": content,
    -                "conversationId": self.id
    -            }
    -        )
    -        send_message_json = send_message_req.json()
    -        if send_message_json["sent"]:
    -            return Message(self.requests, send_message_json["messageId"], self.id)
    -        else:
    -            raise ChatError(send_message_json["statusMessage"])
    -

    Methods

    @@ -407,61 +81,18 @@

    Methods

    -
    - -Expand source code - -
    async def get_message(self, message_id):
    -    return Message(self.requests, message_id, self.id)
    -
    async def send_message(self, content)
    -
    - -Expand source code - -
    async def send_message(self, content):
    -    send_message_req = await self.requests.post(
    -        url=endpoint + "v2/send-message",
    -        data={
    -            "message": content,
    -            "conversationId": self.id
    -        }
    -    )
    -    send_message_json = send_message_req.json()
    -    if send_message_json["sent"]:
    -        return Message(self.requests, send_message_json["messageId"], self.id)
    -    else:
    -        raise ChatError(send_message_json["statusMessage"])
    -
    async def update(self)
    -
    - -Expand source code - -
    async def update(self):
    -    conversation_req = await self.requests.get(
    -        url="https://chat.roblox.com/v2/get-conversations",
    -        params={
    -            "conversationIds": self.id
    -        }
    -    )
    -    data = conversation_req.json()[0]
    -    self.id = data["id"]
    -    self.title = data["title"]
    -    self.initiator = User(self.requests, data["initiator"]["targetId"])
    -    self.type = data["conversationType"]
    -    self.typing = ConversationTyping(self.requests, conversation_id)
    -
    @@ -471,33 +102,6 @@

    Methods

    -
    - -Expand source code - -
    class ConversationTyping:
    -    def __init__(self, requests, conversation_id):
    -        self.requests = requests
    -        self.id = conversation_id
    -
    -    async def __aenter__(self):
    -        await self.requests.post(
    -            url=endpoint + "v2/update-user-typing-status",
    -            data={
    -                "conversationId": self.id,
    -                "isTyping": "true"
    -            }
    -        )
    -
    -    async def __aexit__(self, *args, **kwargs):
    -        await self.requests.post(
    -            url=endpoint + "v2/update-user-typing-status",
    -            data={
    -                "conversationId": self.id,
    -                "isTyping": "false"
    -            }
    -        )
    -
    class Message @@ -514,50 +118,6 @@

    Parameters

    conversation_id
    ID of the conversation that contains the message.
    -
    - -Expand source code - -
    class Message:
    -    """
    -    Represents a single message in a chat conversation.
    -
    -    Parameters
    -    ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    -    message_id
    -        ID of the message.
    -    conversation_id
    -        ID of the conversation that contains the message.
    -    """
    -    def __init__(self, requests, message_id, conversation_id):
    -        self.requests = requests
    -        self.id = message_id
    -        self.conversation_id = conversation_id
    -
    -        self.content = None
    -        self.sender = None
    -        self.read = None
    -
    -    async def update(self):
    -        """
    -        Updates the message with new data.
    -        """
    -        message_req = await self.requests.get(
    -            url="https://chat.roblox.com/v2/get-messages",
    -            params={
    -                "conversationId": self.conversation_id,
    -                "pageSize": 1,
    -                "exclusiveStartMessageId": self.id
    -            }
    -        )
    -
    -        message_json = message_req.json()[0]
    -        self.content = message_json["content"]
    -        self.sender = User(self.requests, message_json["senderTargetId"])
    -        self.read = message_json["read"]
    -

    Methods

    @@ -565,28 +125,6 @@

    Methods

    Updates the message with new data.

    -
    - -Expand source code - -
    async def update(self):
    -    """
    -    Updates the message with new data.
    -    """
    -    message_req = await self.requests.get(
    -        url="https://chat.roblox.com/v2/get-messages",
    -        params={
    -            "conversationId": self.conversation_id,
    -            "pageSize": 1,
    -            "exclusiveStartMessageId": self.id
    -        }
    -    )
    -
    -    message_json = message_req.json()[0]
    -    self.content = message_json["content"]
    -    self.sender = User(self.requests, message_json["senderTargetId"])
    -    self.read = message_json["read"]
    -
    diff --git a/docs/client.html b/docs/client.html index 84e12262..0db40944 100644 --- a/docs/client.html +++ b/docs/client.html @@ -23,250 +23,6 @@

    Module ro_py.client

    This file houses functions and classes that represent the core Roblox web client.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that represent the core Roblox web client.
    -
    -"""
    -
    -from ro_py.users import User
    -from ro_py.games import Game
    -from ro_py.groups import Group
    -from ro_py.assets import Asset
    -from ro_py.badges import Badge
    -from ro_py.chat import ChatWrapper
    -from ro_py.trades import TradesWrapper
    -from ro_py.utilities.cache import CacheType
    -from ro_py.utilities.requests import Requests
    -from ro_py.accountsettings import AccountSettings
    -from ro_py.accountinformation import AccountInformation
    -from ro_py.utilities.errors import UserDoesNotExistError
    -from ro_py.captcha import UnsolvedLoginCaptcha
    -
    -import logging
    -
    -
    -class Client:
    -    """
    -    Represents an authenticated Roblox client.
    -
    -    Parameters
    -    ----------
    -    token : str
    -        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -    requests_cache : bool
    -        Toggle for cached requests using CacheControl.
    -    """
    -
    -    def __init__(self, token: str = None, requests_cache: bool = False):
    -        self.requests = Requests(
    -            request_cache=requests_cache
    -        )
    -
    -        logging.debug("Initialized requests.")
    -
    -        self.accountinformation = None
    -        """AccountInformation object. Only available for authenticated clients."""
    -        self.accountsettings = None
    -        """AccountSettings object. Only available for authenticated clients."""
    -        self.chat = None
    -        """ChatWrapper object. Only available for authenticated clients."""
    -        self.trade = None
    -        """TradesWrapper object. Only available for authenticated clients."""
    -
    -        if token:
    -            self.token_login(token)
    -            logging.debug("Initialized token.")
    -            self.accountinformation = AccountInformation(self.requests)
    -            self.accountsettings = AccountSettings(self.requests)
    -            logging.debug("Initialized AccountInformation and AccountSettings.")
    -            self.chat = ChatWrapper(self.requests)
    -            logging.debug("Initialized chat wrapper.")
    -            self.trade = TradesWrapper(self.requests)
    -            logging.debug("Initialized trade wrapper.")
    -
    -    def token_login(self, token):
    -        """
    -        Authenticates the client with a ROBLOSECURITY token.
    -
    -        Parameters
    -        ----------
    -        token : str
    -            .ROBLOSECURITY token to authenticate with.
    -        """
    -        self.requests.session.cookies[".ROBLOSECURITY"] = token
    -
    -    async def user_login(self, username, password, token=None):
    -        """
    -        Authenticates the client with a username and password.
    -
    -        Parameters
    -        ----------
    -        username : str
    -            Username to log in with.
    -        password : str
    -            Password to log in with.
    -        token : str, optional
    -            If you have already solved the captcha, pass it here.
    -
    -        Returns
    -        -------
    -        ro_py.captcha.UnsolvedCaptcha or request
    -        """
    -        if token:
    -            login_req = self.requests.back_post(
    -                url="https://auth.roblox.com/v2/login",
    -                json={
    -                    "ctype": "Username",
    -                    "cvalue": username,
    -                    "password": password,
    -                    "captchaToken": token,
    -                    "captchaProvider": "PROVIDER_ARKOSE_LABS"
    -                }
    -            )
    -            return login_req
    -        else:
    -            login_req = await self.requests.post(
    -                url="https://auth.roblox.com/v2/login",
    -                json={
    -                    "ctype": "Username",
    -                    "cvalue": username,
    -                    "password": password
    -                },
    -                quickreturn=True
    -            )
    -            if login_req.status_code == 200:
    -                # If we're here, no captcha is required and we're already logged in, so we can return.
    -                return
    -            elif login_req.status_code == 403:
    -                # A captcha is required, so we need to return the captcha to solve.
    -                field_data = login_req.json()["errors"][0]["fieldData"]
    -                captcha_req = await self.requests.post(
    -                    url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81",
    -                    headers={
    -                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    -                    },
    -                    data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}"
    -                )
    -                captcha_json = captcha_req.json()
    -                return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    -
    -    async def get_user(self, user_id):
    -        """
    -        Gets a Roblox user.
    -
    -        Parameters
    -        ----------
    -        user_id
    -            ID of the user to generate the object from.
    -        """
    -        user = self.requests.cache.get(CacheType.Users, user_id)
    -        if not user:
    -            user = User(self.requests, user_id)
    -            self.requests.cache.set(CacheType.Users, user_id, user)
    -            await user.update()
    -        return user
    -
    -    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
    -        """
    -        Gets a Roblox user by their username..
    -
    -        Parameters
    -        ----------
    -        user_name : str
    -            Name of the user to generate the object from.
    -        exclude_banned_users : bool
    -            Whether to exclude banned users in the request.
    -        """
    -        username_req = await self.requests.post(
    -            url="https://users.roblox.com/v1/usernames/users",
    -            data={
    -                "usernames": [
    -                    user_name
    -                ],
    -                "excludeBannedUsers": exclude_banned_users
    -            }
    -        )
    -        username_data = username_req.json()
    -        if len(username_data["data"]) > 0:
    -            user_id = username_req.json()["data"][0]["id"]  # TODO: make this a partialuser
    -            user = self.requests.cache.get(CacheType.Users, user_id)
    -            if not user:
    -                user = User(self.requests, user_id)
    -                self.requests.cache.set(CacheType.Users, user_id, user)
    -                await user.update()
    -            return user
    -        else:
    -            raise UserDoesNotExistError
    -
    -    async def get_group(self, group_id):
    -        """
    -        Gets a Roblox group.
    -
    -        Parameters
    -        ----------
    -        group_id
    -            ID of the group to generate the object from.
    -        """
    -        group = self.requests.cache.get(CacheType.Groups, group_id)
    -        if not group:
    -            group = Group(self.requests, group_id)
    -            self.requests.cache.set(CacheType.Groups, group_id, group)
    -            await group.update()
    -        return group
    -
    -    async def get_game(self, game_id):
    -        """
    -        Gets a Roblox game.
    -
    -        Parameters
    -        ----------
    -        game_id
    -            ID of the game to generate the object from.
    -        """
    -        game = self.requests.cache.get(CacheType.Games, game_id)
    -        if not game:
    -            game = Game(self.requests, game_id)
    -            self.requests.cache.set(CacheType.Games, game_id, game)
    -            await game.update()
    -        return game
    -
    -    async def get_asset(self, asset_id):
    -        """
    -        Gets a Roblox asset.
    -
    -        Parameters
    -        ----------
    -        asset_id
    -            ID of the asset to generate the object from.
    -        """
    -        asset = self.requests.cache.get(CacheType.Assets, asset_id)
    -        if not asset:
    -            asset = Asset(self.requests, asset_id)
    -            self.requests.cache.set(CacheType.Assets, asset_id, asset)
    -            await asset.update()
    -        return asset
    -
    -    async def get_badge(self, badge_id):
    -        """
    -        Gets a Roblox badge.
    -
    -        Parameters
    -        ----------
    -        badge_id
    -            ID of the badge to generate the object from.
    -        """
    -        badge = self.requests.cache.get(CacheType.Assets, badge_id)
    -        if not badge:
    -            badge = Badge(self.requests, badge_id)
    -            self.requests.cache.set(CacheType.Assets, badge_id, badge)
    -            await badge.update()
    -        return badge
    -
    @@ -279,7 +35,7 @@

    Classes

    class Client -(token: str = None, requests_cache: bool = False) +(token: str = None)

    Represents an authenticated Roblox client.

    @@ -287,230 +43,7 @@

    Parameters

    token : str
    Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -
    requests_cache : bool
    -
    Toggle for cached requests using CacheControl.
    -
    - -Expand source code - -
    class Client:
    -    """
    -    Represents an authenticated Roblox client.
    -
    -    Parameters
    -    ----------
    -    token : str
    -        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -    requests_cache : bool
    -        Toggle for cached requests using CacheControl.
    -    """
    -
    -    def __init__(self, token: str = None, requests_cache: bool = False):
    -        self.requests = Requests(
    -            request_cache=requests_cache
    -        )
    -
    -        logging.debug("Initialized requests.")
    -
    -        self.accountinformation = None
    -        """AccountInformation object. Only available for authenticated clients."""
    -        self.accountsettings = None
    -        """AccountSettings object. Only available for authenticated clients."""
    -        self.chat = None
    -        """ChatWrapper object. Only available for authenticated clients."""
    -        self.trade = None
    -        """TradesWrapper object. Only available for authenticated clients."""
    -
    -        if token:
    -            self.token_login(token)
    -            logging.debug("Initialized token.")
    -            self.accountinformation = AccountInformation(self.requests)
    -            self.accountsettings = AccountSettings(self.requests)
    -            logging.debug("Initialized AccountInformation and AccountSettings.")
    -            self.chat = ChatWrapper(self.requests)
    -            logging.debug("Initialized chat wrapper.")
    -            self.trade = TradesWrapper(self.requests)
    -            logging.debug("Initialized trade wrapper.")
    -
    -    def token_login(self, token):
    -        """
    -        Authenticates the client with a ROBLOSECURITY token.
    -
    -        Parameters
    -        ----------
    -        token : str
    -            .ROBLOSECURITY token to authenticate with.
    -        """
    -        self.requests.session.cookies[".ROBLOSECURITY"] = token
    -
    -    async def user_login(self, username, password, token=None):
    -        """
    -        Authenticates the client with a username and password.
    -
    -        Parameters
    -        ----------
    -        username : str
    -            Username to log in with.
    -        password : str
    -            Password to log in with.
    -        token : str, optional
    -            If you have already solved the captcha, pass it here.
    -
    -        Returns
    -        -------
    -        ro_py.captcha.UnsolvedCaptcha or request
    -        """
    -        if token:
    -            login_req = self.requests.back_post(
    -                url="https://auth.roblox.com/v2/login",
    -                json={
    -                    "ctype": "Username",
    -                    "cvalue": username,
    -                    "password": password,
    -                    "captchaToken": token,
    -                    "captchaProvider": "PROVIDER_ARKOSE_LABS"
    -                }
    -            )
    -            return login_req
    -        else:
    -            login_req = await self.requests.post(
    -                url="https://auth.roblox.com/v2/login",
    -                json={
    -                    "ctype": "Username",
    -                    "cvalue": username,
    -                    "password": password
    -                },
    -                quickreturn=True
    -            )
    -            if login_req.status_code == 200:
    -                # If we're here, no captcha is required and we're already logged in, so we can return.
    -                return
    -            elif login_req.status_code == 403:
    -                # A captcha is required, so we need to return the captcha to solve.
    -                field_data = login_req.json()["errors"][0]["fieldData"]
    -                captcha_req = await self.requests.post(
    -                    url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81",
    -                    headers={
    -                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    -                    },
    -                    data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}"
    -                )
    -                captcha_json = captcha_req.json()
    -                return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    -
    -    async def get_user(self, user_id):
    -        """
    -        Gets a Roblox user.
    -
    -        Parameters
    -        ----------
    -        user_id
    -            ID of the user to generate the object from.
    -        """
    -        user = self.requests.cache.get(CacheType.Users, user_id)
    -        if not user:
    -            user = User(self.requests, user_id)
    -            self.requests.cache.set(CacheType.Users, user_id, user)
    -            await user.update()
    -        return user
    -
    -    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
    -        """
    -        Gets a Roblox user by their username..
    -
    -        Parameters
    -        ----------
    -        user_name : str
    -            Name of the user to generate the object from.
    -        exclude_banned_users : bool
    -            Whether to exclude banned users in the request.
    -        """
    -        username_req = await self.requests.post(
    -            url="https://users.roblox.com/v1/usernames/users",
    -            data={
    -                "usernames": [
    -                    user_name
    -                ],
    -                "excludeBannedUsers": exclude_banned_users
    -            }
    -        )
    -        username_data = username_req.json()
    -        if len(username_data["data"]) > 0:
    -            user_id = username_req.json()["data"][0]["id"]  # TODO: make this a partialuser
    -            user = self.requests.cache.get(CacheType.Users, user_id)
    -            if not user:
    -                user = User(self.requests, user_id)
    -                self.requests.cache.set(CacheType.Users, user_id, user)
    -                await user.update()
    -            return user
    -        else:
    -            raise UserDoesNotExistError
    -
    -    async def get_group(self, group_id):
    -        """
    -        Gets a Roblox group.
    -
    -        Parameters
    -        ----------
    -        group_id
    -            ID of the group to generate the object from.
    -        """
    -        group = self.requests.cache.get(CacheType.Groups, group_id)
    -        if not group:
    -            group = Group(self.requests, group_id)
    -            self.requests.cache.set(CacheType.Groups, group_id, group)
    -            await group.update()
    -        return group
    -
    -    async def get_game(self, game_id):
    -        """
    -        Gets a Roblox game.
    -
    -        Parameters
    -        ----------
    -        game_id
    -            ID of the game to generate the object from.
    -        """
    -        game = self.requests.cache.get(CacheType.Games, game_id)
    -        if not game:
    -            game = Game(self.requests, game_id)
    -            self.requests.cache.set(CacheType.Games, game_id, game)
    -            await game.update()
    -        return game
    -
    -    async def get_asset(self, asset_id):
    -        """
    -        Gets a Roblox asset.
    -
    -        Parameters
    -        ----------
    -        asset_id
    -            ID of the asset to generate the object from.
    -        """
    -        asset = self.requests.cache.get(CacheType.Assets, asset_id)
    -        if not asset:
    -            asset = Asset(self.requests, asset_id)
    -            self.requests.cache.set(CacheType.Assets, asset_id, asset)
    -            await asset.update()
    -        return asset
    -
    -    async def get_badge(self, badge_id):
    -        """
    -        Gets a Roblox badge.
    -
    -        Parameters
    -        ----------
    -        badge_id
    -            ID of the badge to generate the object from.
    -        """
    -        badge = self.requests.cache.get(CacheType.Assets, badge_id)
    -        if not badge:
    -            badge = Badge(self.requests, badge_id)
    -            self.requests.cache.set(CacheType.Assets, badge_id, badge)
    -            await badge.update()
    -        return badge
    -

    Subclasses

    • Bot
    • @@ -546,26 +79,6 @@

      Parameters

      asset_id
      ID of the asset to generate the object from.
    -
    - -Expand source code - -
    async def get_asset(self, asset_id):
    -    """
    -    Gets a Roblox asset.
    -
    -    Parameters
    -    ----------
    -    asset_id
    -        ID of the asset to generate the object from.
    -    """
    -    asset = self.requests.cache.get(CacheType.Assets, asset_id)
    -    if not asset:
    -        asset = Asset(self.requests, asset_id)
    -        self.requests.cache.set(CacheType.Assets, asset_id, asset)
    -        await asset.update()
    -    return asset
    -
    async def get_badge(self, badge_id) @@ -577,26 +90,6 @@

    Parameters

    badge_id
    ID of the badge to generate the object from.
    -
    - -Expand source code - -
    async def get_badge(self, badge_id):
    -    """
    -    Gets a Roblox badge.
    -
    -    Parameters
    -    ----------
    -    badge_id
    -        ID of the badge to generate the object from.
    -    """
    -    badge = self.requests.cache.get(CacheType.Assets, badge_id)
    -    if not badge:
    -        badge = Badge(self.requests, badge_id)
    -        self.requests.cache.set(CacheType.Assets, badge_id, badge)
    -        await badge.update()
    -    return badge
    -
    async def get_game(self, game_id) @@ -608,26 +101,6 @@

    Parameters

    game_id
    ID of the game to generate the object from.
    -
    - -Expand source code - -
    async def get_game(self, game_id):
    -    """
    -    Gets a Roblox game.
    -
    -    Parameters
    -    ----------
    -    game_id
    -        ID of the game to generate the object from.
    -    """
    -    game = self.requests.cache.get(CacheType.Games, game_id)
    -    if not game:
    -        game = Game(self.requests, game_id)
    -        self.requests.cache.set(CacheType.Games, game_id, game)
    -        await game.update()
    -    return game
    -
    async def get_group(self, group_id) @@ -639,26 +112,12 @@

    Parameters

    group_id
    ID of the group to generate the object from.
    -
    - -Expand source code - -
    async def get_group(self, group_id):
    -    """
    -    Gets a Roblox group.
    -
    -    Parameters
    -    ----------
    -    group_id
    -        ID of the group to generate the object from.
    -    """
    -    group = self.requests.cache.get(CacheType.Groups, group_id)
    -    if not group:
    -        group = Group(self.requests, group_id)
    -        self.requests.cache.set(CacheType.Groups, group_id, group)
    -        await group.update()
    -    return group
    -
    + +
    +async def get_self(self) +
    +
    +
    async def get_user(self, user_id) @@ -670,26 +129,6 @@

    Parameters

    user_id
    ID of the user to generate the object from.
    -
    - -Expand source code - -
    async def get_user(self, user_id):
    -    """
    -    Gets a Roblox user.
    -
    -    Parameters
    -    ----------
    -    user_id
    -        ID of the user to generate the object from.
    -    """
    -    user = self.requests.cache.get(CacheType.Users, user_id)
    -    if not user:
    -        user = User(self.requests, user_id)
    -        self.requests.cache.set(CacheType.Users, user_id, user)
    -        await user.update()
    -    return user
    -
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False) @@ -703,42 +142,6 @@

    Parameters

    exclude_banned_users : bool
    Whether to exclude banned users in the request.
    -
    - -Expand source code - -
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
    -    """
    -    Gets a Roblox user by their username..
    -
    -    Parameters
    -    ----------
    -    user_name : str
    -        Name of the user to generate the object from.
    -    exclude_banned_users : bool
    -        Whether to exclude banned users in the request.
    -    """
    -    username_req = await self.requests.post(
    -        url="https://users.roblox.com/v1/usernames/users",
    -        data={
    -            "usernames": [
    -                user_name
    -            ],
    -            "excludeBannedUsers": exclude_banned_users
    -        }
    -    )
    -    username_data = username_req.json()
    -    if len(username_data["data"]) > 0:
    -        user_id = username_req.json()["data"][0]["id"]  # TODO: make this a partialuser
    -        user = self.requests.cache.get(CacheType.Users, user_id)
    -        if not user:
    -            user = User(self.requests, user_id)
    -            self.requests.cache.set(CacheType.Users, user_id, user)
    -            await user.update()
    -        return user
    -    else:
    -        raise UserDoesNotExistError
    -
    def token_login(self, token) @@ -750,21 +153,6 @@

    Parameters

    token : str
    .ROBLOSECURITY token to authenticate with.
    -
    - -Expand source code - -
    def token_login(self, token):
    -    """
    -    Authenticates the client with a ROBLOSECURITY token.
    -
    -    Parameters
    -    ----------
    -    token : str
    -        .ROBLOSECURITY token to authenticate with.
    -    """
    -    self.requests.session.cookies[".ROBLOSECURITY"] = token
    -
    async def user_login(self, username, password, token=None) @@ -785,65 +173,6 @@

    Returns

    UnsolvedCaptcha or request
     
    -
    - -Expand source code - -
    async def user_login(self, username, password, token=None):
    -    """
    -    Authenticates the client with a username and password.
    -
    -    Parameters
    -    ----------
    -    username : str
    -        Username to log in with.
    -    password : str
    -        Password to log in with.
    -    token : str, optional
    -        If you have already solved the captcha, pass it here.
    -
    -    Returns
    -    -------
    -    ro_py.captcha.UnsolvedCaptcha or request
    -    """
    -    if token:
    -        login_req = self.requests.back_post(
    -            url="https://auth.roblox.com/v2/login",
    -            json={
    -                "ctype": "Username",
    -                "cvalue": username,
    -                "password": password,
    -                "captchaToken": token,
    -                "captchaProvider": "PROVIDER_ARKOSE_LABS"
    -            }
    -        )
    -        return login_req
    -    else:
    -        login_req = await self.requests.post(
    -            url="https://auth.roblox.com/v2/login",
    -            json={
    -                "ctype": "Username",
    -                "cvalue": username,
    -                "password": password
    -            },
    -            quickreturn=True
    -        )
    -        if login_req.status_code == 200:
    -            # If we're here, no captcha is required and we're already logged in, so we can return.
    -            return
    -        elif login_req.status_code == 403:
    -            # A captcha is required, so we need to return the captcha to solve.
    -            field_data = login_req.json()["errors"][0]["fieldData"]
    -            captcha_req = await self.requests.post(
    -                url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81",
    -                headers={
    -                    "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    -                },
    -                data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}"
    -            )
    -            captcha_json = captcha_req.json()
    -            return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81")
    -
    @@ -873,6 +202,7 @@

    Client<
  • get_badge
  • get_game
  • get_group
  • +
  • get_self
  • get_user
  • get_user_by_username
  • token_login
  • diff --git a/docs/economy.html b/docs/economy.html index 235e47e2..27fac4e5 100644 --- a/docs/economy.html +++ b/docs/economy.html @@ -23,38 +23,6 @@

    Module ro_py.economy

    This file houses functions and classes that pertain to the Roblox economy endpoints.

    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to the Roblox economy endpoints.
    -
    -"""
    -
    -endpoint = "https://economy.roblox.com/"
    -
    -
    -class Currency:
    -    """
    -    Represents currency data.
    -    """
    -    def __init__(self, currency_data):
    -        self.robux = currency_data["robux"]
    -
    -
    -class LimitedResaleData:
    -    """
    -    Represents the resale data of a limited item.
    -    """
    -    def __init__(self, resale_data):
    -        self.asset_stock = resale_data["assetStock"]
    -        self.sales = resale_data["sales"]
    -        self.number_remaining = resale_data["numberRemaining"]
    -        self.recent_average_price = resale_data["recentAveragePrice"]
    -        self.original_price = resale_data["originalPrice"]
    -
    @@ -71,17 +39,6 @@

    Classes

    Represents currency data.

    -
    - -Expand source code - -
    class Currency:
    -    """
    -    Represents currency data.
    -    """
    -    def __init__(self, currency_data):
    -        self.robux = currency_data["robux"]
    -
    class LimitedResaleData @@ -89,21 +46,6 @@

    Classes

    Represents the resale data of a limited item.

    -
    - -Expand source code - -
    class LimitedResaleData:
    -    """
    -    Represents the resale data of a limited item.
    -    """
    -    def __init__(self, resale_data):
    -        self.asset_stock = resale_data["assetStock"]
    -        self.sales = resale_data["sales"]
    -        self.number_remaining = resale_data["numberRemaining"]
    -        self.recent_average_price = resale_data["recentAveragePrice"]
    -        self.original_price = resale_data["originalPrice"]
    -
    diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index 178fc0a1..cef9fcee 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -23,48 +23,6 @@

    Module ro_py.extensions.bots

    This extension houses functions that allow generation of Bot objects, which interpret commands.

    -
    - -Expand source code - -
    """
    -
    -This extension houses functions that allow generation of Bot objects, which interpret commands.
    -
    -"""
    -
    -
    -from ro_py.client import Client
    -import asyncio
    -
    -
    -class Bot(Client):
    -    def __init__(self):
    -        super().__init__()
    -
    -
    -class Command:
    -    def __init__(self, func, **kwargs):
    -        if not asyncio.iscoroutinefunction(func):
    -            raise TypeError('Callback must be a coroutine.')
    -        self._callback = func
    -
    -    @property
    -    def callback(self):
    -        return self._callback
    -
    -    async def __call__(self, *args, **kwargs):
    -        return await self.callback(*args, **kwargs)
    -
    -
    -def command(**attrs):
    -    def decorator(func):
    -        if isinstance(func, Command):
    -            raise TypeError('Callback is already a command.')
    -        return Command(func, **attrs)
    -
    -    return decorator
    -
    @@ -78,18 +36,6 @@

    Functions

    -
    - -Expand source code - -
    def command(**attrs):
    -    def decorator(func):
    -        if isinstance(func, Command):
    -            raise TypeError('Callback is already a command.')
    -        return Command(func, **attrs)
    -
    -    return decorator
    -
    @@ -105,17 +51,7 @@

    Parameters

    token : str
    Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -
    requests_cache : bool
    -
    Toggle for cached requests using CacheControl.
    -
    - -Expand source code - -
    class Bot(Client):
    -    def __init__(self):
    -        super().__init__()
    -

    Ancestors

    • Client
    • @@ -146,36 +82,11 @@

      Inherited members

      -
      - -Expand source code - -
      class Command:
      -    def __init__(self, func, **kwargs):
      -        if not asyncio.iscoroutinefunction(func):
      -            raise TypeError('Callback must be a coroutine.')
      -        self._callback = func
      -
      -    @property
      -    def callback(self):
      -        return self._callback
      -
      -    async def __call__(self, *args, **kwargs):
      -        return await self.callback(*args, **kwargs)
      -

      Instance variables

      var callback
      -
      - -Expand source code - -
      @property
      -def callback(self):
      -    return self._callback
      -
      diff --git a/docs/extensions/index.html b/docs/extensions/index.html index c1cdf452..379318a8 100644 --- a/docs/extensions/index.html +++ b/docs/extensions/index.html @@ -23,16 +23,6 @@

      Module ro_py.extensions

      This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.

      -
      - -Expand source code - -
      """
      -
      -This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.
      -
      -"""
      -

      Sub-modules

      diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html index 6e80283d..7e713eeb 100644 --- a/docs/extensions/prompt.html +++ b/docs/extensions/prompt.html @@ -23,275 +23,6 @@

      Module ro_py.extensions.prompt

      This extension houses functions that allow human verification prompts for interactive applications.

      -
      - -Expand source code - -
      """
      -
      -This extension houses functions that allow human verification prompts for interactive applications.
      -
      -"""
      -
      -
      -import wx
      -import wxasync
      -from wx import html2
      -import os
      -import pytweening
      -
      -
      -async def user_login(client, username, password, key=None):
      -    if key:
      -        return await client.user_login(username, password, key)
      -    else:
      -        return await client.user_login(username, password)
      -
      -
      -class RbxLogin(wx.Frame):
      -    """
      -    wx.Frame wrapper for Roblox authentication.
      -    """
      -    def __init__(self, *args, **kwds):
      -        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
      -        wx.Frame.__init__(self, *args, **kwds)
      -        self.SetSize((512, 512))
      -        self.SetTitle("Login with Roblox")
      -        self.SetBackgroundColour(wx.Colour(255, 255, 255))
      -        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
      -
      -        self.username = None
      -        self.password = None
      -        self.client = None
      -        self.status = False
      -
      -        root_sizer = wx.BoxSizer(wx.VERTICAL)
      -
      -        self.inner_panel = wx.Panel(self, wx.ID_ANY)
      -        root_sizer.Add(self.inner_panel, 1, wx.ALL | wx.EXPAND, 100)
      -
      -        inner_sizer = wx.BoxSizer(wx.VERTICAL)
      -
      -        inner_sizer.Add((0, 20), 0, 0, 0)
      -
      -        login_label = wx.StaticText(self.inner_panel, wx.ID_ANY, "Please log in with your username and password.",
      -                                    style=wx.ALIGN_CENTER_HORIZONTAL)
      -        inner_sizer.Add(login_label, 1, 0, 0)
      -
      -        self.username_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "\n")
      -        self.username_entry.SetFont(
      -            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      -        self.username_entry.SetFocus()
      -        inner_sizer.Add(self.username_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      -
      -        self.password_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "", style=wx.TE_PASSWORD)
      -        self.password_entry.SetFont(
      -            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
      -        inner_sizer.Add(self.password_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
      -
      -        self.log_in_button = wx.Button(self.inner_panel, wx.ID_ANY, "Login")
      -        inner_sizer.Add(self.log_in_button, 1, wx.ALL | wx.EXPAND, 0)
      -
      -        inner_sizer.Add((0, 20), 0, 0, 0)
      -
      -        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
      -        self.web_view.Hide()
      -        self.web_view.EnableAccessToDevTools(False)
      -        self.web_view.EnableContextMenu(False)
      -
      -        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
      -
      -        self.inner_panel.SetSizer(inner_sizer)
      -
      -        self.SetSizer(root_sizer)
      -
      -        self.Layout()
      -
      -        wxasync.AsyncBind(wx.EVT_BUTTON, self.login_click, self.log_in_button)
      -        wxasync.AsyncBind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
      -
      -    async def login_load(self, event):
      -        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
      -        if token == "undefined":
      -            token = False
      -        if token:
      -            self.web_view.Hide()
      -            lr = await user_login(
      -                self.client,
      -                self.username,
      -                self.password,
      -                token
      -            )
      -            if ".ROBLOSECURITY" in self.client.requests.session.cookies:
      -                self.status = True
      -                self.Close()
      -            else:
      -                self.status = False
      -                wx.MessageBox(f"Failed to log in.\n"
      -                              f"Detailed information from server: {lr.json()['errors'][0]['message']}",
      -                              "Error", wx.OK | wx.ICON_ERROR)
      -                self.Close()
      -
      -    async def login_click(self, event):
      -        self.username = self.username_entry.GetValue()
      -        self.password = self.password_entry.GetValue()
      -        self.username.strip("\n")
      -        self.password.strip("\n")
      -
      -        if not (self.username and self.password):
      -            # If either the username or password is missing, return
      -            return
      -
      -        if len(self.username) < 3:
      -            # If the username is shorter than 3, return
      -            return
      -
      -        # Disable the entries to stop people from typing in them.
      -        self.username_entry.Disable()
      -        self.password_entry.Disable()
      -        self.log_in_button.Disable()
      -
      -        # Get the position of the inner_panel
      -        old_pos = self.inner_panel.GetPosition()
      -        start_point = old_pos[0]
      -
      -        # Move the panel over to the right.
      -        for i in range(0, 512):
      -            wx.Yield()
      -            self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
      -
      -        # Hide the panel. The panel is already on the right so it's not visible anyways.
      -        self.inner_panel.Hide()
      -        self.web_view.SetSize((512, 600))
      -
      -        # Expand the window.
      -        for i in range(0, 88):
      -            self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
      -
      -        # Runs the user_login function.
      -        fd = await user_login(self.client, self.username, self.password)
      -
      -        # Load the captcha URL.
      -        if fd:
      -            self.web_view.LoadURL(fd.url)
      -            self.web_view.Show()
      -        else:
      -            # No captcha needed.
      -            self.Close()
      -
      -
      -class RbxCaptcha(wx.Frame):
      -    """
      -    wx.Frame wrapper for Roblox authentication.
      -    """
      -    def __init__(self, *args, **kwds):
      -        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
      -        wx.Frame.__init__(self, *args, **kwds)
      -        self.SetSize((512, 600))
      -        self.SetTitle("Roblox Captcha (ro.py)")
      -        self.SetBackgroundColour(wx.Colour(255, 255, 255))
      -        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
      -
      -        self.status = False
      -        self.token = None
      -
      -        root_sizer = wx.BoxSizer(wx.VERTICAL)
      -
      -        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
      -        self.web_view.SetSize((512, 600))
      -        self.web_view.Show()
      -        self.web_view.EnableAccessToDevTools(False)
      -        self.web_view.EnableContextMenu(False)
      -
      -        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
      -
      -        self.SetSizer(root_sizer)
      -
      -        self.Layout()
      -
      -        self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
      -
      -    def login_load(self, event):
      -        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
      -        if token == "undefined":
      -            token = False
      -        if token:
      -            self.web_view.Hide()
      -            self.status = True
      -            self.token = token
      -            self.Close()
      -
      -
      -class AuthApp(wxasync.WxAsyncApp):
      -    """
      -    wx.App wrapper for Roblox authentication.
      -    """
      -
      -    def OnInit(self):
      -        self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
      -        self.SetTopWindow(self.rbx_login)
      -        self.rbx_login.Show()
      -        return True
      -
      -
      -class CaptchaApp(wxasync.WxAsyncApp):
      -    """
      -    wx.App wrapper for Roblox captcha.
      -    """
      -
      -    def OnInit(self):
      -        self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "")
      -        self.SetTopWindow(self.rbx_captcha)
      -        self.rbx_captcha.Show()
      -        return True
      -
      -
      -async def authenticate_prompt(client):
      -    """
      -    Prompts a login screen.
      -    Returns True if the user has sucessfully been authenticated and False if they have not.
      -
      -    Login prompts look like this:
      -    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_prompt.png
      -    They also display a captcha, which looks very similar to captcha_prompt():
      -    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_captcha_prompt.png
      -
      -    Parameters
      -    ----------
      -    client : ro_py.client.Client
      -        Client object to authenticate.
      -
      -    Returns
      -    ------
      -    bool
      -    """
      -    app = AuthApp(0)
      -    app.rbx_login.client = client
      -    await app.MainLoop()
      -    return app.rbx_login.status
      -
      -
      -async def captcha_prompt(unsolved_captcha):
      -    """
      -    Prompts a captcha solve screen.
      -    First item in tuple is True if the solve was sucessful, and the second item is the token.
      -
      -    Image will be placed here soon.
      -
      -    Parameters
      -    ----------
      -    unsolved_captcha : ro_py.captcha.UnsolvedCaptcha
      -        Captcha to solve.
      -
      -    Returns
      -    ------
      -    tuple of bool and str
      -    """
      -    app = CaptchaApp(0)
      -    app.rbx_captcha.web_view.LoadURL(unsolved_captcha.url)
      -    await app.MainLoop()
      -    return app.rbx_captcha.status, app.rbx_captcha.token
      -
      @@ -320,34 +51,6 @@

      Returns

      bool
       
      -
      - -Expand source code - -
      async def authenticate_prompt(client):
      -    """
      -    Prompts a login screen.
      -    Returns True if the user has sucessfully been authenticated and False if they have not.
      -
      -    Login prompts look like this:
      -    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_prompt.png
      -    They also display a captcha, which looks very similar to captcha_prompt():
      -    .. image:: https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/login_captcha_prompt.png
      -
      -    Parameters
      -    ----------
      -    client : ro_py.client.Client
      -        Client object to authenticate.
      -
      -    Returns
      -    ------
      -    bool
      -    """
      -    app = AuthApp(0)
      -    app.rbx_login.client = client
      -    await app.MainLoop()
      -    return app.rbx_login.status
      -
      async def captcha_prompt(unsolved_captcha) @@ -366,47 +69,12 @@

      Returns

      tuple of bool and str
       
      -
      - -Expand source code - -
      async def captcha_prompt(unsolved_captcha):
      -    """
      -    Prompts a captcha solve screen.
      -    First item in tuple is True if the solve was sucessful, and the second item is the token.
      -
      -    Image will be placed here soon.
      -
      -    Parameters
      -    ----------
      -    unsolved_captcha : ro_py.captcha.UnsolvedCaptcha
      -        Captcha to solve.
      -
      -    Returns
      -    ------
      -    tuple of bool and str
      -    """
      -    app = CaptchaApp(0)
      -    app.rbx_captcha.web_view.LoadURL(unsolved_captcha.url)
      -    await app.MainLoop()
      -    return app.rbx_captcha.status, app.rbx_captcha.token
      -
      async def user_login(client, username, password, key=None)
      -
      - -Expand source code - -
      async def user_login(client, username, password, key=None):
      -    if key:
      -        return await client.user_login(username, password, key)
      -    else:
      -        return await client.user_login(username, password)
      -
      @@ -444,21 +112,6 @@

      Classes

      :note: You should override OnInit to do application initialization to ensure that the system, toolkit and wxWidgets are fully initialized.

      -
      - -Expand source code - -
      class AuthApp(wxasync.WxAsyncApp):
      -    """
      -    wx.App wrapper for Roblox authentication.
      -    """
      -
      -    def OnInit(self):
      -        self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
      -        self.SetTopWindow(self.rbx_login)
      -        self.rbx_login.Show()
      -        return True
      -

      Ancestors

      • wxasync.WxAsyncApp
      • @@ -479,16 +132,6 @@

        Methods

        OnInit(self) -> bool

        -
        - -Expand source code - -
        def OnInit(self):
        -    self.rbx_login = RbxLogin(None, wx.ID_ANY, "")
        -    self.SetTopWindow(self.rbx_login)
        -    self.rbx_login.Show()
        -    return True
        -
        @@ -523,21 +166,6 @@

        Methods

        :note: You should override OnInit to do application initialization to ensure that the system, toolkit and wxWidgets are fully initialized.

        -
        - -Expand source code - -
        class CaptchaApp(wxasync.WxAsyncApp):
        -    """
        -    wx.App wrapper for Roblox captcha.
        -    """
        -
        -    def OnInit(self):
        -        self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "")
        -        self.SetTopWindow(self.rbx_captcha)
        -        self.rbx_captcha.Show()
        -        return True
        -

        Ancestors

        • wxasync.WxAsyncApp
        • @@ -558,16 +186,6 @@

          Methods

          OnInit(self) -> bool

          -
          - -Expand source code - -
          def OnInit(self):
          -    self.rbx_captcha = RbxCaptcha(None, wx.ID_ANY, "")
          -    self.SetTopWindow(self.rbx_captcha)
          -    self.rbx_captcha.Show()
          -    return True
          -
          @@ -577,51 +195,6 @@

          Methods

          wx.Frame wrapper for Roblox authentication.

          -
          - -Expand source code - -
          class RbxCaptcha(wx.Frame):
          -    """
          -    wx.Frame wrapper for Roblox authentication.
          -    """
          -    def __init__(self, *args, **kwds):
          -        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
          -        wx.Frame.__init__(self, *args, **kwds)
          -        self.SetSize((512, 600))
          -        self.SetTitle("Roblox Captcha (ro.py)")
          -        self.SetBackgroundColour(wx.Colour(255, 255, 255))
          -        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
          -
          -        self.status = False
          -        self.token = None
          -
          -        root_sizer = wx.BoxSizer(wx.VERTICAL)
          -
          -        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
          -        self.web_view.SetSize((512, 600))
          -        self.web_view.Show()
          -        self.web_view.EnableAccessToDevTools(False)
          -        self.web_view.EnableContextMenu(False)
          -
          -        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
          -
          -        self.SetSizer(root_sizer)
          -
          -        self.Layout()
          -
          -        self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
          -
          -    def login_load(self, event):
          -        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
          -        if token == "undefined":
          -            token = False
          -        if token:
          -            self.web_view.Hide()
          -            self.status = True
          -            self.token = token
          -            self.Close()
          -

          Ancestors

          • wx._core.Frame
          • @@ -642,20 +215,6 @@

            Methods

            -
            - -Expand source code - -
            def login_load(self, event):
            -    _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
            -    if token == "undefined":
            -        token = False
            -    if token:
            -        self.web_view.Hide()
            -        self.status = True
            -        self.token = token
            -        self.Close()
            -
          @@ -665,141 +224,6 @@

          Methods

          wx.Frame wrapper for Roblox authentication.

          -
          - -Expand source code - -
          class RbxLogin(wx.Frame):
          -    """
          -    wx.Frame wrapper for Roblox authentication.
          -    """
          -    def __init__(self, *args, **kwds):
          -        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
          -        wx.Frame.__init__(self, *args, **kwds)
          -        self.SetSize((512, 512))
          -        self.SetTitle("Login with Roblox")
          -        self.SetBackgroundColour(wx.Colour(255, 255, 255))
          -        self.SetIcon(wx.Icon(os.path.join(os.path.dirname(os.path.realpath(__file__)), "appicon.png")))
          -
          -        self.username = None
          -        self.password = None
          -        self.client = None
          -        self.status = False
          -
          -        root_sizer = wx.BoxSizer(wx.VERTICAL)
          -
          -        self.inner_panel = wx.Panel(self, wx.ID_ANY)
          -        root_sizer.Add(self.inner_panel, 1, wx.ALL | wx.EXPAND, 100)
          -
          -        inner_sizer = wx.BoxSizer(wx.VERTICAL)
          -
          -        inner_sizer.Add((0, 20), 0, 0, 0)
          -
          -        login_label = wx.StaticText(self.inner_panel, wx.ID_ANY, "Please log in with your username and password.",
          -                                    style=wx.ALIGN_CENTER_HORIZONTAL)
          -        inner_sizer.Add(login_label, 1, 0, 0)
          -
          -        self.username_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "\n")
          -        self.username_entry.SetFont(
          -            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
          -        self.username_entry.SetFocus()
          -        inner_sizer.Add(self.username_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
          -
          -        self.password_entry = wx.TextCtrl(self.inner_panel, wx.ID_ANY, "", style=wx.TE_PASSWORD)
          -        self.password_entry.SetFont(
          -            wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, "Segoe UI"))
          -        inner_sizer.Add(self.password_entry, 1, wx.BOTTOM | wx.EXPAND | wx.TOP, 4)
          -
          -        self.log_in_button = wx.Button(self.inner_panel, wx.ID_ANY, "Login")
          -        inner_sizer.Add(self.log_in_button, 1, wx.ALL | wx.EXPAND, 0)
          -
          -        inner_sizer.Add((0, 20), 0, 0, 0)
          -
          -        self.web_view = wx.html2.WebView.New(self, wx.ID_ANY)
          -        self.web_view.Hide()
          -        self.web_view.EnableAccessToDevTools(False)
          -        self.web_view.EnableContextMenu(False)
          -
          -        root_sizer.Add(self.web_view, 1, wx.EXPAND, 0)
          -
          -        self.inner_panel.SetSizer(inner_sizer)
          -
          -        self.SetSizer(root_sizer)
          -
          -        self.Layout()
          -
          -        wxasync.AsyncBind(wx.EVT_BUTTON, self.login_click, self.log_in_button)
          -        wxasync.AsyncBind(wx.html2.EVT_WEBVIEW_NAVIGATED, self.login_load, self.web_view)
          -
          -    async def login_load(self, event):
          -        _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
          -        if token == "undefined":
          -            token = False
          -        if token:
          -            self.web_view.Hide()
          -            lr = await user_login(
          -                self.client,
          -                self.username,
          -                self.password,
          -                token
          -            )
          -            if ".ROBLOSECURITY" in self.client.requests.session.cookies:
          -                self.status = True
          -                self.Close()
          -            else:
          -                self.status = False
          -                wx.MessageBox(f"Failed to log in.\n"
          -                              f"Detailed information from server: {lr.json()['errors'][0]['message']}",
          -                              "Error", wx.OK | wx.ICON_ERROR)
          -                self.Close()
          -
          -    async def login_click(self, event):
          -        self.username = self.username_entry.GetValue()
          -        self.password = self.password_entry.GetValue()
          -        self.username.strip("\n")
          -        self.password.strip("\n")
          -
          -        if not (self.username and self.password):
          -            # If either the username or password is missing, return
          -            return
          -
          -        if len(self.username) < 3:
          -            # If the username is shorter than 3, return
          -            return
          -
          -        # Disable the entries to stop people from typing in them.
          -        self.username_entry.Disable()
          -        self.password_entry.Disable()
          -        self.log_in_button.Disable()
          -
          -        # Get the position of the inner_panel
          -        old_pos = self.inner_panel.GetPosition()
          -        start_point = old_pos[0]
          -
          -        # Move the panel over to the right.
          -        for i in range(0, 512):
          -            wx.Yield()
          -            self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
          -
          -        # Hide the panel. The panel is already on the right so it's not visible anyways.
          -        self.inner_panel.Hide()
          -        self.web_view.SetSize((512, 600))
          -
          -        # Expand the window.
          -        for i in range(0, 88):
          -            self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
          -
          -        # Runs the user_login function.
          -        fd = await user_login(self.client, self.username, self.password)
          -
          -        # Load the captcha URL.
          -        if fd:
          -            self.web_view.LoadURL(fd.url)
          -            self.web_view.Show()
          -        else:
          -            # No captcha needed.
          -            self.Close()
          -

          Ancestors

          • wx._core.Frame
          • @@ -820,89 +244,12 @@

            Methods

            -
            - -Expand source code - -
            async def login_click(self, event):
            -    self.username = self.username_entry.GetValue()
            -    self.password = self.password_entry.GetValue()
            -    self.username.strip("\n")
            -    self.password.strip("\n")
            -
            -    if not (self.username and self.password):
            -        # If either the username or password is missing, return
            -        return
            -
            -    if len(self.username) < 3:
            -        # If the username is shorter than 3, return
            -        return
            -
            -    # Disable the entries to stop people from typing in them.
            -    self.username_entry.Disable()
            -    self.password_entry.Disable()
            -    self.log_in_button.Disable()
            -
            -    # Get the position of the inner_panel
            -    old_pos = self.inner_panel.GetPosition()
            -    start_point = old_pos[0]
            -
            -    # Move the panel over to the right.
            -    for i in range(0, 512):
            -        wx.Yield()
            -        self.inner_panel.SetPosition((int(start_point + pytweening.easeOutQuad(i / 512) * 512), old_pos[1]))
            -
            -    # Hide the panel. The panel is already on the right so it's not visible anyways.
            -    self.inner_panel.Hide()
            -    self.web_view.SetSize((512, 600))
            -
            -    # Expand the window.
            -    for i in range(0, 88):
            -        self.SetSize((512, int(512 + pytweening.easeOutQuad(i / 88) * 88)))
            -
            -    # Runs the user_login function.
            -    fd = await user_login(self.client, self.username, self.password)
            -
            -    # Load the captcha URL.
            -    if fd:
            -        self.web_view.LoadURL(fd.url)
            -        self.web_view.Show()
            -    else:
            -        # No captcha needed.
            -        self.Close()
            -
            async def login_load(self, event)
            -
            - -Expand source code - -
            async def login_load(self, event):
            -    _, token = self.web_view.RunScript("try{document.getElementsByTagName('input')[0].value}catch(e){}")
            -    if token == "undefined":
            -        token = False
            -    if token:
            -        self.web_view.Hide()
            -        lr = await user_login(
            -            self.client,
            -            self.username,
            -            self.password,
            -            token
            -        )
            -        if ".ROBLOSECURITY" in self.client.requests.session.cookies:
            -            self.status = True
            -            self.Close()
            -        else:
            -            self.status = False
            -            wx.MessageBox(f"Failed to log in.\n"
            -                          f"Detailed information from server: {lr.json()['errors'][0]['message']}",
            -                          "Error", wx.OK | wx.ICON_ERROR)
            -            self.Close()
            -
          diff --git a/docs/gamepersistence.html b/docs/gamepersistence.html index a06f491d..15281607 100644 --- a/docs/gamepersistence.html +++ b/docs/gamepersistence.html @@ -23,309 +23,6 @@

          Module ro_py.gamepersistence

          This file houses functions used for tampering with Roblox Datastores

          -
          - -Expand source code - -
          """
          -
          -This file houses functions used for tampering with Roblox Datastores
          -
          -"""
          -
          -from urllib.parse import quote
          -from math import floor
          -import re
          -
          -endpoint = "http://gamepersistence.roblox.com/"
          -
          -
          -class DataStore:
          -    """
          -    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com).
          -    This is only available for authenticated clients, and games that they own.
          -
          -    Parameters
          -    ----------
          -    requests : ro_py.utilities.requests.Requests
          -        Requests object to use for API requests.
          -    place_id : int
          -        PlaceId to modify the DataStores for, 
          -        if the currently authenticated user doesn't have sufficient permissions, 
          -        it will raise a NotAuthorizedToModifyPlaceDataStores exception
          -    name : str
          -        The name of the DataStore, 
          -        as in the Second Parameter of 
          -        `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
          -    scope : str, optional
          -        The scope of the DataStore,
          -        as on the Second Parameter of
          -         `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
          -    legacy : bool, optional
          -        Describes whether or not this will use the legacy endpoints, 
          -        over the new v1 endpoints (Does not apply to getSortedValues)
          -    legacy_naming_scheme : bool, optional
          -        Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), 
          -        there will be no qkeys[idx].target (normally the key that is passed into each method), 
          -        and the qkeys[idx].key will match the key passed into each method.
          -    """
          -
          -    def __init__(self, requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False):
          -        self.requests = requests
          -        self.place_id = place_id
          -        self.legacy = legacy
          -        self.legacy_naming_scheme = legacy_naming_scheme
          -        self.name = name
          -        self.scope = scope if scope is not None else "global"
          -
          -    async def get(self, key):
          -        """
          -        Represents a get request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
          -            r = await self.requests.post(
          -                url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}",
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if len(r.json()['data']) == 0:
          -                return None
          -            else:
          -                return r.json()['data'][0]['Value']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.get(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id)
          -                })
          -            if r.status_code == 204:
          -                return None
          -            else:
          -                return r.text
          -
          -    async def set(self, key, value):
          -        """
          -        Represents a set request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = f"value={quote(str(value))}"
          -            url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if len(r.json()['data']) == 0:
          -                return None
          -            else:
          -                return r.json()['data']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': '*/*',
          -                    'Content-Length': str(len(str(value)))
          -                }, data=quote(str(value)))
          -            if r.status_code == 200:
          -                return value
          -
          -    async def set_if_value(self, key, value, expected_value):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports legacy
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        expected_value
          -            The expected_value for that key, if you know the key doesn't exist, then set this as None
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}"
          -        url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        try:
          -            if r.json()['data'] != 0:
          -                return r.json()['data']
          -        except KeyError:
          -            return r.json()['error']
          -
          -    async def set_if_idx(self, key, value, idx):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports new endpoints,
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        idx : int
          -            The expectedidx, there
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': '*/*',
          -                'Content-Length': str(len(str(value)))
          -            }, data=quote(str(value)))
          -        if r.status_code == 409:
          -            usn = r.headers['roblox-usn']
          -            split = usn.split('.')
          -            msn_hash = split[0]
          -            current_value = split[1]
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
          -            r2 = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': '*/*',
          -                    'Content-Length': str(len(str(value)))
          -                }, data=quote(str(value)))
          -            if r2.status_code == 409:
          -                return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
          -            else:
          -                return value
          -
          -    async def increment(self, key, delta=0):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports legacy
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        delta : int, optional
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        data = ""
          -        url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
          -
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        try:
          -            if r.json()['data'] != 0:
          -                return r.json()['data']
          -        except KeyError:
          -            cap = re.search("\(.+\)", r.json()['error'])
          -            reason = cap.group(0).replace("(", "").replace(")", "")
          -            if reason == "ExistingValueNotNumeric":
          -                return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
          -
          -    async def remove(self, key):
          -        """
          -        Represents a get request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to remove, 
          -            as in the Second Parameter of 
          -            `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = ""
          -            url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if r.json()['data'] is None:
          -                return None
          -            else:
          -                return r.json()['data']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id)
          -                })
          -            if r.status_code == 204:
          -                return None
          -            else:
          -                return r.text
          -
          @@ -367,296 +64,6 @@

          Parameters

          there will be no qkeys[idx].target (normally the key that is passed into each method), and the qkeys[idx].key will match the key passed into each method. -
          - -Expand source code - -
          class DataStore:
          -    """
          -    Represents the in-game datastore system for storing data for games (https://gamepersistence.roblox.com).
          -    This is only available for authenticated clients, and games that they own.
          -
          -    Parameters
          -    ----------
          -    requests : ro_py.utilities.requests.Requests
          -        Requests object to use for API requests.
          -    place_id : int
          -        PlaceId to modify the DataStores for, 
          -        if the currently authenticated user doesn't have sufficient permissions, 
          -        it will raise a NotAuthorizedToModifyPlaceDataStores exception
          -    name : str
          -        The name of the DataStore, 
          -        as in the Second Parameter of 
          -        `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
          -    scope : str, optional
          -        The scope of the DataStore,
          -        as on the Second Parameter of
          -         `std::shared_ptr<RBX::Instance> DataStoreService::getDataStore(const DataStoreService* this, std::string name, std::string scope = "global")`
          -    legacy : bool, optional
          -        Describes whether or not this will use the legacy endpoints, 
          -        over the new v1 endpoints (Does not apply to getSortedValues)
          -    legacy_naming_scheme : bool, optional
          -        Describes whether or not this will use legacy names for data stores, if true, the qkeys[idx].scope will match the current scope (global by default), 
          -        there will be no qkeys[idx].target (normally the key that is passed into each method), 
          -        and the qkeys[idx].key will match the key passed into each method.
          -    """
          -
          -    def __init__(self, requests, place_id, name, scope, legacy=True, legacy_naming_scheme=False):
          -        self.requests = requests
          -        self.place_id = place_id
          -        self.legacy = legacy
          -        self.legacy_naming_scheme = legacy_naming_scheme
          -        self.name = name
          -        self.scope = scope if scope is not None else "global"
          -
          -    async def get(self, key):
          -        """
          -        Represents a get request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
          -            r = await self.requests.post(
          -                url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}",
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if len(r.json()['data']) == 0:
          -                return None
          -            else:
          -                return r.json()['data'][0]['Value']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.get(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id)
          -                })
          -            if r.status_code == 204:
          -                return None
          -            else:
          -                return r.text
          -
          -    async def set(self, key, value):
          -        """
          -        Represents a set request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = f"value={quote(str(value))}"
          -            url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if len(r.json()['data']) == 0:
          -                return None
          -            else:
          -                return r.json()['data']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': '*/*',
          -                    'Content-Length': str(len(str(value)))
          -                }, data=quote(str(value)))
          -            if r.status_code == 200:
          -                return value
          -
          -    async def set_if_value(self, key, value, expected_value):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports legacy
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        expected_value
          -            The expected_value for that key, if you know the key doesn't exist, then set this as None
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}"
          -        url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        try:
          -            if r.json()['data'] != 0:
          -                return r.json()['data']
          -        except KeyError:
          -            return r.json()['error']
          -
          -    async def set_if_idx(self, key, value, idx):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports new endpoints,
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        value
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        idx : int
          -            The expectedidx, there
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': '*/*',
          -                'Content-Length': str(len(str(value)))
          -            }, data=quote(str(value)))
          -        if r.status_code == 409:
          -            usn = r.headers['roblox-usn']
          -            split = usn.split('.')
          -            msn_hash = split[0]
          -            current_value = split[1]
          -            url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
          -            r2 = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': '*/*',
          -                    'Content-Length': str(len(str(value)))
          -                }, data=quote(str(value)))
          -            if r2.status_code == 409:
          -                return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
          -            else:
          -                return value
          -
          -    async def increment(self, key, delta=0):
          -        """
          -        Represents a conditional set request to a data store,
          -        only supports legacy
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to get, 
          -            as in the Second Parameter of 
          -            `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        delta : int, optional
          -            The value to set for the key,
          -            as in the 3rd parameter of
          -            `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -        
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        data = ""
          -        url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
          -
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        try:
          -            if r.json()['data'] != 0:
          -                return r.json()['data']
          -        except KeyError:
          -            cap = re.search("\(.+\)", r.json()['error'])
          -            reason = cap.group(0).replace("(", "").replace(")", "")
          -            if reason == "ExistingValueNotNumeric":
          -                return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
          -
          -    async def remove(self, key):
          -        """
          -        Represents a get request to a data store,
          -        using legacy works the same
          -
          -        Parameters
          -        ----------
          -        key : str
          -            The key of the value you wish to remove, 
          -            as in the Second Parameter of 
          -            `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -
          -        Returns
          -        -------
          -        typing.Any
          -        """
          -        if self.legacy:
          -            data = ""
          -            url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id),
          -                    'Content-Type': 'application/x-www-form-urlencoded'
          -                }, data=data)
          -            if r.json()['data'] is None:
          -                return None
          -            else:
          -                return r.json()['data']
          -        else:
          -            url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -            r = await self.requests.post(
          -                url=url,
          -                headers={
          -                    'Roblox-Place-Id': str(self.place_id)
          -                })
          -            if r.status_code == 204:
          -                return None
          -            else:
          -                return r.text
          -

          Methods

          @@ -677,50 +84,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def get(self, key):
          -    """
          -    Represents a get request to a data store,
          -    using legacy works the same
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to get, 
          -        as in the Second Parameter of 
          -        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    if self.legacy:
          -        data = f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target=&qkeys[0].key={quote(key)}" if self.legacy_naming_scheme == True else f"qkeys[0].scope={quote(self.scope)}&qkeys[0].target={quote(key)}&qkeys[0].key={quote(self.name)}"
          -        r = await self.requests.post(
          -            url=endpoint + f"persistence/getV2?placeId={str(self.place_id)}&type=standard&scope={quote(self.scope)}",
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        if len(r.json()['data']) == 0:
          -            return None
          -        else:
          -            return r.json()['data'][0]['Value']
          -    else:
          -        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -        r = await self.requests.get(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id)
          -            })
          -        if r.status_code == 204:
          -            return None
          -        else:
          -            return r.text
          -
          async def increment(self, key, delta=0) @@ -744,48 +107,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def increment(self, key, delta=0):
          -    """
          -    Represents a conditional set request to a data store,
          -    only supports legacy
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to get, 
          -        as in the Second Parameter of 
          -        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    delta : int, optional
          -        The value to set for the key,
          -        as in the 3rd parameter of
          -        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    data = ""
          -    url = endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&value={str(delta)}" if self.legacy_naming_scheme else endpoint + f"persistence/increment?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&value={str(delta)}"
          -
          -    r = await self.requests.post(
          -        url=url,
          -        headers={
          -            'Roblox-Place-Id': str(self.place_id),
          -            'Content-Type': 'application/x-www-form-urlencoded'
          -        }, data=data)
          -    try:
          -        if r.json()['data'] != 0:
          -            return r.json()['data']
          -    except KeyError:
          -        cap = re.search("\(.+\)", r.json()['error'])
          -        reason = cap.group(0).replace("(", "").replace(")", "")
          -        if reason == "ExistingValueNotNumeric":
          -            return "The requested key you tried to increment had a different value other than byte, short, int, long, long long, float, double or long double"
          -
          async def remove(self, key) @@ -805,51 +126,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def remove(self, key):
          -    """
          -    Represents a get request to a data store,
          -    using legacy works the same
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to remove, 
          -        as in the Second Parameter of 
          -        `void DataStore::removeAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    if self.legacy:
          -        data = ""
          -        url = endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme else endpoint + f"persistence/remove?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        if r.json()['data'] is None:
          -            return None
          -        else:
          -            return r.json()['data']
          -    else:
          -        url = endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py/remove?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id)
          -            })
          -        if r.status_code == 204:
          -            return None
          -        else:
          -            return r.text
          -
          async def set(self, key, value) @@ -873,55 +149,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def set(self, key, value):
          -    """
          -    Represents a set request to a data store,
          -    using legacy works the same
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to get, 
          -        as in the Second Parameter of 
          -        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    value
          -        The value to set for the key,
          -        as in the 3rd parameter of
          -        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    if self.legacy:
          -        data = f"value={quote(str(value))}"
          -        url = endpoint + f"persistence/set?placeId={self.place_id}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': 'application/x-www-form-urlencoded'
          -            }, data=data)
          -        if len(r.json()['data']) == 0:
          -            return None
          -        else:
          -            return r.json()['data']
          -    else:
          -        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}"
          -        r = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': '*/*',
          -                'Content-Length': str(len(str(value)))
          -            }, data=quote(str(value)))
          -        if r.status_code == 200:
          -            return value
          -
          async def set_if_idx(self, key, value, idx) @@ -947,58 +174,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def set_if_idx(self, key, value, idx):
          -    """
          -    Represents a conditional set request to a data store,
          -    only supports new endpoints,
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to get, 
          -        as in the Second Parameter of 
          -        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    value
          -        The value to set for the key,
          -        as in the 3rd parameter of
          -        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    idx : int
          -        The expectedidx, there
          -
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn=0.0"
          -    r = await self.requests.post(
          -        url=url,
          -        headers={
          -            'Roblox-Place-Id': str(self.place_id),
          -            'Content-Type': '*/*',
          -            'Content-Length': str(len(str(value)))
          -        }, data=quote(str(value)))
          -    if r.status_code == 409:
          -        usn = r.headers['roblox-usn']
          -        split = usn.split('.')
          -        msn_hash = split[0]
          -        current_value = split[1]
          -        url = endpoint + f"v1/persistence/ro_py?type=standard&key={quote(key)}&scope={quote(self.scope)}&target=" if self.legacy_naming_scheme == True else endpoint + f"v1/persistence/ro_py?type=standard&key={quote(self.name)}&scope={quote(self.scope)}&target={quote(key)}&usn={msn_hash}.{hex(idx).split('x')[1]}"
          -        r2 = await self.requests.post(
          -            url=url,
          -            headers={
          -                'Roblox-Place-Id': str(self.place_id),
          -                'Content-Type': '*/*',
          -                'Content-Length': str(len(str(value)))
          -            }, data=quote(str(value)))
          -        if r2.status_code == 409:
          -            return "Expected idx did not match current idx, current idx is " + str(floor(int(current_value, 16)))
          -        else:
          -            return value
          -
          async def set_if_value(self, key, value, expected_value) @@ -1024,46 +199,6 @@

          Returns

          typing.Any
           
          -
          - -Expand source code - -
          async def set_if_value(self, key, value, expected_value):
          -    """
          -    Represents a conditional set request to a data store,
          -    only supports legacy
          -
          -    Parameters
          -    ----------
          -    key : str
          -        The key of the value you wish to get, 
          -        as in the Second Parameter of 
          -        `void DataStore::getAsync(const DataStore* this, std::string key, boost::function<void(RBX::Reflection::Variant)> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    value
          -        The value to set for the key,
          -        as in the 3rd parameter of
          -        `void DataStore::setAsync(const DataStore* this, std::string key, RBX::Reflection::Variant value, boost::function<void()> resumeFunction, boost::function<void(std::string)> errorFunction)`
          -    expected_value
          -        The expected_value for that key, if you know the key doesn't exist, then set this as None
          -
          -    Returns
          -    -------
          -    typing.Any
          -    """
          -    data = f"value={quote(str(value))}&expectedValue={quote(str(expected_value)) if expected_value is not None else ''}"
          -    url = endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(key)}&type=standard&scope={quote(self.scope)}&target=&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}" if self.legacy_naming_scheme == True else endpoint + f"persistence/set?placeId={str(self.place_id)}&type=standard&key={quote(self.name)}&type=standard&scope={quote(self.scope)}&target={quote(key)}&valueLength={str(len(str(value)))}&expectedValueLength={str(len(str(expected_value))) if expected_value is not None else str(0)}"
          -    r = await self.requests.post(
          -        url=url,
          -        headers={
          -            'Roblox-Place-Id': str(self.place_id),
          -            'Content-Type': 'application/x-www-form-urlencoded'
          -        }, data=data)
          -    try:
          -        if r.json()['data'] != 0:
          -            return r.json()['data']
          -    except KeyError:
          -        return r.json()['error']
          -
          diff --git a/docs/games.html b/docs/games.html index e5850f0c..99d86f22 100644 --- a/docs/games.html +++ b/docs/games.html @@ -23,135 +23,6 @@

          Module ro_py.games

          This file houses functions and classes that pertain to Roblox universes and places.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to Roblox universes and places.
          -
          -"""
          -
          -from ro_py.users import User
          -from ro_py.groups import Group
          -from ro_py.badges import Badge
          -
          -endpoint = "https://games.roblox.com/"
          -
          -
          -class Votes:
          -    """
          -    Represents a game's votes.
          -    """
          -    def __init__(self, votes_data):
          -        self.up_votes = votes_data["upVotes"]
          -        self.down_votes = votes_data["downVotes"]
          -
          -
          -class Game:
          -    """
          -    Represents a Roblox game universe.
          -    This class represents multiple game-related endpoints.
          -    """
          -    def __init__(self, requests, universe_id):
          -        self.id = universe_id
          -        self.requests = requests
          -        self.name = None
          -        self.description = None
          -        self.creator = None
          -        self.price = None
          -        self.allowed_gear_genres = None
          -        self.allowed_gear_categories = None
          -        self.max_players = None
          -        self.studio_access_to_apis_allowed = None
          -        self.create_vip_servers_allowed = None
          -
          -    async def update(self):
          -        """
          -        Updates the game's information.
          -        """
          -        game_info_req = await self.requests.get(
          -            url=endpoint + "v1/games",
          -            params={
          -                "universeIds": str(self.id)
          -            }
          -        )
          -        game_info = game_info_req.json()
          -        game_info = game_info["data"][0]
          -        self.name = game_info["name"]
          -        self.description = game_info["description"]
          -        if game_info["creator"]["type"] == "User":
          -            self.creator = User(self.requests, game_info["creator"]["id"])
          -        elif game_info["creator"]["type"] == "Group":
          -            self.creator = Group(self.requests, game_info["creator"]["id"])
          -        self.price = game_info["price"]
          -        self.allowed_gear_genres = game_info["allowedGearGenres"]
          -        self.allowed_gear_categories = game_info["allowedGearCategories"]
          -        self.max_players = game_info["maxPlayers"]
          -        self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
          -        self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
          -
          -    async def get_votes(self):
          -        """
          -        :return: An instance of Votes
          -        """
          -        votes_info_req = await self.requests.get(
          -            url=endpoint + "v1/games/votes",
          -            params={
          -                "universeIds": str(self.id)
          -            }
          -        )
          -        votes_info = votes_info_req.json()
          -        votes_info = votes_info["data"][0]
          -        votes = Votes(votes_info)
          -        return votes
          -
          -    async def get_badges(self):
          -        """
          -        Gets the game's badges.
          -        This will be updated soon to use the new Page object.
          -        """
          -        badges_req = await self.requests.get(
          -            url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
          -            params={
          -                "limit": 100,
          -                "sortOrder": "Asc"
          -            }
          -        )
          -        badges_data = badges_req.json()["data"]
          -        badges = []
          -        for badge in badges_data:
          -            badges.append(Badge(self.requests, badge["id"]))
          -        return badges
          -
          -
          -"""
          -def place_id_to_universe_id(place_id):
          -    \"""
          -    Returns the containing universe ID of a place ID.
          -    :param place_id: Place ID
          -    :return: Universe ID
          -    \"""
          -    universe_id_req = self.requests.get(
          -        url="https://api.roblox.com/universes/get-universe-containing-place",
          -        params={
          -            "placeId": place_id
          -        }
          -    )
          -    universe_id = universe_id_req.json()["UniverseId"]
          -    return universe_id
          -
          -
          -def game_from_place_id(place_id):
          -    \"""
          -    Generates an instance of Game with a place ID instead of a game ID.
          -    :param place_id: Place ID
          -    :return: Instace of Game
          -    \"""
          -    return Game(self.requests, place_id_to_universe_id(place_id))
          -"""
          -
          @@ -169,86 +40,6 @@

          Classes

          Represents a Roblox game universe. This class represents multiple game-related endpoints.

          -
          - -Expand source code - -
          class Game:
          -    """
          -    Represents a Roblox game universe.
          -    This class represents multiple game-related endpoints.
          -    """
          -    def __init__(self, requests, universe_id):
          -        self.id = universe_id
          -        self.requests = requests
          -        self.name = None
          -        self.description = None
          -        self.creator = None
          -        self.price = None
          -        self.allowed_gear_genres = None
          -        self.allowed_gear_categories = None
          -        self.max_players = None
          -        self.studio_access_to_apis_allowed = None
          -        self.create_vip_servers_allowed = None
          -
          -    async def update(self):
          -        """
          -        Updates the game's information.
          -        """
          -        game_info_req = await self.requests.get(
          -            url=endpoint + "v1/games",
          -            params={
          -                "universeIds": str(self.id)
          -            }
          -        )
          -        game_info = game_info_req.json()
          -        game_info = game_info["data"][0]
          -        self.name = game_info["name"]
          -        self.description = game_info["description"]
          -        if game_info["creator"]["type"] == "User":
          -            self.creator = User(self.requests, game_info["creator"]["id"])
          -        elif game_info["creator"]["type"] == "Group":
          -            self.creator = Group(self.requests, game_info["creator"]["id"])
          -        self.price = game_info["price"]
          -        self.allowed_gear_genres = game_info["allowedGearGenres"]
          -        self.allowed_gear_categories = game_info["allowedGearCategories"]
          -        self.max_players = game_info["maxPlayers"]
          -        self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
          -        self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
          -
          -    async def get_votes(self):
          -        """
          -        :return: An instance of Votes
          -        """
          -        votes_info_req = await self.requests.get(
          -            url=endpoint + "v1/games/votes",
          -            params={
          -                "universeIds": str(self.id)
          -            }
          -        )
          -        votes_info = votes_info_req.json()
          -        votes_info = votes_info["data"][0]
          -        votes = Votes(votes_info)
          -        return votes
          -
          -    async def get_badges(self):
          -        """
          -        Gets the game's badges.
          -        This will be updated soon to use the new Page object.
          -        """
          -        badges_req = await self.requests.get(
          -            url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
          -            params={
          -                "limit": 100,
          -                "sortOrder": "Asc"
          -            }
          -        )
          -        badges_data = badges_req.json()["data"]
          -        badges = []
          -        for badge in badges_data:
          -            badges.append(Badge(self.requests, badge["id"]))
          -        return badges
          -

          Methods

          @@ -257,88 +48,43 @@

          Methods

          Gets the game's badges. This will be updated soon to use the new Page object.

          -
          - -Expand source code - -
          async def get_badges(self):
          -    """
          -    Gets the game's badges.
          -    This will be updated soon to use the new Page object.
          -    """
          -    badges_req = await self.requests.get(
          -        url=f"https://badges.roblox.com/v1/universes/{self.id}/badges",
          -        params={
          -            "limit": 100,
          -            "sortOrder": "Asc"
          -        }
          -    )
          -    badges_data = badges_req.json()["data"]
          -    badges = []
          -    for badge in badges_data:
          -        badges.append(Badge(self.requests, badge["id"]))
          -    return badges
          -
          async def get_votes(self)

          :return: An instance of Votes

          -
          - -Expand source code - -
          async def get_votes(self):
          -    """
          -    :return: An instance of Votes
          -    """
          -    votes_info_req = await self.requests.get(
          -        url=endpoint + "v1/games/votes",
          -        params={
          -            "universeIds": str(self.id)
          -        }
          -    )
          -    votes_info = votes_info_req.json()
          -    votes_info = votes_info["data"][0]
          -    votes = Votes(votes_info)
          -    return votes
          -
          async def update(self)

          Updates the game's information.

          -
          - -Expand source code - -
          async def update(self):
          -    """
          -    Updates the game's information.
          -    """
          -    game_info_req = await self.requests.get(
          -        url=endpoint + "v1/games",
          -        params={
          -            "universeIds": str(self.id)
          -        }
          -    )
          -    game_info = game_info_req.json()
          -    game_info = game_info["data"][0]
          -    self.name = game_info["name"]
          -    self.description = game_info["description"]
          -    if game_info["creator"]["type"] == "User":
          -        self.creator = User(self.requests, game_info["creator"]["id"])
          -    elif game_info["creator"]["type"] == "Group":
          -        self.creator = Group(self.requests, game_info["creator"]["id"])
          -    self.price = game_info["price"]
          -    self.allowed_gear_genres = game_info["allowedGearGenres"]
          -    self.allowed_gear_categories = game_info["allowedGearCategories"]
          -    self.max_players = game_info["maxPlayers"]
          -    self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"]
          -    self.create_vip_servers_allowed = game_info["createVipServersAllowed"]
          -
          +
          +
          +
          +
          +class Place +(requests, id) +
          +
          +

          DO NOT USE. NOT READY YET.

          +

          Methods

          +
          +
          +async def join(self, launchtime=1609186776825, rloc='en_us', gloc='en_us', negotiate_url='https://www.roblox.com/Login/Negotiate.ashx') +
          +
          +

          Joins the place. +This currently only works on Windows since it looks in AppData for the executable.

          +
          +

          Warning

          +

          Please do not use this part of ro.py maliciously. We've spent lots of time +working on ro.py as a resource for building interactive Roblox programs, and +we would hate to see it be used as a malicious tool. +We do not condone any use of ro.py as an exploit and we are not responsible +if you are banned from Roblox due to malicious use of our library.

          +
          @@ -348,18 +94,6 @@

          Methods

          Represents a game's votes.

          -
          - -Expand source code - -
          class Votes:
          -    """
          -    Represents a game's votes.
          -    """
          -    def __init__(self, votes_data):
          -        self.up_votes = votes_data["upVotes"]
          -        self.down_votes = votes_data["downVotes"]
          -
          @@ -386,6 +120,12 @@

          Game
        • +

          Place

          + +
        • +
        • Votes

        diff --git a/docs/gender.html b/docs/gender.html index e1b3511e..74b15e58 100644 --- a/docs/gender.html +++ b/docs/gender.html @@ -25,28 +25,6 @@

        Module ro_py.gender

        I hate how Roblox stores gender at all, it's really strange as it's not used for anything. There's literally no point in storing this information.

        -
        - -Expand source code - -
        """
        -
        -I hate how Roblox stores gender at all, it's really strange as it's not used for anything.
        -There's literally no point in storing this information.
        -
        -"""
        -
        -import enum
        -
        -
        -class RobloxGender(enum.Enum):
        -    """
        -    Represents the gender of the authenticated Roblox client.
        -    """
        -    Other = 1
        -    Female = 2
        -    Male = 3
        -
        @@ -63,18 +41,6 @@

        Classes

        Represents the gender of the authenticated Roblox client.

        -
        - -Expand source code - -
        class RobloxGender(enum.Enum):
        -    """
        -    Represents the gender of the authenticated Roblox client.
        -    """
        -    Other = 1
        -    Female = 2
        -    Male = 3
        -

        Ancestors

        • enum.Enum
        • diff --git a/docs/groups.html b/docs/groups.html index c1a0c31f..0baeda25 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -23,297 +23,6 @@

          Module ro_py.groups

          This file houses functions and classes that pertain to Roblox groups.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to Roblox groups.
          -
          -"""
          -import iso8601
          -from typing import List
          -from ro_py.users import User
          -from ro_py.roles import Role
          -from ro_py.utilities.errors import NotFound
          -from ro_py.utilities.pages import Pages, SortOrder
          -
          -endpoint = "https://groups.roblox.com"
          -
          -
          -class Shout:
          -    """
          -    Represents a group shout.
          -    """
          -    def __init__(self, requests, shout_data):
          -        self.body = shout_data["body"]
          -        self.poster = None  # User(requests, shout_data["poster"]["userId"])
          -
          -
          -class WallPost:
          -    """
          -    Represents a roblox wall post.
          -    """
          -    def __init__(self, requests, wall_data, group):
          -        self.requests = requests
          -        self.group = group
          -        self.id = wall_data['id']
          -        self.body = wall_data['body']
          -        self.created = iso8601.parse(wall_data['created'])
          -        self.updated = iso8601.parse(wall_data['updated'])
          -        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
          -
          -    async def delete(self):
          -        wall_req = await self.requests.delete(
          -            url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
          -        )
          -        return wall_req.status == 200
          -
          -
          -def wall_post_handeler(requests, this_page, args) -> List[WallPost]:
          -    wall_posts = []
          -    for wall_post in this_page:
          -        wall_posts.append(WallPost(requests, wall_post, args))
          -    return wall_posts
          -
          -
          -class Group:
          -    """
          -    Represents a group.
          -    """
          -    def __init__(self, requests, group_id):
          -        self.requests = requests
          -        self.id = group_id
          -
          -        self.name = None
          -        self.description = None
          -        self.owner = None
          -        self.member_count = None
          -        self.is_builders_club_only = None
          -        self.public_entry_allowed = None
          -        self.shout = None
          -
          -    async def update(self):
          -        """
          -        Updates the group's information.
          -        """
          -        group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}")
          -        group_info = group_info_req.json()
          -        self.name = group_info["name"]
          -        self.description = group_info["description"]
          -        self.owner = User(self.requests, group_info["owner"]["userId"])
          -        self.member_count = group_info["memberCount"]
          -        self.is_builders_club_only = group_info["isBuildersClubOnly"]
          -        self.public_entry_allowed = group_info["publicEntryAllowed"]
          -        if "shout" in group_info:
          -            self.shout = group_info["shout"]
          -        else:
          -            self.shout = None
          -        # self.is_locked = group_info["isLocked"]
          -
          -    async def update_shout(self, message):
          -        """
          -        Updates the shout of the group.
          -
          -        Parameters
          -        ----------
          -        message : str
          -            Message that will overwrite the current shout of a group.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        shout_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.id}/status",
          -            data={
          -                "message": message
          -            }
          -        )
          -        return shout_req.status_code == 200
          -
          -    async def get_roles(self):
          -        """
          -        Gets all roles of the group.
          -
          -        Returns
          -        -------
          -        list
          -        """
          -        role_req = await self.requests.get(
          -            url=endpoint + f"/v1/groups/{self.id}/roles"
          -        )
          -        roles = []
          -        for role in role_req.json()['roles']:
          -            roles.append(Role(self.requests, self, role))
          -        return roles
          -
          -    async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100):
          -        wall_req = Pages(
          -            requests=self.requests,
          -            url=endpoint + f"/v2/groups/{self.id}/wall/posts",
          -            sort_order=sort_order,
          -            limit=limit,
          -            handler=wall_post_handeler,
          -            handler_args=self
          -        )
          -        return wall_req
          -
          -    async def get_member_by_id(self, roblox_id):
          -        # Get list of group user is in.
          -        member_req = await self.requests.get(
          -            url=endpoint + f"/v2/users/{roblox_id}/groups/roles"
          -        )
          -        data = member_req.json()
          -
          -        # Find group in list.
          -        group_data = None
          -        for group in data['data']:
          -            if group['group']['id'] == self.id:
          -                group_data = group
          -                break
          -
          -        # Check if user is in group.
          -        if not group_data:
          -            raise NotFound(f"The user {roblox_id} was not found in group {self.id}")
          -
          -        # Create data to return.
          -        role = Role(self.requests, self, group_data['role'])
          -        member = Member(self.requests, roblox_id, None, self, role)
          -        return await member.update()
          -
          -
          -class PartialGroup(Group):
          -    pass
          -
          -
          -class Member(User):
          -    """
          -    Represents a user in a group.
          -
          -    Parameters
          -    ----------
          -    requests : ro_py.utilities.requests.Requests
          -            Requests object to use for API requests.
          -    roblox_id : int
          -            The id of a user.
          -    name : str
          -            The name of the user.
          -    group : ro_py.groups.Group
          -            The group the user is in.
          -    role : ro_py.roles.Role
          -            The role the user has is the group.
          -    """
          -    def __init__(self, requests, roblox_id, name=None, group=None, role=None):
          -        super().__init__(requests, roblox_id, name)
          -        self.role = role
          -        self.group = group
          -
          -    async def update_role(self, user):
          -        """
          -        Updates the role information of the user.
          -
          -        Returns
          -        -------
          -        ro_py.roles.Role
          -        """
          -        member_req = await self.requests.get(
          -            url=endpoint + f"/v2/users/{user.id}/groups/roles"
          -        )
          -        data = member_req.json()
          -        for role in data['data']:
          -            if role['group']['id'] == self.group.id:
          -                self.role = Role(self.requests, self.group, role['role'])
          -                break
          -        return self.role
          -
          -    async def change_rank(self, num):
          -        """
          -        Changes the users rank specified by a number.
          -        If num is 1 the users role will go up by 1.
          -        If num is -1 the users role will go down by 1.
          -
          -        Parameters
          -        ----------
          -        num : int
          -                How much to change the rank by.
          -        """
          -        await self.update_role()
          -        roles = await self.group.get_roles()
          -        role_counter = -1
          -        for group_role in roles:
          -            role_counter += 1
          -            if group_role.id == self.role.id:
          -                break
          -        if not roles:
          -            raise NotFound(f"User {self.id} is not in group {self.group.id}")
          -        return await self.setrank(roles[role_counter + num].id)
          -
          -    async def promote(self):
          -        """
          -        Promotes the user.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        return await self.change_rank(1)
          -
          -    async def demote(self):
          -        """
          -        Demotes the user.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        return await self.change_rank(-1)
          -
          -    async def setrank(self, rank):
          -        """
          -        Sets the users role to specified role using rank id.
          -
          -        Parameters
          -        ----------
          -        rank : int
          -                Rank id
          -
          -        Returns
          -        -------
          -        bool
          -        """
          -        rank_request = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}",
          -            data={
          -                "roleId": rank
          -            }
          -        )
          -        return rank_request.status == 200
          -
          -    async def setrole(self, role_num):
          -        """
          -         Sets the users role to specified role using role number (1-255).
          -
          -         Parameters
          -         ----------
          -         role_num : int
          -                Role number (1-255)
          -
          -         Returns
          -         -------
          -         bool
          -         """
          -        roles = await self.group.get_roles()
          -        rank_role = None
          -        for role in roles:
          -            if role.role == role_num:
          -                rank_role = role
          -                break
          -        if not rank_role:
          -            raise NotFound(f"Role {role_num} not found")
          -        return await self.setrank(rank_role.id)
          -
          @@ -327,16 +36,6 @@

          Functions

          -
          - -Expand source code - -
          def wall_post_handeler(requests, this_page, args) -> List[WallPost]:
          -    wall_posts = []
          -    for wall_post in this_page:
          -        wall_posts.append(WallPost(requests, wall_post, args))
          -    return wall_posts
          -
          @@ -349,115 +48,6 @@

          Classes

          Represents a group.

          -
          - -Expand source code - -
          class Group:
          -    """
          -    Represents a group.
          -    """
          -    def __init__(self, requests, group_id):
          -        self.requests = requests
          -        self.id = group_id
          -
          -        self.name = None
          -        self.description = None
          -        self.owner = None
          -        self.member_count = None
          -        self.is_builders_club_only = None
          -        self.public_entry_allowed = None
          -        self.shout = None
          -
          -    async def update(self):
          -        """
          -        Updates the group's information.
          -        """
          -        group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}")
          -        group_info = group_info_req.json()
          -        self.name = group_info["name"]
          -        self.description = group_info["description"]
          -        self.owner = User(self.requests, group_info["owner"]["userId"])
          -        self.member_count = group_info["memberCount"]
          -        self.is_builders_club_only = group_info["isBuildersClubOnly"]
          -        self.public_entry_allowed = group_info["publicEntryAllowed"]
          -        if "shout" in group_info:
          -            self.shout = group_info["shout"]
          -        else:
          -            self.shout = None
          -        # self.is_locked = group_info["isLocked"]
          -
          -    async def update_shout(self, message):
          -        """
          -        Updates the shout of the group.
          -
          -        Parameters
          -        ----------
          -        message : str
          -            Message that will overwrite the current shout of a group.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        shout_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.id}/status",
          -            data={
          -                "message": message
          -            }
          -        )
          -        return shout_req.status_code == 200
          -
          -    async def get_roles(self):
          -        """
          -        Gets all roles of the group.
          -
          -        Returns
          -        -------
          -        list
          -        """
          -        role_req = await self.requests.get(
          -            url=endpoint + f"/v1/groups/{self.id}/roles"
          -        )
          -        roles = []
          -        for role in role_req.json()['roles']:
          -            roles.append(Role(self.requests, self, role))
          -        return roles
          -
          -    async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100):
          -        wall_req = Pages(
          -            requests=self.requests,
          -            url=endpoint + f"/v2/groups/{self.id}/wall/posts",
          -            sort_order=sort_order,
          -            limit=limit,
          -            handler=wall_post_handeler,
          -            handler_args=self
          -        )
          -        return wall_req
          -
          -    async def get_member_by_id(self, roblox_id):
          -        # Get list of group user is in.
          -        member_req = await self.requests.get(
          -            url=endpoint + f"/v2/users/{roblox_id}/groups/roles"
          -        )
          -        data = member_req.json()
          -
          -        # Find group in list.
          -        group_data = None
          -        for group in data['data']:
          -            if group['group']['id'] == self.id:
          -                group_data = group
          -                break
          -
          -        # Check if user is in group.
          -        if not group_data:
          -            raise NotFound(f"The user {roblox_id} was not found in group {self.id}")
          -
          -        # Create data to return.
          -        role = Role(self.requests, self, group_data['role'])
          -        member = Member(self.requests, roblox_id, None, self, role)
          -        return await member.update()
          -

          Subclasses

          • PartialGroup
          • @@ -469,33 +59,6 @@

            Methods

            -
            - -Expand source code - -
            async def get_member_by_id(self, roblox_id):
            -    # Get list of group user is in.
            -    member_req = await self.requests.get(
            -        url=endpoint + f"/v2/users/{roblox_id}/groups/roles"
            -    )
            -    data = member_req.json()
            -
            -    # Find group in list.
            -    group_data = None
            -    for group in data['data']:
            -        if group['group']['id'] == self.id:
            -            group_data = group
            -            break
            -
            -    # Check if user is in group.
            -    if not group_data:
            -        raise NotFound(f"The user {roblox_id} was not found in group {self.id}")
            -
            -    # Create data to return.
            -    role = Role(self.requests, self, group_data['role'])
            -    member = Member(self.requests, roblox_id, None, self, role)
            -    return await member.update()
            -
            async def get_roles(self) @@ -507,74 +70,18 @@

            Returns

            list
             
            -
            - -Expand source code - -
            async def get_roles(self):
            -    """
            -    Gets all roles of the group.
            -
            -    Returns
            -    -------
            -    list
            -    """
            -    role_req = await self.requests.get(
            -        url=endpoint + f"/v1/groups/{self.id}/roles"
            -    )
            -    roles = []
            -    for role in role_req.json()['roles']:
            -        roles.append(Role(self.requests, self, role))
            -    return roles
            -
          async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100)
          -
          - -Expand source code - -
          async def get_wall_posts(self, sort_order=SortOrder.Ascending, limit=100):
          -    wall_req = Pages(
          -        requests=self.requests,
          -        url=endpoint + f"/v2/groups/{self.id}/wall/posts",
          -        sort_order=sort_order,
          -        limit=limit,
          -        handler=wall_post_handeler,
          -        handler_args=self
          -    )
          -    return wall_req
          -
          async def update(self)

          Updates the group's information.

          -
          - -Expand source code - -
          async def update(self):
          -    """
          -    Updates the group's information.
          -    """
          -    group_info_req = await self.requests.get(endpoint + f"/v1/groups/{self.id}")
          -    group_info = group_info_req.json()
          -    self.name = group_info["name"]
          -    self.description = group_info["description"]
          -    self.owner = User(self.requests, group_info["owner"]["userId"])
          -    self.member_count = group_info["memberCount"]
          -    self.is_builders_club_only = group_info["isBuildersClubOnly"]
          -    self.public_entry_allowed = group_info["publicEntryAllowed"]
          -    if "shout" in group_info:
          -        self.shout = group_info["shout"]
          -    else:
          -        self.shout = None
          -
          async def update_shout(self, message) @@ -591,37 +98,12 @@

          Returns

          int
           
          -
          - -Expand source code - -
          async def update_shout(self, message):
          -    """
          -    Updates the shout of the group.
          -
          -    Parameters
          -    ----------
          -    message : str
          -        Message that will overwrite the current shout of a group.
          -
          -    Returns
          -    -------
          -    int
          -    """
          -    shout_req = await self.requests.patch(
          -        url=endpoint + f"/v1/groups/{self.id}/status",
          -        data={
          -            "message": message
          -        }
          -    )
          -    return shout_req.status_code == 200
          -
        class Member -(requests, roblox_id, name=None, group=None, role=None) +(requests, roblox_id, name, group, role)

        Represents a user in a group.

        @@ -638,136 +120,6 @@

        Parameters

        role : Role
        The role the user has is the group.
        -
        - -Expand source code - -
        class Member(User):
        -    """
        -    Represents a user in a group.
        -
        -    Parameters
        -    ----------
        -    requests : ro_py.utilities.requests.Requests
        -            Requests object to use for API requests.
        -    roblox_id : int
        -            The id of a user.
        -    name : str
        -            The name of the user.
        -    group : ro_py.groups.Group
        -            The group the user is in.
        -    role : ro_py.roles.Role
        -            The role the user has is the group.
        -    """
        -    def __init__(self, requests, roblox_id, name=None, group=None, role=None):
        -        super().__init__(requests, roblox_id, name)
        -        self.role = role
        -        self.group = group
        -
        -    async def update_role(self, user):
        -        """
        -        Updates the role information of the user.
        -
        -        Returns
        -        -------
        -        ro_py.roles.Role
        -        """
        -        member_req = await self.requests.get(
        -            url=endpoint + f"/v2/users/{user.id}/groups/roles"
        -        )
        -        data = member_req.json()
        -        for role in data['data']:
        -            if role['group']['id'] == self.group.id:
        -                self.role = Role(self.requests, self.group, role['role'])
        -                break
        -        return self.role
        -
        -    async def change_rank(self, num):
        -        """
        -        Changes the users rank specified by a number.
        -        If num is 1 the users role will go up by 1.
        -        If num is -1 the users role will go down by 1.
        -
        -        Parameters
        -        ----------
        -        num : int
        -                How much to change the rank by.
        -        """
        -        await self.update_role()
        -        roles = await self.group.get_roles()
        -        role_counter = -1
        -        for group_role in roles:
        -            role_counter += 1
        -            if group_role.id == self.role.id:
        -                break
        -        if not roles:
        -            raise NotFound(f"User {self.id} is not in group {self.group.id}")
        -        return await self.setrank(roles[role_counter + num].id)
        -
        -    async def promote(self):
        -        """
        -        Promotes the user.
        -
        -        Returns
        -        -------
        -        int
        -        """
        -        return await self.change_rank(1)
        -
        -    async def demote(self):
        -        """
        -        Demotes the user.
        -
        -        Returns
        -        -------
        -        int
        -        """
        -        return await self.change_rank(-1)
        -
        -    async def setrank(self, rank):
        -        """
        -        Sets the users role to specified role using rank id.
        -
        -        Parameters
        -        ----------
        -        rank : int
        -                Rank id
        -
        -        Returns
        -        -------
        -        bool
        -        """
        -        rank_request = await self.requests.patch(
        -            url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}",
        -            data={
        -                "roleId": rank
        -            }
        -        )
        -        return rank_request.status == 200
        -
        -    async def setrole(self, role_num):
        -        """
        -         Sets the users role to specified role using role number (1-255).
        -
        -         Parameters
        -         ----------
        -         role_num : int
        -                Role number (1-255)
        -
        -         Returns
        -         -------
        -         bool
        -         """
        -        roles = await self.group.get_roles()
        -        rank_role = None
        -        for role in roles:
        -            if role.role == role_num:
        -                rank_role = role
        -                break
        -        if not rank_role:
        -            raise NotFound(f"Role {role_num} not found")
        -        return await self.setrank(rank_role.id)
        -

        Ancestors

        • User
        • @@ -786,32 +138,6 @@

          Parameters

          num : int
          How much to change the rank by.
          -
          - -Expand source code - -
          async def change_rank(self, num):
          -    """
          -    Changes the users rank specified by a number.
          -    If num is 1 the users role will go up by 1.
          -    If num is -1 the users role will go down by 1.
          -
          -    Parameters
          -    ----------
          -    num : int
          -            How much to change the rank by.
          -    """
          -    await self.update_role()
          -    roles = await self.group.get_roles()
          -    role_counter = -1
          -    for group_role in roles:
          -        role_counter += 1
          -        if group_role.id == self.role.id:
          -            break
          -    if not roles:
          -        raise NotFound(f"User {self.id} is not in group {self.group.id}")
          -    return await self.setrank(roles[role_counter + num].id)
          -
          async def demote(self) @@ -823,20 +149,12 @@

          Returns

          int
           
          -
          - -Expand source code - -
          async def demote(self):
          -    """
          -    Demotes the user.
          -
          -    Returns
          -    -------
          -    int
          -    """
          -    return await self.change_rank(-1)
          -
          + +
          +async def exile(self) +
          +
          +
          async def promote(self) @@ -848,20 +166,6 @@

          Returns

          int
           
          -
          - -Expand source code - -
          async def promote(self):
          -    """
          -    Promotes the user.
          -
          -    Returns
          -    -------
          -    int
          -    """
          -    return await self.change_rank(1)
          -
          async def setrank(self, rank) @@ -878,31 +182,6 @@

          Returns

          bool
           
          -
          - -Expand source code - -
          async def setrank(self, rank):
          -    """
          -    Sets the users role to specified role using rank id.
          -
          -    Parameters
          -    ----------
          -    rank : int
          -            Rank id
          -
          -    Returns
          -    -------
          -    bool
          -    """
          -    rank_request = await self.requests.patch(
          -        url=endpoint + f"/v1/groups/{self.id}/users/{self.group.id}",
          -        data={
          -            "roleId": rank
          -        }
          -    )
          -    return rank_request.status == 200
          -
          async def setrole(self, role_num) @@ -919,36 +198,9 @@

          Returns

          bool
           
          -
          - -Expand source code - -
          async def setrole(self, role_num):
          -    """
          -     Sets the users role to specified role using role number (1-255).
          -
          -     Parameters
          -     ----------
          -     role_num : int
          -            Role number (1-255)
          -
          -     Returns
          -     -------
          -     bool
          -     """
          -    roles = await self.group.get_roles()
          -    rank_role = None
          -    for role in roles:
          -        if role.role == role_num:
          -            rank_role = role
          -            break
          -    if not rank_role:
          -        raise NotFound(f"Role {role_num} not found")
          -    return await self.setrank(rank_role.id)
          -
          -async def update_role(self, user) +async def update_role(self)

          Updates the role information of the user.

          @@ -957,28 +209,6 @@

          Returns

          Role
           
          -
          - -Expand source code - -
          async def update_role(self, user):
          -    """
          -    Updates the role information of the user.
          -
          -    Returns
          -    -------
          -    ro_py.roles.Role
          -    """
          -    member_req = await self.requests.get(
          -        url=endpoint + f"/v2/users/{user.id}/groups/roles"
          -    )
          -    data = member_req.json()
          -    for role in data['data']:
          -        if role['group']['id'] == self.group.id:
          -            self.role = Role(self.requests, self.group, role['role'])
          -            break
          -    return self.role
          -

          Inherited members

          @@ -989,6 +219,7 @@

          Inherited members

        • get_followings_count
        • get_friends
        • get_friends_count
        • +
        • get_limiteds
        • get_roblox_badges
        • get_status
        • update
        • @@ -998,17 +229,10 @@

          Inherited members

          class PartialGroup -(requests, group_id) +(*args, **kwargs)
          -

          Represents a group.

          -
          - -Expand source code - -
          class PartialGroup(Group):
          -    pass
          -
          +

          Represents a group with less information

          Ancestors

          • Group
          • @@ -1030,18 +254,6 @@

            Inherited members

            Represents a group shout.

            -
            - -Expand source code - -
            class Shout:
            -    """
            -    Represents a group shout.
            -    """
            -    def __init__(self, requests, shout_data):
            -        self.body = shout_data["body"]
            -        self.poster = None  # User(requests, shout_data["poster"]["userId"])
            -
            class WallPost @@ -1049,29 +261,6 @@

            Inherited members

            Represents a roblox wall post.

            -
            - -Expand source code - -
            class WallPost:
            -    """
            -    Represents a roblox wall post.
            -    """
            -    def __init__(self, requests, wall_data, group):
            -        self.requests = requests
            -        self.group = group
            -        self.id = wall_data['id']
            -        self.body = wall_data['body']
            -        self.created = iso8601.parse(wall_data['created'])
            -        self.updated = iso8601.parse(wall_data['updated'])
            -        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
            -
            -    async def delete(self):
            -        wall_req = await self.requests.delete(
            -            url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
            -        )
            -        return wall_req.status == 200
            -

            Methods

            @@ -1079,16 +268,6 @@

            Methods

            -
            - -Expand source code - -
            async def delete(self):
            -    wall_req = await self.requests.delete(
            -        url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
            -    )
            -    return wall_req.status == 200
            -
            @@ -1128,6 +307,7 @@

            Member<
            • change_rank
            • demote
            • +
            • exile
            • promote
            • setrank
            • setrole
            • diff --git a/docs/index.html b/docs/index.html index 634bf1aa..11e66765 100644 --- a/docs/index.html +++ b/docs/index.html @@ -31,24 +31,6 @@

              Package ro_py

              ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/ You can view the source code at https://github.com/jmk-developer/ro.py/ You can also view the documentation at https://ro.py.jmksite.dev/.

              -
              - -Expand source code - -
              """
              -
              -ro.py
              -by jmkdev
              -
              -Welcome to ro.py!
              -ro.py is a powerful wrapper for the Roblox web API.
              -It can be used to create (almost) anything from chat bots to group management systems.
              -ro.py is still fairly new and may have bugs. If you have any issues, you can contact me at https://jmksite.dev/
              -You can view the source code at https://github.com/jmk-developer/ro.py/
              -You can also view the documentation at https://ro.py.jmksite.dev/.
              -
              -"""
              -

              Sub-modules

              diff --git a/docs/notifications.html b/docs/notifications.html index a690b049..248fae43 100644 --- a/docs/notifications.html +++ b/docs/notifications.html @@ -31,163 +31,6 @@

              Module ro_py.notifications

              Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond to Roblox chat messages, which is pretty neat.

              -
              - -Expand source code - -
              """
              -
              -This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger
              -notification menu on the Roblox web client.
              -
              -.. warning::
              -    This part of ro.py may have bugs and I don't recommend relying on it for daily use.
              -    Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond
              -    to Roblox chat messages, which is pretty neat.
              -"""
              -
              -from ro_py.utilities.caseconvert import to_snake_case
              -
              -from signalrcore_async.hub_connection_builder import HubConnectionBuilder
              -from urllib.parse import quote
              -import json
              -import time
              -import asyncio
              -
              -
              -class Notification:
              -    """
              -    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
              -    """
              -
              -    def __init__(self, notification_data):
              -        self.identifier = notification_data["C"]
              -        self.hub = notification_data["M"][0]["H"]
              -        self.type = None
              -        self.rtype = notification_data["M"][0]["M"]
              -        self.atype = notification_data["M"][0]["A"][0]
              -        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
              -        self.data = None
              -
              -        if isinstance(self.raw_data, dict):
              -            self.data = {}
              -            for key, value in self.raw_data.items():
              -                self.data[to_snake_case(key)] = value
              -
              -            if "type" in self.data:
              -                self.type = self.data["type"]
              -            elif "Type" in self.data:
              -                self.type = self.data["Type"]
              -
              -        elif isinstance(self.raw_data, list):
              -            self.data = []
              -            for value in self.raw_data:
              -                self.data.append(value)
              -
              -            if len(self.data) > 0:
              -                if "type" in self.data[0]:
              -                    self.type = self.data[0]["type"]
              -                elif "Type" in self.data[0]:
              -                    self.type = self.data[0]["Type"]
              -
              -
              -class NotificationReceiver:
              -    """
              -    This object is used to receive notifications.
              -    This should only be generated once per client as to not duplicate notifications.
              -    """
              -
              -    def __init__(self, requests, on_open, on_close, on_error, on_notification):
              -        self.requests = requests
              -
              -        self.on_open = on_open
              -        self.on_close = on_close
              -        self.on_error = on_error
              -        self.on_notification = on_notification
              -
              -        self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"]
              -        self.connection = None
              -
              -        self.negotiate_request = None
              -        self.wss_url = None
              -
              -    async def initialize(self):
              -        self.negotiate_request = await self.requests.get(
              -            url="https://realtime.roblox.com/notifications/negotiate"
              -                "?clientProtocol=1.5"
              -                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
              -            cookies={
              -                ".ROBLOSECURITY": self.roblosecurity
              -            }
              -        )
              -        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
              -                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
              -                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
              -        self.connection = HubConnectionBuilder()
              -        self.connection.with_url(
              -            self.wss_url,
              -            options={
              -                "headers": {
              -                    "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
              -                },
              -                "skip_negotiation": False
              -            }
              -        )
              -
              -        async def on_message(_self, raw_notification):
              -            """
              -            Internal callback when a message is received.
              -            """
              -            try:
              -                notification_json = json.loads(raw_notification)
              -            except json.decoder.JSONDecodeError:
              -                return
              -            if len(notification_json) > 0:
              -                notification = Notification(notification_json)
              -                await self.on_notification(notification)
              -            else:
              -                return
              -
              -        def _internal_send(_self, message, protocol=None):
              -
              -            _self.logger.debug("Sending message {0}".format(message))
              -
              -            try:
              -                protocol = _self.protocol if protocol is None else protocol
              -
              -                _self._ws.send(protocol.encode(message))
              -                _self.connection_checker.last_message = time.time()
              -
              -                if _self.reconnection_handler is not None:
              -                    _self.reconnection_handler.reset()
              -
              -            except Exception as ex:
              -                raise ex
              -
              -        self.connection = self.connection.with_automatic_reconnect({
              -            "type": "raw",
              -            "keep_alive_interval": 10,
              -            "reconnect_interval": 5,
              -            "max_attempts": 5
              -        }).build()
              -
              -        if self.on_open:
              -            self.connection.on_open(self.on_open)
              -        if self.on_close:
              -            self.connection.on_close(self.on_close)
              -        if self.on_error:
              -            self.connection.on_error(self.on_error)
              -        self.connection.on_message = on_message
              -        self.connection._internal_send = _internal_send
              -
              -        await self.connection.start()
              -
              -    async def close(self):
              -        """
              -        Closes the connection and stops receiving notifications.
              -        """
              -        self.connection.stop()
              -
              @@ -204,45 +47,6 @@

              Classes

              Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.

              -
              - -Expand source code - -
              class Notification:
              -    """
              -    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
              -    """
              -
              -    def __init__(self, notification_data):
              -        self.identifier = notification_data["C"]
              -        self.hub = notification_data["M"][0]["H"]
              -        self.type = None
              -        self.rtype = notification_data["M"][0]["M"]
              -        self.atype = notification_data["M"][0]["A"][0]
              -        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
              -        self.data = None
              -
              -        if isinstance(self.raw_data, dict):
              -            self.data = {}
              -            for key, value in self.raw_data.items():
              -                self.data[to_snake_case(key)] = value
              -
              -            if "type" in self.data:
              -                self.type = self.data["type"]
              -            elif "Type" in self.data:
              -                self.type = self.data["Type"]
              -
              -        elif isinstance(self.raw_data, list):
              -            self.data = []
              -            for value in self.raw_data:
              -                self.data.append(value)
              -
              -            if len(self.data) > 0:
              -                if "type" in self.data[0]:
              -                    self.type = self.data[0]["type"]
              -                elif "Type" in self.data[0]:
              -                    self.type = self.data[0]["Type"]
              -
              class NotificationReceiver @@ -251,107 +55,6 @@

              Classes

              This object is used to receive notifications. This should only be generated once per client as to not duplicate notifications.

              -
              - -Expand source code - -
              class NotificationReceiver:
              -    """
              -    This object is used to receive notifications.
              -    This should only be generated once per client as to not duplicate notifications.
              -    """
              -
              -    def __init__(self, requests, on_open, on_close, on_error, on_notification):
              -        self.requests = requests
              -
              -        self.on_open = on_open
              -        self.on_close = on_close
              -        self.on_error = on_error
              -        self.on_notification = on_notification
              -
              -        self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"]
              -        self.connection = None
              -
              -        self.negotiate_request = None
              -        self.wss_url = None
              -
              -    async def initialize(self):
              -        self.negotiate_request = await self.requests.get(
              -            url="https://realtime.roblox.com/notifications/negotiate"
              -                "?clientProtocol=1.5"
              -                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
              -            cookies={
              -                ".ROBLOSECURITY": self.roblosecurity
              -            }
              -        )
              -        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
              -                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
              -                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
              -        self.connection = HubConnectionBuilder()
              -        self.connection.with_url(
              -            self.wss_url,
              -            options={
              -                "headers": {
              -                    "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
              -                },
              -                "skip_negotiation": False
              -            }
              -        )
              -
              -        async def on_message(_self, raw_notification):
              -            """
              -            Internal callback when a message is received.
              -            """
              -            try:
              -                notification_json = json.loads(raw_notification)
              -            except json.decoder.JSONDecodeError:
              -                return
              -            if len(notification_json) > 0:
              -                notification = Notification(notification_json)
              -                await self.on_notification(notification)
              -            else:
              -                return
              -
              -        def _internal_send(_self, message, protocol=None):
              -
              -            _self.logger.debug("Sending message {0}".format(message))
              -
              -            try:
              -                protocol = _self.protocol if protocol is None else protocol
              -
              -                _self._ws.send(protocol.encode(message))
              -                _self.connection_checker.last_message = time.time()
              -
              -                if _self.reconnection_handler is not None:
              -                    _self.reconnection_handler.reset()
              -
              -            except Exception as ex:
              -                raise ex
              -
              -        self.connection = self.connection.with_automatic_reconnect({
              -            "type": "raw",
              -            "keep_alive_interval": 10,
              -            "reconnect_interval": 5,
              -            "max_attempts": 5
              -        }).build()
              -
              -        if self.on_open:
              -            self.connection.on_open(self.on_open)
              -        if self.on_close:
              -            self.connection.on_close(self.on_close)
              -        if self.on_error:
              -            self.connection.on_error(self.on_error)
              -        self.connection.on_message = on_message
              -        self.connection._internal_send = _internal_send
              -
              -        await self.connection.start()
              -
              -    async def close(self):
              -        """
              -        Closes the connection and stops receiving notifications.
              -        """
              -        self.connection.stop()
              -

              Methods

              @@ -359,97 +62,12 @@

              Methods

              Closes the connection and stops receiving notifications.

              -
              - -Expand source code - -
              async def close(self):
              -    """
              -    Closes the connection and stops receiving notifications.
              -    """
              -    self.connection.stop()
              -
              async def initialize(self)
              -
              - -Expand source code - -
              async def initialize(self):
              -    self.negotiate_request = await self.requests.get(
              -        url="https://realtime.roblox.com/notifications/negotiate"
              -            "?clientProtocol=1.5"
              -            "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
              -        cookies={
              -            ".ROBLOSECURITY": self.roblosecurity
              -        }
              -    )
              -    self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
              -                   f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
              -                   f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
              -    self.connection = HubConnectionBuilder()
              -    self.connection.with_url(
              -        self.wss_url,
              -        options={
              -            "headers": {
              -                "Cookie": f".ROBLOSECURITY={self.roblosecurity};"
              -            },
              -            "skip_negotiation": False
              -        }
              -    )
              -
              -    async def on_message(_self, raw_notification):
              -        """
              -        Internal callback when a message is received.
              -        """
              -        try:
              -            notification_json = json.loads(raw_notification)
              -        except json.decoder.JSONDecodeError:
              -            return
              -        if len(notification_json) > 0:
              -            notification = Notification(notification_json)
              -            await self.on_notification(notification)
              -        else:
              -            return
              -
              -    def _internal_send(_self, message, protocol=None):
              -
              -        _self.logger.debug("Sending message {0}".format(message))
              -
              -        try:
              -            protocol = _self.protocol if protocol is None else protocol
              -
              -            _self._ws.send(protocol.encode(message))
              -            _self.connection_checker.last_message = time.time()
              -
              -            if _self.reconnection_handler is not None:
              -                _self.reconnection_handler.reset()
              -
              -        except Exception as ex:
              -            raise ex
              -
              -    self.connection = self.connection.with_automatic_reconnect({
              -        "type": "raw",
              -        "keep_alive_interval": 10,
              -        "reconnect_interval": 5,
              -        "max_attempts": 5
              -    }).build()
              -
              -    if self.on_open:
              -        self.connection.on_open(self.on_open)
              -    if self.on_close:
              -        self.connection.on_close(self.on_close)
              -    if self.on_error:
              -        self.connection.on_error(self.on_error)
              -    self.connection.on_message = on_message
              -    self.connection._internal_send = _internal_send
              -
              -    await self.connection.start()
              -
              diff --git a/docs/robloxbadges.html b/docs/robloxbadges.html index 7046e02a..e43953ee 100644 --- a/docs/robloxbadges.html +++ b/docs/robloxbadges.html @@ -23,29 +23,6 @@

              Module ro_py.robloxbadges

              This file houses functions and classes that pertain to Roblox-awarded badges.

              -
              - -Expand source code - -
              """
              -
              -This file houses functions and classes that pertain to Roblox-awarded badges.
              -
              -"""
              -
              -
              -class RobloxBadge:
              -    """
              -    Represents a Roblox badge.
              -    This is not equivalent to a badge you would earn from a game.
              -    This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.
              -    """
              -    def __init__(self, roblox_badge_data):
              -        self.id = roblox_badge_data["id"]
              -        self.name = roblox_badge_data["name"]
              -        self.description = roblox_badge_data["description"]
              -        self.image_url = roblox_badge_data["imageUrl"]
              -
              @@ -64,22 +41,6 @@

              Classes

              Represents a Roblox badge. This is not equivalent to a badge you would earn from a game. This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.

              -
              - -Expand source code - -
              class RobloxBadge:
              -    """
              -    Represents a Roblox badge.
              -    This is not equivalent to a badge you would earn from a game.
              -    This class represents a Roblox-awarded badge as seen in https://www.roblox.com/info/roblox-badges.
              -    """
              -    def __init__(self, roblox_badge_data):
              -        self.id = roblox_badge_data["id"]
              -        self.name = roblox_badge_data["name"]
              -        self.description = roblox_badge_data["description"]
              -        self.image_url = roblox_badge_data["imageUrl"]
              -

          diff --git a/docs/robloxdocs.html b/docs/robloxdocs.html index 7a1d9467..10ab0be7 100644 --- a/docs/robloxdocs.html +++ b/docs/robloxdocs.html @@ -26,126 +26,6 @@

          Module ro_py.robloxdocs

          This file houses functions and classes that pertain to the Roblox API documentation pages. I don't know if this is really that useful, but it might be useful for an API browser program, or for accessing endpoints that aren't supported directly by ro.py yet.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to the Roblox API documentation pages.
          -I don't know if this is really that useful, but it might be useful for an API browser program, or for accessing
          -endpoints that aren't supported directly by ro.py yet.
          -
          -"""
          -
          -from lxml import html
          -from io import StringIO
          -
          -
          -class EndpointDocsPathRequestTypeProperties:
          -    def __init__(self, data):
          -        self.internal = data["internal"]
          -        self.metric_ids = data["metricIds"]
          -
          -
          -class EndpointDocsPathRequestTypeResponse:
          -    def __init__(self, data):
          -        self.description = None
          -        self.schema = None
          -        if "description" in data:
          -            self.description = data["description"]
          -        if "schema" in data:
          -            self.schema = data["schema"]
          -
          -
          -class EndpointDocsPathRequestTypeParameter:
          -    def __init__(self, data):
          -        self.name = data["name"]
          -        self.iin = data["in"]  # I can't make this say "in" so this is close enough
          -
          -        if "description" in data:
          -            self.description = data["description"]
          -        else:
          -            self.description = None
          -
          -        self.required = data["required"]
          -        self.type = None
          -
          -        if "type" in data:
          -            self.type = data["type"]
          -
          -        if "format" in data:
          -            self.format = data["format"]
          -        else:
          -            self.format = None
          -
          -
          -class EndpointDocsPathRequestType:
          -    def __init__(self, data):
          -        self.tags = data["tags"]
          -        self.description = None
          -        self.summary = None
          -
          -        if "summary" in data:
          -            self.summary = data["summary"]
          -
          -        if "description" in data:
          -            self.description = data["description"]
          -
          -        self.consumes = data["consumes"]
          -        self.produces = data["produces"]
          -        self.parameters = []
          -        self.responses = {}
          -        self.properties = EndpointDocsPathRequestTypeProperties(data["properties"])
          -        for raw_parameter in data["parameters"]:
          -            self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter))
          -        for rr_k, rr_v in data["responses"].items():
          -            self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v)
          -
          -
          -class EndpointDocsPath:
          -    def __init__(self, data):
          -        self.data = {}
          -        for type_k, type_v in data.items():
          -            self.data[type_k] = EndpointDocsPathRequestType(type_v)
          -
          -
          -class EndpointDocsDataInfo:
          -    def __init__(self, data):
          -        self.version = data["version"]
          -        self.title = data["title"]
          -
          -
          -class EndpointDocsData:
          -    def __init__(self, data):
          -        self.swagger_version = data["swagger"]
          -        self.info = EndpointDocsDataInfo(data["info"])
          -        self.host = data["host"]
          -        self.schemes = data["schemes"]
          -        self.paths = {}
          -        for path_k, path_v in data["paths"].items():
          -            self.paths[path_k] = EndpointDocsPath(path_v)
          -
          -
          -class EndpointDocs:
          -    def __init__(self, requests, docs_url):
          -        self.requests = requests
          -        self.url = docs_url
          -
          -    async def get_versions(self):
          -        docs_req = await self.requests.get(self.url + "/docs")
          -        root = html.parse(StringIO(docs_req.text)).getroot()
          -        try:
          -            vs_element = root.get_element_by_id("version-selector")
          -            return vs_element.value_options
          -        except KeyError:
          -            return ["v1"]
          -
          -    async def get_data_for_version(self, version):
          -        data_req = await self.requests.get(self.url + "/docs/json/" + version)
          -        version_data = data_req.json()
          -        return EndpointDocsData(version_data)
          -
          @@ -162,29 +42,6 @@

          Classes

          -
          - -Expand source code - -
          class EndpointDocs:
          -    def __init__(self, requests, docs_url):
          -        self.requests = requests
          -        self.url = docs_url
          -
          -    async def get_versions(self):
          -        docs_req = await self.requests.get(self.url + "/docs")
          -        root = html.parse(StringIO(docs_req.text)).getroot()
          -        try:
          -            vs_element = root.get_element_by_id("version-selector")
          -            return vs_element.value_options
          -        except KeyError:
          -            return ["v1"]
          -
          -    async def get_data_for_version(self, version):
          -        data_req = await self.requests.get(self.url + "/docs/json/" + version)
          -        version_data = data_req.json()
          -        return EndpointDocsData(version_data)
          -

          Methods

          @@ -192,34 +49,12 @@

          Methods

          -
          - -Expand source code - -
          async def get_data_for_version(self, version):
          -    data_req = await self.requests.get(self.url + "/docs/json/" + version)
          -    version_data = data_req.json()
          -    return EndpointDocsData(version_data)
          -
          async def get_versions(self)
          -
          - -Expand source code - -
          async def get_versions(self):
          -    docs_req = await self.requests.get(self.url + "/docs")
          -    root = html.parse(StringIO(docs_req.text)).getroot()
          -    try:
          -        vs_element = root.get_element_by_id("version-selector")
          -        return vs_element.value_options
          -    except KeyError:
          -        return ["v1"]
          -
          @@ -229,20 +64,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsData:
          -    def __init__(self, data):
          -        self.swagger_version = data["swagger"]
          -        self.info = EndpointDocsDataInfo(data["info"])
          -        self.host = data["host"]
          -        self.schemes = data["schemes"]
          -        self.paths = {}
          -        for path_k, path_v in data["paths"].items():
          -            self.paths[path_k] = EndpointDocsPath(path_v)
          -
          class EndpointDocsDataInfo @@ -250,15 +71,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsDataInfo:
          -    def __init__(self, data):
          -        self.version = data["version"]
          -        self.title = data["title"]
          -
          class EndpointDocsPath @@ -266,16 +78,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsPath:
          -    def __init__(self, data):
          -        self.data = {}
          -        for type_k, type_v in data.items():
          -            self.data[type_k] = EndpointDocsPathRequestType(type_v)
          -
          class EndpointDocsPathRequestType @@ -283,32 +85,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsPathRequestType:
          -    def __init__(self, data):
          -        self.tags = data["tags"]
          -        self.description = None
          -        self.summary = None
          -
          -        if "summary" in data:
          -            self.summary = data["summary"]
          -
          -        if "description" in data:
          -            self.description = data["description"]
          -
          -        self.consumes = data["consumes"]
          -        self.produces = data["produces"]
          -        self.parameters = []
          -        self.responses = {}
          -        self.properties = EndpointDocsPathRequestTypeProperties(data["properties"])
          -        for raw_parameter in data["parameters"]:
          -            self.parameters.append(EndpointDocsPathRequestTypeParameter(raw_parameter))
          -        for rr_k, rr_v in data["responses"].items():
          -            self.responses[rr_k] = EndpointDocsPathRequestTypeResponse(rr_v)
          -
          class EndpointDocsPathRequestTypeParameter @@ -316,31 +92,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsPathRequestTypeParameter:
          -    def __init__(self, data):
          -        self.name = data["name"]
          -        self.iin = data["in"]  # I can't make this say "in" so this is close enough
          -
          -        if "description" in data:
          -            self.description = data["description"]
          -        else:
          -            self.description = None
          -
          -        self.required = data["required"]
          -        self.type = None
          -
          -        if "type" in data:
          -            self.type = data["type"]
          -
          -        if "format" in data:
          -            self.format = data["format"]
          -        else:
          -            self.format = None
          -
          class EndpointDocsPathRequestTypeProperties @@ -348,15 +99,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsPathRequestTypeProperties:
          -    def __init__(self, data):
          -        self.internal = data["internal"]
          -        self.metric_ids = data["metricIds"]
          -
          class EndpointDocsPathRequestTypeResponse @@ -364,19 +106,6 @@

          Methods

          -
          - -Expand source code - -
          class EndpointDocsPathRequestTypeResponse:
          -    def __init__(self, data):
          -        self.description = None
          -        self.schema = None
          -        if "description" in data:
          -            self.description = data["description"]
          -        if "schema" in data:
          -            self.schema = data["schema"]
          -
          diff --git a/docs/robloxstatus.html b/docs/robloxstatus.html index f212626a..47fcbbdf 100644 --- a/docs/robloxstatus.html +++ b/docs/robloxstatus.html @@ -26,69 +26,6 @@

          Module ro_py.robloxstatus

          This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com) I don't know if this is really that useful, but I was able to find the status API endpoint by looking in the status page source and some of the status.io documentation.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to the Roblox status page (at status.roblox.com)
          -I don't know if this is really that useful, but I was able to find the status API endpoint by looking in the status
          -page source and some of the status.io documentation.
          -
          -"""
          -
          -import iso8601
          -
          -endpoint = "https://4277980205320394.hostedstatus.com/1.0/status/59db90dbcdeb2f04dadcf16d"
          -
          -
          -class RobloxStatusContainer:
          -    """
          -    Represents a tab or item in a tab on the Roblox status site.
          -    The tab items are internally called "containers" so that's what I call them here.
          -    I don't see any difference between the data in tabs and data in containers, so I use the same object here.
          -    """
          -    def __init__(self, container_data):
          -        self.id = container_data["id"]
          -        self.name = container_data["name"]
          -        self.updated = iso8601.parse_date(container_data["updated"])
          -        self.status = container_data["status"]
          -        self.status_code = container_data["status_code"]
          -
          -
          -class RobloxStatusOverall:
          -    """
          -    Represents the overall status on the Roblox status site.
          -    """
          -    def __init__(self, overall_data):
          -        self.updated = iso8601.parse_date(overall_data["updated"])
          -        self.status = overall_data["status"]
          -        self.status_code = overall_data["status_code"]
          -
          -
          -class RobloxStatus:
          -    def __init__(self, requests):
          -        self.requests = requests
          -
          -        self.overall = None
          -        self.user = None
          -        self.player = None
          -        self.creator = None
          -
          -        self.update()
          -
          -    def update(self):
          -        status_req = self.requests.get(
          -            url=endpoint
          -        )
          -        status_data = status_req.json()["result"]
          -
          -        self.overall = RobloxStatusOverall(status_data["status_overall"])
          -        self.user = RobloxStatusContainer(status_data["status"][0])
          -        self.player = RobloxStatusContainer(status_data["status"][1])
          -        self.creator = RobloxStatusContainer(status_data["status"][2])
          -
          @@ -105,32 +42,6 @@

          Classes

          -
          - -Expand source code - -
          class RobloxStatus:
          -    def __init__(self, requests):
          -        self.requests = requests
          -
          -        self.overall = None
          -        self.user = None
          -        self.player = None
          -        self.creator = None
          -
          -        self.update()
          -
          -    def update(self):
          -        status_req = self.requests.get(
          -            url=endpoint
          -        )
          -        status_data = status_req.json()["result"]
          -
          -        self.overall = RobloxStatusOverall(status_data["status_overall"])
          -        self.user = RobloxStatusContainer(status_data["status"][0])
          -        self.player = RobloxStatusContainer(status_data["status"][1])
          -        self.creator = RobloxStatusContainer(status_data["status"][2])
          -

          Methods

          @@ -138,21 +49,6 @@

          Methods

          -
          - -Expand source code - -
          def update(self):
          -    status_req = self.requests.get(
          -        url=endpoint
          -    )
          -    status_data = status_req.json()["result"]
          -
          -    self.overall = RobloxStatusOverall(status_data["status_overall"])
          -    self.user = RobloxStatusContainer(status_data["status"][0])
          -    self.player = RobloxStatusContainer(status_data["status"][1])
          -    self.creator = RobloxStatusContainer(status_data["status"][2])
          -
          @@ -164,23 +60,6 @@

          Methods

          Represents a tab or item in a tab on the Roblox status site. The tab items are internally called "containers" so that's what I call them here. I don't see any difference between the data in tabs and data in containers, so I use the same object here.

          -
          - -Expand source code - -
          class RobloxStatusContainer:
          -    """
          -    Represents a tab or item in a tab on the Roblox status site.
          -    The tab items are internally called "containers" so that's what I call them here.
          -    I don't see any difference between the data in tabs and data in containers, so I use the same object here.
          -    """
          -    def __init__(self, container_data):
          -        self.id = container_data["id"]
          -        self.name = container_data["name"]
          -        self.updated = iso8601.parse_date(container_data["updated"])
          -        self.status = container_data["status"]
          -        self.status_code = container_data["status_code"]
          -
          class RobloxStatusOverall @@ -188,19 +67,6 @@

          Methods

          Represents the overall status on the Roblox status site.

          -
          - -Expand source code - -
          class RobloxStatusOverall:
          -    """
          -    Represents the overall status on the Roblox status site.
          -    """
          -    def __init__(self, overall_data):
          -        self.updated = iso8601.parse_date(overall_data["updated"])
          -        self.status = overall_data["status"]
          -        self.status_code = overall_data["status_code"]
          -
          diff --git a/docs/roles.html b/docs/roles.html index 4b9995f1..0739b553 100644 --- a/docs/roles.html +++ b/docs/roles.html @@ -23,167 +23,6 @@

          Module ro_py.roles

          This file contains classes and functions related to Roblox roles.

          -
          - -Expand source code - -
          """
          -
          -This file contains classes and functions related to Roblox roles.
          -
          -"""
          -
          -
          -import enum
          -
          -endpoint = "https://groups.roblox.com"
          -
          -
          -class RolePermissions(enum.Enum):
          -    """
          -    Represents role permissions.
          -    """
          -    view_wall = None
          -    post_to_wall = None
          -    delete_from_wall = None
          -    view_status = None
          -    post_to_status = None
          -    change_rank = None
          -    invite_members = None
          -    remove_members = None
          -    manage_relationships = None
          -    view_audit_logs = None
          -    spend_group_funds = None
          -    advertise_group = None
          -    create_items = None
          -    manage_items = None
          -    manage_group_games = None
          -
          -
          -def get_rp_names(rp):
          -    """
          -    Converts permissions into something Roblox can read.
          -
          -    Parameters
          -    ----------
          -    rp : ro_py.roles.RolePermissions
          -
          -    Returns
          -    -------
          -    dict
          -    """
          -    return {
          -        "viewWall": rp.view_wall,
          -        "PostToWall": rp.post_to_wall,
          -        "deleteFromWall": rp.delete_from_wall,
          -        "viewStatus": rp.view_status,
          -        "postToStatus": rp.post_to_status,
          -        "changeRank": rp.change_rank,
          -        "inviteMembers": rp.invite_members,
          -        "removeMembers": rp.remove_members,
          -        "manageRelationships": rp.manage_relationships,
          -        "viewAuditLogs": rp.view_audit_logs,
          -        "spendGroupFunds": rp.spend_group_funds,
          -        "advertiseGroup": rp.advertise_group,
          -        "createItems": rp.create_items,
          -        "manageItems": rp.manage_items,
          -        "manageGroupGames": rp.manage_group_games
          -    }
          -
          -
          -class Role:
          -    """
          -    Represents a role
          -
          -    Parameters
          -    ----------
          -    requests : ro_py.utilities.requests.Requests
          -            Requests object to use for API requests.
          -    group : ro_py.groups.Group
          -            Group the role belongs to.
          -    role_data : dict
          -            Dictionary containing role information.
          -    """
          -    def __init__(self, requests, group, role_data):
          -        self.requests = requests
          -        self.group = group
          -        self.id = role_data['id']
          -        self.name = role_data['name']
          -        self.description = role_data.get('description')
          -        self.rank = role_data['rank']
          -        self.member_count = role_data.get('memberCount')
          -
          -    async def update(self):
          -        """
          -        Updates information of the role.
          -        """
          -        update_req = await self.requests.get(
          -            url=endpoint + f"/v1/groups/{self.group.id}/roles"
          -        )
          -        data = update_req.json()
          -        for role in data['roles']:
          -            if role['id'] == self.id:
          -                self.name = role['name']
          -                self.description = role['description']
          -                self.rank = role['rank']
          -                self.member_count = role['memberCount']
          -                break
          -
          -    async def edit(self, name=None, description=None, rank=None):
          -        """
          -        Edits the name, description or rank of a role
          -
          -        Parameters
          -        ----------
          -        name : str, optional
          -            New name for the role.
          -        description : str, optional
          -            New description for the role.
          -        rank : int, optional
          -            Number from 1-254 that determains the new rank number for the role.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        edit_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
          -            data={
          -                "description": description if description else self.description,
          -                "name": name if name else self.name,
          -                "rank": rank if rank else self.rank
          -            }
          -        )
          -        return edit_req.status_code == 200
          -
          -    async def edit_permissions(self, role_permissions):
          -        """
          -        Edits the permissions of a role.
          -
          -        Parameters
          -        ----------
          -        role_permissions : ro_py.roles.RolePermissions
          -            New permissions that will overwrite the old ones.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        data = {
          -            "permissions": {}
          -        }
          -
          -        for key, value in get_rp_names(role_permissions):
          -            if value is True or False:
          -                data['permissions'][key] = value
          -
          -        edit_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
          -            data=data
          -        )
          -
          -        return edit_req.status_code == 200
          -
          @@ -207,40 +46,6 @@

          Returns

          dict
           
          -
          - -Expand source code - -
          def get_rp_names(rp):
          -    """
          -    Converts permissions into something Roblox can read.
          -
          -    Parameters
          -    ----------
          -    rp : ro_py.roles.RolePermissions
          -
          -    Returns
          -    -------
          -    dict
          -    """
          -    return {
          -        "viewWall": rp.view_wall,
          -        "PostToWall": rp.post_to_wall,
          -        "deleteFromWall": rp.delete_from_wall,
          -        "viewStatus": rp.view_status,
          -        "postToStatus": rp.post_to_status,
          -        "changeRank": rp.change_rank,
          -        "inviteMembers": rp.invite_members,
          -        "removeMembers": rp.remove_members,
          -        "manageRelationships": rp.manage_relationships,
          -        "viewAuditLogs": rp.view_audit_logs,
          -        "spendGroupFunds": rp.spend_group_funds,
          -        "advertiseGroup": rp.advertise_group,
          -        "createItems": rp.create_items,
          -        "manageItems": rp.manage_items,
          -        "manageGroupGames": rp.manage_group_games
          -    }
          -
          @@ -262,103 +67,6 @@

          Parameters

          role_data : dict
          Dictionary containing role information.
          -
          - -Expand source code - -
          class Role:
          -    """
          -    Represents a role
          -
          -    Parameters
          -    ----------
          -    requests : ro_py.utilities.requests.Requests
          -            Requests object to use for API requests.
          -    group : ro_py.groups.Group
          -            Group the role belongs to.
          -    role_data : dict
          -            Dictionary containing role information.
          -    """
          -    def __init__(self, requests, group, role_data):
          -        self.requests = requests
          -        self.group = group
          -        self.id = role_data['id']
          -        self.name = role_data['name']
          -        self.description = role_data.get('description')
          -        self.rank = role_data['rank']
          -        self.member_count = role_data.get('memberCount')
          -
          -    async def update(self):
          -        """
          -        Updates information of the role.
          -        """
          -        update_req = await self.requests.get(
          -            url=endpoint + f"/v1/groups/{self.group.id}/roles"
          -        )
          -        data = update_req.json()
          -        for role in data['roles']:
          -            if role['id'] == self.id:
          -                self.name = role['name']
          -                self.description = role['description']
          -                self.rank = role['rank']
          -                self.member_count = role['memberCount']
          -                break
          -
          -    async def edit(self, name=None, description=None, rank=None):
          -        """
          -        Edits the name, description or rank of a role
          -
          -        Parameters
          -        ----------
          -        name : str, optional
          -            New name for the role.
          -        description : str, optional
          -            New description for the role.
          -        rank : int, optional
          -            Number from 1-254 that determains the new rank number for the role.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        edit_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
          -            data={
          -                "description": description if description else self.description,
          -                "name": name if name else self.name,
          -                "rank": rank if rank else self.rank
          -            }
          -        )
          -        return edit_req.status_code == 200
          -
          -    async def edit_permissions(self, role_permissions):
          -        """
          -        Edits the permissions of a role.
          -
          -        Parameters
          -        ----------
          -        role_permissions : ro_py.roles.RolePermissions
          -            New permissions that will overwrite the old ones.
          -
          -        Returns
          -        -------
          -        int
          -        """
          -        data = {
          -            "permissions": {}
          -        }
          -
          -        for key, value in get_rp_names(role_permissions):
          -            if value is True or False:
          -                data['permissions'][key] = value
          -
          -        edit_req = await self.requests.patch(
          -            url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
          -            data=data
          -        )
          -
          -        return edit_req.status_code == 200
          -

          Methods

          @@ -380,37 +88,6 @@

          Returns

          int
           
          -
          - -Expand source code - -
          async def edit(self, name=None, description=None, rank=None):
          -    """
          -    Edits the name, description or rank of a role
          -
          -    Parameters
          -    ----------
          -    name : str, optional
          -        New name for the role.
          -    description : str, optional
          -        New description for the role.
          -    rank : int, optional
          -        Number from 1-254 that determains the new rank number for the role.
          -
          -    Returns
          -    -------
          -    int
          -    """
          -    edit_req = await self.requests.patch(
          -        url=endpoint + f"/v1/groups/{self.group.id}/rolesets/{self.id}",
          -        data={
          -            "description": description if description else self.description,
          -            "name": name if name else self.name,
          -            "rank": rank if rank else self.rank
          -        }
          -    )
          -    return edit_req.status_code == 200
          -
          async def edit_permissions(self, role_permissions) @@ -427,101 +104,20 @@

          Returns

          int
           
          -
          - -Expand source code - -
          async def edit_permissions(self, role_permissions):
          -    """
          -    Edits the permissions of a role.
          -
          -    Parameters
          -    ----------
          -    role_permissions : ro_py.roles.RolePermissions
          -        New permissions that will overwrite the old ones.
          -
          -    Returns
          -    -------
          -    int
          -    """
          -    data = {
          -        "permissions": {}
          -    }
          -
          -    for key, value in get_rp_names(role_permissions):
          -        if value is True or False:
          -            data['permissions'][key] = value
          -
          -    edit_req = await self.requests.patch(
          -        url=endpoint + f"/v1/groups/{self.group.id}/roles/{self.id}/permissions",
          -        data=data
          -    )
          -
          -    return edit_req.status_code == 200
          -
          async def update(self)

          Updates information of the role.

          -
          - -Expand source code - -
          async def update(self):
          -    """
          -    Updates information of the role.
          -    """
          -    update_req = await self.requests.get(
          -        url=endpoint + f"/v1/groups/{self.group.id}/roles"
          -    )
          -    data = update_req.json()
          -    for role in data['roles']:
          -        if role['id'] == self.id:
          -            self.name = role['name']
          -            self.description = role['description']
          -            self.rank = role['rank']
          -            self.member_count = role['memberCount']
          -            break
          -
          class RolePermissions -(value, names=None, *, module=None, qualname=None, type=None, start=1)

          Represents role permissions.

          -
          - -Expand source code - -
          class RolePermissions(enum.Enum):
          -    """
          -    Represents role permissions.
          -    """
          -    view_wall = None
          -    post_to_wall = None
          -    delete_from_wall = None
          -    view_status = None
          -    post_to_status = None
          -    change_rank = None
          -    invite_members = None
          -    remove_members = None
          -    manage_relationships = None
          -    view_audit_logs = None
          -    spend_group_funds = None
          -    advertise_group = None
          -    create_items = None
          -    manage_items = None
          -    manage_group_games = None
          -
          -

          Ancestors

          -
            -
          • enum.Enum
          • -

          Class variables

          var advertise_group
          diff --git a/docs/thumbnails.html b/docs/thumbnails.html index aca96691..9bbee7ad 100644 --- a/docs/thumbnails.html +++ b/docs/thumbnails.html @@ -23,153 +23,6 @@

          Module ro_py.thumbnails

          This file houses functions and classes that pertain to Roblox icons and thumbnails.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to Roblox icons and thumbnails.
          -
          -"""
          -
          -from ro_py.utilities.errors import InvalidShotTypeError
          -
          -endpoint = "https://thumbnails.roblox.com/"
          -
          -# TODO: turn these into enums
          -PlaceHolder = "PlaceHolder"
          -AutoGenerated = "AutoGenerated"
          -ForceAutoGenerated = "ForceAutoGenerated"
          -
          -AvatarFullBody = 0
          -AvatarBust = 1
          -AvatarHeadshot = 2
          -
          -size_30x30 = "30x30"
          -size_42x42 = "42x42"
          -size_48x48 = "48x48"
          -size_50x50 = "50x50"
          -size_60x62 = "60x62"
          -size_75x75 = "75x75"
          -size_110x110 = "110x110"
          -size_128x128 = "128x128"
          -size_140x140 = "140x140"
          -size_150x150 = "150x150"
          -size_160x100 = "160x100"
          -size_250x250 = "250x250"
          -size_256x144 = "256x144"
          -size_256x256 = "256x256"
          -size_300x250 = "300x240"
          -size_304x166 = "304x166"
          -size_384x216 = "384x216"
          -size_396x216 = "396x216"
          -size_420x420 = "420x420"
          -size_480x270 = "480x270"
          -size_512x512 = "512x512"
          -size_576x324 = "576x324"
          -size_720x720 = "720x720"
          -size_768x432 = "768x432"
          -
          -format_png = "Png"
          -format_jpg = "Jpeg"
          -format_jpeg = "Jpeg"
          -
          -
          -class ThumbnailGenerator:
          -    """
          -    This object is used to generate thumbnails.
          -
          -    Parameters
          -    ----------
          -    requests: Requests
          -        Requests object.
          -    """
          -    def __init__(self, requests):
          -        self.requests = requests
          -
          -    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
          -        """
          -        Gets a group's icon.
          -
          -        Parameters
          -        ----------
          -        group: Group
          -            The group.
          -        size: str
          -            The thumbnail size, formatted WIDTHxHEIGHT.
          -        file_format: str
          -            The thumbnail format.
          -        is_circular: bool
          -            Whether to output a circular version of the thumbnail.
          -        """
          -        group_icon_req = self.requests.get(
          -            url=endpoint + "v1/groups/icons",
          -            params={
          -                "groupIds": str(group.id),
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        group_icon = group_icon_req.json()["data"][0]["imageUrl"]
          -        return group_icon
          -
          -    def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
          -        """
          -        Gets a game's icon.
          -        :param game: The game.
          -        :param size: The thumbnail size, formatted widthxheight.
          -        :param file_format: The thumbnail format
          -        :param is_circular: The circle thumbnail output parameter.
          -        :return: Image URL
          -        """
          -        game_icon_req = self.requests.get(
          -            url=endpoint + "v1/games/icons",
          -            params={
          -                "universeIds": str(game.id),
          -                "returnPolicy": PlaceHolder,
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        game_icon = game_icon_req.json()["data"][0]["imageUrl"]
          -        return game_icon
          -
          -    def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
          -        """
          -        Gets a full body, bust, or headshot image of a user.
          -        :param user: User to use for avatar.
          -        :param shot_type: Type of shot.
          -        :param size: The thumbnail size, formatted widthxheight.
          -        :param file_format: The thumbnail format
          -        :param is_circular: The circle thumbnail output parameter.
          -        :return: Image URL
          -        """
          -        shot_endpoint = endpoint + "v1/users/"
          -        if shot_type == AvatarFullBody:
          -            shot_endpoint = shot_endpoint + "avatar"
          -            size = size or size_30x30
          -        elif shot_type == AvatarBust:
          -            shot_endpoint = shot_endpoint + "avatar-bust"
          -            size = size or size_50x50
          -        elif shot_type == AvatarHeadshot:
          -            size = size or size_48x48
          -            shot_endpoint = shot_endpoint + "avatar-headshot"
          -        else:
          -            raise InvalidShotTypeError("Invalid shot type.")
          -        shot_req = self.requests.get(
          -            url=shot_endpoint,
          -            params={
          -                "userIds": str(user.id),
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        return shot_req.json()["data"][0]["imageUrl"]
          -
          @@ -180,250 +33,218 @@

          Module ro_py.thumbnails

          Classes

          -
          -class ThumbnailGenerator -(requests) +
          +class ReturnPolicy
          -

          This object is used to generate thumbnails.

          -

          Parameters

          +
          +

          Class variables

          -
          requests : Requests
          -
          Requests object.
          -
          -
          - -Expand source code - -
          class ThumbnailGenerator:
          -    """
          -    This object is used to generate thumbnails.
          -
          -    Parameters
          -    ----------
          -    requests: Requests
          -        Requests object.
          -    """
          -    def __init__(self, requests):
          -        self.requests = requests
          -
          -    def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
          -        """
          -        Gets a group's icon.
          -
          -        Parameters
          -        ----------
          -        group: Group
          -            The group.
          -        size: str
          -            The thumbnail size, formatted WIDTHxHEIGHT.
          -        file_format: str
          -            The thumbnail format.
          -        is_circular: bool
          -            Whether to output a circular version of the thumbnail.
          -        """
          -        group_icon_req = self.requests.get(
          -            url=endpoint + "v1/groups/icons",
          -            params={
          -                "groupIds": str(group.id),
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        group_icon = group_icon_req.json()["data"][0]["imageUrl"]
          -        return group_icon
          -
          -    def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
          -        """
          -        Gets a game's icon.
          -        :param game: The game.
          -        :param size: The thumbnail size, formatted widthxheight.
          -        :param file_format: The thumbnail format
          -        :param is_circular: The circle thumbnail output parameter.
          -        :return: Image URL
          -        """
          -        game_icon_req = self.requests.get(
          -            url=endpoint + "v1/games/icons",
          -            params={
          -                "universeIds": str(game.id),
          -                "returnPolicy": PlaceHolder,
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        game_icon = game_icon_req.json()["data"][0]["imageUrl"]
          -        return game_icon
          -
          -    def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
          -        """
          -        Gets a full body, bust, or headshot image of a user.
          -        :param user: User to use for avatar.
          -        :param shot_type: Type of shot.
          -        :param size: The thumbnail size, formatted widthxheight.
          -        :param file_format: The thumbnail format
          -        :param is_circular: The circle thumbnail output parameter.
          -        :return: Image URL
          -        """
          -        shot_endpoint = endpoint + "v1/users/"
          -        if shot_type == AvatarFullBody:
          -            shot_endpoint = shot_endpoint + "avatar"
          -            size = size or size_30x30
          -        elif shot_type == AvatarBust:
          -            shot_endpoint = shot_endpoint + "avatar-bust"
          -            size = size or size_50x50
          -        elif shot_type == AvatarHeadshot:
          -            size = size or size_48x48
          -            shot_endpoint = shot_endpoint + "avatar-headshot"
          -        else:
          -            raise InvalidShotTypeError("Invalid shot type.")
          -        shot_req = self.requests.get(
          -            url=shot_endpoint,
          -            params={
          -                "userIds": str(user.id),
          -                "size": size,
          -                "file_format": file_format,
          -                "isCircular": is_circular
          -            }
          -        )
          -        return shot_req.json()["data"][0]["imageUrl"]
          -
          -

          Methods

          +
          var auto_generated
          +
          +
          +
          +
          var force_auto_generated
          +
          +
          +
          +
          var place_holder
          +
          +
          +
          +
          +
          +
          +class ThumbnailFormat +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
          +
          +

          An enumeration.

          +

          Ancestors

          +
            +
          • enum.Enum
          • +
          +

          Class variables

          -
          -def get_avatar_image(self, user, shot_type=0, size=None, file_format='Png', is_circular=False) +
          var format_jpeg
          +
          +
          +
          +
          var format_jpg
          +
          +
          +
          +
          var format_png
          +
          +
          +
          +
          +
          +
          +class ThumbnailSize +(value, names=None, *, module=None, qualname=None, type=None, start=1)
          -

          Gets a full body, bust, or headshot image of a user. -:param user: User to use for avatar. -:param shot_type: Type of shot. -:param size: The thumbnail size, formatted widthxheight. -:param file_format: The thumbnail format -:param is_circular: The circle thumbnail output parameter. -:return: Image URL

          -
          - -Expand source code - -
          def get_avatar_image(self, user, shot_type=AvatarFullBody, size=None, file_format=format_png, is_circular=False):
          -    """
          -    Gets a full body, bust, or headshot image of a user.
          -    :param user: User to use for avatar.
          -    :param shot_type: Type of shot.
          -    :param size: The thumbnail size, formatted widthxheight.
          -    :param file_format: The thumbnail format
          -    :param is_circular: The circle thumbnail output parameter.
          -    :return: Image URL
          -    """
          -    shot_endpoint = endpoint + "v1/users/"
          -    if shot_type == AvatarFullBody:
          -        shot_endpoint = shot_endpoint + "avatar"
          -        size = size or size_30x30
          -    elif shot_type == AvatarBust:
          -        shot_endpoint = shot_endpoint + "avatar-bust"
          -        size = size or size_50x50
          -    elif shot_type == AvatarHeadshot:
          -        size = size or size_48x48
          -        shot_endpoint = shot_endpoint + "avatar-headshot"
          -    else:
          -        raise InvalidShotTypeError("Invalid shot type.")
          -    shot_req = self.requests.get(
          -        url=shot_endpoint,
          -        params={
          -            "userIds": str(user.id),
          -            "size": size,
          -            "file_format": file_format,
          -            "isCircular": is_circular
          -        }
          -    )
          -    return shot_req.json()["data"][0]["imageUrl"]
          -
          -
          -
          -def get_game_icon(self, game, size='256x256', file_format='Png', is_circular=False) +

          An enumeration.

          +

          Ancestors

          +
            +
          • enum.Enum
          • +
          +

          Class variables

          +
          +
          var size_110x110
          +
          +
          +
          +
          var size_128x128
          +
          +
          +
          +
          var size_140x140
          +
          +
          +
          +
          var size_150x150
          +
          +
          +
          +
          var size_160x100
          +
          +
          +
          +
          var size_250x250
          +
          +
          +
          +
          var size_256x144
          +
          +
          +
          +
          var size_256x256
          +
          +
          +
          +
          var size_300x250
          +
          +
          +
          +
          var size_304x166
          +
          +
          +
          +
          var size_30x30
          +
          +
          +
          +
          var size_384x216
          +
          +
          +
          +
          var size_396x216
          +
          +
          +
          +
          var size_420x420
          +
          +
          +
          +
          var size_42x42
          +
          +
          +
          +
          var size_480x270
          +
          +
          +
          +
          var size_48x48
          +
          +
          +
          +
          var size_50x50
          +
          +
          +
          +
          var size_512x512
          +
          +
          +
          +
          var size_576x324
          +
          +
          +
          +
          var size_60x62
          +
          +
          +
          +
          var size_720x720
          +
          +
          +
          +
          var size_75x75
          +
          +
          +
          +
          var size_768x432
          +
          +
          +
          +
          + +
          +class ThumbnailType +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
          +
          +

          An enumeration.

          +

          Ancestors

          +
            +
          • enum.Enum
          • +
          +

          Class variables

          +
          +
          var avatar_bust
          +
          +
          +
          +
          var avatar_full_body
          +
          +
          +
          +
          var avatar_headshot
          +
          +
          +
          +
          +
          +
          +class UserThumbnailGenerator +(requests, id)
          -

          Gets a game's icon. -:param game: The game. -:param size: The thumbnail size, formatted widthxheight. -:param file_format: The thumbnail format -:param is_circular: The circle thumbnail output parameter. -:return: Image URL

          -
          - -Expand source code - -
          def get_game_icon(self, game, size=size_256x256, file_format=format_png, is_circular=False):
          -    """
          -    Gets a game's icon.
          -    :param game: The game.
          -    :param size: The thumbnail size, formatted widthxheight.
          -    :param file_format: The thumbnail format
          -    :param is_circular: The circle thumbnail output parameter.
          -    :return: Image URL
          -    """
          -    game_icon_req = self.requests.get(
          -        url=endpoint + "v1/games/icons",
          -        params={
          -            "universeIds": str(game.id),
          -            "returnPolicy": PlaceHolder,
          -            "size": size,
          -            "file_format": file_format,
          -            "isCircular": is_circular
          -        }
          -    )
          -    game_icon = game_icon_req.json()["data"][0]["imageUrl"]
          -    return game_icon
          -
          -
          -
          -def get_group_icon(self, group, size='150x150', file_format='Png', is_circular=False) +
          +

          Methods

          +
          +
          +async def get_avatar_image(self, shot_type=ThumbnailType.avatar_full_body, size=ThumbnailSize.size_48x48, file_format=ThumbnailFormat.format_png, is_circular=False)
          -

          Gets a group's icon.

          +

          Gets a full body, bust, or headshot image of the user.

          Parameters

          -
          group : Group
          -
          The group.
          -
          size : str
          -
          The thumbnail size, formatted WIDTHxHEIGHT.
          -
          file_format : str
          -
          The thumbnail format.
          +
          shot_type : ThumbnailType
          +
          Type of shot.
          +
          size : ThumbnailSize
          +
          The thumbnail size.
          +
          file_format : ThumbnailFormat
          +
          The thumbnail format
          is_circular : bool
          -
          Whether to output a circular version of the thumbnail.
          +
          The circle thumbnail output parameter.
          +
          +

          Returns

          +
          +
          Image URL
          +
           
          -
          - -Expand source code - -
          def get_group_icon(self, group, size=size_150x150, file_format=format_png, is_circular=False):
          -    """
          -    Gets a group's icon.
          -
          -    Parameters
          -    ----------
          -    group: Group
          -        The group.
          -    size: str
          -        The thumbnail size, formatted WIDTHxHEIGHT.
          -    file_format: str
          -        The thumbnail format.
          -    is_circular: bool
          -        Whether to output a circular version of the thumbnail.
          -    """
          -    group_icon_req = self.requests.get(
          -        url=endpoint + "v1/groups/icons",
          -        params={
          -            "groupIds": str(group.id),
          -            "size": size,
          -            "file_format": file_format,
          -            "isCircular": is_circular
          -        }
          -    )
          -    group_icon = group_icon_req.json()["data"][0]["imageUrl"]
          -    return group_icon
          -
          @@ -444,11 +265,62 @@

          Index

        • Classes

          diff --git a/docs/trades.html b/docs/trades.html index b7f1a141..615e4c99 100644 --- a/docs/trades.html +++ b/docs/trades.html @@ -23,163 +23,6 @@

          Module ro_py.trades

          This file houses functions and classes that pertain to Roblox trades and trading.

          -
          - -Expand source code - -
          """
          -
          -This file houses functions and classes that pertain to Roblox trades and trading.
          -
          -"""
          -
          -from ro_py.utilities.pages import Pages, SortOrder
          -from ro_py.assets import Asset
          -from ro_py.users import User, PartialUser
          -import iso8601
          -import enum
          -
          -endpoint = "https://trades.roblox.com"
          -
          -
          -def trade_page_handler(requests, this_page) -> list:
          -    trades_out = []
          -    for raw_trade in this_page:
          -        trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
          -    return trades_out
          -
          -
          -class Trade:
          -    def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool):
          -        self.trade_id = trade_id
          -        self.requests = requests
          -        self.sender = sender
          -        self.recieve_items = recieve_items
          -        self.send_items = send_items
          -        self.created = iso8601.parse(created)
          -        self.experation = iso8601.parse(expiration)
          -        self.status = status
          -
          -    async def accept(self) -> bool:
          -        """
          -        accepts a trade requests
          -        :returns: true/false
          -        """
          -        accept_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -        )
          -        return accept_req.status_code == 200
          -
          -    async def decline(self) -> bool:
          -        """
          -        decline a trade requests
          -        :returns: true/false
          -        """
          -        decline_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -        )
          -        return decline_req.status_code == 200
          -
          -
          -class PartialTrade:
          -    def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool):
          -        self.requests = requests
          -        self.trade_id = trade_id
          -        self.user = user
          -        self.created = iso8601.parse(created)
          -        self.expiration = iso8601.parse(expiration)
          -        self.status = status
          -
          -    async def accept(self) -> bool:
          -        """
          -        accepts a trade requests
          -        :returns: true/false
          -        """
          -        accept_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -        )
          -        return accept_req.status_code == 200
          -
          -    async def decline(self) -> bool:
          -        """
          -        decline a trade requests
          -        :returns: true/false
          -        """
          -        decline_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -        )
          -        return decline_req.status_code == 200
          -
          -    async def expand(self) -> Trade:
          -        """
          -        gets a more detailed trade request
          -        :return: Trade class
          -        """
          -        expend_req = await self.requests.get(
          -            url=endpoint + f"/v1/trades/{self.trade_id}"
          -        )
          -        data = expend_req.json()
          -
          -        # generate a user class and update it
          -        sender = User(self.requests, data['user']['id'])
          -        await sender.update()
          -
          -        # load items that will be/have been sent and items that you will/have recieve(d)
          -        recieve_items, send_items = [], []
          -        for items_0 in data['offers'][0]['userAssets']:
          -            item_0 = Asset(self.requests, items_0['assetId'])
          -            await item_0.update()
          -            recieve_items.append(item_0)
          -
          -        for items_1 in data['offers'][1]['userAssets']:
          -            item_1 = Asset(self.requests, items_1['assetId'])
          -            await item_1.update()
          -            send_items.append(item_1)
          -
          -        return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
          -
          -
          -class TradeStatusType(enum.Enum):
          -    """
          -    Represents a trade status type.
          -    """
          -    Inbound = "Inbound"
          -    Outbound = "Outbound"
          -    Completed = "Completed"
          -    Inactive = "Inactive"
          -
          -
          -class TradesMetadata:
          -    """
          -    Represents trade system metadata at /v1/trades/metadata
          -    """
          -    def __init__(self, trades_metadata_data):
          -        self.max_items_per_side = trades_metadata_data["maxItemsPerSide"]
          -        self.min_value_ratio = trades_metadata_data["minValueRatio"]
          -        self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"]
          -        self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"]
          -
          -
          -class TradesWrapper:
          -    """
          -    Represents the Roblox trades page.
          -    """
          -    def __init__(self, requests):
          -        self.requests = requests
          -
          -    async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
          -        trades = await Pages(
          -            requests=self.requests,
          -            url=endpoint + f"/v1/trades/{trade_status_type}",
          -            sort_order=sort_order,
          -            limit=limit,
          -            handler=trade_page_handler
          -        )
          -        return trades
          -
          -    async def send_trade(self):
          -        pass
          -
          @@ -193,16 +36,6 @@

          Functions

        • -
          - -Expand source code - -
          def trade_page_handler(requests, this_page) -> list:
          -    trades_out = []
          -    for raw_trade in this_page:
          -        trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
          -    return trades_out
          -
          @@ -215,67 +48,6 @@

          Classes

          -
          - -Expand source code - -
          class PartialTrade:
          -    def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool):
          -        self.requests = requests
          -        self.trade_id = trade_id
          -        self.user = user
          -        self.created = iso8601.parse(created)
          -        self.expiration = iso8601.parse(expiration)
          -        self.status = status
          -
          -    async def accept(self) -> bool:
          -        """
          -        accepts a trade requests
          -        :returns: true/false
          -        """
          -        accept_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -        )
          -        return accept_req.status_code == 200
          -
          -    async def decline(self) -> bool:
          -        """
          -        decline a trade requests
          -        :returns: true/false
          -        """
          -        decline_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -        )
          -        return decline_req.status_code == 200
          -
          -    async def expand(self) -> Trade:
          -        """
          -        gets a more detailed trade request
          -        :return: Trade class
          -        """
          -        expend_req = await self.requests.get(
          -            url=endpoint + f"/v1/trades/{self.trade_id}"
          -        )
          -        data = expend_req.json()
          -
          -        # generate a user class and update it
          -        sender = User(self.requests, data['user']['id'])
          -        await sender.update()
          -
          -        # load items that will be/have been sent and items that you will/have recieve(d)
          -        recieve_items, send_items = [], []
          -        for items_0 in data['offers'][0]['userAssets']:
          -            item_0 = Asset(self.requests, items_0['assetId'])
          -            await item_0.update()
          -            recieve_items.append(item_0)
          -
          -        for items_1 in data['offers'][1]['userAssets']:
          -            item_1 = Asset(self.requests, items_1['assetId'])
          -            await item_1.update()
          -            send_items.append(item_1)
          -
          -        return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
          -

          Methods

          @@ -284,20 +56,6 @@

          Methods

          accepts a trade requests :returns: true/false

          -
          - -Expand source code - -
          async def accept(self) -> bool:
          -    """
          -    accepts a trade requests
          -    :returns: true/false
          -    """
          -    accept_req = await self.requests.post(
          -        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -    )
          -    return accept_req.status_code == 200
          -
          async def decline(self) ‑> bool @@ -305,20 +63,6 @@

          Methods

          decline a trade requests :returns: true/false

          -
          - -Expand source code - -
          async def decline(self) -> bool:
          -    """
          -    decline a trade requests
          -    :returns: true/false
          -    """
          -    decline_req = await self.requests.post(
          -        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -    )
          -    return decline_req.status_code == 200
          -
          async def expand(self) ‑> Trade @@ -326,38 +70,6 @@

          Methods

          gets a more detailed trade request :return: Trade class

          -
          - -Expand source code - -
          async def expand(self) -> Trade:
          -    """
          -    gets a more detailed trade request
          -    :return: Trade class
          -    """
          -    expend_req = await self.requests.get(
          -        url=endpoint + f"/v1/trades/{self.trade_id}"
          -    )
          -    data = expend_req.json()
          -
          -    # generate a user class and update it
          -    sender = User(self.requests, data['user']['id'])
          -    await sender.update()
          -
          -    # load items that will be/have been sent and items that you will/have recieve(d)
          -    recieve_items, send_items = [], []
          -    for items_0 in data['offers'][0]['userAssets']:
          -        item_0 = Asset(self.requests, items_0['assetId'])
          -        await item_0.update()
          -        recieve_items.append(item_0)
          -
          -    for items_1 in data['offers'][1]['userAssets']:
          -        item_1 = Asset(self.requests, items_1['assetId'])
          -        await item_1.update()
          -        send_items.append(item_1)
          -
          -    return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
          -
          @@ -367,41 +79,6 @@

          Methods

          -
          - -Expand source code - -
          class Trade:
          -    def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool):
          -        self.trade_id = trade_id
          -        self.requests = requests
          -        self.sender = sender
          -        self.recieve_items = recieve_items
          -        self.send_items = send_items
          -        self.created = iso8601.parse(created)
          -        self.experation = iso8601.parse(expiration)
          -        self.status = status
          -
          -    async def accept(self) -> bool:
          -        """
          -        accepts a trade requests
          -        :returns: true/false
          -        """
          -        accept_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -        )
          -        return accept_req.status_code == 200
          -
          -    async def decline(self) -> bool:
          -        """
          -        decline a trade requests
          -        :returns: true/false
          -        """
          -        decline_req = await self.requests.post(
          -            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -        )
          -        return decline_req.status_code == 200
          -

          Methods

          @@ -410,20 +87,6 @@

          Methods

          accepts a trade requests :returns: true/false

          -
          - -Expand source code - -
          async def accept(self) -> bool:
          -    """
          -    accepts a trade requests
          -    :returns: true/false
          -    """
          -    accept_req = await self.requests.post(
          -        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
          -    )
          -    return accept_req.status_code == 200
          -
          async def decline(self) ‑> bool @@ -431,20 +94,67 @@

          Methods

          decline a trade requests :returns: true/false

          -
          - -Expand source code - -
          async def decline(self) -> bool:
          -    """
          -    decline a trade requests
          -    :returns: true/false
          -    """
          -    decline_req = await self.requests.post(
          -        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
          -    )
          -    return decline_req.status_code == 200
          -
          +
          +
          +
          +
          +class TradeRequest +
          +
          +
          +

          Instance variables

          +
          +
          var recieve_asset
          +
          +

          Limiteds that will be recieved when the trade is accepted.

          +
          +
          var recieve_robux
          +
          +

          Robux that will be recieved when the trade is accepted.

          +
          +
          var send_asset
          +
          +

          Limiteds that will be sent when the trade is accepted.

          +
          +
          var send_robux
          +
          +

          Robux that will be sent when the trade is accepted.

          +
          +
          +

          Methods

          +
          +
          +def request_item(self, asset: UserAsset) +
          +
          +

          Appends asset to self.recieve_asset.

          +

          Parameters

          +
          +
          asset : UserAsset
          +
           
          +
          +
          +
          +def request_robux(self, robux: int) +
          +
          +

          Sets self.request_robux to robux

          +

          Parameters

          +
          +
          robux : int
          +
           
          +
          +
          +
          +def send_item(self, asset: UserAsset) +
          +
          +

          Appends asset to self.send_asset.

          +

          Parameters

          +
          +
          asset : UserAsset
          +
           
          +
          @@ -454,19 +164,6 @@

          Methods

          Represents a trade status type.

          -
          - -Expand source code - -
          class TradeStatusType(enum.Enum):
          -    """
          -    Represents a trade status type.
          -    """
          -    Inbound = "Inbound"
          -    Outbound = "Outbound"
          -    Completed = "Completed"
          -    Inactive = "Inactive"
          -

          Ancestors

          • enum.Enum
          • @@ -497,51 +194,13 @@

            Class variables

            Represents trade system metadata at /v1/trades/metadata

            -
            - -Expand source code - -
            class TradesMetadata:
            -    """
            -    Represents trade system metadata at /v1/trades/metadata
            -    """
            -    def __init__(self, trades_metadata_data):
            -        self.max_items_per_side = trades_metadata_data["maxItemsPerSide"]
            -        self.min_value_ratio = trades_metadata_data["minValueRatio"]
            -        self.trade_system_max_robux_percent = trades_metadata_data["tradeSystemMaxRobuxPercent"]
            -        self.trade_system_robux_fee = trades_metadata_data["tradeSystemRobuxFee"]
            -
            class TradesWrapper -(requests) +(requests, get_self)

            Represents the Roblox trades page.

            -
            - -Expand source code - -
            class TradesWrapper:
            -    """
            -    Represents the Roblox trades page.
            -    """
            -    def __init__(self, requests):
            -        self.requests = requests
            -
            -    async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
            -        trades = await Pages(
            -            requests=self.requests,
            -            url=endpoint + f"/v1/trades/{trade_status_type}",
            -            sort_order=sort_order,
            -            limit=limit,
            -            handler=trade_page_handler
            -        )
            -        return trades
            -
            -    async def send_trade(self):
            -        pass
            -

            Methods

            @@ -549,33 +208,24 @@

            Methods

            -
            - -Expand source code - -
            async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
            -    trades = await Pages(
            -        requests=self.requests,
            -        url=endpoint + f"/v1/trades/{trade_status_type}",
            -        sort_order=sort_order,
            -        limit=limit,
            -        handler=trade_page_handler
            -    )
            -    return trades
            -
            -async def send_trade(self) +async def send_trade(self, roblox_id, trade)
            -
            -
            - -Expand source code - -
            async def send_trade(self):
            -    pass
            -
            +

            Sends a trade request.

            +

            Parameters

            +
            +
            roblox_id : int
            +
            User who will recieve the trade.
            +
            trade : TradeRequest
            +
            Trade that will be sent to the user.
            +
            +

            Returns

            +
            +
            int
            +
             
            +
            @@ -616,6 +266,18 @@

            Trade
          • +

            TradeRequest

            + +
          • +
          • TradeStatusType

            • Completed
            • diff --git a/docs/users.html b/docs/users.html index 6ccf1b9b..2e9387fa 100644 --- a/docs/users.html +++ b/docs/users.html @@ -23,168 +23,31 @@

              Module ro_py.users

              This file houses functions and classes that pertain to Roblox users and profiles.

              -
              - -Expand source code - -
              """
              -
              -This file houses functions and classes that pertain to Roblox users and profiles.
              -
              -"""
              -
              -from ro_py.robloxbadges import RobloxBadge
              -# from ro_py.groups import PartialGroup
              -import iso8601
              -
              -endpoint = "https://users.roblox.com/"
              -
              -
              -class User:
              -    """
              -    Represents a Roblox user and their profile.
              -    Can be initialized with either a user ID or a username.
              -
              -    Parameters
              -    ----------
              -    requests : ro_py.utilities.requests.Requests
              -            Requests object to use for API requests.
              -    roblox_id : int
              -            The id of a user.
              -    name : str
              -            The name of the user.
              -    """
              -    def __init__(self, requests, roblox_id, name=None):
              -        self.requests = requests
              -        self.id = roblox_id
              -        self.description = None
              -        self.created = None
              -        self.is_banned = None
              -        self.name = name
              -        self.display_name = None
              -
              -    async def update(self):
              -        """
              -        Updates some class values.
              -        :return: Nothing
              -        """
              -        user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
              -        user_info = user_info_req.json()
              -        self.description = user_info["description"]
              -        self.created = iso8601.parse_date(user_info["created"])
              -        self.is_banned = user_info["isBanned"]
              -        self.name = user_info["name"]
              -        self.display_name = user_info["displayName"]
              -        # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
              -        # self.has_premium = has_premium_req
              -        return self
              -
              -    async def get_status(self):
              -        """
              -        Gets the user's status.
              -        :return: A string
              -        """
              -        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
              -        return status_req.json()["status"]
              -
              -    async def get_roblox_badges(self):
              -        """
              -        Gets the user's roblox badges.
              -        :return: A list of RobloxBadge instances
              -        """
              -        roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
              -        roblox_badges = []
              -        for roblox_badge_data in roblox_badges_req.json():
              -            roblox_badges.append(RobloxBadge(roblox_badge_data))
              -        return roblox_badges
              -
              -    async def get_friends_count(self):
              -        """
              -        Gets the user's friends count.
              -        :return: An integer
              -        """
              -        friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
              -        friends_count = friends_count_req.json()["count"]
              -        return friends_count
              -
              -    async def get_followers_count(self):
              -        """
              -        Gets the user's followers count.
              -        :return: An integer
              -        """
              -        followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
              -        followers_count = followers_count_req.json()["count"]
              -        return followers_count
              -
              -    async def get_followings_count(self):
              -        """
              -        Gets the user's followings count.
              -        :return: An integer
              -        """
              -        followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
              -        followings_count = followings_count_req.json()["count"]
              -        return followings_count
              -
              -    async def get_friends(self):
              -        """
              -        Gets the user's friends.
              -        :return: A list of User instances.
              -        """
              -        friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
              -        friends_raw = friends_req.json()["data"]
              -        friends_list = []
              -        for friend_raw in friends_raw:
              -            friends_list.append(
              -                User(self.requests, friend_raw["id"])
              -            )
              -        return friends_list
              -
              -    """
              -    async def get_groups(self):
              -        member_req = await self.requests.get(
              -            url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
              -        )
              -        data = member_req.json()
              -        groups = []
              -        for group in data['data']:
              -            group = group['group']
              -            groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount']))
              -        return groups
              -    """
              -
              -
              -class PartialUser(User):
              -    """
              -    Represents a user with less information then the normal User class.
              -    """
              -    pass
              -
              +

              Functions

              +
              +
              +def limited_handler(requests, data, args) +
              +
              +
              +
              +

              Classes

              class PartialUser -(requests, roblox_id, name=None) +(*args, **kwargs)

              Represents a user with less information then the normal User class.

              -
              - -Expand source code - -
              class PartialUser(User):
              -    """
              -    Represents a user with less information then the normal User class.
              -    """
              -    pass
              -

              Ancestors

              -
              - -Expand source code - -
              class User:
              -    """
              -    Represents a Roblox user and their profile.
              -    Can be initialized with either a user ID or a username.
              -
              -    Parameters
              -    ----------
              -    requests : ro_py.utilities.requests.Requests
              -            Requests object to use for API requests.
              -    roblox_id : int
              -            The id of a user.
              -    name : str
              -            The name of the user.
              -    """
              -    def __init__(self, requests, roblox_id, name=None):
              -        self.requests = requests
              -        self.id = roblox_id
              -        self.description = None
              -        self.created = None
              -        self.is_banned = None
              -        self.name = name
              -        self.display_name = None
              -
              -    async def update(self):
              -        """
              -        Updates some class values.
              -        :return: Nothing
              -        """
              -        user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
              -        user_info = user_info_req.json()
              -        self.description = user_info["description"]
              -        self.created = iso8601.parse_date(user_info["created"])
              -        self.is_banned = user_info["isBanned"]
              -        self.name = user_info["name"]
              -        self.display_name = user_info["displayName"]
              -        # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
              -        # self.has_premium = has_premium_req
              -        return self
              -
              -    async def get_status(self):
              -        """
              -        Gets the user's status.
              -        :return: A string
              -        """
              -        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
              -        return status_req.json()["status"]
              -
              -    async def get_roblox_badges(self):
              -        """
              -        Gets the user's roblox badges.
              -        :return: A list of RobloxBadge instances
              -        """
              -        roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
              -        roblox_badges = []
              -        for roblox_badge_data in roblox_badges_req.json():
              -            roblox_badges.append(RobloxBadge(roblox_badge_data))
              -        return roblox_badges
              -
              -    async def get_friends_count(self):
              -        """
              -        Gets the user's friends count.
              -        :return: An integer
              -        """
              -        friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
              -        friends_count = friends_count_req.json()["count"]
              -        return friends_count
              -
              -    async def get_followers_count(self):
              -        """
              -        Gets the user's followers count.
              -        :return: An integer
              -        """
              -        followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
              -        followers_count = followers_count_req.json()["count"]
              -        return followers_count
              -
              -    async def get_followings_count(self):
              -        """
              -        Gets the user's followings count.
              -        :return: An integer
              -        """
              -        followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
              -        followings_count = followings_count_req.json()["count"]
              -        return followings_count
              -
              -    async def get_friends(self):
              -        """
              -        Gets the user's friends.
              -        :return: A list of User instances.
              -        """
              -        friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
              -        friends_raw = friends_req.json()["data"]
              -        friends_list = []
              -        for friend_raw in friends_raw:
              -            friends_list.append(
              -                User(self.requests, friend_raw["id"])
              -            )
              -        return friends_list
              -
              -    """
              -    async def get_groups(self):
              -        member_req = await self.requests.get(
              -            url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
              -        )
              -        data = member_req.json()
              -        groups = []
              -        for group in data['data']:
              -            group = group['group']
              -            groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount']))
              -        return groups
              -    """
              -

              Subclasses

              • Member
              • @@ -349,19 +97,6 @@

                Methods

                Gets the user's followers count. :return: An integer

                -
                - -Expand source code - -
                async def get_followers_count(self):
                -    """
                -    Gets the user's followers count.
                -    :return: An integer
                -    """
                -    followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
                -    followers_count = followers_count_req.json()["count"]
                -    return followers_count
                -
                async def get_followings_count(self) @@ -369,19 +104,6 @@

                Methods

                Gets the user's followings count. :return: An integer

                -
                - -Expand source code - -
                async def get_followings_count(self):
                -    """
                -    Gets the user's followings count.
                -    :return: An integer
                -    """
                -    followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
                -    followings_count = followings_count_req.json()["count"]
                -    return followings_count
                -
                async def get_friends(self) @@ -389,24 +111,6 @@

                Methods

                Gets the user's friends. :return: A list of User instances.

                -
                - -Expand source code - -
                async def get_friends(self):
                -    """
                -    Gets the user's friends.
                -    :return: A list of User instances.
                -    """
                -    friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
                -    friends_raw = friends_req.json()["data"]
                -    friends_list = []
                -    for friend_raw in friends_raw:
                -        friends_list.append(
                -            User(self.requests, friend_raw["id"])
                -        )
                -    return friends_list
                -
                async def get_friends_count(self) @@ -414,19 +118,23 @@

                Methods

                Gets the user's friends count. :return: An integer

                -
                - -Expand source code - -
                async def get_friends_count(self):
                -    """
                -    Gets the user's friends count.
                -    :return: An integer
                -    """
                -    friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
                -    friends_count = friends_count_req.json()["count"]
                -    return friends_count
                -
                +
                +
                +async def get_groups(self) +
                +
                +
                +
                +
                +async def get_limiteds(self) +
                +
                +

                Gets all limiteds the user owns.

                +

                Returns

                +
                +
                list
                +
                 
                +
                async def get_roblox_badges(self) @@ -434,21 +142,6 @@

                Methods

                Gets the user's roblox badges. :return: A list of RobloxBadge instances

                -
                - -Expand source code - -
                async def get_roblox_badges(self):
                -    """
                -    Gets the user's roblox badges.
                -    :return: A list of RobloxBadge instances
                -    """
                -    roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
                -    roblox_badges = []
                -    for roblox_badge_data in roblox_badges_req.json():
                -        roblox_badges.append(RobloxBadge(roblox_badge_data))
                -    return roblox_badges
                -
                async def get_status(self) @@ -456,18 +149,6 @@

                Methods

                Gets the user's status. :return: A string

                -
                - -Expand source code - -
                async def get_status(self):
                -    """
                -    Gets the user's status.
                -    :return: A string
                -    """
                -    status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
                -    return status_req.json()["status"]
                -
                async def update(self) @@ -475,26 +156,6 @@

                Methods

                Updates some class values. :return: Nothing

                -
                - -Expand source code - -
                async def update(self):
                -    """
                -    Updates some class values.
                -    :return: Nothing
                -    """
                -    user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
                -    user_info = user_info_req.json()
                -    self.description = user_info["description"]
                -    self.created = iso8601.parse_date(user_info["created"])
                -    self.is_banned = user_info["isBanned"]
                -    self.name = user_info["name"]
                -    self.display_name = user_info["displayName"]
                -    # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
                -    # self.has_premium = has_premium_req
                -    return self
                -
          @@ -512,6 +173,11 @@

          Index

        • ro_py
        +
      • Functions

        + +
      • Classes

        • @@ -524,6 +190,8 @@

          Userget_followings_count

        • get_friends
        • get_friends_count
        • +
        • get_groups
        • +
        • get_limiteds
        • get_roblox_badges
        • get_status
        • update
        • diff --git a/docs/utilities/asset_type.html b/docs/utilities/asset_type.html index 5406f3d8..279f660f 100644 --- a/docs/utilities/asset_type.html +++ b/docs/utilities/asset_type.html @@ -24,67 +24,6 @@

          Module ro_py.utilities.asset_type

          ro.py > asset_type.py

          This file is a conversion table for asset type IDs to asset type names.

          -
          - -Expand source code - -
          """
          -
          -ro.py > asset_type.py
          -
          -This file is a conversion table for asset type IDs to asset type names.
          -
          -"""
          -
          -asset_types = [
          -    None,
          -    "Image",
          -    "TeeShirt",
          -    "Audio",
          -    "Mesh",
          -    "Lua",
          -    "Hat",
          -    "Place",
          -    "Model",
          -    "Shirt",
          -    "Pants",
          -    "Decal",
          -    "Head",
          -    "Face",
          -    "Gear",
          -    "Badge",
          -    "Animation",
          -    "Torso",
          -    "RightArm",
          -    "LeftArm",
          -    "LeftLeg",
          -    "RightLeg",
          -    "Package",
          -    "GamePass",
          -    "Plugin",
          -    "MeshPart",
          -    "HairAccessory",
          -    "FaceAccessory",
          -    "NeckAccessory",
          -    "ShoulderAccessory",
          -    "FrontAccesory",
          -    "BackAccessory",
          -    "WaistAccessory",
          -    "ClimbAnimation",
          -    "DeathAnimation",
          -    "FallAnimation",
          -    "IdleAnimation",
          -    "JumpAnimation",
          -    "RunAnimation",
          -    "SwimAnimation",
          -    "WalkAnimation",
          -    "PoseAnimation",
          -    "EarAccessory",
          -    "EyeAccessory",
          -    "EmoteAnimation",
          -    "Video"
          -]
          -
          diff --git a/docs/utilities/cache.html b/docs/utilities/cache.html index 0a07b3b7..9868093d 100644 --- a/docs/utilities/cache.html +++ b/docs/utilities/cache.html @@ -22,40 +22,6 @@

          Module ro_py.utilities.cache

          -
          - -Expand source code - -
          import enum
          -
          -
          -class CacheType(enum.Enum):
          -    Users = "users"
          -    Groups = "groups"
          -    Games = "games"
          -    Assets = "assets"
          -    Badges = "badges"
          -
          -
          -class Cache:
          -    def __init__(self):
          -        self.cache = {
          -            "users": {},
          -            "groups": {},
          -            "games": {},
          -            "assets": {},
          -            "badges": {}
          -        }
          -
          -    def get(self, cache_type: CacheType, item_id: str):
          -        if item_id in self.cache[cache_type.value]:
          -            return self.cache[cache_type.value][item_id]
          -        else:
          -            return False
          -
          -    def set(self, cache_type: CacheType, item_id: str, item_obj):
          -        self.cache[cache_type.value][item_id] = item_obj
          -
          @@ -71,29 +37,6 @@

          Classes

          -
          - -Expand source code - -
          class Cache:
          -    def __init__(self):
          -        self.cache = {
          -            "users": {},
          -            "groups": {},
          -            "games": {},
          -            "assets": {},
          -            "badges": {}
          -        }
          -
          -    def get(self, cache_type: CacheType, item_id: str):
          -        if item_id in self.cache[cache_type.value]:
          -            return self.cache[cache_type.value][item_id]
          -        else:
          -            return False
          -
          -    def set(self, cache_type: CacheType, item_id: str, item_obj):
          -        self.cache[cache_type.value][item_id] = item_obj
          -

          Methods

          @@ -101,29 +44,12 @@

          Methods

          -
          - -Expand source code - -
          def get(self, cache_type: CacheType, item_id: str):
          -    if item_id in self.cache[cache_type.value]:
          -        return self.cache[cache_type.value][item_id]
          -    else:
          -        return False
          -
          def set(self, cache_type: CacheType, item_id: str, item_obj)
          -
          - -Expand source code - -
          def set(self, cache_type: CacheType, item_id: str, item_obj):
          -    self.cache[cache_type.value][item_id] = item_obj
          -
          @@ -133,17 +59,6 @@

          Methods

          An enumeration.

          -
          - -Expand source code - -
          class CacheType(enum.Enum):
          -    Users = "users"
          -    Groups = "groups"
          -    Games = "games"
          -    Assets = "assets"
          -    Badges = "badges"
          -

          Ancestors

          • enum.Enum
          • diff --git a/docs/utilities/caseconvert.html b/docs/utilities/caseconvert.html index 232212ca..d65928ee 100644 --- a/docs/utilities/caseconvert.html +++ b/docs/utilities/caseconvert.html @@ -22,18 +22,6 @@

            Module ro_py.utilities.caseconvert

            -
            - -Expand source code - -
            import re
            -
            -pattern = re.compile(r'(?<!^)(?=[A-Z])')
            -
            -
            -def to_snake_case(string):
            -    return pattern.sub('_', string).lower()
            -
            @@ -47,13 +35,6 @@

            Functions

            -
            - -Expand source code - -
            def to_snake_case(string):
            -    return pattern.sub('_', string).lower()
            -
            diff --git a/docs/utilities/errors.html b/docs/utilities/errors.html index 43ea1b20..692b44d0 100644 --- a/docs/utilities/errors.html +++ b/docs/utilities/errors.html @@ -24,54 +24,6 @@

            Module ro_py.utilities.errors

            ro.py > errors.py

            This file houses custom exceptions unique to this module.

            -
            - -Expand source code - -
            """
            -
            -ro.py > errors.py
            -
            -This file houses custom exceptions unique to this module.
            -
            -"""
            -
            -
            -class NotLimitedError(Exception):
            -    """Called when code attempts to read limited-only information."""
            -    pass
            -
            -
            -class InvalidIconSizeError(Exception):
            -    """Called when code attempts to pass in an improper size to a thumbnail function."""
            -    pass
            -
            -
            -class InvalidShotTypeError(Exception):
            -    """Called when code attempts to pass in an improper avatar image type to a thumbnail function."""
            -    pass
            -
            -
            -class ApiError(Exception):
            -    """Called in requests when an API request fails."""
            -    pass
            -
            -
            -class ChatError(Exception):
            -    """Called in chat when a chat action fails."""
            -
            -
            -class InvalidPageError(Exception):
            -    """Called when an invalid page is requested."""
            -
            -
            -class NotFound(Exception):
            -    """Called when something is not found."""
            -
            -
            -class UserDoesNotExistError(Exception):
            -    """Called when a user does not exist."""
            -
            @@ -88,14 +40,6 @@

            Classes

            Called in requests when an API request fails.

            -
            - -Expand source code - -
            class ApiError(Exception):
            -    """Called in requests when an API request fails."""
            -    pass
            -

            Ancestors

            • builtins.Exception
            • @@ -108,13 +52,18 @@

              Ancestors

              Called in chat when a chat action fails.

              -
              - -Expand source code - -
              class ChatError(Exception):
              -    """Called in chat when a chat action fails."""
              -
              +

              Ancestors

              +
                +
              • builtins.Exception
              • +
              • builtins.BaseException
              • +
              +
              +
              +class GameJoinError +(*args, **kwargs) +
              +
              +

              Called when an error occurs when joining a game.

              Ancestors

              • builtins.Exception
              • @@ -127,14 +76,6 @@

                Ancestors

                Called when code attempts to pass in an improper size to a thumbnail function.

                -
                - -Expand source code - -
                class InvalidIconSizeError(Exception):
                -    """Called when code attempts to pass in an improper size to a thumbnail function."""
                -    pass
                -

                Ancestors

                • builtins.Exception
                • @@ -147,13 +88,6 @@

                  Ancestors

                  Called when an invalid page is requested.

                  -
                  - -Expand source code - -
                  class InvalidPageError(Exception):
                  -    """Called when an invalid page is requested."""
                  -

                  Ancestors

                  • builtins.Exception
                  • @@ -166,14 +100,6 @@

                    Ancestors

                    Called when code attempts to pass in an improper avatar image type to a thumbnail function.

                    -
                    - -Expand source code - -
                    class InvalidShotTypeError(Exception):
                    -    """Called when code attempts to pass in an improper avatar image type to a thumbnail function."""
                    -    pass
                    -

                    Ancestors

                    • builtins.Exception
                    • @@ -186,13 +112,6 @@

                      Ancestors

                      Called when something is not found.

                      -
                      - -Expand source code - -
                      class NotFound(Exception):
                      -    """Called when something is not found."""
                      -

                      Ancestors

                      • builtins.Exception
                      • @@ -205,14 +124,6 @@

                        Ancestors

                        Called when code attempts to read limited-only information.

                        -
                        - -Expand source code - -
                        class NotLimitedError(Exception):
                        -    """Called when code attempts to read limited-only information."""
                        -    pass
                        -

                        Ancestors

                        • builtins.Exception
                        • @@ -225,13 +136,6 @@

                          Ancestors

                          Called when a user does not exist.

                          -
                          - -Expand source code - -
                          class UserDoesNotExistError(Exception):
                          -    """Called when a user does not exist."""
                          -

                          Ancestors

                          • builtins.Exception
                          • @@ -261,6 +165,9 @@

                            ChatError

                          • +

                            GameJoinError

                            +
                          • +
                          • InvalidIconSizeError

                          • diff --git a/docs/utilities/index.html b/docs/utilities/index.html index e1de3e3e..c977b23c 100644 --- a/docs/utilities/index.html +++ b/docs/utilities/index.html @@ -23,16 +23,6 @@

                            Module ro_py.utilities

                            This folder houses utilities that are used internally for ro.py.

                            -
                            - -Expand source code - -
                            """
                            -
                            -This folder houses utilities that are used internally for ro.py.
                            -
                            -"""
                            -

                            Sub-modules

                            diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index cd94dbd0..59111c4f 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -22,106 +22,6 @@

                            Module ro_py.utilities.pages

                            -
                            - -Expand source code - -
                            from ro_py.utilities.errors import InvalidPageError
                            -import enum
                            -
                            -
                            -class SortOrder(enum.Enum):
                            -    """
                            -    Order in which page data should load in.
                            -    """
                            -    Ascending = "Asc"
                            -    Descending = "Desc"
                            -
                            -
                            -class Page:
                            -    """
                            -    Represents a single page from a Pages object.
                            -    """
                            -    def __init__(self, requests, data, handler=None, handler_args=None):
                            -        self.previous_page_cursor = data["previousPageCursor"]
                            -        """Cursor to navigate to the previous page."""
                            -        self.next_page_cursor = data["nextPageCursor"]
                            -        """Cursor to navigate to the next page."""
                            -
                            -        self.data = data["data"]
                            -        """Raw data from this page."""
                            -
                            -        if handler:
                            -            self.data = handler(requests, self.data, handler_args)
                            -
                            -
                            -class Pages:
                            -    """
                            -    Represents a paged object.
                            -
                            -    !!! warning
                            -        This object is *slow*, especially with a custom handler.
                            -        Automatic page caching will be added in the future. It is suggested to
                            -        cache the pages yourself if speed is required.
                            -    """
                            -    def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None):
                            -        if extra_parameters is None:
                            -            extra_parameters = {}
                            -
                            -        self.handler = handler
                            -        """Function that is passed to Page as data handler."""
                            -
                            -        extra_parameters["sortOrder"] = sort_order.value
                            -        extra_parameters["limit"] = limit
                            -
                            -        self.parameters = extra_parameters
                            -        """Extra parameters for the request."""
                            -        self.requests = requests
                            -        """Requests object."""
                            -        self.url = url
                            -        """URL containing the paginated data, accessible with a GET request."""
                            -        self.page = 0
                            -        """Current page number."""
                            -
                            -        self.data = self._get_page()
                            -
                            -    async def _get_page(self, cursor=None):
                            -        """
                            -        Gets a page at the specified cursor position.
                            -        """
                            -        this_parameters = self.parameters
                            -        if cursor:
                            -            this_parameters["cursor"] = cursor
                            -
                            -        page_req = await self.requests.get(
                            -            url=self.url,
                            -            params=this_parameters
                            -        )
                            -        return Page(
                            -            requests=self.requests,
                            -            data=page_req.json(),
                            -            handler=self.handler,
                            -            handler_args=handler_args
                            -        )
                            -
                            -    async def previous(self):
                            -        """
                            -        Moves to the previous page.
                            -        """
                            -        if self.data.previous_page_cursor:
                            -            self.data = await self._get_page(self.data.previous_page_cursor)
                            -        else:
                            -            raise InvalidPageError
                            -
                            -    async def next(self):
                            -        """
                            -        Moves to the next page.
                            -        """
                            -        if self.data.next_page_cursor:
                            -            self.data = await self._get_page(self.data.next_page_cursor)
                            -        else:
                            -            raise InvalidPageError
                            -
                            @@ -138,26 +38,6 @@

                            Classes

                            Represents a single page from a Pages object.

                            -
                            - -Expand source code - -
                            class Page:
                            -    """
                            -    Represents a single page from a Pages object.
                            -    """
                            -    def __init__(self, requests, data, handler=None, handler_args=None):
                            -        self.previous_page_cursor = data["previousPageCursor"]
                            -        """Cursor to navigate to the previous page."""
                            -        self.next_page_cursor = data["nextPageCursor"]
                            -        """Cursor to navigate to the next page."""
                            -
                            -        self.data = data["data"]
                            -        """Raw data from this page."""
                            -
                            -        if handler:
                            -            self.data = handler(requests, self.data, handler_args)
                            -

                            Instance variables

                            var data
                            @@ -186,77 +66,6 @@

                            Instance variables

                            Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required.

                            -
                            - -Expand source code - -
                            class Pages:
                            -    """
                            -    Represents a paged object.
                            -
                            -    !!! warning
                            -        This object is *slow*, especially with a custom handler.
                            -        Automatic page caching will be added in the future. It is suggested to
                            -        cache the pages yourself if speed is required.
                            -    """
                            -    def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None):
                            -        if extra_parameters is None:
                            -            extra_parameters = {}
                            -
                            -        self.handler = handler
                            -        """Function that is passed to Page as data handler."""
                            -
                            -        extra_parameters["sortOrder"] = sort_order.value
                            -        extra_parameters["limit"] = limit
                            -
                            -        self.parameters = extra_parameters
                            -        """Extra parameters for the request."""
                            -        self.requests = requests
                            -        """Requests object."""
                            -        self.url = url
                            -        """URL containing the paginated data, accessible with a GET request."""
                            -        self.page = 0
                            -        """Current page number."""
                            -
                            -        self.data = self._get_page()
                            -
                            -    async def _get_page(self, cursor=None):
                            -        """
                            -        Gets a page at the specified cursor position.
                            -        """
                            -        this_parameters = self.parameters
                            -        if cursor:
                            -            this_parameters["cursor"] = cursor
                            -
                            -        page_req = await self.requests.get(
                            -            url=self.url,
                            -            params=this_parameters
                            -        )
                            -        return Page(
                            -            requests=self.requests,
                            -            data=page_req.json(),
                            -            handler=self.handler,
                            -            handler_args=handler_args
                            -        )
                            -
                            -    async def previous(self):
                            -        """
                            -        Moves to the previous page.
                            -        """
                            -        if self.data.previous_page_cursor:
                            -            self.data = await self._get_page(self.data.previous_page_cursor)
                            -        else:
                            -            raise InvalidPageError
                            -
                            -    async def next(self):
                            -        """
                            -        Moves to the next page.
                            -        """
                            -        if self.data.next_page_cursor:
                            -            self.data = await self._get_page(self.data.next_page_cursor)
                            -        else:
                            -            raise InvalidPageError
                            -

                            Instance variables

                            var handler
                            @@ -282,43 +91,23 @@

                            Instance variables

                            Methods

                            +
                            +async def get_page(self, cursor=None) +
                            +
                            +

                            Gets a page at the specified cursor position.

                            +
                            async def next(self)

                            Moves to the next page.

                            -
                            - -Expand source code - -
                            async def next(self):
                            -    """
                            -    Moves to the next page.
                            -    """
                            -    if self.data.next_page_cursor:
                            -        self.data = await self._get_page(self.data.next_page_cursor)
                            -    else:
                            -        raise InvalidPageError
                            -
                            async def previous(self)

                            Moves to the previous page.

                            -
                            - -Expand source code - -
                            async def previous(self):
                            -    """
                            -    Moves to the previous page.
                            -    """
                            -    if self.data.previous_page_cursor:
                            -        self.data = await self._get_page(self.data.previous_page_cursor)
                            -    else:
                            -        raise InvalidPageError
                            -
                            @@ -328,17 +117,6 @@

                            Methods

                            Order in which page data should load in.

                            -
                            - -Expand source code - -
                            class SortOrder(enum.Enum):
                            -    """
                            -    Order in which page data should load in.
                            -    """
                            -    Ascending = "Asc"
                            -    Descending = "Desc"
                            -

                            Ancestors

                            • enum.Enum
                            • @@ -382,6 +160,7 @@

                              Pages

                                +
                              • get_page
                              • handler
                              • next
                              • page
                              • diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 44a66c45..4940d3c1 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -22,173 +22,6 @@

                                Module ro_py.utilities.requests

                                -
                                - -Expand source code - -
                                from ro_py.utilities.errors import ApiError
                                -from ro_py.utilities.cache import Cache
                                -from ro_py.captcha import CaptchaMetadata
                                -from json.decoder import JSONDecodeError
                                -from cachecontrol import CacheControl
                                -import requests_async
                                -import requests
                                -
                                -
                                -class Requests:
                                -    """
                                -    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
                                -
                                -    Parameters
                                -    ----------
                                -    request_cache: bool
                                -        Enable this to wrap the session in a CacheControl object. Untested.
                                -    jmk_endpoint: str
                                -        Not currently in use.
                                -    """
                                -    def __init__(self, request_cache: bool = True, jmk_endpoint="https://roblox.jmksite.dev/"):
                                -        self.session = requests_async.Session()
                                -        """Session to use for requests."""
                                -        self.cache = Cache()
                                -        """Cache object to use for object storage."""
                                -        if request_cache:
                                -            self.session = CacheControl(self.session)
                                -        """
                                -        Thank you @nsg for letting me know about this!
                                -        This allows us to access some extra content.
                                -        ▼▼▼
                                -        """
                                -        self.session.headers["User-Agent"] = "Roblox/WinInet"
                                -
                                -    async def get(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.get.
                                -        """
                                -
                                -        quickreturn = kwargs.pop("quickreturn", False)
                                -
                                -        get_request = await self.session.get(*args, **kwargs)
                                -
                                -        try:
                                -            get_request_json = get_request.json()
                                -        except JSONDecodeError:
                                -            return get_request
                                -
                                -        if isinstance(get_request_json, dict):
                                -            try:
                                -                get_request_error = get_request_json["errors"]
                                -            except KeyError:
                                -                return get_request
                                -        else:
                                -            return get_request
                                -
                                -        if quickreturn:
                                -            return get_request
                                -
                                -        raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
                                -
                                -    def back_post(self, *args, **kwargs):
                                -        kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies)
                                -        kwargs["headers"] = kwargs.pop("headers", self.session.headers)
                                -
                                -        post_request = requests.post(*args, **kwargs)
                                -
                                -        if "X-CSRF-TOKEN" in post_request.headers:
                                -            self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -            post_request = requests.post(*args, **kwargs)
                                -
                                -        self.session.cookies = post_request.cookies
                                -        return post_request
                                -
                                -    async def post(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.post.
                                -        """
                                -
                                -        quickreturn = kwargs.pop("quickreturn", False)
                                -        doxcsrf = kwargs.pop("doxcsrf", True)
                                -
                                -        post_request = await self.session.post(*args, **kwargs)
                                -
                                -        if doxcsrf:
                                -            if post_request.status_code == 403:
                                -                if "X-CSRF-TOKEN" in post_request.headers:
                                -                    self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -                    post_request = await self.session.post(*args, **kwargs)
                                -
                                -        try:
                                -            post_request_json = post_request.json()
                                -        except JSONDecodeError:
                                -            return post_request
                                -
                                -        if isinstance(post_request_json, dict):
                                -            try:
                                -                post_request_error = post_request_json["errors"]
                                -            except KeyError:
                                -                return post_request
                                -        else:
                                -            return post_request
                                -
                                -        if quickreturn:
                                -            return post_request
                                -
                                -        raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}")
                                -
                                -    async def patch(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.patch.
                                -        """
                                -
                                -        patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -        if patch_request.status_code == 403:
                                -            if "X-CSRF-TOKEN" in patch_request.headers:
                                -                self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
                                -                patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -        patch_request_json = patch_request.json()
                                -
                                -        if isinstance(patch_request_json, dict):
                                -            try:
                                -                patch_request_error = patch_request_json["errors"]
                                -            except KeyError:
                                -                return patch_request
                                -        else:
                                -            return patch_request
                                -
                                -        raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
                                -
                                -    async def delete(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.delete.
                                -        """
                                -
                                -        delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -        if delete_request.status_code == 403:
                                -            if "X-CSRF-TOKEN" in delete_request.headers:
                                -                self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"]
                                -                delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -        delete_request_json = delete_request.json()
                                -
                                -        if isinstance(delete_request_json, dict):
                                -            try:
                                -                delete_request_error = delete_request_json["errors"]
                                -            except KeyError:
                                -                return delete_request
                                -        else:
                                -            return delete_request
                                -
                                -        raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
                                -
                                -    async def get_captcha_metadata(self):
                                -        captcha_meta_req = await self.get(
                                -            url="https://apis.roblox.com/captcha/v1/metadata"
                                -        )
                                -        captcha_meta_raw = captcha_meta_req.json()
                                -        return CaptchaMetadata(captcha_meta_raw)
                                -
                                @@ -201,175 +34,9 @@

                                Classes

                                class Requests -(request_cache: bool = True, jmk_endpoint='https://roblox.jmksite.dev/')
                                -

                                This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.

                                -

                                Parameters

                                -
                                -
                                request_cache : bool
                                -
                                Enable this to wrap the session in a CacheControl object. Untested.
                                -
                                jmk_endpoint : str
                                -
                                Not currently in use.
                                -
                                -
                                - -Expand source code - -
                                class Requests:
                                -    """
                                -    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
                                -
                                -    Parameters
                                -    ----------
                                -    request_cache: bool
                                -        Enable this to wrap the session in a CacheControl object. Untested.
                                -    jmk_endpoint: str
                                -        Not currently in use.
                                -    """
                                -    def __init__(self, request_cache: bool = True, jmk_endpoint="https://roblox.jmksite.dev/"):
                                -        self.session = requests_async.Session()
                                -        """Session to use for requests."""
                                -        self.cache = Cache()
                                -        """Cache object to use for object storage."""
                                -        if request_cache:
                                -            self.session = CacheControl(self.session)
                                -        """
                                -        Thank you @nsg for letting me know about this!
                                -        This allows us to access some extra content.
                                -        ▼▼▼
                                -        """
                                -        self.session.headers["User-Agent"] = "Roblox/WinInet"
                                -
                                -    async def get(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.get.
                                -        """
                                -
                                -        quickreturn = kwargs.pop("quickreturn", False)
                                -
                                -        get_request = await self.session.get(*args, **kwargs)
                                -
                                -        try:
                                -            get_request_json = get_request.json()
                                -        except JSONDecodeError:
                                -            return get_request
                                -
                                -        if isinstance(get_request_json, dict):
                                -            try:
                                -                get_request_error = get_request_json["errors"]
                                -            except KeyError:
                                -                return get_request
                                -        else:
                                -            return get_request
                                -
                                -        if quickreturn:
                                -            return get_request
                                -
                                -        raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
                                -
                                -    def back_post(self, *args, **kwargs):
                                -        kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies)
                                -        kwargs["headers"] = kwargs.pop("headers", self.session.headers)
                                -
                                -        post_request = requests.post(*args, **kwargs)
                                -
                                -        if "X-CSRF-TOKEN" in post_request.headers:
                                -            self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -            post_request = requests.post(*args, **kwargs)
                                -
                                -        self.session.cookies = post_request.cookies
                                -        return post_request
                                -
                                -    async def post(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.post.
                                -        """
                                -
                                -        quickreturn = kwargs.pop("quickreturn", False)
                                -        doxcsrf = kwargs.pop("doxcsrf", True)
                                -
                                -        post_request = await self.session.post(*args, **kwargs)
                                -
                                -        if doxcsrf:
                                -            if post_request.status_code == 403:
                                -                if "X-CSRF-TOKEN" in post_request.headers:
                                -                    self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -                    post_request = await self.session.post(*args, **kwargs)
                                -
                                -        try:
                                -            post_request_json = post_request.json()
                                -        except JSONDecodeError:
                                -            return post_request
                                -
                                -        if isinstance(post_request_json, dict):
                                -            try:
                                -                post_request_error = post_request_json["errors"]
                                -            except KeyError:
                                -                return post_request
                                -        else:
                                -            return post_request
                                -
                                -        if quickreturn:
                                -            return post_request
                                -
                                -        raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}")
                                -
                                -    async def patch(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.patch.
                                -        """
                                -
                                -        patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -        if patch_request.status_code == 403:
                                -            if "X-CSRF-TOKEN" in patch_request.headers:
                                -                self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
                                -                patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -        patch_request_json = patch_request.json()
                                -
                                -        if isinstance(patch_request_json, dict):
                                -            try:
                                -                patch_request_error = patch_request_json["errors"]
                                -            except KeyError:
                                -                return patch_request
                                -        else:
                                -            return patch_request
                                -
                                -        raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
                                -
                                -    async def delete(self, *args, **kwargs):
                                -        """
                                -        Essentially identical to requests_async.Session.delete.
                                -        """
                                -
                                -        delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -        if delete_request.status_code == 403:
                                -            if "X-CSRF-TOKEN" in delete_request.headers:
                                -                self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"]
                                -                delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -        delete_request_json = delete_request.json()
                                -
                                -        if isinstance(delete_request_json, dict):
                                -            try:
                                -                delete_request_error = delete_request_json["errors"]
                                -            except KeyError:
                                -                return delete_request
                                -        else:
                                -            return delete_request
                                -
                                -        raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
                                -
                                -    async def get_captcha_metadata(self):
                                -        captcha_meta_req = await self.get(
                                -            url="https://apis.roblox.com/captcha/v1/metadata"
                                -        )
                                -        captcha_meta_raw = captcha_meta_req.json()
                                -        return CaptchaMetadata(captcha_meta_raw)
                                -
                                +

                                This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.

                                Instance variables

                                var cache
                                @@ -388,189 +55,36 @@

                                Methods

                                -
                                - -Expand source code - -
                                def back_post(self, *args, **kwargs):
                                -    kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies)
                                -    kwargs["headers"] = kwargs.pop("headers", self.session.headers)
                                -
                                -    post_request = requests.post(*args, **kwargs)
                                -
                                -    if "X-CSRF-TOKEN" in post_request.headers:
                                -        self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -        post_request = requests.post(*args, **kwargs)
                                -
                                -    self.session.cookies = post_request.cookies
                                -    return post_request
                                -
                                async def delete(self, *args, **kwargs)

                                Essentially identical to requests_async.Session.delete.

                                -
                                - -Expand source code - -
                                async def delete(self, *args, **kwargs):
                                -    """
                                -    Essentially identical to requests_async.Session.delete.
                                -    """
                                -
                                -    delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -    if delete_request.status_code == 403:
                                -        if "X-CSRF-TOKEN" in delete_request.headers:
                                -            self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"]
                                -            delete_request = await self.session.delete(*args, **kwargs)
                                -
                                -    delete_request_json = delete_request.json()
                                -
                                -    if isinstance(delete_request_json, dict):
                                -        try:
                                -            delete_request_error = delete_request_json["errors"]
                                -        except KeyError:
                                -            return delete_request
                                -    else:
                                -        return delete_request
                                -
                                -    raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
                                -
                                async def get(self, *args, **kwargs)

                                Essentially identical to requests_async.Session.get.

                                -
                                - -Expand source code - -
                                async def get(self, *args, **kwargs):
                                -    """
                                -    Essentially identical to requests_async.Session.get.
                                -    """
                                -
                                -    quickreturn = kwargs.pop("quickreturn", False)
                                -
                                -    get_request = await self.session.get(*args, **kwargs)
                                -
                                -    try:
                                -        get_request_json = get_request.json()
                                -    except JSONDecodeError:
                                -        return get_request
                                -
                                -    if isinstance(get_request_json, dict):
                                -        try:
                                -            get_request_error = get_request_json["errors"]
                                -        except KeyError:
                                -            return get_request
                                -    else:
                                -        return get_request
                                -
                                -    if quickreturn:
                                -        return get_request
                                -
                                -    raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
                                -
                                async def get_captcha_metadata(self)
                                -
                                - -Expand source code - -
                                async def get_captcha_metadata(self):
                                -    captcha_meta_req = await self.get(
                                -        url="https://apis.roblox.com/captcha/v1/metadata"
                                -    )
                                -    captcha_meta_raw = captcha_meta_req.json()
                                -    return CaptchaMetadata(captcha_meta_raw)
                                -
                                async def patch(self, *args, **kwargs)

                                Essentially identical to requests_async.Session.patch.

                                -
                                - -Expand source code - -
                                async def patch(self, *args, **kwargs):
                                -    """
                                -    Essentially identical to requests_async.Session.patch.
                                -    """
                                -
                                -    patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -    if patch_request.status_code == 403:
                                -        if "X-CSRF-TOKEN" in patch_request.headers:
                                -            self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"]
                                -            patch_request = await self.session.patch(*args, **kwargs)
                                -
                                -    patch_request_json = patch_request.json()
                                -
                                -    if isinstance(patch_request_json, dict):
                                -        try:
                                -            patch_request_error = patch_request_json["errors"]
                                -        except KeyError:
                                -            return patch_request
                                -    else:
                                -        return patch_request
                                -
                                -    raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
                                -
                                async def post(self, *args, **kwargs)

                                Essentially identical to requests_async.Session.post.

                                -
                                - -Expand source code - -
                                async def post(self, *args, **kwargs):
                                -    """
                                -    Essentially identical to requests_async.Session.post.
                                -    """
                                -
                                -    quickreturn = kwargs.pop("quickreturn", False)
                                -    doxcsrf = kwargs.pop("doxcsrf", True)
                                -
                                -    post_request = await self.session.post(*args, **kwargs)
                                -
                                -    if doxcsrf:
                                -        if post_request.status_code == 403:
                                -            if "X-CSRF-TOKEN" in post_request.headers:
                                -                self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
                                -                post_request = await self.session.post(*args, **kwargs)
                                -
                                -    try:
                                -        post_request_json = post_request.json()
                                -    except JSONDecodeError:
                                -        return post_request
                                -
                                -    if isinstance(post_request_json, dict):
                                -        try:
                                -            post_request_error = post_request_json["errors"]
                                -        except KeyError:
                                -            return post_request
                                -    else:
                                -        return post_request
                                -
                                -    if quickreturn:
                                -        return post_request
                                -
                                -    raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}")
                                -
                                From b91b5bfe24f53c08412b717ba9567b4f62a222af Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 14 Jan 2021 20:11:30 -0500 Subject: [PATCH 267/518] Update CNAME --- docs/CNAME | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CNAME b/docs/CNAME index 6deca327..c270d7ce 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -ro.py.jmksite.dev +docs.ro.py.jmksite.dev \ No newline at end of file From 21f61cf3c7f8799930181f548505b64487e288d8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 14 Jan 2021 20:14:40 -0500 Subject: [PATCH 268/518] Update CNAME --- docs/CNAME | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CNAME b/docs/CNAME index c270d7ce..b1f93fc7 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -docs.ro.py.jmksite.dev \ No newline at end of file +ro.py.jmksite.dev \ No newline at end of file From fdbf3890fd10e68fb0313ffa83ea048503dc9d05 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 14 Jan 2021 20:20:52 -0500 Subject: [PATCH 269/518] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ff218168..7af58808 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tests/ ro_py_old/ other/ udocs/ +docstemplate/ build.bat docsbuild.bat chat.py From 9a4db2ee93e8aabfc70b9a6cd5c85613e8050ca9 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 14 Jan 2021 21:25:33 -0500 Subject: [PATCH 270/518] Updated Docs --- docs/CNAME | 2 +- docs/accountinformation.html | 399 ++++++++++++++ docs/accountsettings.html | 235 +++++++++ docs/assets.html | 348 +++++++++++++ docs/badges.html | 172 ++++++ docs/captcha.html | 99 ++++ docs/catalog.html | 75 +++ docs/chat.html | 494 ++++++++++++++++++ docs/client.html | 724 ++++++++++++++++++++++++++ docs/economy.html | 90 ++++ docs/extensions/bots.html | 119 +++++ docs/extensions/index.html | 42 ++ docs/extensions/prompt.html | 685 ++++++++++++++++++++++++ docs/gamepersistence.html | 897 ++++++++++++++++++++++++++++++++ docs/games.html | 518 ++++++++++++++++++ docs/gender.html | 66 +++ docs/groups.html | 890 +++++++++++++++++++++++++++++++ docs/index.html | 50 ++ docs/notifications.html | 414 +++++++++++++++ docs/robloxbadges.html | 71 +++ docs/robloxdocs.html | 303 +++++++++++ docs/robloxstatus.html | 166 ++++++ docs/roles.html | 431 +++++++++++++++ docs/thumbnails.html | 371 +++++++++++++ docs/trades.html | 757 +++++++++++++++++++++++++++ docs/users.html | 481 +++++++++++++++++ docs/utilities/asset_type.html | 93 ++++ docs/utilities/cache.html | 117 +++++ docs/utilities/caseconvert.html | 51 ++ docs/utilities/errors.html | 151 ++++++ docs/utilities/index.html | 42 ++ docs/utilities/pages.html | 285 ++++++++++ docs/utilities/requests.html | 495 ++++++++++++++++++ 33 files changed, 10132 insertions(+), 1 deletion(-) diff --git a/docs/CNAME b/docs/CNAME index b1f93fc7..6deca327 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -ro.py.jmksite.dev \ No newline at end of file +ro.py.jmksite.dev diff --git a/docs/accountinformation.html b/docs/accountinformation.html index 41eb5937..cbb23870 100644 --- a/docs/accountinformation.html +++ b/docs/accountinformation.html @@ -14,6 +14,33 @@ +
                                @@ -23,6 +50,146 @@

                                Module ro_py.accountinformation

                                This file houses functions and classes that pertain to Roblox authenticated user account information.

                                +
                                + +Expand source code + +
                                """
                                +
                                +This file houses functions and classes that pertain to Roblox authenticated user account information.
                                +
                                +"""
                                +
                                +from datetime import datetime
                                +from ro_py.gender import RobloxGender
                                +
                                +endpoint = "https://accountinformation.roblox.com/"
                                +
                                +
                                +class AccountInformationMetadata:
                                +    """
                                +    Represents account information metadata.
                                +    """
                                +    def __init__(self, metadata_raw):
                                +        self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"]
                                +        """Unsure what this does."""
                                +        self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"]
                                +        """Whether the account settings policy is enabled (unsure exactly what this does)"""
                                +        self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"]
                                +        """Whether the user's linked phone number is enabled."""
                                +        self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"]
                                +        """Maximum length of the user's description."""
                                +        self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"]
                                +        """Whether the user's description is enabled."""
                                +        self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"]
                                +        """Whether the UserBlock endpoints are updated (unsure exactly what this does)"""
                                +
                                +
                                +class PromotionChannels:
                                +    """
                                +    Represents account information promotion channels.
                                +    """
                                +    def __init__(self, promotion_raw):
                                +        self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"]
                                +        """Visibility of promotion channels."""
                                +        self.facebook = promotion_raw["facebook"]
                                +        """Link to the user's Facebook page."""
                                +        self.twitter = promotion_raw["twitter"]
                                +        """Link to the user's Twitter page."""
                                +        self.youtube = promotion_raw["youtube"]
                                +        """Link to the user's YouTube page."""
                                +        self.twitch = promotion_raw["twitch"]
                                +        """Link to the user's Twitch page."""
                                +
                                +
                                +class AccountInformation:
                                +    """
                                +    Represents authenticated client account information (https://accountinformation.roblox.com/)
                                +    This is only available for authenticated clients as it cannot be accessed otherwise.
                                +
                                +    Parameters
                                +    ----------
                                +    requests : ro_py.utilities.requests.Requests
                                +        Requests object to use for API requests.
                                +    """
                                +    def __init__(self, requests):
                                +        self.requests = requests
                                +        self.account_information_metadata = None
                                +        self.promotion_channels = None
                                +
                                +    async def update(self):
                                +        """
                                +        Updates the account information.
                                +        """
                                +        account_information_req = await self.requests.get(
                                +            url="https://accountinformation.roblox.com/v1/metadata"
                                +        )
                                +        self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
                                +        promotion_channels_req = await self.requests.get(
                                +            url="https://accountinformation.roblox.com/v1/promotion-channels"
                                +        )
                                +        self.promotion_channels = PromotionChannels(promotion_channels_req.json())
                                +
                                +    async def get_gender(self):
                                +        """
                                +        Gets the user's gender.
                                +
                                +        Returns
                                +        -------
                                +        ro_py.gender.RobloxGender
                                +        """
                                +        gender_req = await self.requests.get(endpoint + "v1/gender")
                                +        return RobloxGender(gender_req.json()["gender"])
                                +
                                +    async def set_gender(self, gender):
                                +        """
                                +        Sets the user's gender.
                                +
                                +        Parameters
                                +        ----------
                                +        gender : ro_py.gender.RobloxGender
                                +        """
                                +        await self.requests.post(
                                +            url=endpoint + "v1/gender",
                                +            data={
                                +                "gender": str(gender.value)
                                +            }
                                +        )
                                +
                                +    async def get_birthdate(self):
                                +        """
                                +        Grabs the user's birthdate.
                                +
                                +        Returns
                                +        -------
                                +        datetime.datetime
                                +        """
                                +        birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
                                +        birthdate_raw = birthdate_req.json()
                                +        birthdate = datetime(
                                +            year=birthdate_raw["birthYear"],
                                +            month=birthdate_raw["birthMonth"],
                                +            day=birthdate_raw["birthDay"]
                                +        )
                                +        return birthdate
                                +
                                +    async def set_birthdate(self, birthdate):
                                +        """
                                +        Sets the user's birthdate.
                                +
                                +        Parameters
                                +        ----------
                                +        birthdate : datetime.datetime
                                +        """
                                +        await self.requests.post(
                                +            url=endpoint + "v1/birthdate",
                                +            data={
                                +              "birthMonth": birthdate.month,
                                +              "birthDay": birthdate.day,
                                +              "birthYear": birthdate.year
                                +            }
                                +        )
                                +
                                @@ -45,6 +212,98 @@

                                Parameters

                                requests : Requests
                                Requests object to use for API requests.
                                +
                                + +Expand source code + +
                                class AccountInformation:
                                +    """
                                +    Represents authenticated client account information (https://accountinformation.roblox.com/)
                                +    This is only available for authenticated clients as it cannot be accessed otherwise.
                                +
                                +    Parameters
                                +    ----------
                                +    requests : ro_py.utilities.requests.Requests
                                +        Requests object to use for API requests.
                                +    """
                                +    def __init__(self, requests):
                                +        self.requests = requests
                                +        self.account_information_metadata = None
                                +        self.promotion_channels = None
                                +
                                +    async def update(self):
                                +        """
                                +        Updates the account information.
                                +        """
                                +        account_information_req = await self.requests.get(
                                +            url="https://accountinformation.roblox.com/v1/metadata"
                                +        )
                                +        self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
                                +        promotion_channels_req = await self.requests.get(
                                +            url="https://accountinformation.roblox.com/v1/promotion-channels"
                                +        )
                                +        self.promotion_channels = PromotionChannels(promotion_channels_req.json())
                                +
                                +    async def get_gender(self):
                                +        """
                                +        Gets the user's gender.
                                +
                                +        Returns
                                +        -------
                                +        ro_py.gender.RobloxGender
                                +        """
                                +        gender_req = await self.requests.get(endpoint + "v1/gender")
                                +        return RobloxGender(gender_req.json()["gender"])
                                +
                                +    async def set_gender(self, gender):
                                +        """
                                +        Sets the user's gender.
                                +
                                +        Parameters
                                +        ----------
                                +        gender : ro_py.gender.RobloxGender
                                +        """
                                +        await self.requests.post(
                                +            url=endpoint + "v1/gender",
                                +            data={
                                +                "gender": str(gender.value)
                                +            }
                                +        )
                                +
                                +    async def get_birthdate(self):
                                +        """
                                +        Grabs the user's birthdate.
                                +
                                +        Returns
                                +        -------
                                +        datetime.datetime
                                +        """
                                +        birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
                                +        birthdate_raw = birthdate_req.json()
                                +        birthdate = datetime(
                                +            year=birthdate_raw["birthYear"],
                                +            month=birthdate_raw["birthMonth"],
                                +            day=birthdate_raw["birthDay"]
                                +        )
                                +        return birthdate
                                +
                                +    async def set_birthdate(self, birthdate):
                                +        """
                                +        Sets the user's birthdate.
                                +
                                +        Parameters
                                +        ----------
                                +        birthdate : datetime.datetime
                                +        """
                                +        await self.requests.post(
                                +            url=endpoint + "v1/birthdate",
                                +            data={
                                +              "birthMonth": birthdate.month,
                                +              "birthDay": birthdate.day,
                                +              "birthYear": birthdate.year
                                +            }
                                +        )
                                +

                                Methods

                                @@ -57,6 +316,27 @@

                                Returns

                                datetime.datetime
                                 
                                +
                                + +Expand source code + +
                                async def get_birthdate(self):
                                +    """
                                +    Grabs the user's birthdate.
                                +
                                +    Returns
                                +    -------
                                +    datetime.datetime
                                +    """
                                +    birthdate_req = await self.requests.get(endpoint + "v1/birthdate")
                                +    birthdate_raw = birthdate_req.json()
                                +    birthdate = datetime(
                                +        year=birthdate_raw["birthYear"],
                                +        month=birthdate_raw["birthMonth"],
                                +        day=birthdate_raw["birthDay"]
                                +    )
                                +    return birthdate
                                +
                            async def get_gender(self) @@ -68,6 +348,21 @@

                            Returns

                            RobloxGender
                             
                            +
                            + +Expand source code + +
                            async def get_gender(self):
                            +    """
                            +    Gets the user's gender.
                            +
                            +    Returns
                            +    -------
                            +    ro_py.gender.RobloxGender
                            +    """
                            +    gender_req = await self.requests.get(endpoint + "v1/gender")
                            +    return RobloxGender(gender_req.json()["gender"])
                            +
                          async def set_birthdate(self, birthdate) @@ -79,6 +374,27 @@

                          Parameters

                          birthdate : datetime.datetime
                           
                          +
                          + +Expand source code + +
                          async def set_birthdate(self, birthdate):
                          +    """
                          +    Sets the user's birthdate.
                          +
                          +    Parameters
                          +    ----------
                          +    birthdate : datetime.datetime
                          +    """
                          +    await self.requests.post(
                          +        url=endpoint + "v1/birthdate",
                          +        data={
                          +          "birthMonth": birthdate.month,
                          +          "birthDay": birthdate.day,
                          +          "birthYear": birthdate.year
                          +        }
                          +    )
                          +
                        async def set_gender(self, gender) @@ -90,12 +406,48 @@

                        Parameters

                        gender : RobloxGender
                         
                        +
                        + +Expand source code + +
                        async def set_gender(self, gender):
                        +    """
                        +    Sets the user's gender.
                        +
                        +    Parameters
                        +    ----------
                        +    gender : ro_py.gender.RobloxGender
                        +    """
                        +    await self.requests.post(
                        +        url=endpoint + "v1/gender",
                        +        data={
                        +            "gender": str(gender.value)
                        +        }
                        +    )
                        +
                      async def update(self)

                      Updates the account information.

                      +
                      + +Expand source code + +
                      async def update(self):
                      +    """
                      +    Updates the account information.
                      +    """
                      +    account_information_req = await self.requests.get(
                      +        url="https://accountinformation.roblox.com/v1/metadata"
                      +    )
                      +    self.account_information_metadata = AccountInformationMetadata(account_information_req.json())
                      +    promotion_channels_req = await self.requests.get(
                      +        url="https://accountinformation.roblox.com/v1/promotion-channels"
                      +    )
                      +    self.promotion_channels = PromotionChannels(promotion_channels_req.json())
                      +
                    @@ -105,6 +457,28 @@

                    Parameters

                    Represents account information metadata.

                    +
                    + +Expand source code + +
                    class AccountInformationMetadata:
                    +    """
                    +    Represents account information metadata.
                    +    """
                    +    def __init__(self, metadata_raw):
                    +        self.is_allowed_notifications_endpoint_disabled = metadata_raw["isAllowedNotificationsEndpointDisabled"]
                    +        """Unsure what this does."""
                    +        self.is_account_settings_policy_enabled = metadata_raw["isAccountSettingsPolicyEnabled"]
                    +        """Whether the account settings policy is enabled (unsure exactly what this does)"""
                    +        self.is_phone_number_enabled = metadata_raw["isPhoneNumberEnabled"]
                    +        """Whether the user's linked phone number is enabled."""
                    +        self.max_user_description_length = metadata_raw["MaxUserDescriptionLength"]
                    +        """Maximum length of the user's description."""
                    +        self.is_user_description_enabled = metadata_raw["isUserDescriptionEnabled"]
                    +        """Whether the user's description is enabled."""
                    +        self.is_user_block_endpoints_updated = metadata_raw["isUserBlockEndpointsUpdated"]
                    +        """Whether the UserBlock endpoints are updated (unsure exactly what this does)"""
                    +

                    Instance variables

                    var is_account_settings_policy_enabled
                    @@ -139,6 +513,26 @@

                    Instance variables

                    Represents account information promotion channels.

                    +
                    + +Expand source code + +
                    class PromotionChannels:
                    +    """
                    +    Represents account information promotion channels.
                    +    """
                    +    def __init__(self, promotion_raw):
                    +        self.promotion_channels_visibility_privacy = promotion_raw["promotionChannelsVisibilityPrivacy"]
                    +        """Visibility of promotion channels."""
                    +        self.facebook = promotion_raw["facebook"]
                    +        """Link to the user's Facebook page."""
                    +        self.twitter = promotion_raw["twitter"]
                    +        """Link to the user's Twitter page."""
                    +        self.youtube = promotion_raw["youtube"]
                    +        """Link to the user's YouTube page."""
                    +        self.twitch = promotion_raw["twitch"]
                    +        """Link to the user's Twitch page."""
                    +

                    Instance variables

                    var facebook
                    @@ -167,6 +561,11 @@

                    Instance variables

                    +
                    + +Expand source code + +
                    class AccountSettings:
                    +    """
                    +    Represents authenticated client account settings (https://accountsettings.roblox.com/)
                    +    This is only available for authenticated clients as it cannot be accessed otherwise.
                    +
                    +    Parameters
                    +    ----------
                    +    requests : ro_py.utilities.requests.Requests
                    +        Requests object to use for API requests.
                    +    """
                    +    def __init__(self, requests):
                    +        self.requests = requests
                    +
                    +    def get_privacy_setting(self, privacy_setting):
                    +        """
                    +        Gets the value of a privacy setting.
                    +        """
                    +        privacy_setting = privacy_setting.value
                    +        privacy_endpoint = [
                    +            "app-chat-privacy",
                    +            "game-chat-privacy",
                    +            "inventory-privacy",
                    +            "privacy",
                    +            "privacy/info",
                    +            "private-message-privacy"
                    +        ][privacy_setting]
                    +        privacy_key = [
                    +            "appChatPrivacy",
                    +            "gameChatPrivacy",
                    +            "inventoryPrivacy",
                    +            "phoneDiscovery",
                    +            "isPhoneDiscoveryEnabled",
                    +            "privateMessagePrivacy"
                    +        ][privacy_setting]
                    +        privacy_endpoint = endpoint + "v1/" + privacy_endpoint
                    +        privacy_req = self.requests.get(privacy_endpoint)
                    +        return privacy_req.json()[privacy_key]
                    +

                    Methods

                    @@ -52,6 +209,35 @@

                    Methods

                    Gets the value of a privacy setting.

                    +
                    + +Expand source code + +
                    def get_privacy_setting(self, privacy_setting):
                    +    """
                    +    Gets the value of a privacy setting.
                    +    """
                    +    privacy_setting = privacy_setting.value
                    +    privacy_endpoint = [
                    +        "app-chat-privacy",
                    +        "game-chat-privacy",
                    +        "inventory-privacy",
                    +        "privacy",
                    +        "privacy/info",
                    +        "private-message-privacy"
                    +    ][privacy_setting]
                    +    privacy_key = [
                    +        "appChatPrivacy",
                    +        "gameChatPrivacy",
                    +        "inventoryPrivacy",
                    +        "phoneDiscovery",
                    +        "isPhoneDiscoveryEnabled",
                    +        "privateMessagePrivacy"
                    +    ][privacy_setting]
                    +    privacy_endpoint = endpoint + "v1/" + privacy_endpoint
                    +    privacy_req = self.requests.get(privacy_endpoint)
                    +    return privacy_req.json()[privacy_key]
                    +
                    @@ -61,6 +247,18 @@

                    Methods

                    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.

                    +
                    + +Expand source code + +
                    class PrivacyLevel(enum.Enum):
                    +    """
                    +    Represents a privacy level as you might see at https://www.roblox.com/my/account#!/privacy.
                    +    """
                    +    no_one = "NoOne"
                    +    friends = "Friends"
                    +    everyone = "AllUsers"
                    +

                    Ancestors

                    • enum.Enum
                    • @@ -87,6 +285,21 @@

                      Class variables

                      Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.

                      +
                      + +Expand source code + +
                      class PrivacySettings(enum.Enum):
                      +    """
                      +    Represents a privacy setting as you might see at https://www.roblox.com/my/account#!/privacy.
                      +    """
                      +    app_chat_privacy = 0
                      +    game_chat_privacy = 1
                      +    inventory_privacy = 2
                      +    phone_discovery = 3
                      +    phone_discovery_enabled = 4
                      +    private_message_privacy = 5
                      +

                      Ancestors

                      • enum.Enum
                      • @@ -130,11 +343,33 @@

                        Parameters

                        email_data : dict
                        Raw data to parse from.
                    +
                    + +Expand source code + +
                    class RobloxEmail:
                    +    """
                    +    Represents an obfuscated version of the email you have set on your account.
                    +
                    +    Parameters
                    +    ----------
                    +    email_data : dict
                    +        Raw data to parse from.
                    +    """
                    +    def __init__(self, email_data: dict):
                    +        self.email_address = email_data["emailAddress"]
                    +        self.verified = email_data["verified"]
                    +
                  async def get_remaining(self) @@ -73,12 +348,71 @@

                  Returns

                  int
                   
                  +
                  + +Expand source code + +
                  async def get_remaining(self):
                  +    """
                  +    Gets the remaining amount of this asset. (used for Limited U items)
                  +
                  +    Returns
                  +    -------
                  +    int
                  +    """
                  +    asset_info_req = await self.requests.get(
                  +        url=endpoint + "marketplace/productinfo",
                  +        params={
                  +            "assetId": self.asset_id
                  +        }
                  +    )
                  +    asset_info = asset_info_req.json()
                  +    return asset_info["Remaining"]
                  +
                async def update(self)

                Updates the asset's information.

                +
                + +Expand source code + +
                async def update(self):
                +    """
                +    Updates the asset's information.
                +    """
                +    asset_info_req = await self.requests.get(
                +        url=endpoint + "marketplace/productinfo",
                +        params={
                +            "assetId": self.id
                +        }
                +    )
                +    asset_info = asset_info_req.json()
                +    self.target_id = asset_info["TargetId"]
                +    self.product_type = asset_info["ProductType"]
                +    self.asset_id = asset_info["AssetId"]
                +    self.product_id = asset_info["ProductId"]
                +    self.name = asset_info["Name"]
                +    self.description = asset_info["Description"]
                +    self.asset_type_id = asset_info["AssetTypeId"]
                +    self.asset_type_name = asset_types[self.asset_type_id]
                +    # if asset_info["Creator"]["CreatorType"] == "User":
                +    #    self.creator = User(self.requests, asset_info["Creator"]["Id"])
                +    # if asset_info["Creator"]["CreatorType"] == "Group":
                +    #    self.creator = Group(self.requests, asset_info["Creator"]["CreatorTargetId"])
                +    self.created = iso8601.parse_date(asset_info["Created"])
                +    self.updated = iso8601.parse_date(asset_info["Updated"])
                +    self.price = asset_info["PriceInRobux"]
                +    self.is_new = asset_info["IsNew"]
                +    self.is_for_sale = asset_info["IsForSale"]
                +    self.is_public_domain = asset_info["IsPublicDomain"]
                +    self.is_limited = asset_info["IsLimited"]
                +    self.is_limited_unique = asset_info["IsLimitedUnique"]
                +    self.minimum_membership_level = asset_info["MinimumMembershipLevel"]
                +    self.content_rating_type_id = asset_info["ContentRatingTypeId"]
                +
              @@ -95,6 +429,15 @@

              Parameters

              asset_id
              ID of the asset.
              +
              + +Expand source code + +
              class UserAsset(Asset):
              +    def __init__(self, requests, asset_id, user_asset_id):
              +        super().__init__(requests, asset_id)
              +        self.user_asset_id = user_asset_id
              +

              Ancestors

              • Asset
              • @@ -114,6 +457,11 @@

                Inherited members

            @@ -62,11 +216,29 @@

            Methods

            Represents a badge's statistics.

            +
            + +Expand source code + +
            class BadgeStatistics:
            +    """
            +    Represents a badge's statistics.
            +    """
            +    def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage):
            +        self.past_date_awarded_count = past_date_awarded_count
            +        self.awarded_count = awarded_count
            +        self.win_rate_percentage = win_rate_percentage
            +
          @@ -74,6 +374,59 @@

          Parameters

          +
          + +Expand source code + +
          class Conversation:
          +    def __init__(self, requests, conversation_id=None, raw=False, raw_data=None):
          +        self.requests = requests
          +        self.raw = raw
          +        self.id = None
          +        self.title = None
          +        self.initiator = None
          +        self.type = None
          +        self.typing = ConversationTyping(self.requests, conversation_id)
          +
          +        if self.raw:
          +            data = raw_data
          +            self.id = data["id"]
          +            self.title = data["title"]
          +            self.initiator = User(self.requests, data["initiator"]["targetId"])
          +            self.type = data["conversationType"]
          +            self.typing = ConversationTyping(self.requests, conversation_id)
          +
          +    async def update(self):
          +        conversation_req = await self.requests.get(
          +            url="https://chat.roblox.com/v2/get-conversations",
          +            params={
          +                "conversationIds": self.id
          +            }
          +        )
          +        data = conversation_req.json()[0]
          +        self.id = data["id"]
          +        self.title = data["title"]
          +        self.initiator = User(self.requests, data["initiator"]["targetId"])
          +        self.type = data["conversationType"]
          +        self.typing = ConversationTyping(self.requests, conversation_id)
          +
          +    async def get_message(self, message_id):
          +        return Message(self.requests, message_id, self.id)
          +
          +    async def send_message(self, content):
          +        send_message_req = await self.requests.post(
          +            url=endpoint + "v2/send-message",
          +            data={
          +                "message": content,
          +                "conversationId": self.id
          +            }
          +        )
          +        send_message_json = send_message_req.json()
          +        if send_message_json["sent"]:
          +            return Message(self.requests, send_message_json["messageId"], self.id)
          +        else:
          +            raise ChatError(send_message_json["statusMessage"])
          +

          Methods

          @@ -81,18 +434,61 @@

          Methods

          +
          + +Expand source code + +
          async def get_message(self, message_id):
          +    return Message(self.requests, message_id, self.id)
          +
          async def send_message(self, content)
          +
          + +Expand source code + +
          async def send_message(self, content):
          +    send_message_req = await self.requests.post(
          +        url=endpoint + "v2/send-message",
          +        data={
          +            "message": content,
          +            "conversationId": self.id
          +        }
          +    )
          +    send_message_json = send_message_req.json()
          +    if send_message_json["sent"]:
          +        return Message(self.requests, send_message_json["messageId"], self.id)
          +    else:
          +        raise ChatError(send_message_json["statusMessage"])
          +
          async def update(self)
          +
          + +Expand source code + +
          async def update(self):
          +    conversation_req = await self.requests.get(
          +        url="https://chat.roblox.com/v2/get-conversations",
          +        params={
          +            "conversationIds": self.id
          +        }
          +    )
          +    data = conversation_req.json()[0]
          +    self.id = data["id"]
          +    self.title = data["title"]
          +    self.initiator = User(self.requests, data["initiator"]["targetId"])
          +    self.type = data["conversationType"]
          +    self.typing = ConversationTyping(self.requests, conversation_id)
          +
          @@ -102,6 +498,33 @@

          Methods

          +
          + +Expand source code + +
          class ConversationTyping:
          +    def __init__(self, requests, conversation_id):
          +        self.requests = requests
          +        self.id = conversation_id
          +
          +    async def __aenter__(self):
          +        await self.requests.post(
          +            url=endpoint + "v2/update-user-typing-status",
          +            data={
          +                "conversationId": self.id,
          +                "isTyping": "true"
          +            }
          +        )
          +
          +    async def __aexit__(self, *args, **kwargs):
          +        await self.requests.post(
          +            url=endpoint + "v2/update-user-typing-status",
          +            data={
          +                "conversationId": self.id,
          +                "isTyping": "false"
          +            }
          +        )
          +
          class Message @@ -118,6 +541,50 @@

          Parameters

          conversation_id
          ID of the conversation that contains the message.
          +
          + +Expand source code + +
          class Message:
          +    """
          +    Represents a single message in a chat conversation.
          +
          +    Parameters
          +    ----------
          +    requests : ro_py.utilities.requests.Requests
          +        Requests object to use for API requests.
          +    message_id
          +        ID of the message.
          +    conversation_id
          +        ID of the conversation that contains the message.
          +    """
          +    def __init__(self, requests, message_id, conversation_id):
          +        self.requests = requests
          +        self.id = message_id
          +        self.conversation_id = conversation_id
          +
          +        self.content = None
          +        self.sender = None
          +        self.read = None
          +
          +    async def update(self):
          +        """
          +        Updates the message with new data.
          +        """
          +        message_req = await self.requests.get(
          +            url="https://chat.roblox.com/v2/get-messages",
          +            params={
          +                "conversationId": self.conversation_id,
          +                "pageSize": 1,
          +                "exclusiveStartMessageId": self.id
          +            }
          +        )
          +
          +        message_json = message_req.json()[0]
          +        self.content = message_json["content"]
          +        self.sender = User(self.requests, message_json["senderTargetId"])
          +        self.read = message_json["read"]
          +

          Methods

          @@ -125,6 +592,28 @@

          Methods

          Updates the message with new data.

          +
          + +Expand source code + +
          async def update(self):
          +    """
          +    Updates the message with new data.
          +    """
          +    message_req = await self.requests.get(
          +        url="https://chat.roblox.com/v2/get-messages",
          +        params={
          +            "conversationId": self.conversation_id,
          +            "pageSize": 1,
          +            "exclusiveStartMessageId": self.id
          +        }
          +    )
          +
          +    message_json = message_req.json()[0]
          +    self.content = message_json["content"]
          +    self.sender = User(self.requests, message_json["senderTargetId"])
          +    self.read = message_json["read"]
          +
          @@ -132,6 +621,11 @@

          Methods

        diff --git a/docs/roles.html b/docs/roles.html index 7ba3996a..2f6a7b92 100644 --- a/docs/roles.html +++ b/docs/roles.html @@ -131,8 +131,9 @@

        Module ro_py.roles

        role_data : dict Dictionary containing role information. """ - def __init__(self, requests, group, role_data): - self.requests = requests + def __init__(self, cso, group, role_data): + self.cso = cso + self.requests = cso.requests self.group = group self.id = role_data['id'] self.name = role_data['name'] @@ -276,7 +277,7 @@

        Classes

        class Role -(requests, group, role_data) +(cso, group, role_data)

        Represents a role

        @@ -306,8 +307,9 @@

        Parameters

        role_data : dict Dictionary containing role information. """ - def __init__(self, requests, group, role_data): - self.requests = requests + def __init__(self, cso, group, role_data): + self.cso = cso + self.requests = cso.requests self.group = group self.id = role_data['id'] self.name = role_data['name'] diff --git a/docs/thumbnails.html b/docs/thumbnails.html index d2256ff2..fa99b3c1 100644 --- a/docs/thumbnails.html +++ b/docs/thumbnails.html @@ -66,10 +66,7 @@

        Module ro_py.thumbnails

        endpoint = "https://thumbnails.roblox.com/" -# TODO: turn these into enums - - -class ReturnPolicy: +class ReturnPolicy(enum.Enum): place_holder = "PlaceHolder" auto_generated = "AutoGenerated" force_auto_generated = "ForceAutoGenerated" @@ -114,6 +111,47 @@

        Module ro_py.thumbnails

        format_jpeg = "Jpeg" +class GameThumbnailGenerator: + def __init__(self, requests, id): + self.requests = requests + self.id = id + + async def get_game_icon(self, size=ThumbnailSize.size_50x50, file_format=ThumbnailFormat.format_png, + is_circular=False): + """ + Gets a game's icon. + + Parameters + ---------- + size : ro_py.thumbnails.ThumbnailSize + The thumbnail size, formatted widthxheight. + file_format : ro_py.thumbnails.ThumbnailFormat + The thumbnail format + is_circular : bool + The circle thumbnail output parameter. + + Returns + ------- + Image URL + """ + + file_format = file_format.value + size = size.value + + game_icon_req = await self.requests.get( + url=endpoint + "v1/games/icons", + params={ + "universeIds": str(self.id), + "returnPolicy": ReturnPolicy.place_holder.value, + "size": size, + "format": file_format, + "isCircular": is_circular + } + ) + game_icon = game_icon_req.json()["data"][0]["imageUrl"] + return game_icon + + class UserThumbnailGenerator: def __init__(self, requests, id): self.requests = requests @@ -239,20 +277,138 @@

        Module ro_py.thumbnails

        Classes

        +
        +class GameThumbnailGenerator +(requests, id) +
        +
        +
        +
        + +Expand source code + +
        class GameThumbnailGenerator:
        +    def __init__(self, requests, id):
        +        self.requests = requests
        +        self.id = id
        +
        +    async def get_game_icon(self, size=ThumbnailSize.size_50x50, file_format=ThumbnailFormat.format_png,
        +                            is_circular=False):
        +        """
        +        Gets a game's icon.
        +
        +        Parameters
        +        ----------
        +        size : ro_py.thumbnails.ThumbnailSize
        +            The thumbnail size, formatted widthxheight.
        +        file_format : ro_py.thumbnails.ThumbnailFormat
        +            The thumbnail format
        +        is_circular : bool
        +            The circle thumbnail output parameter.
        +
        +        Returns
        +        -------
        +        Image URL
        +        """
        +
        +        file_format = file_format.value
        +        size = size.value
        +
        +        game_icon_req = await self.requests.get(
        +            url=endpoint + "v1/games/icons",
        +            params={
        +                "universeIds": str(self.id),
        +                "returnPolicy": ReturnPolicy.place_holder.value,
        +                "size": size,
        +                "format": file_format,
        +                "isCircular": is_circular
        +            }
        +        )
        +        game_icon = game_icon_req.json()["data"][0]["imageUrl"]
        +        return game_icon
        +
        +

        Methods

        +
        +
        +async def get_game_icon(self, size=ThumbnailSize.size_50x50, file_format=ThumbnailFormat.format_png, is_circular=False) +
        +
        +

        Gets a game's icon.

        +

        Parameters

        +
        +
        size : ThumbnailSize
        +
        The thumbnail size, formatted widthxheight.
        +
        file_format : ThumbnailFormat
        +
        The thumbnail format
        +
        is_circular : bool
        +
        The circle thumbnail output parameter.
        +
        +

        Returns

        +
        +
        Image URL
        +
         
        +
        +
        + +Expand source code + +
        async def get_game_icon(self, size=ThumbnailSize.size_50x50, file_format=ThumbnailFormat.format_png,
        +                        is_circular=False):
        +    """
        +    Gets a game's icon.
        +
        +    Parameters
        +    ----------
        +    size : ro_py.thumbnails.ThumbnailSize
        +        The thumbnail size, formatted widthxheight.
        +    file_format : ro_py.thumbnails.ThumbnailFormat
        +        The thumbnail format
        +    is_circular : bool
        +        The circle thumbnail output parameter.
        +
        +    Returns
        +    -------
        +    Image URL
        +    """
        +
        +    file_format = file_format.value
        +    size = size.value
        +
        +    game_icon_req = await self.requests.get(
        +        url=endpoint + "v1/games/icons",
        +        params={
        +            "universeIds": str(self.id),
        +            "returnPolicy": ReturnPolicy.place_holder.value,
        +            "size": size,
        +            "format": file_format,
        +            "isCircular": is_circular
        +        }
        +    )
        +    game_icon = game_icon_req.json()["data"][0]["imageUrl"]
        +    return game_icon
        +
        +
        +
        +
        class ReturnPolicy +(value, names=None, *, module=None, qualname=None, type=None, start=1)
        -
        +

        An enumeration.

        Expand source code -
        class ReturnPolicy:
        +
        class ReturnPolicy(enum.Enum):
             place_holder = "PlaceHolder"
             auto_generated = "AutoGenerated"
             force_auto_generated = "ForceAutoGenerated"
        +

        Ancestors

        +
          +
        • enum.Enum
        • +

        Class variables

        var auto_generated
        @@ -636,6 +792,12 @@

        Index

      • Classes

        • +

          GameThumbnailGenerator

          + +
        • +
        • ReturnPolicy

          • auto_generated
          • diff --git a/docs/trades.html b/docs/trades.html index d7a64794..093d0346 100644 --- a/docs/trades.html +++ b/docs/trades.html @@ -83,8 +83,8 @@

            Module ro_py.trades

            self.sender = sender self.recieve_items = recieve_items self.send_items = send_items - self.created = iso8601.parse(created) - self.experation = iso8601.parse(expiration) + self.created = iso8601.parse_date(created) + self.experation = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: @@ -504,8 +504,8 @@

            Methods

            self.sender = sender self.recieve_items = recieve_items self.send_items = send_items - self.created = iso8601.parse(created) - self.experation = iso8601.parse(expiration) + self.created = iso8601.parse_date(created) + self.experation = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: diff --git a/docs/users.html b/docs/users.html index 273ac327..210385ba 100644 --- a/docs/users.html +++ b/docs/users.html @@ -90,8 +90,9 @@

            Module ro_py.users

            name : str The name of the user. """ - def __init__(self, requests, roblox_id, name=None): - self.requests = requests + def __init__(self, cso, roblox_id, name=None): + self.cso = cso + self.requests = cso.requests self.id = roblox_id self.description = None self.created = None @@ -172,7 +173,7 @@

            Module ro_py.users

            friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.requests, friend_raw["id"]) + User(self.cso, friend_raw["id"]) ) return friends_list @@ -185,7 +186,7 @@

            Module ro_py.users

            groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) return groups async def get_limiteds(self): @@ -278,7 +279,7 @@

            Inherited members

      • class User -(requests, roblox_id, name=None) +(cso, roblox_id, name=None)

        Represents a Roblox user and their profile. @@ -310,8 +311,9 @@

        Parameters

        name : str The name of the user. """ - def __init__(self, requests, roblox_id, name=None): - self.requests = requests + def __init__(self, cso, roblox_id, name=None): + self.cso = cso + self.requests = cso.requests self.id = roblox_id self.description = None self.created = None @@ -392,7 +394,7 @@

        Parameters

        friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.requests, friend_raw["id"]) + User(self.cso, friend_raw["id"]) ) return friends_list @@ -405,7 +407,7 @@

        Parameters

        groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) return groups async def get_limiteds(self): @@ -489,7 +491,7 @@

        Methods

        friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.requests, friend_raw["id"]) + User(self.cso, friend_raw["id"]) ) return friends_list
        @@ -532,7 +534,7 @@

        Methods

        groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.requests, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) return groups
        diff --git a/docs/utilities/errors.html b/docs/utilities/errors.html index 1f24c1f3..df08866c 100644 --- a/docs/utilities/errors.html +++ b/docs/utilities/errors.html @@ -64,6 +64,53 @@

        Module ro_py.utilities.errors

        """ +# The following are HTTP generic errors used by requests.py +class ApiError(Exception): + """Called in requests when an API request fails with an error code that doesn't have an independent error.""" + pass + + +class BadRequest(Exception): + """400 HTTP error""" + pass + + +class Unauthorized(Exception): + """401 HTTP error""" + pass + + +class Forbidden(Exception): + """403 HTTP error""" + pass + + +class NotFound(Exception): + """404 HTTP error (also used for other things)""" + pass + + +class Conflict(Exception): + """409 HTTP error""" + pass + + +class TooManyRequests(Exception): + """429 HTTP error""" + pass + + +class InternalServerError(Exception): + """500 HTTP error""" + pass + + +class BadGateway(Exception): + """502 HTTP error""" + pass + + +# The following errors are specific to certain parts of ro.py class NotLimitedError(Exception): """Called when code attempts to read limited-only information.""" pass @@ -79,11 +126,6 @@

        Module ro_py.utilities.errors

        pass -class ApiError(Exception): - """Called in requests when an API request fails.""" - pass - - class ChatError(Exception): """Called in chat when a chat action fails.""" @@ -92,10 +134,6 @@

        Module ro_py.utilities.errors

        """Called when an invalid page is requested.""" -class NotFound(Exception): - """Called when something is not found.""" - - class UserDoesNotExistError(Exception): """Called when a user does not exist.""" @@ -120,7 +158,19 @@

        Module ro_py.utilities.errors

        class NoAvailableWorkersError(Exception): """Raised when there are no available workers.""" - pass + pass + + +c_errors = { + "400": BadRequest, + "401": Unauthorized, + "403": Forbidden, + "404": NotFound, + "409": Conflict, + "429": TooManyRequests, + "500": InternalServerError, + "502": BadGateway +}
        @@ -137,13 +187,53 @@

        Classes

        (*args, **kwargs)
        -

        Called in requests when an API request fails.

        +

        Called in requests when an API request fails with an error code that doesn't have an independent error.

        Expand source code
        class ApiError(Exception):
        -    """Called in requests when an API request fails."""
        +    """Called in requests when an API request fails with an error code that doesn't have an independent error."""
        +    pass
        +
        +

        Ancestors

        +
          +
        • builtins.Exception
        • +
        • builtins.BaseException
        • +
        +
        +
        +class BadGateway +(*args, **kwargs) +
        +
        +

        502 HTTP error

        +
        + +Expand source code + +
        class BadGateway(Exception):
        +    """502 HTTP error"""
        +    pass
        +
        +

        Ancestors

        +
          +
        • builtins.Exception
        • +
        • builtins.BaseException
        • +
        +
        +
        +class BadRequest +(*args, **kwargs) +
        +
        +

        400 HTTP error

        +
        + +Expand source code + +
        class BadRequest(Exception):
        +    """400 HTTP error"""
             pass

        Ancestors

        @@ -171,6 +261,46 @@

        Ancestors

      • builtins.BaseException
      +
      +class Conflict +(*args, **kwargs) +
      +
      +

      409 HTTP error

      +
      + +Expand source code + +
      class Conflict(Exception):
      +    """409 HTTP error"""
      +    pass
      +
      +

      Ancestors

      +
        +
      • builtins.Exception
      • +
      • builtins.BaseException
      • +
      +
      +
      +class Forbidden +(*args, **kwargs) +
      +
      +

      403 HTTP error

      +
      + +Expand source code + +
      class Forbidden(Exception):
      +    """403 HTTP error"""
      +    pass
      +
      +

      Ancestors

      +
        +
      • builtins.Exception
      • +
      • builtins.BaseException
      • +
      +
      class GameJoinError (*args, **kwargs) @@ -230,6 +360,26 @@

      Ancestors

    • builtins.BaseException
    +
    +class InternalServerError +(*args, **kwargs) +
    +
    +

    500 HTTP error

    +
    + +Expand source code + +
    class InternalServerError(Exception):
    +    """500 HTTP error"""
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    class InvalidIconSizeError (*args, **kwargs) @@ -333,13 +483,14 @@

    Ancestors

    (*args, **kwargs)
    -

    Called when something is not found.

    +

    404 HTTP error (also used for other things)

    Expand source code
    class NotFound(Exception):
    -    """Called when something is not found."""
    + """404 HTTP error (also used for other things)""" + pass

    Ancestors

      @@ -367,6 +518,46 @@

      Ancestors

    • builtins.BaseException
    +
    +class TooManyRequests +(*args, **kwargs) +
    +
    +

    429 HTTP error

    +
    + +Expand source code + +
    class TooManyRequests(Exception):
    +    """429 HTTP error"""
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    +
    +class Unauthorized +(*args, **kwargs) +
    +
    +

    401 HTTP error

    +
    + +Expand source code + +
    class Unauthorized(Exception):
    +    """401 HTTP error"""
    +    pass
    +
    +

    Ancestors

    +
      +
    • builtins.Exception
    • +
    • builtins.BaseException
    • +
    +
    class UserDoesNotExistError (*args, **kwargs) @@ -411,9 +602,21 @@

    Index

    ApiError

  • +

    BadGateway

    +
  • +
  • +

    BadRequest

    +
  • +
  • ChatError

  • +

    Conflict

    +
  • +
  • +

    Forbidden

    +
  • +
  • GameJoinError

  • @@ -423,6 +626,9 @@

    InsufficientCreditError

  • +

    InternalServerError

    +
  • +
  • InvalidIconSizeError

  • @@ -444,6 +650,12 @@

    NotLimitedError

  • +

    TooManyRequests

    +
  • +
  • +

    Unauthorized

    +
  • +
  • UserDoesNotExistError

  • diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index 77658e8c..50a473bd 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -91,7 +91,7 @@

    Module ro_py.utilities.pages

    Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required. """ - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): + def __init__(self, cso, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): if extra_parameters is None: extra_parameters = {} @@ -103,15 +103,15 @@

    Module ro_py.utilities.pages

    self.parameters = extra_parameters """Extra parameters for the request.""" - self.requests = requests + self.cso = cso + self.requests = cso.requests """Requests object.""" self.url = url """URL containing the paginated data, accessible with a GET request.""" self.page = 0 """Current page number.""" self.handler_args = handler_args - - # self.data = self._get_page() + self.data = None async def get_page(self, cursor=None): """ @@ -125,19 +125,19 @@

    Module ro_py.utilities.pages

    url=self.url, params=this_parameters ) - return Page( - requests=self.requests, + self.data = Page( + requests=self.cso, data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ) + ).data async def previous(self): """ Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = await self._get_page(self.data.previous_page_cursor) + await self.get_page(self.data.previous_page_cursor) else: raise InvalidPageError @@ -146,7 +146,7 @@

    Module ro_py.utilities.pages

    Moves to the next page. """ if self.data.next_page_cursor: - self.data = await self._get_page(self.data.next_page_cursor) + await self.get_page(self.data.next_page_cursor) else: raise InvalidPageError
    @@ -204,7 +204,7 @@

    Instance variables

    class Pages -(requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None) +(cso, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None)

    Represents a paged object.

    @@ -227,7 +227,7 @@

    Instance variables

    Automatic page caching will be added in the future. It is suggested to cache the pages yourself if speed is required. """ - def __init__(self, requests, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): + def __init__(self, cso, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=None): if extra_parameters is None: extra_parameters = {} @@ -239,15 +239,15 @@

    Instance variables

    self.parameters = extra_parameters """Extra parameters for the request.""" - self.requests = requests + self.cso = cso + self.requests = cso.requests """Requests object.""" self.url = url """URL containing the paginated data, accessible with a GET request.""" self.page = 0 """Current page number.""" self.handler_args = handler_args - - # self.data = self._get_page() + self.data = None async def get_page(self, cursor=None): """ @@ -261,19 +261,19 @@

    Instance variables

    url=self.url, params=this_parameters ) - return Page( - requests=self.requests, + self.data = Page( + requests=self.cso, data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ) + ).data async def previous(self): """ Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = await self._get_page(self.data.previous_page_cursor) + await self.get_page(self.data.previous_page_cursor) else: raise InvalidPageError @@ -282,7 +282,7 @@

    Instance variables

    Moves to the next page. """ if self.data.next_page_cursor: - self.data = await self._get_page(self.data.next_page_cursor) + await self.get_page(self.data.next_page_cursor) else: raise InvalidPageError @@ -332,12 +332,12 @@

    Methods

    url=self.url, params=this_parameters ) - return Page( - requests=self.requests, + self.data = Page( + requests=self.cso, data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ) + ).data
    @@ -354,7 +354,7 @@

    Methods

    Moves to the next page. """ if self.data.next_page_cursor: - self.data = await self._get_page(self.data.next_page_cursor) + await self.get_page(self.data.next_page_cursor) else: raise InvalidPageError
    @@ -373,7 +373,7 @@

    Methods

    Moves to the previous page. """ if self.data.previous_page_cursor: - self.data = await self._get_page(self.data.previous_page_cursor) + await self.get_page(self.data.previous_page_cursor) else: raise InvalidPageError diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index f3b75832..89649ee3 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -53,12 +53,32 @@

    Module ro_py.utilities.requests

    Expand source code -
    from ro_py.utilities.errors import ApiError
    -from ro_py.utilities.cache import Cache
    +
    from ro_py.utilities.errors import ApiError, c_errors
     from ro_py.captcha import CaptchaMetadata
     from json.decoder import JSONDecodeError
    -import requests_async
     import requests
    +import httpx
    +
    +
    +class AsyncSession(httpx.AsyncClient):
    +    """
    +    This serves no purpose other than to get around an annoying HTTPX warning.
    +    """
    +    def __init__(self):
    +        super().__init__()
    +
    +    def __del__(self):
    +        pass
    +
    +
    +def status_code_error(status_code):
    +    """
    +    Converts a status code to the proper exception.
    +    """
    +    if str(status_code) in c_errors:
    +        return c_errors[str(status_code)]
    +    else:
    +        return ApiError
     
     
     class Requests:
    @@ -66,10 +86,8 @@ 

    Module ro_py.utilities.requests

    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox. """ def __init__(self): - self.session = requests_async.Session() + self.session = AsyncSession() """Session to use for requests.""" - self.cache = Cache() - """Cache object to use for object storage.""" """ Thank you @nsg for letting me know about this! @@ -88,6 +106,10 @@

    Module ro_py.utilities.requests

    get_request = await self.session.get(*args, **kwargs) + if kwargs.pop("stream", False): + # Skip request checking and just get on with it. + return get_request + try: get_request_json = get_request.json() except JSONDecodeError: @@ -104,7 +126,7 @@

    Module ro_py.utilities.requests

    if quickreturn: return get_request - raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) @@ -151,7 +173,7 @@

    Module ro_py.utilities.requests

    if quickreturn: return post_request - raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}") async def patch(self, *args, **kwargs): """ @@ -175,7 +197,7 @@

    Module ro_py.utilities.requests

    else: return patch_request - raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}") async def delete(self, *args, **kwargs): """ @@ -199,7 +221,7 @@

    Module ro_py.utilities.requests

    else: return delete_request - raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") + raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}") async def get_captcha_metadata(self): captcha_meta_req = await self.get( @@ -214,10 +236,57 @@

    Module ro_py.utilities.requests

    +

    Functions

    +
    +
    +def status_code_error(status_code) +
    +
    +

    Converts a status code to the proper exception.

    +
    + +Expand source code + +
    def status_code_error(status_code):
    +    """
    +    Converts a status code to the proper exception.
    +    """
    +    if str(status_code) in c_errors:
    +        return c_errors[str(status_code)]
    +    else:
    +        return ApiError
    +
    +
    +

    Classes

    +
    +class AsyncSession +
    +
    +

    This serves no purpose other than to get around an annoying HTTPX warning.

    +
    + +Expand source code + +
    class AsyncSession(httpx.AsyncClient):
    +    """
    +    This serves no purpose other than to get around an annoying HTTPX warning.
    +    """
    +    def __init__(self):
    +        super().__init__()
    +
    +    def __del__(self):
    +        pass
    +
    +

    Ancestors

    +
      +
    • httpx.AsyncClient
    • +
    • httpx._client.BaseClient
    • +
    +
    class Requests
    @@ -232,10 +301,8 @@

    Classes

    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox. """ def __init__(self): - self.session = requests_async.Session() + self.session = AsyncSession() """Session to use for requests.""" - self.cache = Cache() - """Cache object to use for object storage.""" """ Thank you @nsg for letting me know about this! @@ -254,6 +321,10 @@

    Classes

    get_request = await self.session.get(*args, **kwargs) + if kwargs.pop("stream", False): + # Skip request checking and just get on with it. + return get_request + try: get_request_json = get_request.json() except JSONDecodeError: @@ -270,7 +341,7 @@

    Classes

    if quickreturn: return get_request - raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}") + raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) @@ -317,7 +388,7 @@

    Classes

    if quickreturn: return post_request - raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}") + raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}") async def patch(self, *args, **kwargs): """ @@ -341,7 +412,7 @@

    Classes

    else: return patch_request - raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}") + raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}") async def delete(self, *args, **kwargs): """ @@ -365,7 +436,7 @@

    Classes

    else: return delete_request - raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}") + raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}") async def get_captcha_metadata(self): captcha_meta_req = await self.get( @@ -376,10 +447,6 @@

    Classes

    Instance variables

    -
    var cache
    -
    -

    Cache object to use for object storage.

    -
    var session

    Session to use for requests.

    @@ -441,7 +508,7 @@

    Methods

    else: return delete_request - raise ApiError(f"[{str(delete_request.status_code)}] {delete_request_error[0]['message']}")
    + raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}")
    @@ -462,6 +529,10 @@

    Methods

    get_request = await self.session.get(*args, **kwargs) + if kwargs.pop("stream", False): + # Skip request checking and just get on with it. + return get_request + try: get_request_json = get_request.json() except JSONDecodeError: @@ -478,7 +549,7 @@

    Methods

    if quickreturn: return get_request - raise ApiError(f"[{str(get_request.status_code)}] {get_request_error[0]['message']}")
    + raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}")
    @@ -529,7 +600,7 @@

    Methods

    else: return patch_request - raise ApiError(f"[{str(patch_request.status_code)}] {patch_request_error[0]['message']}")
    + raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}")
    @@ -573,7 +644,7 @@

    Methods

    if quickreturn: return post_request - raise ApiError(f"[{str(post_request.status_code)}] {post_request_error[0]['message']}")
    + raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}") @@ -597,13 +668,20 @@

    Index

  • ro_py.utilities
  • +
  • Functions

    + +
  • Classes

    • +

      AsyncSession

      +
    • +
    • Requests

      • back_post
      • -
      • cache
      • delete
      • get
      • get_captcha_metadata
      • diff --git a/docs/wall.html b/docs/wall.html new file mode 100644 index 00000000..35563f21 --- /dev/null +++ b/docs/wall.html @@ -0,0 +1,354 @@ + + + + + + +ro_py.wall API documentation + + + + + + + + + + + + +
        +
        +
        +

        Module ro_py.wall

        +
        +
        +
        + +Expand source code + +
        import iso8601
        +from typing import List
        +from ro_py.captcha import UnsolvedCaptcha
        +from ro_py.utilities.pages import Pages, SortOrder
        +
        +
        +class WallPost:
        +    """
        +    Represents a roblox wall post.
        +    """
        +    def __init__(self, cso, wall_data, group):
        +        self.requests = cso.requests
        +        self.group = group
        +        self.id = wall_data['id']
        +        self.body = wall_data['body']
        +        self.created = iso8601.parse_date(wall_data['created'])
        +        self.updated = iso8601.parse_date(wall_data['updated'])
        +        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
        +
        +    async def delete(self):
        +        wall_req = await self.requests.delete(
        +            url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
        +        )
        +        return wall_req.status == 200
        +
        +
        +def wall_post_handler(requests, this_page, args) -> List[WallPost]:
        +    wall_posts = []
        +    for wall_post in this_page:
        +        wall_posts.append(WallPost(requests, wall_post, args))
        +    return wall_posts
        +
        +
        +class Wall:
        +    def __init__(self, cso, group):
        +        self.cso = cso
        +        self.requests = cso.requests
        +        self.group = group
        +
        +    async def get_posts(self, sort_order=SortOrder.Ascending, limit=100):
        +        wall_req = Pages(
        +            requests=self.cso,
        +            url=endpoint + f"/v2/groups/{self.group.id}/wall/posts",
        +            sort_order=sort_order,
        +            limit=limit,
        +            handler=wall_post_handler,
        +            handler_args=self.group
        +        )
        +        return wall_req
        +
        +    async def post(self, content, captcha_key=None):
        +        data = {
        +            "body": content
        +        }
        +
        +        if captcha_key:
        +            data['captchaProvider'] = "PROVIDER_ARKOSE_LABS"
        +            data['captchaToken'] = captcha_key
        +
        +        post_req = await self.requests.post(
        +            url=endpoint + f"/v1/groups/2695946/wall/posts",
        +            data=data,
        +            quickreturn=True
        +        )
        +
        +        if post_req.status_code == 403:
        +            return UnsolvedCaptcha(pkey="63E4117F-E727-42B4-6DAA-C8448E9B137F")
        +        else:
        +            return post_req.status_code == 200
        +
        +
        +
        +
        +
        +
        +
        +

        Functions

        +
        +
        +def wall_post_handler(requests, this_page, args) ‑> List[WallPost] +
        +
        +
        +
        + +Expand source code + +
        def wall_post_handler(requests, this_page, args) -> List[WallPost]:
        +    wall_posts = []
        +    for wall_post in this_page:
        +        wall_posts.append(WallPost(requests, wall_post, args))
        +    return wall_posts
        +
        +
        +
        +
        +
        +

        Classes

        +
        +
        +class Wall +(cso, group) +
        +
        +
        +
        + +Expand source code + +
        class Wall:
        +    def __init__(self, cso, group):
        +        self.cso = cso
        +        self.requests = cso.requests
        +        self.group = group
        +
        +    async def get_posts(self, sort_order=SortOrder.Ascending, limit=100):
        +        wall_req = Pages(
        +            requests=self.cso,
        +            url=endpoint + f"/v2/groups/{self.group.id}/wall/posts",
        +            sort_order=sort_order,
        +            limit=limit,
        +            handler=wall_post_handler,
        +            handler_args=self.group
        +        )
        +        return wall_req
        +
        +    async def post(self, content, captcha_key=None):
        +        data = {
        +            "body": content
        +        }
        +
        +        if captcha_key:
        +            data['captchaProvider'] = "PROVIDER_ARKOSE_LABS"
        +            data['captchaToken'] = captcha_key
        +
        +        post_req = await self.requests.post(
        +            url=endpoint + f"/v1/groups/2695946/wall/posts",
        +            data=data,
        +            quickreturn=True
        +        )
        +
        +        if post_req.status_code == 403:
        +            return UnsolvedCaptcha(pkey="63E4117F-E727-42B4-6DAA-C8448E9B137F")
        +        else:
        +            return post_req.status_code == 200
        +
        +

        Methods

        +
        +
        +async def get_posts(self, sort_order=SortOrder.Ascending, limit=100) +
        +
        +
        +
        + +Expand source code + +
        async def get_posts(self, sort_order=SortOrder.Ascending, limit=100):
        +    wall_req = Pages(
        +        requests=self.cso,
        +        url=endpoint + f"/v2/groups/{self.group.id}/wall/posts",
        +        sort_order=sort_order,
        +        limit=limit,
        +        handler=wall_post_handler,
        +        handler_args=self.group
        +    )
        +    return wall_req
        +
        +
        +
        +async def post(self, content, captcha_key=None) +
        +
        +
        +
        + +Expand source code + +
        async def post(self, content, captcha_key=None):
        +    data = {
        +        "body": content
        +    }
        +
        +    if captcha_key:
        +        data['captchaProvider'] = "PROVIDER_ARKOSE_LABS"
        +        data['captchaToken'] = captcha_key
        +
        +    post_req = await self.requests.post(
        +        url=endpoint + f"/v1/groups/2695946/wall/posts",
        +        data=data,
        +        quickreturn=True
        +    )
        +
        +    if post_req.status_code == 403:
        +        return UnsolvedCaptcha(pkey="63E4117F-E727-42B4-6DAA-C8448E9B137F")
        +    else:
        +        return post_req.status_code == 200
        +
        +
        +
        +
        +
        +class WallPost +(cso, wall_data, group) +
        +
        +

        Represents a roblox wall post.

        +
        + +Expand source code + +
        class WallPost:
        +    """
        +    Represents a roblox wall post.
        +    """
        +    def __init__(self, cso, wall_data, group):
        +        self.requests = cso.requests
        +        self.group = group
        +        self.id = wall_data['id']
        +        self.body = wall_data['body']
        +        self.created = iso8601.parse_date(wall_data['created'])
        +        self.updated = iso8601.parse_date(wall_data['updated'])
        +        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
        +
        +    async def delete(self):
        +        wall_req = await self.requests.delete(
        +            url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
        +        )
        +        return wall_req.status == 200
        +
        +

        Methods

        +
        +
        +async def delete(self) +
        +
        +
        +
        + +Expand source code + +
        async def delete(self):
        +    wall_req = await self.requests.delete(
        +        url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}"
        +    )
        +    return wall_req.status == 200
        +
        +
        +
        +
        +
        +
        +
        + +
        + + + \ No newline at end of file diff --git a/setup.py b/setup.py index d4942f66..67320518 100644 --- a/setup.py +++ b/setup.py @@ -20,9 +20,9 @@ ], python_requires='>=3.6', install_requires=[ + "httpx", "iso8601", "signalrcore", - "requests-async", "pytweening", "wxPython", "wxasync" From 9b5ea9feee43d7e1ffa6646e444a4a26a2145b85 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 19 Jan 2021 17:14:07 -0500 Subject: [PATCH 317/518] Updated version identifier :fireworks: --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 67320518..60c511fa 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="ro-py", - version="1.0.7", + version="1.1.0", author="jmkdev and iranathan", author_email="jmk@jmksite.dev", description="ro.py is a Python wrapper for the Roblox web API.", From 8a9bb9f8f6008ca3270f7d48f273437c29046434 Mon Sep 17 00:00:00 2001 From: iranathan Date: Wed, 20 Jan 2021 09:16:28 +0100 Subject: [PATCH 318/518] fix rank functions --- ro_py/groups.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 498210c8..02968bee 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -90,8 +90,7 @@ async def update(self): self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] if "shout" in group_info: - if group_info["shout"]: - self.shout = Shout(self.cso, group_info["shout"]) + self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None # self.is_locked = group_info["isLocked"] @@ -225,7 +224,7 @@ async def update_role(self): data = member_req.json() for role in data['data']: if role['group']['id'] == self.group.id: - self.role = Role(self.requests, self.group, role['role']) + self.role = Role(self.cso, self.group, role['role']) break return self.role From 278ad06afb2949bde69ad7facfdaa231f0dda6b0 Mon Sep 17 00:00:00 2001 From: iranathan Date: Wed, 20 Jan 2021 10:33:07 +0100 Subject: [PATCH 319/518] promote oldrole, newrole & on asset change --- examples/on_asset_change.py | 15 +++++++++++++++ examples/on_shout_event.py | 4 ++-- ro_py/assets.py | 23 ++++++++++++++++++----- ro_py/events.py | 1 + ro_py/groups.py | 13 ++++++++----- ro_py/wall.py | 3 ++- 6 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 examples/on_asset_change.py diff --git a/examples/on_asset_change.py b/examples/on_asset_change.py new file mode 100644 index 00000000..6dfb431c --- /dev/null +++ b/examples/on_asset_change.py @@ -0,0 +1,15 @@ +from ro_py.client import Client +import asyncio +client = Client() + + +async def on_asset_change(old, new): + if old.price != new.price: + print('new price ', new.price) + + +async def main(): + asset = await client.get_asset(3897171912) + await asset.events.bind(on_asset_change, client.events.on_asset_change) + +asyncio.run(main()) diff --git a/examples/on_shout_event.py b/examples/on_shout_event.py index d517511f..9ccdda96 100644 --- a/examples/on_shout_event.py +++ b/examples/on_shout_event.py @@ -3,8 +3,8 @@ client = Client() -async def on_shout(shout): - print(shout) +async def on_shout(old_shout, new_shout): + print(old_shout, new_shout) async def main(): diff --git a/ro_py/assets.py b/ro_py/assets.py index 46a2f00c..ecc45ce0 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -7,6 +7,8 @@ from ro_py.economy import LimitedResaleData from ro_py.utilities.asset_type import asset_types import iso8601 +import asyncio +import copy endpoint = "https://api.roblox.com/" @@ -27,6 +29,7 @@ def __init__(self, cso, asset_id): self.id = asset_id self.cso = cso self.requests = cso.requests + self.events = Events(cso, self) self.target_id = None self.product_type = None self.asset_id = None @@ -121,11 +124,21 @@ def __init__(self, requests, asset_id, user_asset_id): class Events: - def __init__(self, cso): + def __init__(self, cso, asset): self.cso = cso + self.asset = asset async def bind(self, func, event, delay=15): - pass - - - + if event == self.cso.client.events.on_asset_change: + await asyncio.create_task(self.on_asset_change(func, delay)) + + async def on_asset_change(self, func, delay): + await self.asset.update() + old_asset = copy.copy(self.asset) + while True: + await asyncio.sleep(delay) + await self.asset.update() + for attr, value in self.asset.__dict__.items(): + if getattr(old_asset, attr) != value: + await func(old_asset, self.asset) + old_asset = self.asset diff --git a/ro_py/events.py b/ro_py/events.py index 1af7de86..1302fd58 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -5,3 +5,4 @@ class EventTypes(enum.Enum): on_join_request = "on_join_request" on_wall_post = "on_wall_post" on_shout_update = "on_shout_update" + on_asset_change = "on_asset_change" diff --git a/ro_py/groups.py b/ro_py/groups.py index 02968bee..dd57375e 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -5,9 +5,9 @@ """ import iso8601 import asyncio -from typing import List from ro_py.wall import Wall from ro_py.roles import Role +from typing import List, Tuple from ro_py.captcha import UnsolvedCaptcha from ro_py.users import User, PartialUser from ro_py.utilities.errors import NotFound @@ -89,7 +89,7 @@ async def update(self): self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] - if "shout" in group_info: + if group_info.get('shout'): self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None @@ -228,7 +228,7 @@ async def update_role(self): break return self.role - async def change_rank(self, num): + async def change_rank(self, num) -> Tuple[Role, Role]: """ Changes the users rank specified by a number. If num is 1 the users role will go up by 1. @@ -241,6 +241,7 @@ async def change_rank(self, num): """ await self.update_role() roles = await self.group.get_roles() + old_role = self.role role_counter = -1 for group_role in roles: role_counter += 1 @@ -248,7 +249,9 @@ async def change_rank(self, num): break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") - return await self.setrank(roles[role_counter + num].id) + await self.setrank(roles[role_counter + num].id) + self.role = roles[role_counter + num].id + return old_role, roles[role_counter + num].id async def promote(self): """ @@ -385,5 +388,5 @@ async def on_shout_update(self, func, delay): await asyncio.sleep(delay) await self.group.update() if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body: + await func(current_shout, self.group.shout) current_shout = self.group.shout - await func(self.group.shout) diff --git a/ro_py/wall.py b/ro_py/wall.py index ab2028b9..872fe618 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -46,6 +46,7 @@ async def get_posts(self, sort_order=SortOrder.Ascending, limit=100): handler=wall_post_handler, handler_args=self.group ) + await wall_req.get_page() return wall_req async def post(self, content, captcha_key=None): @@ -66,4 +67,4 @@ async def post(self, content, captcha_key=None): if post_req.status_code == 403: return UnsolvedCaptcha(pkey="63E4117F-E727-42B4-6DAA-C8448E9B137F") else: - return post_req.status_code == 200 \ No newline at end of file + return post_req.status_code == 200 From 89cdc4cbcd8d027c8534288d3c9c0ed691c6fa1e Mon Sep 17 00:00:00 2001 From: iranathan Date: Wed, 20 Jan 2021 10:43:37 +0100 Subject: [PATCH 320/518] promote fix --- ro_py/groups.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index dd57375e..1ba569a4 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -3,6 +3,7 @@ This file houses functions and classes that pertain to Roblox groups. """ +import copy import iso8601 import asyncio from ro_py.wall import Wall @@ -241,7 +242,7 @@ async def change_rank(self, num) -> Tuple[Role, Role]: """ await self.update_role() roles = await self.group.get_roles() - old_role = self.role + old_role = copy.copy(self.role) role_counter = -1 for group_role in roles: role_counter += 1 @@ -251,7 +252,7 @@ async def change_rank(self, num) -> Tuple[Role, Role]: raise NotFound(f"User {self.id} is not in group {self.group.id}") await self.setrank(roles[role_counter + num].id) self.role = roles[role_counter + num].id - return old_role, roles[role_counter + num].id + return old_role, roles[role_counter + num] async def promote(self): """ From 8d62996958556a1b31c5a3177fd4fab0cc0529df Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 09:53:49 -0500 Subject: [PATCH 321/518] moved resources + shorter client --- {ro_py/extensions => resources}/appicon.png | Bin {ro_py/extensions => resources}/appicon_large.png | Bin ro_py/__init__.py | 2 ++ 3 files changed, 2 insertions(+) rename {ro_py/extensions => resources}/appicon.png (100%) rename {ro_py/extensions => resources}/appicon_large.png (100%) diff --git a/ro_py/extensions/appicon.png b/resources/appicon.png similarity index 100% rename from ro_py/extensions/appicon.png rename to resources/appicon.png diff --git a/ro_py/extensions/appicon_large.png b/resources/appicon_large.png similarity index 100% rename from ro_py/extensions/appicon_large.png rename to resources/appicon_large.png diff --git a/ro_py/__init__.py b/ro_py/__init__.py index d9b9f946..4c5eb415 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -16,3 +16,5 @@

        """ + +from ro_py.client import Client From 206fa2585ed7f929d7e3f7892a30d9f291730c5f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 11:02:55 -0500 Subject: [PATCH 322/518] Fixed wall.py --- ro_py/wall.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ro_py/wall.py b/ro_py/wall.py index 872fe618..c31d7945 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -2,6 +2,10 @@ from typing import List from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.pages import Pages, SortOrder +from ro_py.users import User + + +endpoint = "https://groups.roblox.com" class WallPost: @@ -9,13 +13,14 @@ class WallPost: Represents a roblox wall post. """ def __init__(self, cso, wall_data, group): + self.cso = cso self.requests = cso.requests self.group = group self.id = wall_data['id'] self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username']) + self.poster = User(self.cso, wall_data['user']['userId'], wall_data['user']['username']) async def delete(self): wall_req = await self.requests.delete( @@ -39,7 +44,7 @@ def __init__(self, cso, group): async def get_posts(self, sort_order=SortOrder.Ascending, limit=100): wall_req = Pages( - requests=self.cso, + cso=self.cso, url=endpoint + f"/v2/groups/{self.group.id}/wall/posts", sort_order=sort_order, limit=limit, From e2dba3c872c15374b2e29feb00a4cec0e9e1138c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 13:15:06 -0500 Subject: [PATCH 323/518] Some modification to client/notif --- ro_py/client.py | 16 ++++++++-------- ro_py/notifications.py | 17 ----------------- 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 538c6484..b34d1f5f 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -66,14 +66,6 @@ def __init__(self, token: str = None): if token: self.token_login(token) - logging.debug("Initialized token.") - self.accountinformation = AccountInformation(self.cso) - self.accountsettings = AccountSettings(self.cso) - logging.debug("Initialized AccountInformation and AccountSettings.") - self.chat = ChatWrapper(self.cso) - logging.debug("Initialized chat wrapper.") - self.trade = TradesWrapper(self.cso, self.get_self) - logging.debug("Initialized trade wrapper.") def token_login(self, token): """ @@ -85,6 +77,14 @@ def token_login(self, token): .ROBLOSECURITY token to authenticate with. """ self.requests.session.cookies[".ROBLOSECURITY"] = token + logging.debug("Initialized token.") + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + logging.debug("Initialized AccountInformation and AccountSettings.") + self.chat = ChatWrapper(self.cso) + logging.debug("Initialized chat wrapper.") + self.trade = TradesWrapper(self.cso, self.get_self) + logging.debug("Initialized trade wrapper.") async def user_login(self, username, password, token=None): """ diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 0acf5c68..db07b666 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -111,22 +111,6 @@ async def on_message(_self, raw_notification): else: return - def _internal_send(_self, message, protocol=None): - - _self.logger.debug("Sending message {0}".format(message)) - - try: - protocol = _self.protocol if protocol is None else protocol - - _self._ws.send(protocol.encode(message)) - _self.connection_checker.last_message = time.time() - - if _self.reconnection_handler is not None: - _self.reconnection_handler.reset() - - except Exception as ex: - raise ex - self.connection = self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, @@ -141,7 +125,6 @@ def _internal_send(_self, message, protocol=None): if self.on_error: self.connection.on_error(self.on_error) self.connection.on_message = on_message - self.connection._internal_send = _internal_send await self.connection.start() From 8aa609d5e03f54243377932a209b25f95edca57e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 13:21:40 -0500 Subject: [PATCH 324/518] Cleaned up client --- ro_py/client.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index b34d1f5f..46a8cbb2 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -19,8 +19,6 @@ from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError from ro_py.captcha import UnsolvedLoginCaptcha -import logging - class ClientSharedObject: """ @@ -50,9 +48,6 @@ def __init__(self, token: str = None): """ClientSharedObject. Passed to each new object to share information.""" self.requests = self.cso.requests """See self.cso.requests""" - - logging.debug("Initialized requests.") - self.accountinformation = None """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None @@ -77,14 +72,10 @@ def token_login(self, token): .ROBLOSECURITY token to authenticate with. """ self.requests.session.cookies[".ROBLOSECURITY"] = token - logging.debug("Initialized token.") self.accountinformation = AccountInformation(self.cso) self.accountsettings = AccountSettings(self.cso) - logging.debug("Initialized AccountInformation and AccountSettings.") self.chat = ChatWrapper(self.cso) - logging.debug("Initialized chat wrapper.") self.trade = TradesWrapper(self.cso, self.get_self) - logging.debug("Initialized trade wrapper.") async def user_login(self, username, password, token=None): """ From 140622a695687123c58cfb83a97ca03b2780da88 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 13:30:53 -0500 Subject: [PATCH 325/518] Chat fixes --- ro_py/chat.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 0eaf0030..bd5239f9 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -97,15 +97,16 @@ class Message: Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. message_id ID of the message. conversation_id ID of the conversation that contains the message. """ - def __init__(self, requests, message_id, conversation_id): - self.requests = requests + def __init__(self, cso, message_id, conversation_id): + self.cso = cso + self.requests = cso.requests self.id = message_id self.conversation_id = conversation_id @@ -128,7 +129,7 @@ async def update(self): message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.requests, message_json["senderTargetId"]) + self.sender = User(self.cso, message_json["senderTargetId"]) self.read = message_json["read"] @@ -137,8 +138,9 @@ class ChatWrapper: Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right of the Roblox web client. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests async def get_conversation(self, conversation_id): """ @@ -167,7 +169,7 @@ async def get_conversations(self, page_number=1, page_size=10): conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.requests, + cso=self.cso, raw=True, raw_data=conversation_raw )) From 7a174b01aa6c832b92d60ed5d2d46a971e7b23a2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 13:54:01 -0500 Subject: [PATCH 326/518] CSO changes (with user) --- ro_py/chat.py | 4 ++-- ro_py/games.py | 2 +- ro_py/groups.py | 7 +++++-- ro_py/trades.py | 29 ++++++++++++++++++++--------- ro_py/users.py | 6 +++--- ro_py/wall.py | 2 +- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index bd5239f9..147a7b92 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -56,7 +56,7 @@ def __init__(self, cso, conversation_id=None, raw=False, raw_data=None): data = raw_data self.id = data["id"] self.title = data["title"] - self.initiator = User(self.cso, data["initiator"]["targetId"]) + self.initiator = data["initiator"]["targetId"] self.type = data["conversationType"] self.typing = ConversationTyping(self.cso, conversation_id) @@ -70,7 +70,7 @@ async def update(self): data = conversation_req.json()[0] self.id = data["id"] self.title = data["title"] - self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.initiator = await self.cso.client.get_user(data["initiator"]["targetId"]) self.type = data["conversationType"] async def get_message(self, message_id): diff --git a/ro_py/games.py b/ro_py/games.py index 1bd750f2..594be562 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -65,7 +65,7 @@ async def update(self): if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: - self.creator = User(self.cso, game_info["creator"]["id"]) + self.creator = await self.cso.client.get_user(game_info["creator"]["id"]) self.cso.cache.set(CacheType.Users, game_info["creator"]["id"], self.creator) await self.creator.update() elif game_info["creator"]["type"] == "Group": diff --git a/ro_py/groups.py b/ro_py/groups.py index 1ba569a4..5bffce64 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -22,8 +22,11 @@ class Shout: Represents a group shout. """ def __init__(self, cso, shout_data): + self.cso = cso + self.data = shout_data self.body = shout_data["body"] - self.poster = User(cso, shout_data["poster"]["userId"], shout_data['poster']['username']) + # TODO: Make this a PartialUser + self.poster = None class JoinRequest: @@ -86,7 +89,7 @@ async def update(self): group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.cso, group_info["owner"]["userId"]) + self.owner = await self.cso.client.get_user(group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] diff --git a/ro_py/trades.py b/ro_py/trades.py index df17714d..8640d449 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -6,7 +6,7 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset -from ro_py.users import User, PartialUser +from ro_py.users import PartialUser import iso8601 import enum @@ -53,8 +53,9 @@ async def decline(self) -> bool: class PartialTrade: - def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool): - self.requests = requests + def __init__(self, cso, trade_id: int, user: PartialUser, created, expiration, status: bool): + self.cso = cso + self.requests = cso.requests self.trade_id = trade_id self.user = user self.created = iso8601.parse(created) @@ -92,7 +93,7 @@ async def expand(self) -> Trade: data = expend_req.json() # generate a user class and update it - sender = User(self.requests, data['user']['id']) + sender = await self.cso.client.get_user(data['user']['id']) await sender.update() # load items that will be/have been sent and items that you will/have recieve(d) @@ -107,7 +108,16 @@ async def expand(self) -> Trade: await item_1.update() send_items.append(item_1) - return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status']) + return Trade( + self.cso, + self.trade_id, + sender, + recieve_items, + send_items, + data['created'], + data['expiration'], + data['status'] + ) class TradeStatusType(enum.Enum): @@ -187,14 +197,15 @@ class TradesWrapper: """ Represents the Roblox trades page. """ - def __init__(self, requests, get_self): - self.requests = requests + def __init__(self, cso, get_self): + self.cso = cso + self.requests = cso.requests self.get_self = get_self self.TradeRequest = TradeRequest async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: - trades = await Pages( - requests=self.requests, + trades = Pages( + cso=self.cso, url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, diff --git a/ro_py/users.py b/ro_py/users.py index a52d73e3..be8609b0 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -27,8 +27,8 @@ class User: Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. roblox_id : int The id of a user. name : str @@ -117,7 +117,7 @@ async def get_friends(self): friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.cso, friend_raw["id"]) + await self.cso.client.get_user(friend_raw["id"]) ) return friends_list diff --git a/ro_py/wall.py b/ro_py/wall.py index c31d7945..a37bc0f8 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -10,7 +10,7 @@ class WallPost: """ - Represents a roblox wall post. + Represents a Roblox wall post. """ def __init__(self, cso, wall_data, group): self.cso = cso From e25f05d55dfb81a1fe2a6e2d99340a2fcfa14168 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 14:06:55 -0500 Subject: [PATCH 327/518] Modified setup behavior --- setup.py | 27 ++------------------------- setup_info.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 25 deletions(-) create mode 100644 setup_info.py diff --git a/setup.py b/setup.py index 60c511fa..3d326d14 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,7 @@ import setuptools +import setup_info with open("README.md", "r") as fh: long_description = fh.read() -setuptools.setup( - name="ro-py", - version="1.1.0", - author="jmkdev and iranathan", - author_email="jmk@jmksite.dev", - description="ro.py is a Python wrapper for the Roblox web API.", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/rbx-libdev/ro.py", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Operating System :: OS Independent", - ], - python_requires='>=3.6', - install_requires=[ - "httpx", - "iso8601", - "signalrcore", - "pytweening", - "wxPython", - "wxasync" - ] -) +setuptools.setup(**setup_info.setup_info) diff --git a/setup_info.py b/setup_info.py new file mode 100644 index 00000000..60ee8c3a --- /dev/null +++ b/setup_info.py @@ -0,0 +1,30 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup_info = { + "name": "ro-py", + "version": "1.1.0", + "author": "jmkdev and iranathan", + "author_email": "jmk@jmksite.dev", + "description": "ro.py is a Python wrapper for the Roblox web API.", + "long_description": long_description, + "long_description_content_type": "text/markdown", + "url": "https://github.com/rbx-libdev/ro.py", + "packages": setuptools.find_packages(), + "classifiers": [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + ], + "python_requires": '>=3.6', + "install_requires": [ + "httpx", + "iso8601", + "signalrcore", + "pytweening", + "wxPython", + "wxasync" + ] +} From 91b5904fd1ba5f4605d2da4edb2aa3a06fb44fef Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 14:07:17 -0500 Subject: [PATCH 328/518] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7af58808..1036f4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ ro_py_old/ other/ udocs/ docstemplate/ -build.bat +build.* docsbuild.bat chat.py From cae63ad0f46c2f2c936a04c12303faa79a7acdb3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 14:24:08 -0500 Subject: [PATCH 329/518] Updated version identifier --- setup_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index 60ee8c3a..151c3347 100644 --- a/setup_info.py +++ b/setup_info.py @@ -5,7 +5,7 @@ setup_info = { "name": "ro-py", - "version": "1.1.0", + "version": "1.1.1", "author": "jmkdev and iranathan", "author_email": "jmk@jmksite.dev", "description": "ro.py is a Python wrapper for the Roblox web API.", From aeaa69ad74d4a51b189b8dabb22da25f15ab8493 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 20 Jan 2021 22:25:54 -0500 Subject: [PATCH 330/518] some bots changes --- ro_py/extensions/bots.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index e3018bc3..57bff079 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -13,6 +13,14 @@ class Bot(Client): def __init__(self): super().__init__() + def command(self, _="_", **kwargs): + def decorator(func): + if isinstance(func, Command): + raise TypeError('Callback is already a command.') + return Command(func=func, **kwargs) + + return decorator + class Command: def __init__(self, func, **kwargs): @@ -28,10 +36,4 @@ async def __call__(self, *args, **kwargs): return await self.callback(*args, **kwargs) -def command(**attrs): - def decorator(func): - if isinstance(func, Command): - raise TypeError('Callback is already a command.') - return Command(func, **attrs) - return decorator From f13d12d95bac1bb4b86daa933a6c42eb0d2a8597 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 08:46:24 -0500 Subject: [PATCH 331/518] Added run and command functions --- ro_py/extensions/bots.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 57bff079..2a2f92cb 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -12,12 +12,24 @@ class Bot(Client): def __init__(self): super().__init__() + self.commands = {} + self.evtloop = asyncio.new_event_loop() + + def run(self, token): + self.token_login(token) + self.evtloop = asyncio.new_event_loop() + self.evtloop.run_until_complete(self._run()) + + async def _run(self): + pass def command(self, _="_", **kwargs): def decorator(func): if isinstance(func, Command): raise TypeError('Callback is already a command.') - return Command(func=func, **kwargs) + command = Command(func=func, **kwargs) + self.commands[func.__name__] = command + return command return decorator From 898f05227ee6b615958e1829a50be21091e91ea2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 09:39:56 -0500 Subject: [PATCH 332/518] Event loop changes + Notification fixes + Bots fixes --- ro_py/client.py | 7 +++++++ ro_py/extensions/bots.py | 2 +- ro_py/notifications.py | 44 +++++++++++++--------------------------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 46a8cbb2..2f7b7202 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -15,9 +15,11 @@ from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.utilities.cache import Cache, CacheType +from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError from ro_py.captcha import UnsolvedLoginCaptcha +import asyncio class ClientSharedObject: @@ -31,6 +33,8 @@ def __init__(self, client): """Cache object to keep objects that don't need to be recreated.""" self.requests = Requests() """Reqests object for all web requests.""" + self.evtloop = asyncio.new_event_loop() + """Event loop for certain things.""" class Client: @@ -56,6 +60,8 @@ def __init__(self, token: str = None): """ChatWrapper object. Only available for authenticated clients.""" self.trade = None """TradesWrapper object. Only available for authenticated clients.""" + self.notifications = None + """NotificationReceiver object. Only available for authenticated clients.""" self.events = EventTypes """Types of events used for binding events to a function.""" @@ -76,6 +82,7 @@ def token_login(self, token): self.accountsettings = AccountSettings(self.cso) self.chat = ChatWrapper(self.cso) self.trade = TradesWrapper(self.cso, self.get_self) + self.notifications = NotificationReceiver(self.cso) async def user_login(self, username, password, token=None): """ diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 2a2f92cb..8a76f05f 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -17,7 +17,7 @@ def __init__(self): def run(self, token): self.token_login(token) - self.evtloop = asyncio.new_event_loop() + self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) async def _run(self): diff --git a/ro_py/notifications.py b/ro_py/notifications.py index db07b666..88908ed1 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -11,11 +11,9 @@ from ro_py.utilities.caseconvert import to_snake_case -from signalrcore_async.hub_connection_builder import HubConnectionBuilder +from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json -import time -import asyncio class Notification: @@ -60,28 +58,20 @@ class NotificationReceiver: This should only be generated once per client as to not duplicate notifications. """ - def __init__(self, requests, on_open, on_close, on_error, on_notification): - self.requests = requests - - self.on_open = on_open - self.on_close = on_close - self.on_error = on_error - self.on_notification = on_notification - - self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.connection = None - + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests + self.evtloop = cso.evtloop self.negotiate_request = None self.wss_url = None + self.connection = None async def initialize(self): self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", - cookies={ - ".ROBLOSECURITY": self.roblosecurity - } + cookies=self.requests.session.cookies ) self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ @@ -91,13 +81,13 @@ async def initialize(self): self.wss_url, options={ "headers": { - "Cookie": f".ROBLOSECURITY={self.roblosecurity};" + "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};" }, "skip_negotiation": False } ) - async def on_message(_self, raw_notification): + def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -107,28 +97,22 @@ async def on_message(_self, raw_notification): return if len(notification_json) > 0: notification = Notification(notification_json) - await self.on_notification(notification) + self.evtloop.run_until_complete(self.on_notification(notification)) else: return - self.connection = self.connection.with_automatic_reconnect({ + self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, "max_attempts": 5 }).build() - if self.on_open: - self.connection.on_open(self.on_open) - if self.on_close: - self.connection.on_close(self.on_close) - if self.on_error: - self.connection.on_error(self.on_error) - self.connection.on_message = on_message + self.connection.hub.on_message = on_message - await self.connection.start() + self.connection.start() - async def close(self): + def close(self): """ Closes the connection and stops receiving notifications. """ From b131bada87c962d0dbe23249db9e78f2f2b635fb Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 09:46:12 -0500 Subject: [PATCH 333/518] Fixed readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99bb5544..91436fdc 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Known issue: wxPython sometimes has trouble building on certain devices. I put w ## Credits [@iranathan](https://github.com/iranathan) - maintainer -[@jmkdev](https://github.com/iranathan) - maintainer +[@jmk-developer](https://github.com/jmk-developer) - maintainer [@nsg-mfd](https://github.com/nsg-mfd) - helped with endpoints ## Other Libraries From 0d106c84fcdb22bd91b6f4269f4e0e634454d973 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 10:04:29 -0500 Subject: [PATCH 334/518] Bots modification --- ro_py/extensions/bots.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 8a76f05f..3cfe0528 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -17,11 +17,15 @@ def __init__(self): def run(self, token): self.token_login(token) + self.notifications.on_notification = self._on_notification self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) + async def _on_notification(self, notification): + print(notification.__dict__) + async def _run(self): - pass + await self.notifications.initialize() def command(self, _="_", **kwargs): def decorator(func): From ca5c3f55947327168eeff58d4a0b3a24f9d5f320 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 10:11:21 -0500 Subject: [PATCH 335/518] Bots updated --- ro_py/extensions/bots.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 3cfe0528..2e52ac6f 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -4,7 +4,6 @@ """ - from ro_py.client import Client import asyncio @@ -22,7 +21,16 @@ def run(self, token): self.evtloop.run_until_complete(self._run()) async def _on_notification(self, notification): - print(notification.__dict__) + if notification.type == "NewMessage": + latest_req = await self.requests.get( + url="https://chat.roblox.com/v2/get-messages", + params={ + "conversationId": notification.data["conversation_id"], + "pageSize": 1 + } + ) + latest_data = latest_req.json()[0] + latest_content = latest_data["content"] async def _run(self): await self.notifications.initialize() @@ -50,6 +58,3 @@ def callback(self): async def __call__(self, *args, **kwargs): return await self.callback(*args, **kwargs) - - - From b83f4a16f2f0a2776c014973795d03cadf081b18 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 10:34:01 -0500 Subject: [PATCH 336/518] Update bots.py --- ro_py/extensions/bots.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 2e52ac6f..8fe77ea7 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -8,10 +8,17 @@ import asyncio -class Bot(Client): +class Context: def __init__(self): + pass + + +class Bot(Client): + def __init__(self, prefix="!"): super().__init__() + self.prefix = prefix self.commands = {} + self.events = {} self.evtloop = asyncio.new_event_loop() def run(self, token): @@ -20,6 +27,16 @@ def run(self, token): self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) + async def _process_command(self, data): + content = data["content"] + if content.startswith(self.prefix): + content = content[len(self.prefix):] + content_split = content.split(" ") + command = content_split[0] + if command in self.commands: + context = Context() + await self.commands[command](context) + async def _on_notification(self, notification): if notification.type == "NewMessage": latest_req = await self.requests.get( @@ -30,7 +47,7 @@ async def _on_notification(self, notification): } ) latest_data = latest_req.json()[0] - latest_content = latest_data["content"] + await self._process_command(latest_data) async def _run(self): await self.notifications.initialize() From f6dc6bdce7e96516e09981af80fbe86477929dad Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 11:13:42 -0500 Subject: [PATCH 337/518] Lots of bots modifications --- ro_py/extensions/bots.py | 47 +++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 8fe77ea7..37270c72 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -4,13 +4,46 @@ """ +from ro_py.utilities.errors import ChatError from ro_py.client import Client +from ro_py.chat import Message import asyncio +import iso8601 class Context: - def __init__(self): - pass + def __init__(self, cso, latest_data, notif_data): + self.cso = cso + self.requests = cso.requests + + self.conversation_id = notif_data["conversation_id"] + self.actor_target_id = notif_data["actor_target_id"] + self.actor_type = notif_data["actor_type"] + self.type = notif_data["type"] + self.sequence_number = notif_data["sequence_number"] + + self.id = latest_data["id"] + self.content = latest_data["content"] + self.sender_type = latest_data["senderType"] + self.sender_id = latest_data["senderTargetId"] + self.decorators = latest_data["decorators"] + self.message_type = latest_data["messageType"] + self.read = latest_data["read"] + self.sent = iso8601.parse_date(latest_data["sent"]) + + async def send(self, content): + send_message_req = await self.requests.post( + url="https://chat.roblox.com/v2/send-message", + data={ + "message": content, + "conversationId": self.conversation_id + } + ) + send_message_json = send_message_req.json() + if send_message_json["sent"]: + return Message(self.requests, send_message_json["messageId"], self.id) + else: + raise ChatError(send_message_json["statusMessage"]) class Bot(Client): @@ -27,14 +60,18 @@ def run(self, token): self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) - async def _process_command(self, data): + async def _process_command(self, data, n_data): content = data["content"] if content.startswith(self.prefix): content = content[len(self.prefix):] content_split = content.split(" ") command = content_split[0] if command in self.commands: - context = Context() + context = Context( + cso=self.cso, + latest_data=data, + notif_data=n_data + ) await self.commands[command](context) async def _on_notification(self, notification): @@ -47,7 +84,7 @@ async def _on_notification(self, notification): } ) latest_data = latest_req.json()[0] - await self._process_command(latest_data) + await self._process_command(latest_data, notification.data) async def _run(self): await self.notifications.initialize() From 98c84d32589947ecac6b77b04533fb530b1618ff Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 21 Jan 2021 11:41:23 -0500 Subject: [PATCH 338/518] This is a buggy mess --- ro_py/extensions/bots.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 37270c72..0711ed60 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -47,13 +47,27 @@ async def send(self, content): class Bot(Client): - def __init__(self, prefix="!"): + def __init__(self, prefix="!", auto_help=True): super().__init__() self.prefix = prefix self.commands = {} self.events = {} self.evtloop = asyncio.new_event_loop() + if auto_help: + command_help = self._generate_help() + + @self.command(a=0) + async def command_help_func(ctx): + print("HEA") + await ctx.send(command_help) + + def _generate_help(self): + help_string = f"Prefix: {self.prefix}" + for command in self.commands: + help_string = help_string + "\n" + command + ": " + command.help[:24] + return help_string + def run(self, token): self.token_login(token) self.notifications.on_notification = self._on_notification @@ -72,7 +86,10 @@ async def _process_command(self, data, n_data): latest_data=data, notif_data=n_data ) - await self.commands[command](context) + try: + await self.commands[command](context) + except Exception as e: + await context.send("Something went wrong when running this command.") async def _on_notification(self, notification): if notification.type == "NewMessage": @@ -105,6 +122,7 @@ def __init__(self, func, **kwargs): if not asyncio.iscoroutinefunction(func): raise TypeError('Callback must be a coroutine.') self._callback = func + self.help = func.__doc__ or "No help available." @property def callback(self): From 6e0fe8fc40c08bf80bfcfb1a332b7d60227460a0 Mon Sep 17 00:00:00 2001 From: ira Date: Thu, 21 Jan 2021 19:57:02 +0100 Subject: [PATCH 339/518] add partial user + other small fixes --- ro_py/chat.py | 4 +- ro_py/client.py | 18 ++++---- ro_py/games.py | 1 - ro_py/groups.py | 7 +-- ro_py/thumbnails.py | 6 +-- ro_py/trades.py | 31 +++++++------ ro_py/users.py | 110 ++++++++++++++++++++++++++------------------ ro_py/wall.py | 4 +- 8 files changed, 102 insertions(+), 79 deletions(-) diff --git a/ro_py/chat.py b/ro_py/chat.py index 147a7b92..6e41e8c9 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -5,7 +5,7 @@ """ from ro_py.utilities.errors import ChatError -from ro_py.users import User +from ro_py.users import PartialUser endpoint = "https://chat.roblox.com/" @@ -129,7 +129,7 @@ async def update(self): message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.cso, message_json["senderTargetId"]) + self.sender = PartialUser(self.cso, message_json["senderTargetId"]) self.read = message_json["read"] diff --git a/ro_py/client.py b/ro_py/client.py index 2f7b7202..57c9b98d 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -4,7 +4,6 @@ """ -from ro_py.users import User from ro_py.games import Game from ro_py.groups import Group from ro_py.assets import Asset @@ -12,6 +11,7 @@ from ro_py.chat import ChatWrapper from ro_py.events import EventTypes from ro_py.trades import TradesWrapper +from ro_py.users import PartialUser from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.utilities.cache import Cache, CacheType @@ -144,7 +144,7 @@ async def get_self(self): url="https://roblox.com/my/profile" ) data = self_req.json() - return User(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data['UserId'], data['Username']) async def get_user(self, user_id): """ @@ -157,9 +157,10 @@ async def get_user(self, user_id): """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): @@ -187,9 +188,10 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user else: raise UserDoesNotExistError diff --git a/ro_py/games.py b/ro_py/games.py index 594be562..2a0a4afd 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -4,7 +4,6 @@ """ -from ro_py.users import User from ro_py.groups import Group from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator diff --git a/ro_py/groups.py b/ro_py/groups.py index 5bffce64..ec254562 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -157,7 +157,7 @@ async def get_member_by_id(self, roblox_id): # Create data to return. role = Role(self.cso, self, group_data['role']) member = Member(self.cso, roblox_id, "", self, role) - return await member.update() + return member async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( @@ -192,13 +192,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -class Member(User): +class Member(PartialUser): """ Represents a user in a group. Parameters ---------- - requests : ro_py.utilities.requests.Requests + cso : ro_py.utilities.requests.Requests Requests object to use for API requests. roblox_id : int The id of a user. @@ -211,6 +211,7 @@ class Member(User): """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) + self.requests = cso.requests self.role = role self.group = group diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index 2b014440..dba25ae9 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -97,9 +97,9 @@ async def get_game_icon(self, size=ThumbnailSize.size_50x50, file_format=Thumbna class UserThumbnailGenerator: - def __init__(self, requests, id): - self.requests = requests - self.id = id + def __init__(self, cso, roblox_id): + self.requests = cso.requests + self.id = roblox_id async def get_avatar_image(self, shot_type=ThumbnailType.avatar_full_body, size=ThumbnailSize.size_48x48, file_format=ThumbnailFormat.format_png, is_circular=False): diff --git a/ro_py/trades.py b/ro_py/trades.py index 8640d449..4a2ad709 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -7,6 +7,7 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset from ro_py.users import PartialUser +import datetime import iso8601 import enum @@ -21,14 +22,14 @@ def trade_page_handler(requests, this_page) -> list: class Trade: - def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender - self.recieve_items = recieve_items + self.receive_items = receive_items self.send_items = send_items self.created = iso8601.parse_date(created) - self.experation = iso8601.parse_date(expiration) + self.expiration = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: @@ -53,13 +54,13 @@ async def decline(self) -> bool: class PartialTrade: - def __init__(self, cso, trade_id: int, user: PartialUser, created, expiration, status: bool): + def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool): self.cso = cso self.requests = cso.requests self.trade_id = trade_id self.user = user - self.created = iso8601.parse(created) - self.expiration = iso8601.parse(expiration) + self.created = iso8601.parse_date(created) + self.expiration = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: @@ -97,11 +98,11 @@ async def expand(self) -> Trade: await sender.update() # load items that will be/have been sent and items that you will/have recieve(d) - recieve_items, send_items = [], [] + receive_items, send_items = [], [] for items_0 in data['offers'][0]['userAssets']: item_0 = Asset(self.requests, items_0['assetId']) await item_0.update() - recieve_items.append(item_0) + receive_items.append(item_0) for items_1 in data['offers'][1]['userAssets']: item_1 = Asset(self.requests, items_1['assetId']) @@ -112,7 +113,7 @@ async def expand(self) -> Trade: self.cso, self.trade_id, sender, - recieve_items, + receive_items, send_items, data['created'], data['expiration'], @@ -143,13 +144,13 @@ def __init__(self, trades_metadata_data): class TradeRequest: def __init__(self): - self.recieve_asset = [] + self.receive_asset = [] """Limiteds that will be recieved when the trade is accepted.""" self.send_asset = [] """Limiteds that will be sent when the trade is accepted.""" self.send_robux = 0 """Robux that will be sent when the trade is accepted.""" - self.recieve_robux = 0 + self.receive_robux = 0 """Robux that will be recieved when the trade is accepted.""" def request_item(self, asset: UserAsset): @@ -160,7 +161,7 @@ def request_item(self, asset: UserAsset): ---------- asset : ro_py.assets.UserAsset """ - self.recieve_asset.append(asset) + self.receive_asset.append(asset) def send_item(self, asset: UserAsset): """ @@ -180,7 +181,7 @@ def request_robux(self, robux: int): ---------- robux : int """ - self.recieve_robux = robux + self.receive_robux = robux def send_robux(self, robux: int): """ @@ -248,10 +249,10 @@ async def send_trade(self, roblox_id, trade): for asset in trade.send_asset: data['offers'][1]['userAssetIds'].append(asset.user_asset_id) - for asset in trade.recieve_asset: + for asset in trade.receive_asset: data['offers'][0]['userAssetIds'].append(asset.user_asset_id) - data['offers'][0]['robux'] = trade.recieve_robux + data['offers'][0]['robux'] = trade.receive_robux data['offers'][1]['robux'] = trade.send_robux trade_req = await self.requests.post( diff --git a/ro_py/users.py b/ro_py/users.py index be8609b0..dd9e3184 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -3,6 +3,7 @@ This file houses functions and classes that pertain to Roblox users and profiles. """ +from typing import List from ro_py.robloxbadges import RobloxBadge from ro_py.thumbnails import UserThumbnailGenerator @@ -20,56 +21,30 @@ def limited_handler(requests, data, args): return assets -class User: - """ - Represents a Roblox user and their profile. - Can be initialized with either a user ID or a username. - - Parameters - ---------- - cso : ro_py.client.ClientSharedObject - ClientSharedObject. - roblox_id : int - The id of a user. - name : str - The name of the user. - """ - def __init__(self, cso, roblox_id, name=None): +class PartialUser: + def __init__(self, cso, roblox_id, roblox_name=None): self.cso = cso self.requests = cso.requests self.id = roblox_id - self.description = None - self.created = None - self.is_banned = None - self.name = name - self.display_name = None - self.thumbnails = UserThumbnailGenerator(self.requests, self.id) + self.name = roblox_name - async def update(self): + async def expand(self): """ Updates some class values. :return: Nothing """ user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() - self.description = user_info["description"] - self.created = iso8601.parse_date(user_info["created"]) - self.is_banned = user_info["isBanned"] - self.name = user_info["name"] - self.display_name = user_info["displayName"] + description = user_info["description"] + created = iso8601.parse_date(user_info["created"]) + is_banned = user_info["isBanned"] + name = user_info["name"] + display_name = user_info["displayName"] # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - return self + return User(self.cso, self.id, name, description, created, is_banned, display_name) - async def get_status(self): - """ - Gets the user's status. - :return: A string - """ - status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") - return status_req.json()["status"] - - async def get_roblox_badges(self): + async def get_roblox_badges(self) -> List[RobloxBadge]: """ Gets the user's roblox badges. :return: A list of RobloxBadge instances @@ -80,7 +55,7 @@ async def get_roblox_badges(self): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - async def get_friends_count(self): + async def get_friends_count(self) -> int: """ Gets the user's friends count. :return: An integer @@ -89,7 +64,7 @@ async def get_friends_count(self): friends_count = friends_count_req.json()["count"] return friends_count - async def get_followers_count(self): + async def get_followers_count(self) -> int: """ Gets the user's followers count. :return: An integer @@ -98,7 +73,7 @@ async def get_followers_count(self): followers_count = followers_count_req.json()["count"] return followers_count - async def get_followings_count(self): + async def get_followings_count(self) -> int: """ Gets the user's followings count. :return: An integer @@ -142,15 +117,60 @@ async def get_limiteds(self): list """ return Pages( - requests=self.requests, + cso=self.cso, url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler ) + async def get_status(self): + """ + Gets the user's status. + :return: A string + """ + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") + return status_req.json()["status"] -class PartialUser(User): + +class User(PartialUser): """ - Represents a user with less information then the normal User class. + Represents a Roblox user and their profile. + Can be initialized with either a user ID or a username. + + Parameters + ---------- + cso : ro_py.client.ClientSharedObject + ClientSharedObject. + roblox_id : int + The id of a user. + roblox_name : str + The name of the user. + description : str + The description of the user. + created : any + Time the user was created. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): + super().__init__(cso, roblox_id, roblox_name) + self.cso = cso + self.id = roblox_id + self.name = roblox_name + self.description = description + self.created = created + self.is_banned = banned + self.display_name = display_name + self.thumbnails = UserThumbnailGenerator(cso, roblox_id) + + async def update(self): + """ + Updates some class values. + :return: Nothing + """ + user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") + user_info = user_info_req.json() + self.description = user_info["description"] + self.created = iso8601.parse_date(user_info["created"]) + self.is_banned = user_info["isBanned"] + self.name = user_info["name"] + self.display_name = user_info["displayName"] + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req diff --git a/ro_py/wall.py b/ro_py/wall.py index a37bc0f8..d8fabd52 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -2,7 +2,7 @@ from typing import List from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.pages import Pages, SortOrder -from ro_py.users import User +from ro_py.users import PartialUser endpoint = "https://groups.roblox.com" @@ -20,7 +20,7 @@ def __init__(self, cso, wall_data, group): self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = User(self.cso, wall_data['user']['userId'], wall_data['user']['username']) + self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username']) async def delete(self): wall_req = await self.requests.delete( From 6b3efcf7d62545caccc0bd798225cf3d95f36932 Mon Sep 17 00:00:00 2001 From: ira Date: Fri, 22 Jan 2021 09:18:32 +0100 Subject: [PATCH 340/518] Fixed bug in on_asset_change, Replaced on_group_shout with on_group_change, Added on_user_change. --- ro_py/assets.py | 5 +++-- ro_py/events.py | 3 ++- ro_py/groups.py | 37 +++++++++++++++++++++++++------------ ro_py/users.py | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index ecc45ce0..b56bb8e6 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -138,7 +138,8 @@ async def on_asset_change(self, func, delay): while True: await asyncio.sleep(delay) await self.asset.update() + has_changed = False for attr, value in self.asset.__dict__.items(): if getattr(old_asset, attr) != value: - await func(old_asset, self.asset) - old_asset = self.asset + has_changed = True + func(old_asset, self.asset) diff --git a/ro_py/events.py b/ro_py/events.py index 1302fd58..803477da 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -4,5 +4,6 @@ class EventTypes(enum.Enum): on_join_request = "on_join_request" on_wall_post = "on_wall_post" - on_shout_update = "on_shout_update" + on_group_change = "on_group_change" on_asset_change = "on_asset_change" + on_user_change = "on_user_change" diff --git a/ro_py/groups.py b/ro_py/groups.py index ec254562..a7fadbf9 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -8,9 +8,9 @@ import asyncio from ro_py.wall import Wall from ro_py.roles import Role -from typing import List, Tuple -from ro_py.captcha import UnsolvedCaptcha -from ro_py.users import User, PartialUser +from ro_py.users import PartialUser +from ro_py.events import EventTypes +from typing import Tuple, Callable from ro_py.utilities.errors import NotFound from ro_py.utilities.pages import Pages, SortOrder @@ -334,7 +334,7 @@ def __init__(self, cso, group): self.cso = cso self.group = group - async def bind(self, func, event, delay=15): + async def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -342,19 +342,19 @@ async def bind(self, func, event, delay=15): ---------- func : function Function that will be bound to the event. - event : str + event : ro_py.events.EventTypes Event that will be bound to the function. delay : int How many seconds between each poll. """ - if event == "on_join_request": + if event == EventTypes.on_join_request: return await asyncio.create_task(self.on_join_request(func, delay)) - if event == "on_wall_post": + if event == EventTypes.on_wall_post: return await asyncio.create_task(self.on_wall_post(func, delay)) - if event == "on_shout_update": - return await asyncio.create_task(self.on_shout_update(func, delay)) + if event == EventTypes.on_group_change: + return await asyncio.create_task(self.on_group_change(func, delay)) - async def on_join_request(self, func, delay): + async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() old_req = current_group_reqs.data.requester.id while True: @@ -370,7 +370,7 @@ async def on_join_request(self, func, delay): for new_req in new_reqs: await func(new_req) - async def on_wall_post(self, func, delay): + async def on_wall_post(self, func: Callable, delay: int): current_wall_posts = await self.group.wall.get_posts() newest_wall_poster = current_wall_posts.data[0].poster.id while True: @@ -386,7 +386,7 @@ async def on_wall_post(self, func, delay): for new_post in new_posts: await func(new_post) - async def on_shout_update(self, func, delay): + async def on_shout_update(self, func: Callable, delay: int): await self.group.update() current_shout = self.group.shout while True: @@ -395,3 +395,16 @@ async def on_shout_update(self, func, delay): if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body: await func(current_shout, self.group.shout) current_shout = self.group.shout + + async def on_group_change(self, func: Callable, delay: int): + await self.group.update() + current_group = copy.copy(self.group) + while True: + await asyncio.sleep(delay) + await self.group.update() + has_changed = False + for attr, value in current_group.__dict__.items(): + if getattr(self.group, attr) != value: + has_changed = True + if has_changed: + func(current_group, self.group) diff --git a/ro_py/users.py b/ro_py/users.py index dd9e3184..c0ee6de4 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -3,13 +3,15 @@ This file houses functions and classes that pertain to Roblox users and profiles. """ -from typing import List - +import copy +from typing import List, Callable +from ro_py.events import EventTypes from ro_py.robloxbadges import RobloxBadge from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.pages import Pages from ro_py.assets import UserAsset import iso8601 +import asyncio endpoint = "https://users.roblox.com/" @@ -49,7 +51,8 @@ async def get_roblox_badges(self) -> List[RobloxBadge]: Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = await self.requests.get( + f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) @@ -78,7 +81,8 @@ async def get_followings_count(self) -> int: Gets the user's followings count. :return: An integer """ - followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get( + f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -149,6 +153,7 @@ class User(PartialUser): created : any Time the user was created. """ + def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): super().__init__(cso, roblox_id, roblox_name) self.cso = cso @@ -172,5 +177,40 @@ async def update(self): self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] + return self # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req + + +class Events: + def __init__(self, cso, user): + self.cso = cso + self.user = user + + async def bind(self, func: Callable, event: str, delay: int = 15): + """ + Binds an event to the provided function. + + Parameters + ---------- + func : function + Function that will be called when the event fires. + event : ro_py.events.EventTypes + The name of the event. + delay : int + How many seconds between requests. + """ + if event == EventTypes.on_user_change: + return await asyncio.create_task(self.on_user_change(func, delay)) + + async def on_user_change(self, func: Callable, delay: int): + old_user = copy.copy(await self.user.update()) + while True: + await asyncio.sleep(delay) + new_user = await self.user.update() + has_changed = False + for attr, value in old_user.__dict__.items(): + if getattr(new_user, attr) != value: + has_changed = True + if has_changed: + func(old_user, new_user) From fe1711898101080a7a98825e2f1b284a60a5a781 Mon Sep 17 00:00:00 2001 From: ira Date: Sat, 23 Jan 2021 12:24:14 +0100 Subject: [PATCH 341/518] fix event bugs. --- ro_py/assets.py | 5 ++++- ro_py/groups.py | 26 +++++++++++++------------- ro_py/users.py | 5 ++++- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index b56bb8e6..a364733e 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -142,4 +142,7 @@ async def on_asset_change(self, func, delay): for attr, value in self.asset.__dict__.items(): if getattr(old_asset, attr) != value: has_changed = True - func(old_asset, self.asset) + if asyncio.iscoroutinefunction(func): + await func(old_asset, self.asset) + else: + func(old_asset, self.asset) diff --git a/ro_py/groups.py b/ro_py/groups.py index a7fadbf9..424ee029 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -368,7 +368,10 @@ async def on_join_request(self, func: Callable, delay: int): new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - await func(new_req) + if asyncio.iscoroutinefunction(func): + await func(new_req) + else: + func(new_req) async def on_wall_post(self, func: Callable, delay: int): current_wall_posts = await self.group.wall.get_posts() @@ -384,17 +387,10 @@ async def on_wall_post(self, func: Callable, delay: int): new_posts.append(post) newest_wall_poster = current_wall_posts[0].poster.id for new_post in new_posts: - await func(new_post) - - async def on_shout_update(self, func: Callable, delay: int): - await self.group.update() - current_shout = self.group.shout - while True: - await asyncio.sleep(delay) - await self.group.update() - if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body: - await func(current_shout, self.group.shout) - current_shout = self.group.shout + if asyncio.iscoroutinefunction(func): + await func(new_post) + else: + func(new_post) async def on_group_change(self, func: Callable, delay: int): await self.group.update() @@ -407,4 +403,8 @@ async def on_group_change(self, func: Callable, delay: int): if getattr(self.group, attr) != value: has_changed = True if has_changed: - func(current_group, self.group) + if asyncio.iscoroutinefunction(func): + await func(current_group, self.group) + else: + func(current_group, self.group) + current_group = self.group diff --git a/ro_py/users.py b/ro_py/users.py index c0ee6de4..04147dc9 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -213,4 +213,7 @@ async def on_user_change(self, func: Callable, delay: int): if getattr(new_user, attr) != value: has_changed = True if has_changed: - func(old_user, new_user) + if asyncio.iscoroutinefunction(func): + await func(old_user, new_user) + else: + func(old_user, new_user) From 97a489f98b2ae0902217b10cc74ac468270ff161 Mon Sep 17 00:00:00 2001 From: KILR <65610641+KILR007@users.noreply.github.com> Date: Sat, 23 Jan 2021 17:40:37 +0530 Subject: [PATCH 342/518] Removed print statement --- ro_py/extensions/twocaptcha.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ro_py/extensions/twocaptcha.py b/ro_py/extensions/twocaptcha.py index b683e5db..a9db4194 100644 --- a/ro_py/extensions/twocaptcha.py +++ b/ro_py/extensions/twocaptcha.py @@ -19,10 +19,8 @@ async def solve(self, captcha: UnsolvedCaptcha): url += "&surl=https://roblox-api.arkoselabs.com" url += "&pageurl=https://www.roblox.com" url += "&json=1" - print(url) solve_req = await requests_async.post(url) - print(solve_req.text) data = solve_req.json() if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST": raise IncorrectKeyError("The provided 2captcha api key was incorrect.") From d7a0e193ba95cd0db696136ed78b0ac3c6aa5584 Mon Sep 17 00:00:00 2001 From: ira Date: Sat, 23 Jan 2021 17:23:18 +0100 Subject: [PATCH 343/518] fix event bugs. --- ro_py/assets.py | 1 + ro_py/groups.py | 2 +- ro_py/users.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index a364733e..c6e9c8f9 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -146,3 +146,4 @@ async def on_asset_change(self, func, delay): await func(old_asset, self.asset) else: func(old_asset, self.asset) + old_asset = copy.copy(self.asset) diff --git a/ro_py/groups.py b/ro_py/groups.py index 424ee029..4a37442e 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -407,4 +407,4 @@ async def on_group_change(self, func: Callable, delay: int): await func(current_group, self.group) else: func(current_group, self.group) - current_group = self.group + current_group = copy.copy(self.group) diff --git a/ro_py/users.py b/ro_py/users.py index 04147dc9..ea277d38 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -217,3 +217,4 @@ async def on_user_change(self, func: Callable, delay: int): await func(old_user, new_user) else: func(old_user, new_user) + old_user = copy.copy(new_user) From 3a952973414f7d1dbb9bfa725f0dfbc6d9d776a5 Mon Sep 17 00:00:00 2001 From: ira Date: Sat, 23 Jan 2021 20:55:25 +0100 Subject: [PATCH 344/518] better solution for on_asset_change --- ro_py/assets.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index c6e9c8f9..9a2277fe 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -138,12 +138,9 @@ async def on_asset_change(self, func, delay): while True: await asyncio.sleep(delay) await self.asset.update() - has_changed = False - for attr, value in self.asset.__dict__.items(): - if getattr(old_asset, attr) != value: - has_changed = True - if asyncio.iscoroutinefunction(func): - await func(old_asset, self.asset) - else: - func(old_asset, self.asset) - old_asset = copy.copy(self.asset) + if old_asset.updated < self.asset.updated: + if asyncio.iscoroutinefunction(func): + await func(old_asset, self.asset) + else: + func(old_asset, self.asset) + old_asset = copy.copy(self.asset) From a06c134c49c7494f32126ad20ae790e51b6e014e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 24 Jan 2021 12:49:11 -0500 Subject: [PATCH 345/518] Fixed requests issue + 2captcha formatting --- ro_py/extensions/twocaptcha.py | 5 ++++- ro_py/utilities/requests.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ro_py/extensions/twocaptcha.py b/ro_py/extensions/twocaptcha.py index b683e5db..36c0f837 100644 --- a/ro_py/extensions/twocaptcha.py +++ b/ro_py/extensions/twocaptcha.py @@ -35,7 +35,10 @@ async def solve(self, captcha: UnsolvedCaptcha): solution = None while True: await asyncio.sleep(5) - captcha_req = await requests_async.get(endpoint + f"/res.php?key={self.api_key}&id={task_id}&json=1&action=get") + captcha_req = await requests_async.get(endpoint + f"/res.php" + f"?key={self.api_key}" + f"&id={task_id}" + f"&json=1&action=get") captcha_data = captcha_req.json() if captcha_data['request'] != "CAPCHA_NOT_READY": solution = captcha_data['request'] diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 9cb3bfe6..acfd313b 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -71,7 +71,7 @@ async def get(self, *args, **kwargs): if quickreturn: return get_request - raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") + raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) From ea9485b6361b7da90b57712aef6241985551846a Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 26 Jan 2021 12:55:35 +0100 Subject: [PATCH 346/518] fix events --- ro_py/assets.py | 4 ++-- ro_py/groups.py | 8 ++++---- ro_py/users.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index 9a2277fe..e635ecd5 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -128,9 +128,9 @@ def __init__(self, cso, asset): self.cso = cso self.asset = asset - async def bind(self, func, event, delay=15): + def bind(self, func, event, delay=15): if event == self.cso.client.events.on_asset_change: - await asyncio.create_task(self.on_asset_change(func, delay)) + return asyncio.create_task(self.on_asset_change(func, delay)) async def on_asset_change(self, func, delay): await self.asset.update() diff --git a/ro_py/groups.py b/ro_py/groups.py index 4a37442e..9567722a 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -334,7 +334,7 @@ def __init__(self, cso, group): self.cso = cso self.group = group - async def bind(self, func: Callable, event: EventTypes, delay: int = 15): + def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -348,11 +348,11 @@ async def bind(self, func: Callable, event: EventTypes, delay: int = 15): How many seconds between each poll. """ if event == EventTypes.on_join_request: - return await asyncio.create_task(self.on_join_request(func, delay)) + return asyncio.create_task(self.on_join_request(func, delay)) if event == EventTypes.on_wall_post: - return await asyncio.create_task(self.on_wall_post(func, delay)) + return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: - return await asyncio.create_task(self.on_group_change(func, delay)) + return asyncio.create_task(self.on_group_change(func, delay)) async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() diff --git a/ro_py/users.py b/ro_py/users.py index ea277d38..daad05ff 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -187,7 +187,7 @@ def __init__(self, cso, user): self.cso = cso self.user = user - async def bind(self, func: Callable, event: str, delay: int = 15): + def bind(self, func: Callable, event: str, delay: int = 15): """ Binds an event to the provided function. @@ -201,7 +201,7 @@ async def bind(self, func: Callable, event: str, delay: int = 15): How many seconds between requests. """ if event == EventTypes.on_user_change: - return await asyncio.create_task(self.on_user_change(func, delay)) + return asyncio.create_task(self.on_user_change(func, delay)) async def on_user_change(self, func: Callable, delay: int): old_user = copy.copy(await self.user.update()) From 9e0b995010dd02e9fa182174dea4f469d8ee3452 Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 26 Jan 2021 13:18:36 +0100 Subject: [PATCH 347/518] fix events --- ro_py/assets.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index e635ecd5..9866c62f 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -138,7 +138,11 @@ async def on_asset_change(self, func, delay): while True: await asyncio.sleep(delay) await self.asset.update() - if old_asset.updated < self.asset.updated: + has_changed = False + for attr, value in old_asset.__dict__.items(): + if getattr(self.asset, attr) != value: + has_changed = True + if has_changed: if asyncio.iscoroutinefunction(func): await func(old_asset, self.asset) else: From 846bec4533d798511f2052d5b7b9ff7d28ce4f60 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 26 Jan 2021 09:56:46 -0500 Subject: [PATCH 348/518] small header + accinfo/accset patch --- resources/header_small.png | Bin 0 -> 10405 bytes ro_py/accountinformation.py | 9 +++++---- ro_py/accountsettings.py | 9 +++++---- 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 resources/header_small.png diff --git a/resources/header_small.png b/resources/header_small.png new file mode 100644 index 0000000000000000000000000000000000000000..9d9b79c41f55ea5628c6a29b456972f1ccb3f520 GIT binary patch literal 10405 zcmb7qWm{ZL(=84`2Y0s#7TmdU2@b)7I}9?o1_ zpi>mT-zG>lk}8sLaCHe7PiDw(^LJnceK$Bb?7n{&JRwjq2o4S?{82_y%iH)g7bAy4 zJ9X&1LKT68x?mf9ydO0Z#7!EERECO1!$fL~!3K+=;V9xjgDGjIEuk!dp-(B36xZe=o`TOrHjnZps{a(mo8erHMfJv#BH`t1ABQQq}|SNoAp_}S6X z_s2E2bD;w=Sq*~!M?l6F$TDiUccdGHl8 z;Tsl+!$@vWqv!fagYeq@3u7mS^((uu!wDG=X|@P0-YbBLRO+7HxbIfgK^kK)0;YG( zjr31|kMJLU?xTQEne+lT-RZAJ57mQ|Lk4Nf@COsH9fSEJ_?!p2DMGLKPHK8K#LUq5 zQS>-Y0r_GlEIhMN%vZyI`12?XXq3F_7Q3n2KSXyf> z#SIugsO`iG>o*#o2=akj91Y(z`G80_olzLYJ{oR%rb$G!OAlMZFX)hN55sj`a0xCL^at|y9xxB0Hc!`TkVlPi=^k-KYxuWI%1Luz;UrV%N0L$aroAEjR@_AMVLYb*r>a1Auqg>3@!BjdXLf0U zi1J-{ebx+kBg{Hm+N6m-=SR6pJ(uDON6u0{AIh8F<(TB9^*ogz=2|P5pp*C8gjvP* zLA<@_7ZYAZ_NTS`H^4_p-l?H9x+}k&CC&CBf2z_!DEDW3%r~jyK z4R$nSv2;e_?(dq{k=;;=q#LEyW5fL@$<9uTq#x)y`i|>PWw*e@#k)(RXT36=Z)#sl zX2#eJsv8K=JEpcA5Mla`(u4j$-!({Fn?3yy!HHr;9ywHK-Q#ZY>V!6mU#krLdY>D9 zN}vbC?-SQ~GdJohi?8l%UF!yZwpx^wNgI85nd>5fi!rmf`=jQF9hh-{{@Y3trc<80 zVrXuWAsS;Byb7#UN2z9elUpQlXo-DA3pZ!-GxfV;aIeDeXAqhv-{_n_A37&l~{}tSKTOG8K)v5xaNx2ZjHRS z;gIG$1`*a%W@(qX1b%k^b_wVwxVy7xXTyX12RZoPK2=q;21$s{T&&o;wfgs_rg6fQ^xn1u#ye5gjjmfYZ9kwBTy;_G6FZLz#?_+XJPRsAsk5w zTWyHC!}*pkCx&{wl#BlRD`X7#-!+;N(WkZUv0{Msd?_KM(y_-mPQO13z>VJXI_UYT zN|eMgrKkzPr}X2=M^lm2m~4G(Z=7}NoXYHoDma} znvHpjCy?Z{9A)aar2_{x$f}j(VBk>z-(CcjPP58bG?IPuOPhg+fXcTWx@3$CENZmd zA>x+p$0Z|Zby~bE!sWfq$$DHLgbzB788b37WhQ!?^Gka6C(a$NUz3tX2CneeM{Zk! z2Tr#yn2%<|)lxxM%Fnascv#;VMb_8}P31|;34`H0=*vKK1OjLf$c>#1R2GayFvk|;GJ zoSRHOz7UlPtg?@@wR5l0y85R()~$$FrG|Z7@Ct8VQRjhw-~2%?pNoV;0W)$W-9RMW ziCSA4WkQPg9R({Tb!gIYOL*$vUb1sIB@6Nv*dp4?7%zf~viaor@uwdE zTuWOGU#P|RIxPz9i7%xlG=5 z%M?n7Vhk_y%^C~CuuWVLau@x|emJ2iSyJ=f=0qwHfKKHHSsP^*;Ow|@wzBhxwaIh$ z^zWqbg7Ek4ZWILisW}M}ObRS-GwCHRVHJYTv#<=}I@S>5KMCqNcx6m*;^%>MG0a5X z0f$euz^YWMw9EO{grY63CB}B$g6n&Q#(dHy{KVx7CBwRui}izMPc{w@+9unQj-Kb5 zI$WN^C>@*v;;B)sHfL*u|C~QK9Fl5JNwZ-z%gfu-PQ9;`Yx0cDjL~g7w0WIwM zG!#=#5jCk?rlV~Ap}{sDWtp#6=C>3tYTe||wjLf3ZNmI4e#Ne&QY?uJ&EDqW6(6u$ zvUS?vni%)kV*JrFn8XM1c0KBO4SW*)a@MR7aTa||h*-bh(2cn8ql%J!@$L-KKPN;r#t%&R5p5@lOCyLi{{yWTdB0{3vOuE_Wlidp1Tg#~$ zFD^~!J{Iey+S@Tdsqt;cj<;$e`bAu5!9Y=OCXrR?MeLX4Cm} zb*@W{J#3S1$v7RIg=w~ev?D%*kbnI?QoBM8=B%^xxn0gj{#9(!&EO~TQpu-uuAG=v>bUh9 zR((&n*A3w(6HNF`JQU~*PmGJ)nS+G3-cFB*`wamNXCq>Y*d@(L@ST|7!9Dp3&;8mx zdD~3*UA#yOKb<$VHFJ4o3x;l?ibX)}KP-{n6K(?o?wO|5uke?ms$*DmD*HbE*JC_z z2e@2!MN8v(Zk?{UY(s8;+XSjPGm+S@iCn&g^?&5JXn3W>3N)YX5D~qdV_dF<^~9?l z80TX28|?HR`*9CgIOoaEg z6w-b{9;sJ^vpXFyRFU5Ec>exO5S(#B%=6PE_iw+{bH4I{Uh9ltQi%MxUAqw z8g?9e%(+MlV4iBxjx{2k2;-Vr2z!lc=A-}gFAL7IjH@11^f}ZIK+zfTE4Cj&abqSk zE(?Y_XOxA33YVDGHuGToKh+@H9aILn(4>y;%j%L0h}5CSy#vf7ZD0r86>f(`Q$HW2 z6C91dG!Fjim^@ zOKcp-{kqZIZyI|xp!hv9@fqG*jU=er+AAvYHAQcN(d7Z?CKm`grXjU2(tw2lvPKS77L8!~x|Zx|=@!@{q@ zni0o=m}X%1M}`$Yr_IrUQd=WE6jopzaC^4a{P)`4(fb46vzwk%YbmDVjP|w~m&i!a zaXWplAC7es31ZoJ1=~S7Ntu_c3(tO&vemKx$BI5p7mjwb%e)8!woL3bjdEBm>EU1p zI<0<$EAH5AV!YS3{2GRC1!dTAv}i(dO-ILiC&!?bDWc}ZqX~!1}9`%00(<+Ab z_<*GMwruNE3-&QhsuraHh%OC-SFw9B1-j(QJAw5sn7v7#c=;Y50S=efc8BJ;(YBz8~O|K?MU{G?_&CNuv3{4my&jyNUQ z8|eJ|IO|jHuWu@BR+1jx)?&U!ti=XK=8QuIjhA3Yn!MrnmYFYkpiG#0&l*{K(l~8L zvp#lRo3cCp($?e_XWsB&-M&Km*yQ{;6jP4Ja1nfPP?k3+ik*oT87a^FiQ`1mPv(2{ za&zUS#jgon9r8HiKXTuVQVN?(%BZKH5W`1dCAD@z(6w%Ro+8yfYhF4VrJCUqca4Mh zjKJN_%~>rs)1NCAeFHx&hICB1(JL`8?jLT zWaIhmp9*SMn%Scp6u4WRJHD@$LXBxee>leKuMgYQ4f*<)4T)B)sJ3m%p(TPWw|NCv zp8AX_^jOLj$*h{h0N!N^P!8^*eGG~I`VwFHcElX_Cr#sh@d448eMa4V2iZ;esN3Wr zk0KXVTfHyrTg^Q?nzbO8OG!Et+=di>DK zy_9~|4n4g!9HT(5jCRu8pO7Yg)e}i;R!fAXY2U)`%1OR!Njw4MWM8EW=kwcK*!%e;RVHFXHU-fl?_YZPi|6O5oRx=~cH=Qwu^iIt z+Dt2cO|3>EK7aW8M=k#$Us({R>9j{qHjb-K=$6yogJYv3(mJ){e0Yp(D^w{@23-6W zjcs+pLA7R1{~Svj9yf{308O_hQtK<@kGY?9yFbi_liv^C=G~tqb@@=P4@}mGd~r(M zXJLZBma%v0_|Gl1HQIqmm1@vO%zxcboRwnJ7W20LdDgfM#sn&*j{_-Y&R&MF0XWhy zBkC0QS5C$XPQ_TN;@&k_?|S||b&s&H&+qzzzj$U*9f;dpS?G`;p%8!|`5-t31wh^n z-pbBW$4m7pOPB~zrz~OtUh9$?{}gvW0=gQMdWbW%bJ8W(E5O*39%z*TJZ#QW#?~>) z2vJ5UxUQ#2uM&oOu-jcqrL_i)P%SzZ@omm||yn_@&g!=S`4 zyC>TpCXl9-NSnocU{;@%m=zR>t7^SBbLl{`s4B679PEmRw8#Bqj#JE~k4W2E+vN6M z%su=H->~i{|GIfV%zmla(6D24K!^7|46~wgGXW;2tl2p6d0^xKzt2FQs{f-n;8Xye z`Dd!6bQV!ZGql3QDXMv4CsW(oLtp7NzoeuI;_bWZ+50{T}zSClHi%X0Rz=~<8VqlVUa9!l0L&HFO9 zjmTqZh4b7G#QmjU{8B+@++6;y=W3p+E?jNe{dsSXI6fbe!2HG# zgijsmZuX@19xx=FeQu9YuqG}bupflX+w~~F@HdRga?!J8qSNeRIBhM2YGUR7As-{b zG7>3j8}?0QC$TM9?-tu=J}Tc*^uvQPB3=2`Let%sa*_>?t3WLp0~8`{{INP9wRC0( zx4!~E=%@OB6lY9`NOr^yT_lS+Fa#q1qD9J|NK3ML@y(SX{OJgq@l+kow~ornZ!mV= zeZp_|CfTI|offW#ek@(5B%k1cxxV5q)JsoD!*22;?Xe_%U1(^(7@_iHJto)c5E-EB zzL9AR&40Mb9HG2-akcw0y3R|qgu|@3(a16}oeuGn^Gb)PKrx`&*2dI_<`3Uf6E9j` zu*trpZ)Mjnr%Doi1*sc;Vw`Py?CxJ1aH%6}twKJqH#FaI75A5Y!^BljZ4fS}Ad6K+ za{R&eRF7LoxTn<7W!W&gGJVouI$JN#CcSG+L0ep^$|z`wxL;A9Cd$O)zjP>?ge< zhZmo6YeifkcO!;hr%VheYe5=grTm3n%n{w=P3_Z#UkW!5^4!@HQ8Hdh(5n*HW{Uem zTHa+^8xLanqbO>34(jnPKcRl)B3`8VvWChNHYxB{F0czWSlPx3sQM0>cejc;j0o9H zZHzBunGJ7Ny|J6olnm%y@=G%ht612vsyt^0AXB>q`$>mz|k@ z4potBZl@-w;%e$(oWm7lGRyv~zB zt;hoPN;Dcb3Sem~{ON2)QuaZ!Y9So*Hp4clgbEQ_qm2ieK@{ntYqJ>_PT%x1OF2H5lQw?j2GJh`{t@i~a zh=vR$91n3<;)?^Gwcxj1^4+_R;rxr2X($}T#b)& zb8B~xCr$bpK{>E4;3_39cIg+uUbnh!>!oi!Z5Q#L90Wr{P*gX{8?17hP{vM$6c>O8 zZP8tf@lKRME?KV2sCJN^420mxRJ%!i@t*!GWjOj+^z3fi^N?V$Nf=!4!FNWR=b;bf zTm|Sb^ZhqlU9S+)b{&6}P8f*jXQ9KTftk6^6o)b*#KT=IaOaa{TPshMb+D3s%B(lm zs|~;O_PeA;WbRQirIx|)L4!aq-hF2Ie=OgJ9TNH<^K}5E{SGWqcLKL}(+MBB;dDmc)k-PO%R6)8Y(?jhFCU?auR5d0G02ghabCnZ{JFHYeANnxQt|3|xoMw-#}qo`bhEU?)7^V|FKLD(ojWtP z&Vy@`gH;z3F?Tm}(aMIAMJo9EQ83Ve@ikKFZqBP12IcD=HHQ{VL$;Rw>ga5&i!t-f zl0=#N;p_mWq_(ntDUbHqB&Xb-M7rzOE!}+(gnf65CPkf#4gd0kkLSV|bLN==op{y! z_Sp3fOFW&3a>p1N%&?vyC!)J4;uwN8Uj~DDh=*Nq7}75tXObrqB(^S6AI5**`5(8+ z6gg7xV7G)c+as_!WUa36K`=pF^8O-0*IwdRIdK+ob7D(0?!J@xPBS~Yk#{Q!vfuRt zKms+Qoc0fn0arrL7x9mS|U7~E1hgSKX8V&q#{#a1}! z*;yCxeOzas5N!mxt9y&*c`ZUl9*kJc^T{l1H40GQZqZgi>L`~a3N6yx7@tEbXTT*6IsL4uxqZ3Lawd#a4E;*maBGbDX5f#a z+jCqD%a}LHBI**e5O-DTo@*u*X4oR*M-^{zR=N#-I*|I@)%g_P;$PHlvL`n!tVE`3 z1ewk2gd}(B3l;x=wktgG3T+4{c@Gk5>vm8IJ0$zX*{b} zFkHc`UG0wu{~2#CG&Jy{RzL|Eb-xA0VnQ+2lDerE$F6E%93FMe<04V7K)q=0+Ny$VlB2aL6!!a!!^KGX=F<` zu*zY8a>B>-2UBSJHMU{8!LGSdtTM8t;9C)rB7+|LvbK>$gXPbPv{!w@0R$u-SKlNY z9@~@WP&6D*{8E8kKz7Qu79IRw)mYOgX=Z#eYHj=JFw9&qm z#fi`f9rftP#IR!311f8RO)U#gkR_wqDeP1LNf8&qonE~Tax*9r63snvZsblqb?LWv z6Fc|qMkPW`v8B?s(#*_Jh-aG+&!dmJLU`UGKI?bh6S= zW{^Ko_I01Zf@tLNWq@HqTi)ZcLMIj$76O-`E0Ir>E$#5dUEG(9)!hn)+fj+*A$ZFJN||~18p$xwuBxvRzsWZ8u|#B z256hn={T73rO)Fd3`VG|25K<4_K~5X(KWl?T1RdWvL-WK%A%yD$x$zVXBgeXdpkjZ zreMVGSP}bSC?;sr&o%PM4q`7a3^N`tr=dy@T^K4bL$VYE5U7bd`}xZb6@W)#g0DEx z6o(|^xPz{3LkL|h+-cm8b4`W8N|QJVn7uqWl7?lQaxv{inMIaEf_Q*3h6>M8iD-Fd zXul5(JA`snuE6a7+Zl3FpGd6;?>85Dhfg^6ZyF2>Ffa|n{103!)!cWmfm{cS)4Q1{ zFGC{et6S!bBw+d*dbBC6r2WPZ}nU|hCl2Sfx58E3H=LrgTd%|y{6jY1CQ1iJe< zh!3MN*s(Z3e%xJ;rs^)mTuyH=0g#UX0s}D}@Kp6V}Zd4NXYOS#+}eP*_kEce`2t0^<&*-+bpC5yZLh83wN-zh?c@LlW7N zfCl(kp>W3Gv|yYwgeWx`Rm2Wypxi7GMnwM|KaRZ-0{r3~pDxWyHT(jT zKpwzF8`A+m#9Tr+za<6t709LE#7EH7L>*9~v0BQy)AT_3Ouky?V8OSlmu0HSN4$Lo zX|Z7|u?BxyT7vhzEn*RZCOiN?3%@3Uq)|ehf2cq?q5$G?6GAwbFgfvNQK-cev^35= zk5vOTv$QJOe*VDdBFkWO1q2Col92kiNCJAC59nhAXz0UH5f+22Ihd>^1JtYQAncIH zRmM+<`EGB`D67Q?$p`>~p@I&q^cKQkSHP}WU}V2vEmjDbY+f|ek9>?Hnn*18xWl7z zprS<)LNa_(!Aa0~r?X|@L}-v94tys0bJV}4p+W>rL>eISd*$WczlaHU9!;|*|C-PK h@5AcJY~mHs(;>t8>8`cmt+f#DqpYe7RLV5?{{c-}!z=&* literal 0 HcmV?d00001 diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 6ba8a1e0..463d3cbd 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -53,11 +53,12 @@ class AccountInformation: Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests self.account_information_metadata = None self.promotion_channels = None diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 264bb3ad..6f4eae51 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -51,11 +51,12 @@ class AccountSettings: Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests def get_privacy_setting(self, privacy_setting): """ From 0c5850e64825784e150dbe4ba799ccccbbc89da2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 26 Jan 2021 10:24:17 -0500 Subject: [PATCH 349/518] Update accountsettings.py --- ro_py/accountsettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 6f4eae51..4a7c238a 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -58,7 +58,7 @@ def __init__(self, cso): self.cso = cso self.requests = cso.requests - def get_privacy_setting(self, privacy_setting): + async def get_privacy_setting(self, privacy_setting): """ Gets the value of a privacy setting. """ @@ -80,5 +80,5 @@ def get_privacy_setting(self, privacy_setting): "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.requests.get(privacy_endpoint) + privacy_req = await self.requests.get(privacy_endpoint) return privacy_req.json()[privacy_key] From b628d057053a526efd6f004a03a61735e37f1359 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 26 Jan 2021 10:37:19 -0500 Subject: [PATCH 350/518] Updated version identifier --- setup_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index 151c3347..ecfcfea2 100644 --- a/setup_info.py +++ b/setup_info.py @@ -5,7 +5,7 @@ setup_info = { "name": "ro-py", - "version": "1.1.1", + "version": "1.1.2", "author": "jmkdev and iranathan", "author_email": "jmk@jmksite.dev", "description": "ro.py is a Python wrapper for the Roblox web API.", From cb66f6739ffc90874114bc7707f4dcdaafbceeee Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 26 Jan 2021 10:40:12 -0500 Subject: [PATCH 351/518] added link to devforum --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 91436fdc..8775a079 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Welcome, and thank you for using ro.py! ro.py is an object oriented, asynchronous wrapper for the Roblox Web API (and other Roblox-related APIs) with many new and interesting features. ro.py allows you to automate much of what you would do on the Roblox website and on other Roblox-related websites. +## Get Started +If you are looking for a full tutorial on ro.py, check out [the new DevForum article!](https://devforum.roblox.com/t/use-python-to-interact-with-the-roblox-api-with-ro-py/1006465) + ## Requirements - httpx (for sending requests) - iso8601 (for parsing dates) From cd088eb3f5c1b2f0b2ceb2f7c3077634b5ac748a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 26 Jan 2021 12:18:57 -0500 Subject: [PATCH 352/518] new resource --- resources/ro.py modern test.pdn | Bin 0 -> 51113 bytes resources/ro.py modern test.png | Bin 0 -> 34841 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/ro.py modern test.pdn create mode 100644 resources/ro.py modern test.png diff --git a/resources/ro.py modern test.pdn b/resources/ro.py modern test.pdn new file mode 100644 index 0000000000000000000000000000000000000000..8b61df4b43c061d7281414cecabbcef09043321b GIT binary patch literal 51113 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%0A;#_$nY;p!anVP|=X`%SnnB)wb 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 G;(q}Ny_!n^ literal 0 HcmV?d00001 diff --git a/resources/ro.py modern test.png b/resources/ro.py modern test.png new file mode 100644 index 0000000000000000000000000000000000000000..8bfdf78da81646a8c759c3c6fc7baef18f403ad8 GIT binary patch literal 34841 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 XpF8l_ZOtz&lsDJaf@>CCvJUw_y(8Vd literal 0 HcmV?d00001 From 0b663d3022b85267fd5680605cc84528572f6ccf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 27 Jan 2021 11:23:21 -0500 Subject: [PATCH 353/518] =?UTF-8?q?Bots=20is=20getting=20some=20love=20?= =?UTF-8?q?=E2=99=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/extensions/bots.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 0711ed60..038b96d1 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -7,6 +7,8 @@ from ro_py.utilities.errors import ChatError from ro_py.client import Client from ro_py.chat import Message +from sys import stderr +from time import sleep import asyncio import iso8601 @@ -41,26 +43,19 @@ async def send(self, content): ) send_message_json = send_message_req.json() if send_message_json["sent"]: - return Message(self.requests, send_message_json["messageId"], self.id) + return Message(self.cso, send_message_json["messageId"], self.id) else: raise ChatError(send_message_json["statusMessage"]) class Bot(Client): - def __init__(self, prefix="!", auto_help=True): + def __init__(self, prefix="!"): super().__init__() self.prefix = prefix self.commands = {} self.events = {} self.evtloop = asyncio.new_event_loop() - - if auto_help: - command_help = self._generate_help() - - @self.command(a=0) - async def command_help_func(ctx): - print("HEA") - await ctx.send(command_help) + self.keepgoing = False def _generate_help(self): help_string = f"Prefix: {self.prefix}" @@ -69,10 +64,13 @@ def _generate_help(self): return help_string def run(self, token): + self.keepgoing = True self.token_login(token) self.notifications.on_notification = self._on_notification self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) + while self.keepgoing: + sleep(1/32) async def _process_command(self, data, n_data): content = data["content"] @@ -87,9 +85,13 @@ async def _process_command(self, data, n_data): notif_data=n_data ) try: - await self.commands[command](context) + await self.commands[command](context, *content_split[1:]) except Exception as e: - await context.send("Something went wrong when running this command.") + if "on_error" in self.events: + await self.events["on_error"](context, e) + else: + stderr.write("Ignoring error: " + str(e) + "\n") + await context.send("Something went wrong when running this command.") async def _on_notification(self, notification): if notification.type == "NewMessage": @@ -106,7 +108,7 @@ async def _on_notification(self, notification): async def _run(self): await self.notifications.initialize() - def command(self, _="_", **kwargs): + def command(self, *args, **kwargs): def decorator(func): if isinstance(func, Command): raise TypeError('Callback is already a command.') @@ -116,6 +118,14 @@ def decorator(func): return decorator + def event(self, *args, **kwargs): + def decorator(func): + command = Command(func=func, **kwargs) + self.events[func.__name__] = command + return command + + return decorator + class Command: def __init__(self, func, **kwargs): From 1014a43bdef9a7a928f391173dc49f27280cf0f6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 27 Jan 2021 13:29:34 -0500 Subject: [PATCH 354/518] Docs updated --- docs/accountinformation.html | 24 +- docs/accountsettings.html | 38 ++- docs/assets.html | 91 +++++- docs/chat.html | 60 ++-- docs/client.html | 113 ++++--- docs/events.html | 22 +- docs/extensions/bots.html | 379 ++++++++++++++++++--- docs/extensions/twocaptcha.html | 21 +- docs/games.html | 7 +- docs/groups.html | 290 +++++++++------- docs/index.html | 4 +- docs/notifications.html | 163 +++------ docs/thumbnails.html | 14 +- docs/trades.html | 152 +++++---- docs/users.html | 563 +++++++++++++++++++++++--------- docs/utilities/requests.html | 6 +- docs/wall.html | 25 +- 17 files changed, 1316 insertions(+), 656 deletions(-) diff --git a/docs/accountinformation.html b/docs/accountinformation.html index cbb23870..5cd7aadc 100644 --- a/docs/accountinformation.html +++ b/docs/accountinformation.html @@ -109,11 +109,12 @@

        Module ro_py.accountinformation

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests self.account_information_metadata = None self.promotion_channels = None @@ -202,15 +203,15 @@

        Classes

        class AccountInformation -(requests) +(cso)

        Represents authenticated client account information (https://accountinformation.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise.

        Parameters

        -
        requests : Requests
        -
        Requests object to use for API requests.
        +
        cso : ClientSharedObject
        +
        ClientSharedObject.
        @@ -223,11 +224,12 @@

        Parameters

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests self.account_information_metadata = None self.promotion_channels = None diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 8c0e4a2f..9cfe5578 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -107,13 +107,14 @@

        Module ro_py.accountsettings

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests - def get_privacy_setting(self, privacy_setting): + async def get_privacy_setting(self, privacy_setting): """ Gets the value of a privacy setting. """ @@ -135,7 +136,7 @@

        Module ro_py.accountsettings

        "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.requests.get(privacy_endpoint) + privacy_req = await self.requests.get(privacy_endpoint) return privacy_req.json()[privacy_key]
        @@ -150,15 +151,15 @@

        Classes

        class AccountSettings -(requests) +(cso)

        Represents authenticated client account settings (https://accountsettings.roblox.com/) This is only available for authenticated clients as it cannot be accessed otherwise.

        Parameters

        -
        requests : Requests
        -
        Requests object to use for API requests.
        +
        cso : ClientSharedObject
        +
        ClientSharedObject.
        @@ -171,13 +172,14 @@

        Parameters

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests - def get_privacy_setting(self, privacy_setting): + async def get_privacy_setting(self, privacy_setting): """ Gets the value of a privacy setting. """ @@ -199,13 +201,13 @@

        Parameters

        "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.requests.get(privacy_endpoint) + privacy_req = await self.requests.get(privacy_endpoint) return privacy_req.json()[privacy_key]

        Methods

        -def get_privacy_setting(self, privacy_setting) +async def get_privacy_setting(self, privacy_setting)

        Gets the value of a privacy setting.

        @@ -213,7 +215,7 @@

        Methods

        Expand source code -
        def get_privacy_setting(self, privacy_setting):
        +
        async def get_privacy_setting(self, privacy_setting):
             """
             Gets the value of a privacy setting.
             """
        @@ -235,7 +237,7 @@ 

        Methods

        "privateMessagePrivacy" ][privacy_setting] privacy_endpoint = endpoint + "v1/" + privacy_endpoint - privacy_req = self.requests.get(privacy_endpoint) + privacy_req = await self.requests.get(privacy_endpoint) return privacy_req.json()[privacy_key]
        diff --git a/docs/assets.html b/docs/assets.html index c7d8788d..2ee54958 100644 --- a/docs/assets.html +++ b/docs/assets.html @@ -63,6 +63,8 @@

        Module ro_py.assets

        from ro_py.economy import LimitedResaleData from ro_py.utilities.asset_type import asset_types import iso8601 +import asyncio +import copy endpoint = "https://api.roblox.com/" @@ -83,6 +85,7 @@

        Module ro_py.assets

        self.id = asset_id self.cso = cso self.requests = cso.requests + self.events = Events(cso, self) self.target_id = None self.product_type = None self.asset_id = None @@ -177,11 +180,30 @@

        Module ro_py.assets

        class Events: - def __init__(self, cso): + def __init__(self, cso, asset): self.cso = cso + self.asset = asset - async def bind(self, func, event, delay=15): - pass
        + def bind(self, func, event, delay=15): + if event == self.cso.client.events.on_asset_change: + return asyncio.create_task(self.on_asset_change(func, delay)) + + async def on_asset_change(self, func, delay): + await self.asset.update() + old_asset = copy.copy(self.asset) + while True: + await asyncio.sleep(delay) + await self.asset.update() + has_changed = False + for attr, value in old_asset.__dict__.items(): + if getattr(self.asset, attr) != value: + has_changed = True + if has_changed: + if asyncio.iscoroutinefunction(func): + await func(old_asset, self.asset) + else: + func(old_asset, self.asset) + old_asset = copy.copy(self.asset)
        @@ -226,6 +248,7 @@

        Parameters

        self.id = asset_id self.cso = cso self.requests = cso.requests + self.events = Events(cso, self) self.target_id = None self.product_type = None self.asset_id = None @@ -428,7 +451,7 @@

        Returns

        class Events -(cso) +(cso, asset)
        @@ -437,16 +460,49 @@

        Returns

        Expand source code
        class Events:
        -    def __init__(self, cso):
        +    def __init__(self, cso, asset):
                 self.cso = cso
        +        self.asset = asset
        +
        +    def bind(self, func, event, delay=15):
        +        if event == self.cso.client.events.on_asset_change:
        +            return asyncio.create_task(self.on_asset_change(func, delay))
         
        -    async def bind(self, func, event, delay=15):
        -        pass
        + async def on_asset_change(self, func, delay): + await self.asset.update() + old_asset = copy.copy(self.asset) + while True: + await asyncio.sleep(delay) + await self.asset.update() + has_changed = False + for attr, value in old_asset.__dict__.items(): + if getattr(self.asset, attr) != value: + has_changed = True + if has_changed: + if asyncio.iscoroutinefunction(func): + await func(old_asset, self.asset) + else: + func(old_asset, self.asset) + old_asset = copy.copy(self.asset)

        Methods

        -async def bind(self, func, event, delay=15) +def bind(self, func, event, delay=15) +
        +
        +
        +
        + +Expand source code + +
        def bind(self, func, event, delay=15):
        +    if event == self.cso.client.events.on_asset_change:
        +        return asyncio.create_task(self.on_asset_change(func, delay))
        +
        +
        +
        +async def on_asset_change(self, func, delay)
        @@ -454,8 +510,22 @@

        Methods

        Expand source code -
        async def bind(self, func, event, delay=15):
        -    pass
        +
        async def on_asset_change(self, func, delay):
        +    await self.asset.update()
        +    old_asset = copy.copy(self.asset)
        +    while True:
        +        await asyncio.sleep(delay)
        +        await self.asset.update()
        +        has_changed = False
        +        for attr, value in old_asset.__dict__.items():
        +            if getattr(self.asset, attr) != value:
        +                has_changed = True
        +        if has_changed:
        +            if asyncio.iscoroutinefunction(func):
        +                await func(old_asset, self.asset)
        +            else:
        +                func(old_asset, self.asset)
        +            old_asset = copy.copy(self.asset)
        @@ -530,6 +600,7 @@

        AssetEvents

      • diff --git a/docs/chat.html b/docs/chat.html index cfb43d2c..5705552c 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -61,7 +61,7 @@

        Module ro_py.chat

        """ from ro_py.utilities.errors import ChatError -from ro_py.users import User +from ro_py.users import PartialUser endpoint = "https://chat.roblox.com/" @@ -112,7 +112,7 @@

        Module ro_py.chat

        data = raw_data self.id = data["id"] self.title = data["title"] - self.initiator = User(self.cso, data["initiator"]["targetId"]) + self.initiator = data["initiator"]["targetId"] self.type = data["conversationType"] self.typing = ConversationTyping(self.cso, conversation_id) @@ -126,7 +126,7 @@

        Module ro_py.chat

        data = conversation_req.json()[0] self.id = data["id"] self.title = data["title"] - self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.initiator = await self.cso.client.get_user(data["initiator"]["targetId"]) self.type = data["conversationType"] async def get_message(self, message_id): @@ -153,15 +153,16 @@

        Module ro_py.chat

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. message_id ID of the message. conversation_id ID of the conversation that contains the message. """ - def __init__(self, requests, message_id, conversation_id): - self.requests = requests + def __init__(self, cso, message_id, conversation_id): + self.cso = cso + self.requests = cso.requests self.id = message_id self.conversation_id = conversation_id @@ -184,7 +185,7 @@

        Module ro_py.chat

        message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.requests, message_json["senderTargetId"]) + self.sender = PartialUser(self.cso, message_json["senderTargetId"]) self.read = message_json["read"] @@ -193,8 +194,9 @@

        Module ro_py.chat

        Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right of the Roblox web client. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests async def get_conversation(self, conversation_id): """ @@ -223,7 +225,7 @@

        Module ro_py.chat

        conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.requests, + cso=self.cso, raw=True, raw_data=conversation_raw )) @@ -257,7 +259,7 @@

        Classes

      • class ChatWrapper -(requests) +(cso)

        Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right @@ -271,8 +273,9 @@

        Classes

        Represents the Roblox chat client. It essentially mirrors the functionality of the chat window at the bottom right of the Roblox web client. """ - def __init__(self, requests): - self.requests = requests + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests async def get_conversation(self, conversation_id): """ @@ -301,7 +304,7 @@

        Classes

        conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.requests, + cso=self.cso, raw=True, raw_data=conversation_raw )) @@ -360,7 +363,7 @@

        Parameters

        conversations = [] for conversation_raw in conversations_json: conversations.append(Conversation( - requests=self.requests, + cso=self.cso, raw=True, raw_data=conversation_raw )) @@ -394,7 +397,7 @@

        Parameters

        data = raw_data self.id = data["id"] self.title = data["title"] - self.initiator = User(self.cso, data["initiator"]["targetId"]) + self.initiator = data["initiator"]["targetId"] self.type = data["conversationType"] self.typing = ConversationTyping(self.cso, conversation_id) @@ -408,7 +411,7 @@

        Parameters

        data = conversation_req.json()[0] self.id = data["id"] self.title = data["title"] - self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.initiator = await self.cso.client.get_user(data["initiator"]["targetId"]) self.type = data["conversationType"] async def get_message(self, message_id): @@ -486,7 +489,7 @@

        Methods

        data = conversation_req.json()[0] self.id = data["id"] self.title = data["title"] - self.initiator = User(self.requests, data["initiator"]["targetId"]) + self.initiator = await self.cso.client.get_user(data["initiator"]["targetId"]) self.type = data["conversationType"]
        @@ -529,14 +532,14 @@

        Methods

        class Message -(requests, message_id, conversation_id) +(cso, message_id, conversation_id)

        Represents a single message in a chat conversation.

        Parameters

        -
        requests : Requests
        -
        Requests object to use for API requests.
        +
        cso : ClientSharedObject
        +
        ClientSharedObject.
        message_id
        ID of the message.
        conversation_id
        @@ -552,15 +555,16 @@

        Parameters

        Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.client.ClientSharedObject + ClientSharedObject. message_id ID of the message. conversation_id ID of the conversation that contains the message. """ - def __init__(self, requests, message_id, conversation_id): - self.requests = requests + def __init__(self, cso, message_id, conversation_id): + self.cso = cso + self.requests = cso.requests self.id = message_id self.conversation_id = conversation_id @@ -583,7 +587,7 @@

        Parameters

        message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.requests, message_json["senderTargetId"]) + self.sender = PartialUser(self.cso, message_json["senderTargetId"]) self.read = message_json["read"]

        Methods

        @@ -612,7 +616,7 @@

        Methods

        message_json = message_req.json()[0] self.content = message_json["content"] - self.sender = User(self.requests, message_json["senderTargetId"]) + self.sender = PartialUser(self.cso, message_json["senderTargetId"]) self.read = message_json["read"]
        diff --git a/docs/client.html b/docs/client.html index a82cbe30..f0c26f46 100644 --- a/docs/client.html +++ b/docs/client.html @@ -60,7 +60,6 @@

        Module ro_py.client

        """ -from ro_py.users import User from ro_py.games import Game from ro_py.groups import Group from ro_py.assets import Asset @@ -68,14 +67,15 @@

        Module ro_py.client

        from ro_py.chat import ChatWrapper from ro_py.events import EventTypes from ro_py.trades import TradesWrapper +from ro_py.users import PartialUser from ro_py.utilities.requests import Requests from ro_py.accountsettings import AccountSettings from ro_py.utilities.cache import Cache, CacheType +from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError from ro_py.captcha import UnsolvedLoginCaptcha - -import logging +import asyncio class ClientSharedObject: @@ -89,6 +89,8 @@

        Module ro_py.client

        """Cache object to keep objects that don't need to be recreated.""" self.requests = Requests() """Reqests object for all web requests.""" + self.evtloop = asyncio.new_event_loop() + """Event loop for certain things.""" class Client: @@ -106,9 +108,6 @@

        Module ro_py.client

        """ClientSharedObject. Passed to each new object to share information.""" self.requests = self.cso.requests """See self.cso.requests""" - - logging.debug("Initialized requests.") - self.accountinformation = None """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None @@ -117,19 +116,13 @@

        Module ro_py.client

        """ChatWrapper object. Only available for authenticated clients.""" self.trade = None """TradesWrapper object. Only available for authenticated clients.""" + self.notifications = None + """NotificationReceiver object. Only available for authenticated clients.""" self.events = EventTypes """Types of events used for binding events to a function.""" if token: self.token_login(token) - logging.debug("Initialized token.") - self.accountinformation = AccountInformation(self.cso) - self.accountsettings = AccountSettings(self.cso) - logging.debug("Initialized AccountInformation and AccountSettings.") - self.chat = ChatWrapper(self.cso) - logging.debug("Initialized chat wrapper.") - self.trade = TradesWrapper(self.cso, self.get_self) - logging.debug("Initialized trade wrapper.") def token_login(self, token): """ @@ -141,6 +134,11 @@

        Module ro_py.client

        .ROBLOSECURITY token to authenticate with. """ self.requests.session.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + self.chat = ChatWrapper(self.cso) + self.trade = TradesWrapper(self.cso, self.get_self) + self.notifications = NotificationReceiver(self.cso) async def user_login(self, username, password, token=None): """ @@ -202,7 +200,7 @@

        Module ro_py.client

        url="https://roblox.com/my/profile" ) data = self_req.json() - return User(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data['UserId'], data['Username']) async def get_user(self, user_id): """ @@ -215,9 +213,10 @@

        Module ro_py.client

        """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): @@ -245,9 +244,10 @@

        Module ro_py.client

        user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user else: raise UserDoesNotExistError @@ -382,9 +382,6 @@

        Parameters

        """ClientSharedObject. Passed to each new object to share information.""" self.requests = self.cso.requests """See self.cso.requests""" - - logging.debug("Initialized requests.") - self.accountinformation = None """AccountInformation object. Only available for authenticated clients.""" self.accountsettings = None @@ -393,19 +390,13 @@

        Parameters

        """ChatWrapper object. Only available for authenticated clients.""" self.trade = None """TradesWrapper object. Only available for authenticated clients.""" + self.notifications = None + """NotificationReceiver object. Only available for authenticated clients.""" self.events = EventTypes """Types of events used for binding events to a function.""" if token: self.token_login(token) - logging.debug("Initialized token.") - self.accountinformation = AccountInformation(self.cso) - self.accountsettings = AccountSettings(self.cso) - logging.debug("Initialized AccountInformation and AccountSettings.") - self.chat = ChatWrapper(self.cso) - logging.debug("Initialized chat wrapper.") - self.trade = TradesWrapper(self.cso, self.get_self) - logging.debug("Initialized trade wrapper.") def token_login(self, token): """ @@ -417,6 +408,11 @@

        Parameters

        .ROBLOSECURITY token to authenticate with. """ self.requests.session.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + self.chat = ChatWrapper(self.cso) + self.trade = TradesWrapper(self.cso, self.get_self) + self.notifications = NotificationReceiver(self.cso) async def user_login(self, username, password, token=None): """ @@ -478,7 +474,7 @@

        Parameters

        url="https://roblox.com/my/profile" ) data = self_req.json() - return User(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data['UserId'], data['Username']) async def get_user(self, user_id): """ @@ -491,9 +487,10 @@

        Parameters

        """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): @@ -521,9 +518,10 @@

        Parameters

        user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user else: raise UserDoesNotExistError @@ -644,6 +642,10 @@

        Instance variables

        Types of events used for binding events to a function.

        +
        var notifications
        +
        +

        NotificationReceiver object. Only available for authenticated clients.

        +
        var requests

        See self.cso.requests

        @@ -834,7 +836,7 @@

        Parameters

        url="https://roblox.com/my/profile" ) data = self_req.json() - return User(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data['UserId'], data['Username'])
        @@ -862,9 +864,10 @@

        Parameters

        """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user
        @@ -909,9 +912,10 @@

        Parameters

        user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) - await user.update() + user = PartialUser(self.cso, user_id) + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user else: raise UserDoesNotExistError @@ -940,7 +944,12 @@

        Parameters

        token : str .ROBLOSECURITY token to authenticate with. """ - self.requests.session.cookies[".ROBLOSECURITY"] = token + self.requests.session.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + self.chat = ChatWrapper(self.cso) + self.trade = TradesWrapper(self.cso, self.get_self) + self.notifications = NotificationReceiver(self.cso)
        @@ -1044,7 +1053,9 @@

        Returns

        self.cache = Cache() """Cache object to keep objects that don't need to be recreated.""" self.requests = Requests() - """Reqests object for all web requests."""
        + """Reqests object for all web requests.""" + self.evtloop = asyncio.new_event_loop() + """Event loop for certain things."""

        Instance variables

        @@ -1056,6 +1067,10 @@

        Instance variables

        Client (parent) of this object.

        +
        var evtloop
        +
        +

        Event loop for certain things.

        +
        var requests

        Reqests object for all web requests.

        @@ -1099,6 +1114,7 @@

        Client<
      • get_self
      • get_user
      • get_user_by_username
      • +
      • notifications
      • requests
      • token_login
      • trade
      • @@ -1110,6 +1126,7 @@

      • cache
      • client
      • +
      • evtloop
      • requests
    • diff --git a/docs/events.html b/docs/events.html index 02f637d2..a1e4c0a3 100644 --- a/docs/events.html +++ b/docs/events.html @@ -59,7 +59,9 @@

      Module ro_py.events

      class EventTypes(enum.Enum): on_join_request = "on_join_request" on_wall_post = "on_wall_post" - on_shout_update = "on_shout_update"
      + on_group_change = "on_group_change" + on_asset_change = "on_asset_change" + on_user_change = "on_user_change"
      @@ -84,7 +86,9 @@

      Classes

      class EventTypes(enum.Enum):
           on_join_request = "on_join_request"
           on_wall_post = "on_wall_post"
      -    on_shout_update = "on_shout_update"
      + on_group_change = "on_group_change" + on_asset_change = "on_asset_change" + on_user_change = "on_user_change"

      Ancestors

        @@ -92,11 +96,19 @@

        Ancestors

      Class variables

      +
      var on_asset_change
      +
      +
      +
      +
      var on_group_change
      +
      +
      +
      var on_join_request
      -
      var on_shout_update
      +
      var on_user_change
      @@ -130,8 +142,10 @@

      Index

    • EventTypes

    • diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index a4d8bfcf..05997337 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -60,14 +60,127 @@

      Module ro_py.extensions.bots

      """ - +from ro_py.utilities.errors import ChatError from ro_py.client import Client +from ro_py.chat import Message +from sys import stderr +from time import sleep import asyncio +import iso8601 + + +class Context: + def __init__(self, cso, latest_data, notif_data): + self.cso = cso + self.requests = cso.requests + + self.conversation_id = notif_data["conversation_id"] + self.actor_target_id = notif_data["actor_target_id"] + self.actor_type = notif_data["actor_type"] + self.type = notif_data["type"] + self.sequence_number = notif_data["sequence_number"] + + self.id = latest_data["id"] + self.content = latest_data["content"] + self.sender_type = latest_data["senderType"] + self.sender_id = latest_data["senderTargetId"] + self.decorators = latest_data["decorators"] + self.message_type = latest_data["messageType"] + self.read = latest_data["read"] + self.sent = iso8601.parse_date(latest_data["sent"]) + + async def send(self, content): + send_message_req = await self.requests.post( + url="https://chat.roblox.com/v2/send-message", + data={ + "message": content, + "conversationId": self.conversation_id + } + ) + send_message_json = send_message_req.json() + if send_message_json["sent"]: + return Message(self.cso, send_message_json["messageId"], self.id) + else: + raise ChatError(send_message_json["statusMessage"]) class Bot(Client): - def __init__(self): + def __init__(self, prefix="!"): super().__init__() + self.prefix = prefix + self.commands = {} + self.events = {} + self.evtloop = asyncio.new_event_loop() + self.keepgoing = False + + def _generate_help(self): + help_string = f"Prefix: {self.prefix}" + for command in self.commands: + help_string = help_string + "\n" + command + ": " + command.help[:24] + return help_string + + def run(self, token): + self.keepgoing = True + self.token_login(token) + self.notifications.on_notification = self._on_notification + self.evtloop = self.cso.evtloop + self.evtloop.run_until_complete(self._run()) + while self.keepgoing: + sleep(1/32) + + async def _process_command(self, data, n_data): + content = data["content"] + if content.startswith(self.prefix): + content = content[len(self.prefix):] + content_split = content.split(" ") + command = content_split[0] + if command in self.commands: + context = Context( + cso=self.cso, + latest_data=data, + notif_data=n_data + ) + try: + await self.commands[command](context, *content_split[1:]) + except Exception as e: + if "on_error" in self.events: + await self.events["on_error"](context, e) + else: + stderr.write("Ignoring error: " + str(e) + "\n") + await context.send("Something went wrong when running this command.") + + async def _on_notification(self, notification): + if notification.type == "NewMessage": + latest_req = await self.requests.get( + url="https://chat.roblox.com/v2/get-messages", + params={ + "conversationId": notification.data["conversation_id"], + "pageSize": 1 + } + ) + latest_data = latest_req.json()[0] + await self._process_command(latest_data, notification.data) + + async def _run(self): + await self.notifications.initialize() + + def command(self, *args, **kwargs): + def decorator(func): + if isinstance(func, Command): + raise TypeError('Callback is already a command.') + command = Command(func=func, **kwargs) + self.commands[func.__name__] = command + return command + + return decorator + + def event(self, *args, **kwargs): + def decorator(func): + command = Command(func=func, **kwargs) + self.events[func.__name__] = command + return command + + return decorator class Command: @@ -75,22 +188,14 @@

      Module ro_py.extensions.bots

      if not asyncio.iscoroutinefunction(func): raise TypeError('Callback must be a coroutine.') self._callback = func + self.help = func.__doc__ or "No help available." @property def callback(self): return self._callback async def __call__(self, *args, **kwargs): - return await self.callback(*args, **kwargs) - - -def command(**attrs): - def decorator(func): - if isinstance(func, Command): - raise TypeError('Callback is already a command.') - return Command(func, **attrs) - - return decorator + return await self.callback(*args, **kwargs)
      @@ -98,33 +203,13 @@

      Module ro_py.extensions.bots

      -

      Functions

      -
      -
      -def command(**attrs) -
      -
      -
      -
      - -Expand source code - -
      def command(**attrs):
      -    def decorator(func):
      -        if isinstance(func, Command):
      -            raise TypeError('Callback is already a command.')
      -        return Command(func, **attrs)
      -
      -    return decorator
      -
      -
      -

      Classes

      class Bot +(prefix='!')

      Represents an authenticated Roblox client.

      @@ -138,13 +223,147 @@

      Parameters

      Expand source code
      class Bot(Client):
      -    def __init__(self):
      -        super().__init__()
      + def __init__(self, prefix="!"): + super().__init__() + self.prefix = prefix + self.commands = {} + self.events = {} + self.evtloop = asyncio.new_event_loop() + self.keepgoing = False + + def _generate_help(self): + help_string = f"Prefix: {self.prefix}" + for command in self.commands: + help_string = help_string + "\n" + command + ": " + command.help[:24] + return help_string + + def run(self, token): + self.keepgoing = True + self.token_login(token) + self.notifications.on_notification = self._on_notification + self.evtloop = self.cso.evtloop + self.evtloop.run_until_complete(self._run()) + while self.keepgoing: + sleep(1/32) + + async def _process_command(self, data, n_data): + content = data["content"] + if content.startswith(self.prefix): + content = content[len(self.prefix):] + content_split = content.split(" ") + command = content_split[0] + if command in self.commands: + context = Context( + cso=self.cso, + latest_data=data, + notif_data=n_data + ) + try: + await self.commands[command](context, *content_split[1:]) + except Exception as e: + if "on_error" in self.events: + await self.events["on_error"](context, e) + else: + stderr.write("Ignoring error: " + str(e) + "\n") + await context.send("Something went wrong when running this command.") + + async def _on_notification(self, notification): + if notification.type == "NewMessage": + latest_req = await self.requests.get( + url="https://chat.roblox.com/v2/get-messages", + params={ + "conversationId": notification.data["conversation_id"], + "pageSize": 1 + } + ) + latest_data = latest_req.json()[0] + await self._process_command(latest_data, notification.data) + + async def _run(self): + await self.notifications.initialize() + + def command(self, *args, **kwargs): + def decorator(func): + if isinstance(func, Command): + raise TypeError('Callback is already a command.') + command = Command(func=func, **kwargs) + self.commands[func.__name__] = command + return command + + return decorator + + def event(self, *args, **kwargs): + def decorator(func): + command = Command(func=func, **kwargs) + self.events[func.__name__] = command + return command + + return decorator

      Ancestors

      +

      Methods

      +
      +
      +def command(self, *args, **kwargs) +
      +
      +
      +
      + +Expand source code + +
      def command(self, *args, **kwargs):
      +    def decorator(func):
      +        if isinstance(func, Command):
      +            raise TypeError('Callback is already a command.')
      +        command = Command(func=func, **kwargs)
      +        self.commands[func.__name__] = command
      +        return command
      +
      +    return decorator
      +
      +
      +
      +def event(self, *args, **kwargs) +
      +
      +
      +
      + +Expand source code + +
      def event(self, *args, **kwargs):
      +    def decorator(func):
      +        command = Command(func=func, **kwargs)
      +        self.events[func.__name__] = command
      +        return command
      +
      +    return decorator
      +
      +
      +
      +def run(self, token) +
      +
      +
      +
      + +Expand source code + +
      def run(self, token):
      +    self.keepgoing = True
      +    self.token_login(token)
      +    self.notifications.on_notification = self._on_notification
      +    self.evtloop = self.cso.evtloop
      +    self.evtloop.run_until_complete(self._run())
      +    while self.keepgoing:
      +        sleep(1/32)
      +
      +
      +

      Inherited members

      • Client: @@ -161,6 +380,7 @@

        Inherited members

      • get_group
      • get_user
      • get_user_by_username
      • +
      • notifications
      • requests
      • token_login
      • trade
      • @@ -184,6 +404,7 @@

        Inherited members

        if not asyncio.iscoroutinefunction(func): raise TypeError('Callback must be a coroutine.') self._callback = func + self.help = func.__doc__ or "No help available." @property def callback(self): @@ -208,6 +429,78 @@

        Instance variables

      +
      +class Context +(cso, latest_data, notif_data) +
      +
      +
      +
      + +Expand source code + +
      class Context:
      +    def __init__(self, cso, latest_data, notif_data):
      +        self.cso = cso
      +        self.requests = cso.requests
      +
      +        self.conversation_id = notif_data["conversation_id"]
      +        self.actor_target_id = notif_data["actor_target_id"]
      +        self.actor_type = notif_data["actor_type"]
      +        self.type = notif_data["type"]
      +        self.sequence_number = notif_data["sequence_number"]
      +
      +        self.id = latest_data["id"]
      +        self.content = latest_data["content"]
      +        self.sender_type = latest_data["senderType"]
      +        self.sender_id = latest_data["senderTargetId"]
      +        self.decorators = latest_data["decorators"]
      +        self.message_type = latest_data["messageType"]
      +        self.read = latest_data["read"]
      +        self.sent = iso8601.parse_date(latest_data["sent"])
      +
      +    async def send(self, content):
      +        send_message_req = await self.requests.post(
      +            url="https://chat.roblox.com/v2/send-message",
      +            data={
      +                "message": content,
      +                "conversationId": self.conversation_id
      +            }
      +        )
      +        send_message_json = send_message_req.json()
      +        if send_message_json["sent"]:
      +            return Message(self.cso, send_message_json["messageId"], self.id)
      +        else:
      +            raise ChatError(send_message_json["statusMessage"])
      +
      +

      Methods

      +
      +
      +async def send(self, content) +
      +
      +
      +
      + +Expand source code + +
      async def send(self, content):
      +    send_message_req = await self.requests.post(
      +        url="https://chat.roblox.com/v2/send-message",
      +        data={
      +            "message": content,
      +            "conversationId": self.conversation_id
      +        }
      +    )
      +    send_message_json = send_message_req.json()
      +    if send_message_json["sent"]:
      +        return Message(self.cso, send_message_json["messageId"], self.id)
      +    else:
      +        raise ChatError(send_message_json["statusMessage"])
      +
      +
      +
      +
      @@ -227,15 +520,15 @@

      Index

    • ro_py.extensions
  • -
  • Functions

    - -
  • Classes

  • +
  • +

    Context

    + +
  • diff --git a/docs/extensions/twocaptcha.html b/docs/extensions/twocaptcha.html index 825e17ee..369efed4 100644 --- a/docs/extensions/twocaptcha.html +++ b/docs/extensions/twocaptcha.html @@ -74,10 +74,8 @@

    Module ro_py.extensions.twocaptcha

    url += "&surl=https://roblox-api.arkoselabs.com" url += "&pageurl=https://www.roblox.com" url += "&json=1" - print(url) solve_req = await requests_async.post(url) - print(solve_req.text) data = solve_req.json() if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST": raise IncorrectKeyError("The provided 2captcha api key was incorrect.") @@ -90,7 +88,10 @@

    Module ro_py.extensions.twocaptcha

    solution = None while True: await asyncio.sleep(5) - captcha_req = await requests_async.get(endpoint + f"/res.php?key={self.api_key}&id={task_id}&json=1&action=get") + captcha_req = await requests_async.get(endpoint + f"/res.php" + f"?key={self.api_key}" + f"&id={task_id}" + f"&json=1&action=get") captcha_data = captcha_req.json() if captcha_data['request'] != "CAPCHA_NOT_READY": solution = captcha_data['request'] @@ -130,10 +131,8 @@

    Classes

    url += "&surl=https://roblox-api.arkoselabs.com" url += "&pageurl=https://www.roblox.com" url += "&json=1" - print(url) solve_req = await requests_async.post(url) - print(solve_req.text) data = solve_req.json() if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST": raise IncorrectKeyError("The provided 2captcha api key was incorrect.") @@ -146,7 +145,10 @@

    Classes

    solution = None while True: await asyncio.sleep(5) - captcha_req = await requests_async.get(endpoint + f"/res.php?key={self.api_key}&id={task_id}&json=1&action=get") + captcha_req = await requests_async.get(endpoint + f"/res.php" + f"?key={self.api_key}" + f"&id={task_id}" + f"&json=1&action=get") captcha_data = captcha_req.json() if captcha_data['request'] != "CAPCHA_NOT_READY": solution = captcha_data['request'] @@ -172,10 +174,8 @@

    Methods

    url += "&surl=https://roblox-api.arkoselabs.com" url += "&pageurl=https://www.roblox.com" url += "&json=1" - print(url) solve_req = await requests_async.post(url) - print(solve_req.text) data = solve_req.json() if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST": raise IncorrectKeyError("The provided 2captcha api key was incorrect.") @@ -188,7 +188,10 @@

    Methods

    solution = None while True: await asyncio.sleep(5) - captcha_req = await requests_async.get(endpoint + f"/res.php?key={self.api_key}&id={task_id}&json=1&action=get") + captcha_req = await requests_async.get(endpoint + f"/res.php" + f"?key={self.api_key}" + f"&id={task_id}" + f"&json=1&action=get") captcha_data = captcha_req.json() if captcha_data['request'] != "CAPCHA_NOT_READY": solution = captcha_data['request'] diff --git a/docs/games.html b/docs/games.html index b3c34ab7..51d703fb 100644 --- a/docs/games.html +++ b/docs/games.html @@ -60,7 +60,6 @@

    Module ro_py.games

    """ -from ro_py.users import User from ro_py.groups import Group from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator @@ -121,7 +120,7 @@

    Module ro_py.games

    if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: - self.creator = User(self.cso, game_info["creator"]["id"]) + self.creator = await self.cso.client.get_user(game_info["creator"]["id"]) self.cso.cache.set(CacheType.Users, game_info["creator"]["id"], self.creator) await self.creator.update() elif game_info["creator"]["type"] == "Group": @@ -304,7 +303,7 @@

    Classes

    if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: - self.creator = User(self.cso, game_info["creator"]["id"]) + self.creator = await self.cso.client.get_user(game_info["creator"]["id"]) self.cso.cache.set(CacheType.Users, game_info["creator"]["id"], self.creator) await self.creator.update() elif game_info["creator"]["type"] == "Group": @@ -444,7 +443,7 @@

    Methods

    if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: - self.creator = User(self.cso, game_info["creator"]["id"]) + self.creator = await self.cso.client.get_user(game_info["creator"]["id"]) self.cso.cache.set(CacheType.Users, game_info["creator"]["id"], self.creator) await self.creator.update() elif game_info["creator"]["type"] == "Group": diff --git a/docs/groups.html b/docs/groups.html index d07b01c8..44eca4a7 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -59,13 +59,14 @@

    Module ro_py.groups

    This file houses functions and classes that pertain to Roblox groups. """ +import copy import iso8601 import asyncio -from typing import List from ro_py.wall import Wall from ro_py.roles import Role -from ro_py.captcha import UnsolvedCaptcha -from ro_py.users import User, PartialUser +from ro_py.users import PartialUser +from ro_py.events import EventTypes +from typing import Tuple, Callable from ro_py.utilities.errors import NotFound from ro_py.utilities.pages import Pages, SortOrder @@ -77,8 +78,11 @@

    Module ro_py.groups

    Represents a group shout. """ def __init__(self, cso, shout_data): + self.cso = cso + self.data = shout_data self.body = shout_data["body"] - self.poster = User(cso, shout_data["poster"]["userId"], shout_data['poster']['username']) + # TODO: Make this a PartialUser + self.poster = None class JoinRequest: @@ -141,13 +145,12 @@

    Module ro_py.groups

    group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.cso, group_info["owner"]["userId"]) + self.owner = await self.cso.client.get_user(group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] - if "shout" in group_info: - if group_info["shout"]: - self.shout = Shout(self.cso, group_info["shout"]) + if group_info.get('shout'): + self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None # self.is_locked = group_info["isLocked"] @@ -210,7 +213,7 @@

    Module ro_py.groups

    # Create data to return. role = Role(self.cso, self, group_data['role']) member = Member(self.cso, roblox_id, "", self, role) - return await member.update() + return member async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( @@ -245,13 +248,13 @@

    Module ro_py.groups

    super().__init__(*args, **kwargs) -class Member(User): +class Member(PartialUser): """ Represents a user in a group. Parameters ---------- - requests : ro_py.utilities.requests.Requests + cso : ro_py.utilities.requests.Requests Requests object to use for API requests. roblox_id : int The id of a user. @@ -264,6 +267,7 @@

    Module ro_py.groups

    """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) + self.requests = cso.requests self.role = role self.group = group @@ -281,11 +285,11 @@

    Module ro_py.groups

    data = member_req.json() for role in data['data']: if role['group']['id'] == self.group.id: - self.role = Role(self.requests, self.group, role['role']) + self.role = Role(self.cso, self.group, role['role']) break return self.role - async def change_rank(self, num): + async def change_rank(self, num) -> Tuple[Role, Role]: """ Changes the users rank specified by a number. If num is 1 the users role will go up by 1. @@ -298,6 +302,7 @@

    Module ro_py.groups

    """ await self.update_role() roles = await self.group.get_roles() + old_role = copy.copy(self.role) role_counter = -1 for group_role in roles: role_counter += 1 @@ -305,7 +310,9 @@

    Module ro_py.groups

    break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") - return await self.setrank(roles[role_counter + num].id) + await self.setrank(roles[role_counter + num].id) + self.role = roles[role_counter + num].id + return old_role, roles[role_counter + num] async def promote(self): """ @@ -383,7 +390,7 @@

    Module ro_py.groups

    self.cso = cso self.group = group - async def bind(self, func, event, delay=15): + def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -391,19 +398,19 @@

    Module ro_py.groups

    ---------- func : function Function that will be bound to the event. - event : str + event : ro_py.events.EventTypes Event that will be bound to the function. delay : int How many seconds between each poll. """ - if event == "on_join_request": - return await asyncio.create_task(self.on_join_request(func, delay)) - if event == "on_wall_post": - return await asyncio.create_task(self.on_wall_post(func, delay)) - if event == "on_shout_update": - return await asyncio.create_task(self.on_shout_update(func, delay)) - - async def on_join_request(self, func, delay): + if event == EventTypes.on_join_request: + return asyncio.create_task(self.on_join_request(func, delay)) + if event == EventTypes.on_wall_post: + return asyncio.create_task(self.on_wall_post(func, delay)) + if event == EventTypes.on_group_change: + return asyncio.create_task(self.on_group_change(func, delay)) + + async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() old_req = current_group_reqs.data.requester.id while True: @@ -417,9 +424,12 @@

    Module ro_py.groups

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - await func(new_req) + if asyncio.iscoroutinefunction(func): + await func(new_req) + else: + func(new_req) - async def on_wall_post(self, func, delay): + async def on_wall_post(self, func: Callable, delay: int): current_wall_posts = await self.group.wall.get_posts() newest_wall_poster = current_wall_posts.data[0].poster.id while True: @@ -433,17 +443,27 @@

    Module ro_py.groups

    new_posts.append(post) newest_wall_poster = current_wall_posts[0].poster.id for new_post in new_posts: - await func(new_post) + if asyncio.iscoroutinefunction(func): + await func(new_post) + else: + func(new_post) - async def on_shout_update(self, func, delay): + async def on_group_change(self, func: Callable, delay: int): await self.group.update() - current_shout = self.group.shout + current_group = copy.copy(self.group) while True: await asyncio.sleep(delay) await self.group.update() - if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body: - current_shout = self.group.shout - await func(self.group.shout) + has_changed = False + for attr, value in current_group.__dict__.items(): + if getattr(self.group, attr) != value: + has_changed = True + if has_changed: + if asyncio.iscoroutinefunction(func): + await func(current_group, self.group) + else: + func(current_group, self.group) + current_group = copy.copy(self.group)
    @@ -505,7 +525,7 @@

    Classes

    self.cso = cso self.group = group - async def bind(self, func, event, delay=15): + def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -513,19 +533,19 @@

    Classes

    ---------- func : function Function that will be bound to the event. - event : str + event : ro_py.events.EventTypes Event that will be bound to the function. delay : int How many seconds between each poll. """ - if event == "on_join_request": - return await asyncio.create_task(self.on_join_request(func, delay)) - if event == "on_wall_post": - return await asyncio.create_task(self.on_wall_post(func, delay)) - if event == "on_shout_update": - return await asyncio.create_task(self.on_shout_update(func, delay)) - - async def on_join_request(self, func, delay): + if event == EventTypes.on_join_request: + return asyncio.create_task(self.on_join_request(func, delay)) + if event == EventTypes.on_wall_post: + return asyncio.create_task(self.on_wall_post(func, delay)) + if event == EventTypes.on_group_change: + return asyncio.create_task(self.on_group_change(func, delay)) + + async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() old_req = current_group_reqs.data.requester.id while True: @@ -539,9 +559,12 @@

    Classes

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - await func(new_req) + if asyncio.iscoroutinefunction(func): + await func(new_req) + else: + func(new_req) - async def on_wall_post(self, func, delay): + async def on_wall_post(self, func: Callable, delay: int): current_wall_posts = await self.group.wall.get_posts() newest_wall_poster = current_wall_posts.data[0].poster.id while True: @@ -555,22 +578,32 @@

    Classes

    new_posts.append(post) newest_wall_poster = current_wall_posts[0].poster.id for new_post in new_posts: - await func(new_post) + if asyncio.iscoroutinefunction(func): + await func(new_post) + else: + func(new_post) - async def on_shout_update(self, func, delay): + async def on_group_change(self, func: Callable, delay: int): await self.group.update() - current_shout = self.group.shout + current_group = copy.copy(self.group) while True: await asyncio.sleep(delay) await self.group.update() - if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body: - current_shout = self.group.shout - await func(self.group.shout) + has_changed = False + for attr, value in current_group.__dict__.items(): + if getattr(self.group, attr) != value: + has_changed = True + if has_changed: + if asyncio.iscoroutinefunction(func): + await func(current_group, self.group) + else: + func(current_group, self.group) + current_group = copy.copy(self.group)

    Methods

    -async def bind(self, func, event, delay=15) +def bind(self, func: Callable, event: EventTypes, delay: int = 15)

    Binds a function to an event.

    @@ -578,7 +611,7 @@

    Parameters

    func : function
    Function that will be bound to the event.
    -
    event : str
    +
    event : EventTypes
    Event that will be bound to the function.
    delay : int
    How many seconds between each poll.
    @@ -587,7 +620,7 @@

    Parameters

    Expand source code -
    async def bind(self, func, event, delay=15):
    +
    def bind(self, func: Callable, event: EventTypes, delay: int = 15):
         """
         Binds a function to an event.
     
    @@ -595,21 +628,48 @@ 

    Parameters

    ---------- func : function Function that will be bound to the event. - event : str + event : ro_py.events.EventTypes Event that will be bound to the function. delay : int How many seconds between each poll. """ - if event == "on_join_request": - return await asyncio.create_task(self.on_join_request(func, delay)) - if event == "on_wall_post": - return await asyncio.create_task(self.on_wall_post(func, delay)) - if event == "on_shout_update": - return await asyncio.create_task(self.on_shout_update(func, delay))
    + if event == EventTypes.on_join_request: + return asyncio.create_task(self.on_join_request(func, delay)) + if event == EventTypes.on_wall_post: + return asyncio.create_task(self.on_wall_post(func, delay)) + if event == EventTypes.on_group_change: + return asyncio.create_task(self.on_group_change(func, delay))
    + +
    +
    +async def on_group_change(self, func: Callable, delay: int) +
    +
    +
    +
    + +Expand source code + +
    async def on_group_change(self, func: Callable, delay: int):
    +    await self.group.update()
    +    current_group = copy.copy(self.group)
    +    while True:
    +        await asyncio.sleep(delay)
    +        await self.group.update()
    +        has_changed = False
    +        for attr, value in current_group.__dict__.items():
    +            if getattr(self.group, attr) != value:
    +                has_changed = True
    +        if has_changed:
    +            if asyncio.iscoroutinefunction(func):
    +                await func(current_group, self.group)
    +            else:
    +                func(current_group, self.group)
    +            current_group = copy.copy(self.group)
    -async def on_join_request(self, func, delay) +async def on_join_request(self, func: Callable, delay: int)
    @@ -617,7 +677,7 @@

    Parameters

    Expand source code -
    async def on_join_request(self, func, delay):
    +
    async def on_join_request(self, func: Callable, delay: int):
         current_group_reqs = await self.group.get_join_requests()
         old_req = current_group_reqs.data.requester.id
         while True:
    @@ -631,31 +691,14 @@ 

    Parameters

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - await func(new_req)
    - -
    -
    -async def on_shout_update(self, func, delay) -
    -
    -
    -
    - -Expand source code - -
    async def on_shout_update(self, func, delay):
    -    await self.group.update()
    -    current_shout = self.group.shout
    -    while True:
    -        await asyncio.sleep(delay)
    -        await self.group.update()
    -        if current_shout.poster.id != self.group.shout.poster.id or current_shout.body != self.group.shout.body:
    -            current_shout = self.group.shout
    -            await func(self.group.shout)
    + if asyncio.iscoroutinefunction(func): + await func(new_req) + else: + func(new_req)
    -async def on_wall_post(self, func, delay) +async def on_wall_post(self, func: Callable, delay: int)
    @@ -663,7 +706,7 @@

    Parameters

    Expand source code -
    async def on_wall_post(self, func, delay):
    +
    async def on_wall_post(self, func: Callable, delay: int):
         current_wall_posts = await self.group.wall.get_posts()
         newest_wall_poster = current_wall_posts.data[0].poster.id
         while True:
    @@ -677,7 +720,10 @@ 

    Parameters

    new_posts.append(post) newest_wall_poster = current_wall_posts[0].poster.id for new_post in new_posts: - await func(new_post)
    + if asyncio.iscoroutinefunction(func): + await func(new_post) + else: + func(new_post)
    @@ -718,13 +764,12 @@

    Parameters

    group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.cso, group_info["owner"]["userId"]) + self.owner = await self.cso.client.get_user(group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] - if "shout" in group_info: - if group_info["shout"]: - self.shout = Shout(self.cso, group_info["shout"]) + if group_info.get('shout'): + self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None # self.is_locked = group_info["isLocked"] @@ -787,7 +832,7 @@

    Parameters

    # Create data to return. role = Role(self.cso, self, group_data['role']) member = Member(self.cso, roblox_id, "", self, role) - return await member.update() + return member async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( @@ -871,7 +916,7 @@

    Methods

    # Create data to return. role = Role(self.cso, self, group_data['role']) member = Member(self.cso, roblox_id, "", self, role) - return await member.update() + return member
    @@ -944,13 +989,12 @@

    Returns

    group_info = group_info_req.json() self.name = group_info["name"] self.description = group_info["description"] - self.owner = User(self.cso, group_info["owner"]["userId"]) + self.owner = await self.cso.client.get_user(group_info["owner"]["userId"]) self.member_count = group_info["memberCount"] self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] - if "shout" in group_info: - if group_info["shout"]: - self.shout = Shout(self.cso, group_info["shout"]) + if group_info.get('shout'): + self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None
    @@ -1071,7 +1115,7 @@

    Methods

    Represents a user in a group.

    Parameters

    -
    requests : Requests
    +
    cso : Requests
    Requests object to use for API requests.
    roblox_id : int
    The id of a user.
    @@ -1086,13 +1130,13 @@

    Parameters

    Expand source code -
    class Member(User):
    +
    class Member(PartialUser):
         """
         Represents a user in a group.
     
         Parameters
         ----------
    -    requests : ro_py.utilities.requests.Requests
    +    cso : ro_py.utilities.requests.Requests
                 Requests object to use for API requests.
         roblox_id : int
                 The id of a user.
    @@ -1105,6 +1149,7 @@ 

    Parameters

    """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) + self.requests = cso.requests self.role = role self.group = group @@ -1122,11 +1167,11 @@

    Parameters

    data = member_req.json() for role in data['data']: if role['group']['id'] == self.group.id: - self.role = Role(self.requests, self.group, role['role']) + self.role = Role(self.cso, self.group, role['role']) break return self.role - async def change_rank(self, num): + async def change_rank(self, num) -> Tuple[Role, Role]: """ Changes the users rank specified by a number. If num is 1 the users role will go up by 1. @@ -1139,6 +1184,7 @@

    Parameters

    """ await self.update_role() roles = await self.group.get_roles() + old_role = copy.copy(self.role) role_counter = -1 for group_role in roles: role_counter += 1 @@ -1146,7 +1192,9 @@

    Parameters

    break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") - return await self.setrank(roles[role_counter + num].id) + await self.setrank(roles[role_counter + num].id) + self.role = roles[role_counter + num].id + return old_role, roles[role_counter + num] async def promote(self): """ @@ -1220,12 +1268,12 @@

    Parameters

    Ancestors

    Methods

    -async def change_rank(self, num) +async def change_rank(self, num) ‑> Tuple[RoleRole]

    Changes the users rank specified by a number. @@ -1240,7 +1288,7 @@

    Parameters

    Expand source code -
    async def change_rank(self, num):
    +
    async def change_rank(self, num) -> Tuple[Role, Role]:
         """
         Changes the users rank specified by a number.
         If num is 1 the users role will go up by 1.
    @@ -1253,6 +1301,7 @@ 

    Parameters

    """ await self.update_role() roles = await self.group.get_roles() + old_role = copy.copy(self.role) role_counter = -1 for group_role in roles: role_counter += 1 @@ -1260,7 +1309,9 @@

    Parameters

    break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") - return await self.setrank(roles[role_counter + num].id)
    + await self.setrank(roles[role_counter + num].id) + self.role = roles[role_counter + num].id + return old_role, roles[role_counter + num]
    @@ -1441,7 +1492,7 @@

    Returns

    data = member_req.json() for role in data['data']: if role['group']['id'] == self.group.id: - self.role = Role(self.requests, self.group, role['role']) + self.role = Role(self.cso, self.group, role['role']) break return self.role
    @@ -1449,16 +1500,16 @@

    Returns

    Inherited members

    @@ -1510,8 +1561,11 @@

    Inherited members

    Represents a group shout. """ def __init__(self, cso, shout_data): + self.cso = cso + self.data = shout_data self.body = shout_data["body"] - self.poster = User(cso, shout_data["poster"]["userId"], shout_data['poster']['username'])
    + # TODO: Make this a PartialUser + self.poster = None @@ -1545,8 +1599,8 @@

    Index

    Events

    diff --git a/docs/index.html b/docs/index.html index 971bf7bf..5b83f249 100644 --- a/docs/index.html +++ b/docs/index.html @@ -87,7 +87,9 @@

    ro.py is a powerful Python 3 wrapper for the Roblox Web API.< <a href="https://github.com/rbx-libdev/ro.py/blob/main/LICENSE">License</a> </p> -""" +""" + +from ro_py.client import Client

    diff --git a/docs/notifications.html b/docs/notifications.html index a31abe61..9ec162a8 100644 --- a/docs/notifications.html +++ b/docs/notifications.html @@ -75,11 +75,9 @@

    Module ro_py.notifications

    from ro_py.utilities.caseconvert import to_snake_case -from signalrcore_async.hub_connection_builder import HubConnectionBuilder +from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import quote import json -import time -import asyncio class Notification: @@ -124,28 +122,20 @@

    Module ro_py.notifications

    This should only be generated once per client as to not duplicate notifications. """ - def __init__(self, requests, on_open, on_close, on_error, on_notification): - self.requests = requests - - self.on_open = on_open - self.on_close = on_close - self.on_error = on_error - self.on_notification = on_notification - - self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.connection = None - + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests + self.evtloop = cso.evtloop self.negotiate_request = None self.wss_url = None + self.connection = None async def initialize(self): self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", - cookies={ - ".ROBLOSECURITY": self.roblosecurity - } + cookies=self.requests.session.cookies ) self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ @@ -155,13 +145,13 @@

    Module ro_py.notifications

    self.wss_url, options={ "headers": { - "Cookie": f".ROBLOSECURITY={self.roblosecurity};" + "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};" }, "skip_negotiation": False } ) - async def on_message(_self, raw_notification): + def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -171,45 +161,22 @@

    Module ro_py.notifications

    return if len(notification_json) > 0: notification = Notification(notification_json) - await self.on_notification(notification) + self.evtloop.run_until_complete(self.on_notification(notification)) else: return - def _internal_send(_self, message, protocol=None): - - _self.logger.debug("Sending message {0}".format(message)) - - try: - protocol = _self.protocol if protocol is None else protocol - - _self._ws.send(protocol.encode(message)) - _self.connection_checker.last_message = time.time() - - if _self.reconnection_handler is not None: - _self.reconnection_handler.reset() - - except Exception as ex: - raise ex - - self.connection = self.connection.with_automatic_reconnect({ + self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, "max_attempts": 5 }).build() - if self.on_open: - self.connection.on_open(self.on_open) - if self.on_close: - self.connection.on_close(self.on_close) - if self.on_error: - self.connection.on_error(self.on_error) - self.connection.on_message = on_message - self.connection._internal_send = _internal_send + self.connection.hub.on_message = on_message - await self.connection.start() + self.connection.start() - async def close(self): + def close(self): """ Closes the connection and stops receiving notifications. """ @@ -273,7 +240,7 @@

    Classes

    class NotificationReceiver -(requests, on_open, on_close, on_error, on_notification) +(cso)

    This object is used to receive notifications. @@ -288,28 +255,20 @@

    Classes

    This should only be generated once per client as to not duplicate notifications. """ - def __init__(self, requests, on_open, on_close, on_error, on_notification): - self.requests = requests - - self.on_open = on_open - self.on_close = on_close - self.on_error = on_error - self.on_notification = on_notification - - self.roblosecurity = self.requests.session.cookies[".ROBLOSECURITY"] - self.connection = None - + def __init__(self, cso): + self.cso = cso + self.requests = cso.requests + self.evtloop = cso.evtloop self.negotiate_request = None self.wss_url = None + self.connection = None async def initialize(self): self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", - cookies={ - ".ROBLOSECURITY": self.roblosecurity - } + cookies=self.requests.session.cookies ) self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ @@ -319,13 +278,13 @@

    Classes

    self.wss_url, options={ "headers": { - "Cookie": f".ROBLOSECURITY={self.roblosecurity};" + "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};" }, "skip_negotiation": False } ) - async def on_message(_self, raw_notification): + def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -335,45 +294,22 @@

    Classes

    return if len(notification_json) > 0: notification = Notification(notification_json) - await self.on_notification(notification) + self.evtloop.run_until_complete(self.on_notification(notification)) else: return - def _internal_send(_self, message, protocol=None): - - _self.logger.debug("Sending message {0}".format(message)) - - try: - protocol = _self.protocol if protocol is None else protocol - - _self._ws.send(protocol.encode(message)) - _self.connection_checker.last_message = time.time() - - if _self.reconnection_handler is not None: - _self.reconnection_handler.reset() - - except Exception as ex: - raise ex - - self.connection = self.connection.with_automatic_reconnect({ + self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, "max_attempts": 5 }).build() - if self.on_open: - self.connection.on_open(self.on_open) - if self.on_close: - self.connection.on_close(self.on_close) - if self.on_error: - self.connection.on_error(self.on_error) - self.connection.on_message = on_message - self.connection._internal_send = _internal_send + self.connection.hub.on_message = on_message - await self.connection.start() + self.connection.start() - async def close(self): + def close(self): """ Closes the connection and stops receiving notifications. """ @@ -382,7 +318,7 @@

    Classes

    Methods

    -async def close(self) +def close(self)

    Closes the connection and stops receiving notifications.

    @@ -390,7 +326,7 @@

    Methods

    Expand source code -
    async def close(self):
    +
    def close(self):
         """
         Closes the connection and stops receiving notifications.
         """
    @@ -411,9 +347,7 @@ 

    Methods

    url="https://realtime.roblox.com/notifications/negotiate" "?clientProtocol=1.5" "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", - cookies={ - ".ROBLOSECURITY": self.roblosecurity - } + cookies=self.requests.session.cookies ) self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ @@ -423,13 +357,13 @@

    Methods

    self.wss_url, options={ "headers": { - "Cookie": f".ROBLOSECURITY={self.roblosecurity};" + "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};" }, "skip_negotiation": False } ) - async def on_message(_self, raw_notification): + def on_message(_self, raw_notification): """ Internal callback when a message is received. """ @@ -439,43 +373,20 @@

    Methods

    return if len(notification_json) > 0: notification = Notification(notification_json) - await self.on_notification(notification) + self.evtloop.run_until_complete(self.on_notification(notification)) else: return - def _internal_send(_self, message, protocol=None): - - _self.logger.debug("Sending message {0}".format(message)) - - try: - protocol = _self.protocol if protocol is None else protocol - - _self._ws.send(protocol.encode(message)) - _self.connection_checker.last_message = time.time() - - if _self.reconnection_handler is not None: - _self.reconnection_handler.reset() - - except Exception as ex: - raise ex - - self.connection = self.connection.with_automatic_reconnect({ + self.connection.with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, "max_attempts": 5 }).build() - if self.on_open: - self.connection.on_open(self.on_open) - if self.on_close: - self.connection.on_close(self.on_close) - if self.on_error: - self.connection.on_error(self.on_error) - self.connection.on_message = on_message - self.connection._internal_send = _internal_send + self.connection.hub.on_message = on_message - await self.connection.start()
    + self.connection.start()
    diff --git a/docs/thumbnails.html b/docs/thumbnails.html index fa99b3c1..ef2d072d 100644 --- a/docs/thumbnails.html +++ b/docs/thumbnails.html @@ -153,9 +153,9 @@

    Module ro_py.thumbnails

    class UserThumbnailGenerator: - def __init__(self, requests, id): - self.requests = requests - self.id = id + def __init__(self, cso, roblox_id): + self.requests = cso.requests + self.id = roblox_id async def get_avatar_image(self, shot_type=ThumbnailType.avatar_full_body, size=ThumbnailSize.size_48x48, file_format=ThumbnailFormat.format_png, is_circular=False): @@ -637,7 +637,7 @@

    Class variables

    class UserThumbnailGenerator -(requests, id) +(cso, roblox_id)
    @@ -646,9 +646,9 @@

    Class variables

    Expand source code
    class UserThumbnailGenerator:
    -    def __init__(self, requests, id):
    -        self.requests = requests
    -        self.id = id
    +    def __init__(self, cso, roblox_id):
    +        self.requests = cso.requests
    +        self.id = roblox_id
     
         async def get_avatar_image(self, shot_type=ThumbnailType.avatar_full_body, size=ThumbnailSize.size_48x48,
                                    file_format=ThumbnailFormat.format_png, is_circular=False):
    diff --git a/docs/trades.html b/docs/trades.html
    index 093d0346..31d55059 100644
    --- a/docs/trades.html
    +++ b/docs/trades.html
    @@ -62,7 +62,8 @@ 

    Module ro_py.trades

    from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset -from ro_py.users import User, PartialUser +from ro_py.users import PartialUser +import datetime import iso8601 import enum @@ -77,14 +78,14 @@

    Module ro_py.trades

    class Trade: - def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool): + def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool): self.trade_id = trade_id self.requests = requests self.sender = sender - self.recieve_items = recieve_items + self.receive_items = receive_items self.send_items = send_items self.created = iso8601.parse_date(created) - self.experation = iso8601.parse_date(expiration) + self.expiration = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: @@ -109,12 +110,13 @@

    Module ro_py.trades

    class PartialTrade: - def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool): - self.requests = requests + def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool): + self.cso = cso + self.requests = cso.requests self.trade_id = trade_id self.user = user - self.created = iso8601.parse(created) - self.expiration = iso8601.parse(expiration) + self.created = iso8601.parse_date(created) + self.expiration = iso8601.parse_date(expiration) self.status = status async def accept(self) -> bool: @@ -148,22 +150,31 @@

    Module ro_py.trades

    data = expend_req.json() # generate a user class and update it - sender = User(self.requests, data['user']['id']) + sender = await self.cso.client.get_user(data['user']['id']) await sender.update() # load items that will be/have been sent and items that you will/have recieve(d) - recieve_items, send_items = [], [] + receive_items, send_items = [], [] for items_0 in data['offers'][0]['userAssets']: item_0 = Asset(self.requests, items_0['assetId']) await item_0.update() - recieve_items.append(item_0) + receive_items.append(item_0) for items_1 in data['offers'][1]['userAssets']: item_1 = Asset(self.requests, items_1['assetId']) await item_1.update() send_items.append(item_1) - return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status']) + return Trade( + self.cso, + self.trade_id, + sender, + receive_items, + send_items, + data['created'], + data['expiration'], + data['status'] + ) class TradeStatusType(enum.Enum): @@ -189,13 +200,13 @@

    Module ro_py.trades

    class TradeRequest: def __init__(self): - self.recieve_asset = [] + self.receive_asset = [] """Limiteds that will be recieved when the trade is accepted.""" self.send_asset = [] """Limiteds that will be sent when the trade is accepted.""" self.send_robux = 0 """Robux that will be sent when the trade is accepted.""" - self.recieve_robux = 0 + self.receive_robux = 0 """Robux that will be recieved when the trade is accepted.""" def request_item(self, asset: UserAsset): @@ -206,7 +217,7 @@

    Module ro_py.trades

    ---------- asset : ro_py.assets.UserAsset """ - self.recieve_asset.append(asset) + self.receive_asset.append(asset) def send_item(self, asset: UserAsset): """ @@ -226,7 +237,7 @@

    Module ro_py.trades

    ---------- robux : int """ - self.recieve_robux = robux + self.receive_robux = robux def send_robux(self, robux: int): """ @@ -243,14 +254,15 @@

    Module ro_py.trades

    """ Represents the Roblox trades page. """ - def __init__(self, requests, get_self): - self.requests = requests + def __init__(self, cso, get_self): + self.cso = cso + self.requests = cso.requests self.get_self = get_self self.TradeRequest = TradeRequest async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: - trades = await Pages( - requests=self.requests, + trades = Pages( + cso=self.cso, url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, @@ -293,10 +305,10 @@

    Module ro_py.trades

    for asset in trade.send_asset: data['offers'][1]['userAssetIds'].append(asset.user_asset_id) - for asset in trade.recieve_asset: + for asset in trade.receive_asset: data['offers'][0]['userAssetIds'].append(asset.user_asset_id) - data['offers'][0]['robux'] = trade.recieve_robux + data['offers'][0]['robux'] = trade.receive_robux data['offers'][1]['robux'] = trade.send_robux trade_req = await self.requests.post( @@ -337,7 +349,7 @@

    Classes

    class PartialTrade -(requests, trade_id: int, user: PartialUser, created, expiration, status: bool) +(cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool)
    @@ -346,12 +358,13 @@

    Classes

    Expand source code
    class PartialTrade:
    -    def __init__(self, requests, trade_id: int, user: PartialUser, created, expiration, status: bool):
    -        self.requests = requests
    +    def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool):
    +        self.cso = cso
    +        self.requests = cso.requests
             self.trade_id = trade_id
             self.user = user
    -        self.created = iso8601.parse(created)
    -        self.expiration = iso8601.parse(expiration)
    +        self.created = iso8601.parse_date(created)
    +        self.expiration = iso8601.parse_date(expiration)
             self.status = status
     
         async def accept(self) -> bool:
    @@ -385,22 +398,31 @@ 

    Classes

    data = expend_req.json() # generate a user class and update it - sender = User(self.requests, data['user']['id']) + sender = await self.cso.client.get_user(data['user']['id']) await sender.update() # load items that will be/have been sent and items that you will/have recieve(d) - recieve_items, send_items = [], [] + receive_items, send_items = [], [] for items_0 in data['offers'][0]['userAssets']: item_0 = Asset(self.requests, items_0['assetId']) await item_0.update() - recieve_items.append(item_0) + receive_items.append(item_0) for items_1 in data['offers'][1]['userAssets']: item_1 = Asset(self.requests, items_1['assetId']) await item_1.update() send_items.append(item_1) - return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status'])
    + return Trade( + self.cso, + self.trade_id, + sender, + receive_items, + send_items, + data['created'], + data['expiration'], + data['status'] + )

    Methods

    @@ -467,29 +489,38 @@

    Methods

    data = expend_req.json() # generate a user class and update it - sender = User(self.requests, data['user']['id']) + sender = await self.cso.client.get_user(data['user']['id']) await sender.update() # load items that will be/have been sent and items that you will/have recieve(d) - recieve_items, send_items = [], [] + receive_items, send_items = [], [] for items_0 in data['offers'][0]['userAssets']: item_0 = Asset(self.requests, items_0['assetId']) await item_0.update() - recieve_items.append(item_0) + receive_items.append(item_0) for items_1 in data['offers'][1]['userAssets']: item_1 = Asset(self.requests, items_1['assetId']) await item_1.update() send_items.append(item_1) - return Trade(self.requests, self.trade_id, sender, recieve_items, send_items, data['created'], data['expiration'], data['status']) + return Trade( + self.cso, + self.trade_id, + sender, + receive_items, + send_items, + data['created'], + data['expiration'], + data['status'] + )
    class Trade -(requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool) +(requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool)
    @@ -498,14 +529,14 @@

    Methods

    Expand source code
    class Trade:
    -    def __init__(self, requests, trade_id: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool):
    +    def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool):
             self.trade_id = trade_id
             self.requests = requests
             self.sender = sender
    -        self.recieve_items = recieve_items
    +        self.receive_items = receive_items
             self.send_items = send_items
             self.created = iso8601.parse_date(created)
    -        self.experation = iso8601.parse_date(expiration)
    +        self.expiration = iso8601.parse_date(expiration)
             self.status = status
     
         async def accept(self) -> bool:
    @@ -585,13 +616,13 @@ 

    Methods

    class TradeRequest:
         def __init__(self):
    -        self.recieve_asset = []
    +        self.receive_asset = []
             """Limiteds that will be recieved when the trade is accepted."""
             self.send_asset = []
             """Limiteds that will be sent when the trade is accepted."""
             self.send_robux = 0
             """Robux that will be sent when the trade is accepted."""
    -        self.recieve_robux = 0
    +        self.receive_robux = 0
             """Robux that will be recieved when the trade is accepted."""
     
         def request_item(self, asset: UserAsset):
    @@ -602,7 +633,7 @@ 

    Methods

    ---------- asset : ro_py.assets.UserAsset """ - self.recieve_asset.append(asset) + self.receive_asset.append(asset) def send_item(self, asset: UserAsset): """ @@ -622,7 +653,7 @@

    Methods

    ---------- robux : int """ - self.recieve_robux = robux + self.receive_robux = robux def send_robux(self, robux: int): """ @@ -636,11 +667,11 @@

    Methods

    Instance variables

    -
    var recieve_asset
    +
    var receive_asset

    Limiteds that will be recieved when the trade is accepted.

    -
    var recieve_robux
    +
    var receive_robux

    Robux that will be recieved when the trade is accepted.

    @@ -691,7 +722,7 @@

    Parameters

    ---------- asset : ro_py.assets.UserAsset """ - self.recieve_asset.append(asset)
    + self.receive_asset.append(asset)
    @@ -716,7 +747,7 @@

    Parameters

    ---------- robux : int """ - self.recieve_robux = robux
    + self.receive_robux = robux
    @@ -812,7 +843,7 @@

    Class variables

    class TradesWrapper -(requests, get_self) +(cso, get_self)

    Represents the Roblox trades page.

    @@ -824,14 +855,15 @@

    Class variables

    """ Represents the Roblox trades page. """ - def __init__(self, requests, get_self): - self.requests = requests + def __init__(self, cso, get_self): + self.cso = cso + self.requests = cso.requests self.get_self = get_self self.TradeRequest = TradeRequest async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: - trades = await Pages( - requests=self.requests, + trades = Pages( + cso=self.cso, url=endpoint + f"/v1/trades/{trade_status_type}", sort_order=sort_order, limit=limit, @@ -874,10 +906,10 @@

    Class variables

    for asset in trade.send_asset: data['offers'][1]['userAssetIds'].append(asset.user_asset_id) - for asset in trade.recieve_asset: + for asset in trade.receive_asset: data['offers'][0]['userAssetIds'].append(asset.user_asset_id) - data['offers'][0]['robux'] = trade.recieve_robux + data['offers'][0]['robux'] = trade.receive_robux data['offers'][1]['robux'] = trade.send_robux trade_req = await self.requests.post( @@ -899,8 +931,8 @@

    Methods

    Expand source code
    async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
    -    trades = await Pages(
    -        requests=self.requests,
    +    trades = Pages(
    +        cso=self.cso,
             url=endpoint + f"/v1/trades/{trade_status_type}",
             sort_order=sort_order,
             limit=limit,
    @@ -965,10 +997,10 @@ 

    Returns

    for asset in trade.send_asset: data['offers'][1]['userAssetIds'].append(asset.user_asset_id) - for asset in trade.recieve_asset: + for asset in trade.receive_asset: data['offers'][0]['userAssetIds'].append(asset.user_asset_id) - data['offers'][0]['robux'] = trade.recieve_robux + data['offers'][0]['robux'] = trade.receive_robux data['offers'][1]['robux'] = trade.send_robux trade_req = await self.requests.post( @@ -1025,8 +1057,8 @@

    Trade

    TradeRequest

      -
    • recieve_asset
    • -
    • recieve_robux
    • +
    • receive_asset
    • +
    • receive_robux
    • request_item
    • request_robux
    • send_asset
    • diff --git a/docs/users.html b/docs/users.html index 210385ba..fd147cf3 100644 --- a/docs/users.html +++ b/docs/users.html @@ -59,12 +59,15 @@

      Module ro_py.users

      This file houses functions and classes that pertain to Roblox users and profiles. """ - +import copy +from typing import List, Callable +from ro_py.events import EventTypes from ro_py.robloxbadges import RobloxBadge from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.pages import Pages from ro_py.assets import UserAsset import iso8601 +import asyncio endpoint = "https://users.roblox.com/" @@ -76,67 +79,42 @@

      Module ro_py.users

      return assets -class User: - """ - Represents a Roblox user and their profile. - Can be initialized with either a user ID or a username. - - Parameters - ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. - roblox_id : int - The id of a user. - name : str - The name of the user. - """ - def __init__(self, cso, roblox_id, name=None): +class PartialUser: + def __init__(self, cso, roblox_id, roblox_name=None): self.cso = cso self.requests = cso.requests self.id = roblox_id - self.description = None - self.created = None - self.is_banned = None - self.name = name - self.display_name = None - self.thumbnails = UserThumbnailGenerator(self.requests, self.id) + self.name = roblox_name - async def update(self): + async def expand(self): """ Updates some class values. :return: Nothing """ user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() - self.description = user_info["description"] - self.created = iso8601.parse_date(user_info["created"]) - self.is_banned = user_info["isBanned"] - self.name = user_info["name"] - self.display_name = user_info["displayName"] + description = user_info["description"] + created = iso8601.parse_date(user_info["created"]) + is_banned = user_info["isBanned"] + name = user_info["name"] + display_name = user_info["displayName"] # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req - return self - - async def get_status(self): - """ - Gets the user's status. - :return: A string - """ - status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") - return status_req.json()["status"] + return User(self.cso, self.id, name, description, created, is_banned, display_name) - async def get_roblox_badges(self): + async def get_roblox_badges(self) -> List[RobloxBadge]: """ Gets the user's roblox badges. :return: A list of RobloxBadge instances """ - roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges_req = await self.requests.get( + f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") roblox_badges = [] for roblox_badge_data in roblox_badges_req.json(): roblox_badges.append(RobloxBadge(roblox_badge_data)) return roblox_badges - async def get_friends_count(self): + async def get_friends_count(self) -> int: """ Gets the user's friends count. :return: An integer @@ -145,7 +123,7 @@

      Module ro_py.users

      friends_count = friends_count_req.json()["count"] return friends_count - async def get_followers_count(self): + async def get_followers_count(self) -> int: """ Gets the user's followers count. :return: An integer @@ -154,12 +132,13 @@

      Module ro_py.users

      followers_count = followers_count_req.json()["count"] return followers_count - async def get_followings_count(self): + async def get_followings_count(self) -> int: """ Gets the user's followings count. :return: An integer """ - followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get( + f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -173,7 +152,7 @@

      Module ro_py.users

      friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.cso, friend_raw["id"]) + await self.cso.client.get_user(friend_raw["id"]) ) return friends_list @@ -198,18 +177,103 @@

      Module ro_py.users

      list """ return Pages( - requests=self.requests, + cso=self.cso, url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler ) + async def get_status(self): + """ + Gets the user's status. + :return: A string + """ + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") + return status_req.json()["status"] -class PartialUser(User): + +class User(PartialUser): """ - Represents a user with less information then the normal User class. + Represents a Roblox user and their profile. + Can be initialized with either a user ID or a username. + + Parameters + ---------- + cso : ro_py.client.ClientSharedObject + ClientSharedObject. + roblox_id : int + The id of a user. + roblox_name : str + The name of the user. + description : str + The description of the user. + created : any + Time the user was created. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs)

    + + def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): + super().__init__(cso, roblox_id, roblox_name) + self.cso = cso + self.id = roblox_id + self.name = roblox_name + self.description = description + self.created = created + self.is_banned = banned + self.display_name = display_name + self.thumbnails = UserThumbnailGenerator(cso, roblox_id) + + async def update(self): + """ + Updates some class values. + :return: Nothing + """ + user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") + user_info = user_info_req.json() + self.description = user_info["description"] + self.created = iso8601.parse_date(user_info["created"]) + self.is_banned = user_info["isBanned"] + self.name = user_info["name"] + self.display_name = user_info["displayName"] + return self + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req + + +class Events: + def __init__(self, cso, user): + self.cso = cso + self.user = user + + def bind(self, func: Callable, event: str, delay: int = 15): + """ + Binds an event to the provided function. + + Parameters + ---------- + func : function + Function that will be called when the event fires. + event : ro_py.events.EventTypes + The name of the event. + delay : int + How many seconds between requests. + """ + if event == EventTypes.on_user_change: + return asyncio.create_task(self.on_user_change(func, delay)) + + async def on_user_change(self, func: Callable, delay: int): + old_user = copy.copy(await self.user.update()) + while True: + await asyncio.sleep(delay) + new_user = await self.user.update() + has_changed = False + for attr, value in old_user.__dict__.items(): + if getattr(new_user, attr) != value: + has_changed = True + if has_changed: + if asyncio.iscoroutinefunction(func): + await func(old_user, new_user) + else: + func(old_user, new_user) + old_user = copy.copy(new_user)
    @@ -240,124 +304,164 @@

    Functions

    Classes

    -
    -class PartialUser -(*args, **kwargs) +
    +class Events +(cso, user)
    -

    Represents a user with less information then the normal User class.

    +
    Expand source code -
    class PartialUser(User):
    -    """
    -    Represents a user with less information then the normal User class.
    -    """
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    +
    class Events:
    +    def __init__(self, cso, user):
    +        self.cso = cso
    +        self.user = user
    +
    +    def bind(self, func: Callable, event: str, delay: int = 15):
    +        """
    +        Binds an event to the provided function.
    +
    +        Parameters
    +        ----------
    +        func : function
    +                Function that will be called when the event fires.
    +        event : ro_py.events.EventTypes
    +                The name of the event.
    +        delay : int
    +                How many seconds between requests.
    +        """
    +        if event == EventTypes.on_user_change:
    +            return asyncio.create_task(self.on_user_change(func, delay))
    +
    +    async def on_user_change(self, func: Callable, delay: int):
    +        old_user = copy.copy(await self.user.update())
    +        while True:
    +            await asyncio.sleep(delay)
    +            new_user = await self.user.update()
    +            has_changed = False
    +            for attr, value in old_user.__dict__.items():
    +                if getattr(new_user, attr) != value:
    +                    has_changed = True
    +            if has_changed:
    +                if asyncio.iscoroutinefunction(func):
    +                    await func(old_user, new_user)
    +                else:
    +                    func(old_user, new_user)
    +                old_user = copy.copy(new_user)
    -

    Ancestors

    - -

    Inherited members

    - -
    -
    -class User -(cso, roblox_id, name=None) +

    Methods

    +
    +
    +def bind(self, func: Callable, event: str, delay: int = 15)
    -

    Represents a Roblox user and their profile. -Can be initialized with either a user ID or a username.

    +

    Binds an event to the provided function.

    Parameters

    -
    requests : Requests
    -
    Requests object to use for API requests.
    -
    roblox_id : int
    -
    The id of a user.
    -
    name : str
    -
    The name of the user.
    +
    func : function
    +
    Function that will be called when the event fires.
    +
    event : EventTypes
    +
    The name of the event.
    +
    delay : int
    +
    How many seconds between requests.
    Expand source code -
    class User:
    +
    def bind(self, func: Callable, event: str, delay: int = 15):
         """
    -    Represents a Roblox user and their profile.
    -    Can be initialized with either a user ID or a username.
    +    Binds an event to the provided function.
     
         Parameters
         ----------
    -    requests : ro_py.utilities.requests.Requests
    -            Requests object to use for API requests.
    -    roblox_id : int
    -            The id of a user.
    -    name : str
    -            The name of the user.
    +    func : function
    +            Function that will be called when the event fires.
    +    event : ro_py.events.EventTypes
    +            The name of the event.
    +    delay : int
    +            How many seconds between requests.
         """
    -    def __init__(self, cso, roblox_id, name=None):
    +    if event == EventTypes.on_user_change:
    +        return asyncio.create_task(self.on_user_change(func, delay))
    +
    +
    +
    +async def on_user_change(self, func: Callable, delay: int) +
    +
    +
    +
    + +Expand source code + +
    async def on_user_change(self, func: Callable, delay: int):
    +    old_user = copy.copy(await self.user.update())
    +    while True:
    +        await asyncio.sleep(delay)
    +        new_user = await self.user.update()
    +        has_changed = False
    +        for attr, value in old_user.__dict__.items():
    +            if getattr(new_user, attr) != value:
    +                has_changed = True
    +        if has_changed:
    +            if asyncio.iscoroutinefunction(func):
    +                await func(old_user, new_user)
    +            else:
    +                func(old_user, new_user)
    +            old_user = copy.copy(new_user)
    +
    +
    +
    + +
    +class PartialUser +(cso, roblox_id, roblox_name=None) +
    +
    +
    +
    + +Expand source code + +
    class PartialUser:
    +    def __init__(self, cso, roblox_id, roblox_name=None):
             self.cso = cso
             self.requests = cso.requests
             self.id = roblox_id
    -        self.description = None
    -        self.created = None
    -        self.is_banned = None
    -        self.name = name
    -        self.display_name = None
    -        self.thumbnails = UserThumbnailGenerator(self.requests, self.id)
    +        self.name = roblox_name
     
    -    async def update(self):
    +    async def expand(self):
             """
             Updates some class values.
             :return: Nothing
             """
             user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
             user_info = user_info_req.json()
    -        self.description = user_info["description"]
    -        self.created = iso8601.parse_date(user_info["created"])
    -        self.is_banned = user_info["isBanned"]
    -        self.name = user_info["name"]
    -        self.display_name = user_info["displayName"]
    +        description = user_info["description"]
    +        created = iso8601.parse_date(user_info["created"])
    +        is_banned = user_info["isBanned"]
    +        name = user_info["name"]
    +        display_name = user_info["displayName"]
             # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
             # self.has_premium = has_premium_req
    -        return self
    +        return User(self.cso, self.id, name, description, created, is_banned, display_name)
     
    -    async def get_status(self):
    -        """
    -        Gets the user's status.
    -        :return: A string
    -        """
    -        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    -        return status_req.json()["status"]
    -
    -    async def get_roblox_badges(self):
    +    async def get_roblox_badges(self) -> List[RobloxBadge]:
             """
             Gets the user's roblox badges.
             :return: A list of RobloxBadge instances
             """
    -        roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    +        roblox_badges_req = await self.requests.get(
    +            f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
             roblox_badges = []
             for roblox_badge_data in roblox_badges_req.json():
                 roblox_badges.append(RobloxBadge(roblox_badge_data))
             return roblox_badges
     
    -    async def get_friends_count(self):
    +    async def get_friends_count(self) -> int:
             """
             Gets the user's friends count.
             :return: An integer
    @@ -366,7 +470,7 @@ 

    Parameters

    friends_count = friends_count_req.json()["count"] return friends_count - async def get_followers_count(self): + async def get_followers_count(self) -> int: """ Gets the user's followers count. :return: An integer @@ -375,12 +479,13 @@

    Parameters

    followers_count = followers_count_req.json()["count"] return followers_count - async def get_followings_count(self): + async def get_followings_count(self) -> int: """ Gets the user's followings count. :return: An integer """ - followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count_req = await self.requests.get( + f"https://friends.roblox.com/v1/users/{self.id}/followings/count") followings_count = followings_count_req.json()["count"] return followings_count @@ -394,7 +499,7 @@

    Parameters

    friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.cso, friend_raw["id"]) + await self.cso.client.get_user(friend_raw["id"]) ) return friends_list @@ -419,20 +524,55 @@

    Parameters

    list """ return Pages( - requests=self.requests, + cso=self.cso, url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler - )
    + ) + + async def get_status(self): + """ + Gets the user's status. + :return: A string + """ + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") + return status_req.json()["status"]

    Subclasses

    Methods

    -
    -async def get_followers_count(self) +
    +async def expand(self) +
    +
    +

    Updates some class values. +:return: Nothing

    +
    + +Expand source code + +
    async def expand(self):
    +    """
    +    Updates some class values.
    +    :return: Nothing
    +    """
    +    user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
    +    user_info = user_info_req.json()
    +    description = user_info["description"]
    +    created = iso8601.parse_date(user_info["created"])
    +    is_banned = user_info["isBanned"]
    +    name = user_info["name"]
    +    display_name = user_info["displayName"]
    +    # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
    +    # self.has_premium = has_premium_req
    +    return User(self.cso, self.id, name, description, created, is_banned, display_name)
    +
    +
    +
    +async def get_followers_count(self) ‑> int

    Gets the user's followers count. @@ -441,7 +581,7 @@

    Methods

    Expand source code -
    async def get_followers_count(self):
    +
    async def get_followers_count(self) -> int:
         """
         Gets the user's followers count.
         :return: An integer
    @@ -451,8 +591,8 @@ 

    Methods

    return followers_count
    -
    -async def get_followings_count(self) +
    +async def get_followings_count(self) ‑> int

    Gets the user's followings count. @@ -461,17 +601,18 @@

    Methods

    Expand source code -
    async def get_followings_count(self):
    +
    async def get_followings_count(self) -> int:
         """
         Gets the user's followings count.
         :return: An integer
         """
    -    followings_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    +    followings_count_req = await self.requests.get(
    +        f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
         followings_count = followings_count_req.json()["count"]
         return followings_count
    -
    +
    async def get_friends(self)
    @@ -491,13 +632,13 @@

    Methods

    friends_list = [] for friend_raw in friends_raw: friends_list.append( - User(self.cso, friend_raw["id"]) + await self.cso.client.get_user(friend_raw["id"]) ) return friends_list
    -
    -async def get_friends_count(self) +
    +async def get_friends_count(self) ‑> int

    Gets the user's friends count. @@ -506,7 +647,7 @@

    Methods

    Expand source code -
    async def get_friends_count(self):
    +
    async def get_friends_count(self) -> int:
         """
         Gets the user's friends count.
         :return: An integer
    @@ -516,7 +657,7 @@ 

    Methods

    return friends_count
    -
    +
    async def get_groups(self)
    @@ -538,7 +679,7 @@

    Methods

    return groups
    -
    +
    async def get_limiteds(self)
    @@ -561,14 +702,14 @@

    Returns

    list """ return Pages( - requests=self.requests, + cso=self.cso, url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler )
    -
    -async def get_roblox_badges(self) +
    +async def get_roblox_badges(self) ‑> List[RobloxBadge]

    Gets the user's roblox badges. @@ -577,19 +718,20 @@

    Returns

    Expand source code -
    async def get_roblox_badges(self):
    +
    async def get_roblox_badges(self) -> List[RobloxBadge]:
         """
         Gets the user's roblox badges.
         :return: A list of RobloxBadge instances
         """
    -    roblox_badges_req = await self.requests.get(f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    +    roblox_badges_req = await self.requests.get(
    +        f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
         roblox_badges = []
         for roblox_badge_data in roblox_badges_req.json():
             roblox_badges.append(RobloxBadge(roblox_badge_data))
         return roblox_badges
    -
    +
    async def get_status(self)
    @@ -608,6 +750,82 @@

    Returns

    return status_req.json()["status"]
    +
    +
    +
    +class User +(cso, roblox_id, roblox_name, description, created, banned, display_name) +
    +
    +

    Represents a Roblox user and their profile. +Can be initialized with either a user ID or a username.

    +

    Parameters

    +
    +
    cso : ClientSharedObject
    +
    ClientSharedObject.
    +
    roblox_id : int
    +
    The id of a user.
    +
    roblox_name : str
    +
    The name of the user.
    +
    description : str
    +
    The description of the user.
    +
    created : any
    +
    Time the user was created.
    +
    +
    + +Expand source code + +
    class User(PartialUser):
    +    """
    +    Represents a Roblox user and their profile.
    +    Can be initialized with either a user ID or a username.
    +
    +    Parameters
    +    ----------
    +    cso : ro_py.client.ClientSharedObject
    +            ClientSharedObject.
    +    roblox_id : int
    +            The id of a user.
    +    roblox_name : str
    +            The name of the user.
    +    description : str
    +            The description of the user.
    +    created : any
    +            Time the user was created.
    +    """
    +
    +    def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name):
    +        super().__init__(cso, roblox_id, roblox_name)
    +        self.cso = cso
    +        self.id = roblox_id
    +        self.name = roblox_name
    +        self.description = description
    +        self.created = created
    +        self.is_banned = banned
    +        self.display_name = display_name
    +        self.thumbnails = UserThumbnailGenerator(cso, roblox_id)
    +
    +    async def update(self):
    +        """
    +        Updates some class values.
    +        :return: Nothing
    +        """
    +        user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
    +        user_info = user_info_req.json()
    +        self.description = user_info["description"]
    +        self.created = iso8601.parse_date(user_info["created"])
    +        self.is_banned = user_info["isBanned"]
    +        self.name = user_info["name"]
    +        self.display_name = user_info["displayName"]
    +        return self
    +
    +

    Ancestors

    + +

    Methods

    +
    async def update(self)
    @@ -630,12 +848,25 @@

    Returns

    self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") - # self.has_premium = has_premium_req return self
    +

    Inherited members

    +
    @@ -664,19 +895,29 @@

    Index

  • Classes

    • +

      Events

      + +
    • +
    • PartialUser

      +
    • User

    • diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 89649ee3..19ce432a 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -126,7 +126,7 @@

      Module ro_py.utilities.requests

      if quickreturn: return get_request - raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") + raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) @@ -341,7 +341,7 @@

      Ancestors

      if quickreturn: return get_request - raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") + raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") def back_post(self, *args, **kwargs): kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) @@ -549,7 +549,7 @@

      Methods

      if quickreturn: return get_request - raise status_code_error(get_request.status_code)(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}")
      + raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}")
      diff --git a/docs/wall.html b/docs/wall.html index 35563f21..6adc8c73 100644 --- a/docs/wall.html +++ b/docs/wall.html @@ -57,20 +57,25 @@

      Module ro_py.wall

      from typing import List from ro_py.captcha import UnsolvedCaptcha from ro_py.utilities.pages import Pages, SortOrder +from ro_py.users import PartialUser + + +endpoint = "https://groups.roblox.com" class WallPost: """ - Represents a roblox wall post. + Represents a Roblox wall post. """ def __init__(self, cso, wall_data, group): + self.cso = cso self.requests = cso.requests self.group = group self.id = wall_data['id'] self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username']) + self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username']) async def delete(self): wall_req = await self.requests.delete( @@ -94,13 +99,14 @@

      Module ro_py.wall

      async def get_posts(self, sort_order=SortOrder.Ascending, limit=100): wall_req = Pages( - requests=self.cso, + cso=self.cso, url=endpoint + f"/v2/groups/{self.group.id}/wall/posts", sort_order=sort_order, limit=limit, handler=wall_post_handler, handler_args=self.group ) + await wall_req.get_page() return wall_req async def post(self, content, captcha_key=None): @@ -170,13 +176,14 @@

      Classes

      async def get_posts(self, sort_order=SortOrder.Ascending, limit=100): wall_req = Pages( - requests=self.cso, + cso=self.cso, url=endpoint + f"/v2/groups/{self.group.id}/wall/posts", sort_order=sort_order, limit=limit, handler=wall_post_handler, handler_args=self.group ) + await wall_req.get_page() return wall_req async def post(self, content, captcha_key=None): @@ -212,13 +219,14 @@

      Methods

      async def get_posts(self, sort_order=SortOrder.Ascending, limit=100):
           wall_req = Pages(
      -        requests=self.cso,
      +        cso=self.cso,
               url=endpoint + f"/v2/groups/{self.group.id}/wall/posts",
               sort_order=sort_order,
               limit=limit,
               handler=wall_post_handler,
               handler_args=self.group
           )
      +    await wall_req.get_page()
           return wall_req
      @@ -259,23 +267,24 @@

      Methods

      (cso, wall_data, group)
      -

      Represents a roblox wall post.

      +

      Represents a Roblox wall post.

      Expand source code
      class WallPost:
           """
      -    Represents a roblox wall post.
      +    Represents a Roblox wall post.
           """
           def __init__(self, cso, wall_data, group):
      +        self.cso = cso
               self.requests = cso.requests
               self.group = group
               self.id = wall_data['id']
               self.body = wall_data['body']
               self.created = iso8601.parse_date(wall_data['created'])
               self.updated = iso8601.parse_date(wall_data['updated'])
      -        self.poster = User(requests, wall_data['user']['userId'], wall_data['user']['username'])
      +        self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username'])
       
           async def delete(self):
               wall_req = await self.requests.delete(
      
      From 9a24b722b7e7ab5ea006e26091bf14639b366b10 Mon Sep 17 00:00:00 2001
      From: jmkdev 
      Date: Wed, 27 Jan 2021 14:22:30 -0500
      Subject: [PATCH 355/518] Update README.md
      
      ---
       README.md | 5 +++--
       1 file changed, 3 insertions(+), 2 deletions(-)
      
      diff --git a/README.md b/README.md
      index 8775a079..1af0588f 100644
      --- a/README.md
      +++ b/README.md
      @@ -2,7 +2,7 @@
           ro.py
           
      -

      ro.py is a powerful Python 3 wrapper for the Roblox Web API.

      +

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by [@jmkd3v](https://github.com/jmkd3v) and [@iranathan](https://github.com/iranathan)

      Information | Requirements | @@ -53,8 +53,9 @@ Known issue: wxPython sometimes has trouble building on certain devices. I put w ## Credits [@iranathan](https://github.com/iranathan) - maintainer -[@jmk-developer](https://github.com/jmk-developer) - maintainer +[@jmkd3v](https://github.com/jmkd3v) - maintainer [@nsg-mfd](https://github.com/nsg-mfd) - helped with endpoints +(adding more soon) ## Other Libraries ro.py not for you? Come check out these other libraries! From 5c85303ad43e3307adf21ef74b920048015991f5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 27 Jan 2021 14:23:27 -0500 Subject: [PATCH 356/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1af0588f..3121d54a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ro.py
      -

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by [@jmkd3v](https://github.com/jmkd3v) and [@iranathan](https://github.com/iranathan)

      +

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and iranathan.

      Information | Requirements | From 10b6bf5bad59b48a7c13f7e362b2d5f7c842b4c6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 09:23:28 -0500 Subject: [PATCH 357/518] New badges + Discord --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3121d54a..2ff3019b 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,25 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and iranathan.

      +

      - Information | - Requirements | - Disclaimer | - Documentation | - Examples | - Credits | - License + ro.py Discord + ro.py PyPI + ro.py PyPI Downloads + ro.py PyPI License + ro.py GitHub Commit Activity + ro.py GitHub Last Commit +

      + +

      + Information | + Discord | + Requirements | + Disclaimer | + Documentation | + Examples | + Credits | + License

      ## Information @@ -18,6 +29,9 @@ Welcome, and thank you for using ro.py! ro.py is an object oriented, asynchronous wrapper for the Roblox Web API (and other Roblox-related APIs) with many new and interesting features. ro.py allows you to automate much of what you would do on the Roblox website and on other Roblox-related websites. +## Update: ro.py on Discord +I’ve set up a small ro.py Discord server. It’s obviously very tiny, but some of you can be the first people to help found the server. If you need support for the library, you can ask your questions here if you need faster support. http://j-mk.ml/ro.py + ## Get Started If you are looking for a full tutorial on ro.py, check out [the new DevForum article!](https://devforum.roblox.com/t/use-python-to-interact-with-the-roblox-api-with-ro-py/1006465) From 3ddfe1084074e443cbd420bd2fd753d99213f359 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 09:27:42 -0500 Subject: [PATCH 358/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ff3019b..7f056c08 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ro.py
      -

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and iranathan.

      +

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      ro.py Discord From 01d3ce9182172c8f2a5ca47dad1be5cdfcc7c1ff Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 11:20:13 -0500 Subject: [PATCH 359/518] Non-background bots --- ro_py/extensions/bots.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ro_py/extensions/bots.py b/ro_py/extensions/bots.py index 038b96d1..fa6063f0 100644 --- a/ro_py/extensions/bots.py +++ b/ro_py/extensions/bots.py @@ -63,14 +63,18 @@ def _generate_help(self): help_string = help_string + "\n" + command + ": " + command.help[:24] return help_string - def run(self, token): + def run(self, token, background=False): self.keepgoing = True self.token_login(token) self.notifications.on_notification = self._on_notification self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) - while self.keepgoing: - sleep(1/32) + if not background: + while self.keepgoing: + sleep(1/32) + + def stop(self): + self.keepgoing = False async def _process_command(self, data, n_data): content = data["content"] From 85969428796649f45b39f5af803bbd02a826f850 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 11:43:36 -0500 Subject: [PATCH 360/518] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7f056c08..51eace6a 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,11 @@ pip install git+git://github.com/rbx-libdev/ro.py.git ``` Known issue: wxPython sometimes has trouble building on certain devices. I put wxPython last on the requirements so Python attempts to install it last, so you can safely ignore this error as everything else should be installed. -## Credits -[@iranathan](https://github.com/iranathan) - maintainer -[@jmkd3v](https://github.com/jmkd3v) - maintainer -[@nsg-mfd](https://github.com/nsg-mfd) - helped with endpoints -(adding more soon) +## Contributors + + + + ## Other Libraries ro.py not for you? Come check out these other libraries! From fce791f210fa31058824f30c64e0e889310dff76 Mon Sep 17 00:00:00 2001 From: ira Date: Thu, 28 Jan 2021 17:44:06 +0100 Subject: [PATCH 361/518] Make events faster, wall_command example + more. --- examples/filter_wall.py | 20 +++++++++++ examples/on_asset_change.py | 5 +-- examples/on_shout_event.py | 5 +-- examples/wall_commands.py | 67 +++++++++++++++++++++++++++++++++++++ ro_py/client.py | 8 +---- ro_py/groups.py | 56 ++++++++++++++++++++----------- ro_py/utilities/pages.py | 3 ++ ro_py/wall.py | 5 ++- 8 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 examples/filter_wall.py create mode 100644 examples/wall_commands.py diff --git a/examples/filter_wall.py b/examples/filter_wall.py new file mode 100644 index 00000000..b5872393 --- /dev/null +++ b/examples/filter_wall.py @@ -0,0 +1,20 @@ +from ro_py import Client +import asyncio + +group_id = 1 +swear_words = ["cow"] +client = Client("COOKIE") + + +async def on_wall_post(post): + for word in swear_words: + if word in post.body: + await post.delete() + + +async def main(): + group = await client.get_group(group_id) + group.events.bind(client.events.on_wall_post, on_wall_post) + +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/on_asset_change.py b/examples/on_asset_change.py index 6dfb431c..828769f4 100644 --- a/examples/on_asset_change.py +++ b/examples/on_asset_change.py @@ -10,6 +10,7 @@ async def on_asset_change(old, new): async def main(): asset = await client.get_asset(3897171912) - await asset.events.bind(on_asset_change, client.events.on_asset_change) + asset.events.bind(on_asset_change, client.events.on_asset_change) -asyncio.run(main()) +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/on_shout_event.py b/examples/on_shout_event.py index 9ccdda96..983d1332 100644 --- a/examples/on_shout_event.py +++ b/examples/on_shout_event.py @@ -9,6 +9,7 @@ async def on_shout(old_shout, new_shout): async def main(): g = await client.get_group(1) - await g.events.bind(on_shout, "on_shout_update") + g.events.bind(on_shout, "on_shout_update") -asyncio.run(main()) +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/wall_commands.py b/examples/wall_commands.py new file mode 100644 index 00000000..dca8ed19 --- /dev/null +++ b/examples/wall_commands.py @@ -0,0 +1,67 @@ +from ro_py import Client +import asyncio + +group_id = 2695946 # group id +auto_delete = False # Automatically delete the wall post when the command is executed +prefix = "!" # prefix for commands +allowed_roles = [255, 254] # roles allowed to use commands +client = Client("COOKIE") + + +async def on_wall_post(post): + print('new post from:', post.poster.name) + # Check if the post starts with prefix. + if post.body.startswith(prefix): + # Get the user that posted. + member = await client.group.get_member_by_id(post.poster.id) + # Check if the member is allowed to execute commands. + if member.role.rank in allowed_roles: + # set args and command variables. + args = post.body.split(" ") + command = args[0].replace(prefix, "") + + # check if we need to delete the wall post + if auto_delete: + # delete the post + await post.delete() + + # !promote + # Promotes the user in the group. + if command == "promote": + target = await client.group.get_member_by_username(args[1]) + old_role, new_role = await target.promote() + print( + f'[!] {target.name} ({target.id}) was promoted from {old_role.name} to {new_role.name} by {member.name} ({member.id})') + + # demote + # Demotes a user in the group. + if command == "demote": + target = await client.group.get_member_by_username(args[1]) + old_role, new_role = await target.demote() + print( + f'[!] {target.name} ({target.id}) was demoted from {old_role.name} to {new_role.name} by {member.name} ({member.id})') + + # setrank + # Sets the rank of a user. + if command == "setrank": + target = await client.group.get_member_by_username(args[1]) + roles = await client.group.get_roles() + for role in roles: + if role.name == args[2]: + await target.setrank(role.id) + + # shout + # shouts something to the group. + if command == "shout": + args.pop(0) + content = " ".join(args) + await client.group.update_shout(content) + + +async def main(): + client.group = await client.get_group(group_id) + await client.group.events.bind(on_wall_post, client.events.on_wall_post) + + +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/ro_py/client.py b/ro_py/client.py index 57c9b98d..0e64f86b 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -186,13 +186,7 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - user = self.cso.cache.get(CacheType.Users, user_id) - if not user: - user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded - return user + return await self.get_user(user_id) else: raise UserDoesNotExistError diff --git a/ro_py/groups.py b/ro_py/groups.py index 9567722a..992b7f8a 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -6,6 +6,7 @@ import copy import iso8601 import asyncio + from ro_py.wall import Wall from ro_py.roles import Role from ro_py.users import PartialUser @@ -159,6 +160,29 @@ async def get_member_by_id(self, roblox_id): member = Member(self.cso, roblox_id, "", self, role) return member + async def get_member_by_username(self, name): + user = await self.cso.client.get_user_by_username(name) + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {name} was not found in group {self.id}") + + # Create data to return. + role = Role(self.cso, self, group_data['role']) + member = Member(self.cso, user.id, user.name, self, role) + return member + async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( cso=self.cso, @@ -368,29 +392,25 @@ async def on_join_request(self, func: Callable, delay: int): new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - if asyncio.iscoroutinefunction(func): - await func(new_req) - else: - func(new_req) + asyncio.create_task(func(new_req)) async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts() - newest_wall_poster = current_wall_posts.data[0].poster.id + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + newest_wall_post = current_wall_posts.data[0].id while True: await asyncio.sleep(delay) - current_wall_posts = await self.group.wall.get_posts() + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) current_wall_posts = current_wall_posts.data - if current_wall_posts[0].poster.id != newest_wall_poster: + post = current_wall_posts[0] + if post.id != newest_wall_post: new_posts = [] for post in current_wall_posts: - if post.poster.id != newest_wall_poster: - new_posts.append(post) - newest_wall_poster = current_wall_posts[0].poster.id + if post.id == newest_wall_post: + break + new_posts.append(post) + newest_wall_post = current_wall_posts[0].id for new_post in new_posts: - if asyncio.iscoroutinefunction(func): - await func(new_post) - else: - func(new_post) + asyncio.create_task(func(new_post)) async def on_group_change(self, func: Callable, delay: int): await self.group.update() @@ -403,8 +423,4 @@ async def on_group_change(self, func: Callable, delay: int): if getattr(self.group, attr) != value: has_changed = True if has_changed: - if asyncio.iscoroutinefunction(func): - await func(current_group, self.group) - else: - func(current_group, self.group) - current_group = copy.copy(self.group) + asyncio.create_task(func(current_group, self.group)) diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 6342bc9d..e59aaa4f 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -66,6 +66,9 @@ async def get_page(self, cursor=None): if cursor: this_parameters["cursor"] = cursor + for name, value in self.parameters.items(): + this_parameters[name] = value + page_req = await self.requests.get( url=self.url, params=this_parameters diff --git a/ro_py/wall.py b/ro_py/wall.py index d8fabd52..2390ddf3 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -20,7 +20,10 @@ def __init__(self, cso, wall_data, group): self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username']) + if wall_data['poster']: + self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + else: + self.poster = None async def delete(self): wall_req = await self.requests.delete( From 6ab882b3e8c16db3ddd7735bf35aa1bf0e8f3e31 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 12:07:24 -0500 Subject: [PATCH 362/518] wxPython is no longer required --- ro_py/extensions/prompt.py | 9 ++++++--- setup_info.py | 4 +--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ro_py/extensions/prompt.py b/ro_py/extensions/prompt.py index 5c65f424..e8f3fac3 100644 --- a/ro_py/extensions/prompt.py +++ b/ro_py/extensions/prompt.py @@ -5,9 +5,12 @@ """ -import wx -import wxasync -from wx import html2 +try: + import wx + import wxasync + from wx import html2 +except ModuleNotFoundError: + raise Exception("Please install wxPython and wxAsync from pip to use the prompt extension.") import pytweening from wx.lib.embeddedimage import PyEmbeddedImage diff --git a/setup_info.py b/setup_info.py index ecfcfea2..e1d52502 100644 --- a/setup_info.py +++ b/setup_info.py @@ -23,8 +23,6 @@ "httpx", "iso8601", "signalrcore", - "pytweening", - "wxPython", - "wxasync" + "pytweening" ] } From 67d7a4865d6b383aafc6cb6bb0107117bb21dab6 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 28 Jan 2021 14:05:07 -0500 Subject: [PATCH 363/518] Locked groups --- ro_py/groups.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 992b7f8a..f25a4e56 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -81,6 +81,7 @@ def __init__(self, cso, group_id): self.public_entry_allowed = None self.shout = None self.events = Events(cso, self) + self.is_locked = False async def update(self): """ @@ -98,7 +99,8 @@ async def update(self): self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None - # self.is_locked = group_info["isLocked"] + if "isLocked" in group_info: + self.is_locked = group_info["isLocked"] async def update_shout(self, message): """ From 7fbb808126d220f6795fe833bd96b743da4776e6 Mon Sep 17 00:00:00 2001 From: KILR <65610641+KILR007@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:55:02 +0530 Subject: [PATCH 364/518] fix discord invite --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51eace6a..d295ff8f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      - ro.py Discord + ro.py Discord ro.py PyPI ro.py PyPI Downloads ro.py PyPI License From d04db5b1351130b6b1b2e59016797f046a48bd7d Mon Sep 17 00:00:00 2001 From: KILR <65610641+KILR007@users.noreply.github.com> Date: Fri, 29 Jan 2021 09:56:18 +0530 Subject: [PATCH 365/518] https link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d295ff8f..3793f0e1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      - ro.py Discord + ro.py Discord ro.py PyPI ro.py PyPI Downloads ro.py PyPI License From af906e9c2db7c058a0488625172097aae65d8884 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 29 Jan 2021 13:10:12 -0500 Subject: [PATCH 366/518] Documented group values --- ro_py/groups.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index f25a4e56..4c1a9bcc 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -70,18 +70,31 @@ class Group: """ def __init__(self, cso, group_id): self.cso = cso + """Client Shared Object""" self.requests = cso.requests + "Requests object." self.id = group_id + "Group ID." self.wall = Wall(self.cso, self) + """Wall object.""" self.name = None + """Group name.""" self.description = None + """Group description.""" self.owner = None + """Group owner.""" self.member_count = None + """Group member count.""" self.is_builders_club_only = None + """True if the group is Builders Club (Premium) only. This seems to have been removed.""" self.public_entry_allowed = None + """Public entry allowed (private/public group)""" self.shout = None + """Current group shout (Shout)""" self.events = Events(cso, self) + """Events object.""" self.is_locked = False + """True if this is a locked group.""" async def update(self): """ From 2e8d8d85a5c8808c59720fe39c1d63f2a3b30358 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 29 Jan 2021 19:02:59 -0500 Subject: [PATCH 367/518] group data test --- ro_py/groups.py | 18 ++++++++++++++---- ro_py/users.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 4c1a9bcc..02777bdd 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -223,12 +223,22 @@ async def get_members(self, sort_order=SortOrder.Ascending, limit=100): return pages -class PartialGroup(Group): +class PartialGroup: """ - Represents a group with less information + Represents a group with less information. + + Different information will be present here in different circumstances. + If it was generated as a game owner, it might only contain an ID and a name. + If it was generated from, let's say, groups/v2/users/userid/groups/roles, it'll also contain a member count. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, cso, data): + self.cso = cso + self.requests = cso.requests + self.id = data["id"] + self.name = data["name"] + self.member_count = None + if "memberCount" in data: + self.member_count = data["memberCount"] class Member(PartialUser): diff --git a/ro_py/users.py b/ro_py/users.py index daad05ff..249cc4c6 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -109,7 +109,7 @@ async def get_groups(self): groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group)) return groups async def get_limiteds(self): From dcad7a46a8b7b1c8826ad4c6e56e98fce210b261 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 29 Jan 2021 19:06:33 -0500 Subject: [PATCH 368/518] Update groups.py --- ro_py/groups.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index 02777bdd..83c6c153 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -240,6 +240,9 @@ def __init__(self, cso, data): if "memberCount" in data: self.member_count = data["memberCount"] + async def expand(self): + return self.cso.client.get_group(self.id) + class Member(PartialUser): """ From ff5be49b0f2612b399284b90c45f4f1e757c3fa8 Mon Sep 17 00:00:00 2001 From: James Vong Date: Sat, 30 Jan 2021 10:15:52 -0800 Subject: [PATCH 369/518] Fix member_handler Add member as an argument to members.append() so that it can return an array of members. --- ro_py/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 83c6c153..64dda480 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -60,7 +60,7 @@ def join_request_handler(cso, data, args): def member_handler(cso, data, args): members = [] for member in data: - members.append() + members.append(member) return members From 505071f84aac31abf5af7edf034478963ef52a0f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 30 Jan 2021 21:47:03 -0500 Subject: [PATCH 370/518] Updated version identifier --- setup_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index e1d52502..7f655abc 100644 --- a/setup_info.py +++ b/setup_info.py @@ -5,7 +5,7 @@ setup_info = { "name": "ro-py", - "version": "1.1.2", + "version": "1.1.3", "author": "jmkdev and iranathan", "author_email": "jmk@jmksite.dev", "description": "ro.py is a Python wrapper for the Roblox web API.", From c26e13ac0404588b702d0123f3a88d0d92ed4a55 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 30 Jan 2021 22:14:48 -0500 Subject: [PATCH 371/518] removed pytweening + added it here --- ro_py/extensions/prompt.py | 6 +++--- setup_info.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ro_py/extensions/prompt.py b/ro_py/extensions/prompt.py index e8f3fac3..6f98ed7d 100644 --- a/ro_py/extensions/prompt.py +++ b/ro_py/extensions/prompt.py @@ -9,10 +9,10 @@ import wx import wxasync from wx import html2 + import pytweening + from wx.lib.embeddedimage import PyEmbeddedImage except ModuleNotFoundError: - raise Exception("Please install wxPython and wxAsync from pip to use the prompt extension.") -import pytweening -from wx.lib.embeddedimage import PyEmbeddedImage + raise Exception("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") icon_image = PyEmbeddedImage( b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B' diff --git a/setup_info.py b/setup_info.py index 7f655abc..2cd4e544 100644 --- a/setup_info.py +++ b/setup_info.py @@ -22,7 +22,6 @@ "install_requires": [ "httpx", "iso8601", - "signalrcore", - "pytweening" + "signalrcore" ] } From 0fdd0291ecc5d7ded5540a395244a06f4eadde36 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 30 Jan 2021 22:16:29 -0500 Subject: [PATCH 372/518] change exception --- ro_py/extensions/prompt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/extensions/prompt.py b/ro_py/extensions/prompt.py index 6f98ed7d..29fd18e7 100644 --- a/ro_py/extensions/prompt.py +++ b/ro_py/extensions/prompt.py @@ -12,7 +12,7 @@ import pytweening from wx.lib.embeddedimage import PyEmbeddedImage except ModuleNotFoundError: - raise Exception("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") + raise ModuleNotFoundError("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") icon_image = PyEmbeddedImage( b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B' From bdcdc465e08bd6a93f9fb2ef1a70a14699236dee Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 30 Jan 2021 22:22:27 -0500 Subject: [PATCH 373/518] Update README.md --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3793f0e1..fa083758 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,16 @@ If you are looking for a full tutorial on ro.py, check out [the new DevForum art - httpx (for sending requests) - iso8601 (for parsing dates) - signalrcore (for recieving notifications) -- ~~cachecontrol (for caching requests)~~ -- ~~requests-async (for sending requests, might be updated to a new lib soon)~~ -- pytweening (for UI animations for the "prompts" extension, optional) -- wxPython (for the "prompts" extension, optional) -- wxasync (see above) + +#### Previous Requirements +- cachecontrol (for caching requests) +- requests-async (for sending requests, might be updated to a new lib soon) +- ~~pytweening (for animations, see below)~~ +- ~~wxPython (for the "prompts" extension, optional)~~ +- ~~wxasync (see above)~~ + +#### Prompts Extension Requirements +You'll need to install `wxPython`, `wxasync` and `pytweening` to use the prompts extension. If it is not present, an error will be raised. ## Disclaimer We are not responsible for any malicious use of this library. From 0077d60703ee19f3a3a66a24f9b073624ced1e20 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 09:54:58 -0500 Subject: [PATCH 374/518] Create README.md --- examples/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..b6ef20f8 --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +# NOT READY From 092040ba65fc0ede19a59dad4aaa989d0866ac71 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 10:18:42 -0500 Subject: [PATCH 375/518] Update README.md --- examples/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index b6ef20f8..fd5605e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1 +1,16 @@ -# NOT READY +

      + ro.py +
      +

      + +# Examples +- [user.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/user.py): Basic example that grabs a user and some information about it. +- [username.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/username.py): Same as user.py except searches via a username instead of a user ID. +- [guilogin.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/guilogin.py): Example of the prompts extension's `authenticate_prompt`. +- [wall_commands.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/wall_commands.py): Simple bot command like example using the `on_wall_post` event. +- [filter_wall.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/filter_wall.py): Extra banned words for group wall. +- [joingame.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/joingame.py): Example of joining a game. +- [on_asset_change.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/on_asset_change.py): Example of the `on_asset_change` event. +- [on_shout_event.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/on_shout_event.py): Example of the `on_shout_event` event. +- [twocaptcha_login.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/twocaptcha_login.py): Logs in to an account using a username and password using 2captcha to solve the captcha. +- [anti_captcha_login.py](https://github.com/rbx-libdev/ro.py/blob/main/examples/anti_captcha_login.py): Logs in to an account using a username and password using anticaptcha to solve the captcha. From 4ab5a578c568b79d7e1e85a40ba38fb9ea060630 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 11:37:29 -0500 Subject: [PATCH 376/518] Create README.md --- resources/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 resources/README.md diff --git a/resources/README.md b/resources/README.md new file mode 100644 index 00000000..c0096c79 --- /dev/null +++ b/resources/README.md @@ -0,0 +1,7 @@ +

      + ro.py +
      +

      + +# Resources +This folder contains ro.py icons, images, and other resources. From 0954b7c44884286b32dd774f4afb73b0880e6d54 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 11:38:27 -0500 Subject: [PATCH 377/518] Create README.md --- ro_py/README.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 ro_py/README.md diff --git a/ro_py/README.md b/ro_py/README.md new file mode 100644 index 00000000..2b215518 --- /dev/null +++ b/ro_py/README.md @@ -0,0 +1,7 @@ +

      + ro.py +
      +

      + +# Package +This folder is the root package for ro.py, which should be imported as `ro_py`. From 20cbe81c3988cc1521895294e4c1f8d51c17d7b7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 15:40:25 -0500 Subject: [PATCH 378/518] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1036f4b5..a58e61e3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ docstemplate/ build.* docsbuild.bat chat.py +twocaptcha.py +anticaptcha.py From 442ffa74f3247006e8c6b0dc21405b0e6ab45c6a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 15:41:02 -0500 Subject: [PATCH 379/518] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index a58e61e3..dc8e988a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ docsbuild.bat chat.py twocaptcha.py anticaptcha.py +ro_py/extensions/twocaptcha.py +ro_py/extensions/anticaptcha.py From 67838fc656ef9c5cf74c6477e48398ddd0302be5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 15:41:28 -0500 Subject: [PATCH 380/518] Update .gitignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index dc8e988a..1036f4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,3 @@ docstemplate/ build.* docsbuild.bat chat.py -twocaptcha.py -anticaptcha.py -ro_py/extensions/twocaptcha.py -ro_py/extensions/anticaptcha.py From 6dcdcc734934dc005bb2d0d741771d5147d2444d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 15:42:12 -0500 Subject: [PATCH 381/518] Removed possible rule-breaking extensions We've removed these extensions. --- ro_py/extensions/anticaptcha.py | 62 --------------------------------- ro_py/extensions/twocaptcha.py | 44 ----------------------- 2 files changed, 106 deletions(-) delete mode 100644 ro_py/extensions/anticaptcha.py delete mode 100644 ro_py/extensions/twocaptcha.py diff --git a/ro_py/extensions/anticaptcha.py b/ro_py/extensions/anticaptcha.py deleted file mode 100644 index f83ada8f..00000000 --- a/ro_py/extensions/anticaptcha.py +++ /dev/null @@ -1,62 +0,0 @@ -from ro_py.utilities.errors import IncorrectKeyError, InsufficientCreditError, NoAvailableWorkersError -from ro_py.captcha import UnsolvedCaptcha -import requests_async -import asyncio - -endpoint = "https://2captcha.com" - - -class Task: - def __init__(self): - self.type = "FunCaptchaTaskProxyless" - self.website_url = None - self.website_public_key = None - self.funcaptcha_api_js_subdomain = None - - def get_raw(self): - return { - "type": self.type, - "websiteURL": self.website_url, - "websitePublicKey": self.website_public_key, - "funcaptchaApiJSSubdomain": self.funcaptcha_api_js_subdomain - } - - -class AntiCaptcha: - def __init__(self, api_key): - self.api_key = api_key - - async def solve(self, captcha: UnsolvedCaptcha): - task = Task() - task.website_url = "https://roblox.com" - task.website_public_key = captcha.pkey - task.funcaptcha_api_js_subdomain = "https://roblox-api.arkoselabs.com" - - data = { - "clientKey": self.api_key, - "task": task.get_raw() - } - - create_req = await requests_async.post('https://api.anti-captcha.com/createTask', json=data) - create_res = create_req.json() - if create_res['errorId'] == 1: - raise IncorrectKeyError("The provided anit-captcha api key was incorrect.") - if create_res['errorId'] == 2: - raise NoAvailableWorkersError("There are currently no available workers.") - if create_res['errorId'] == 10: - raise InsufficientCreditError("Insufficient credit in the 2captcha account.") - - solution = None - while True: - await asyncio.sleep(5) - check_data = { - "clientKey": self.api_key, - "taskId": create_res['taskId'] - } - check_req = await requests_async.get("https://api.anti-captcha.com/getTaskResult", json=check_data) - check_res = check_req.json() - if check_res['status'] == "ready": - solution = check_res['solution']['token'] - break - - return solution diff --git a/ro_py/extensions/twocaptcha.py b/ro_py/extensions/twocaptcha.py deleted file mode 100644 index 17137023..00000000 --- a/ro_py/extensions/twocaptcha.py +++ /dev/null @@ -1,44 +0,0 @@ -from ro_py.utilities.errors import IncorrectKeyError, InsufficientCreditError, NoAvailableWorkersError -from ro_py.captcha import UnsolvedCaptcha -import requests_async -import asyncio - -endpoint = "https://2captcha.com" - - -class TwoCaptcha: - # roblox-api.arkoselabs.com - def __init__(self, api_key): - self.api_key = api_key - - async def solve(self, captcha: UnsolvedCaptcha): - url = endpoint + "/in.php" - url += f"?key={self.api_key}" - url += "&method=funcaptcha" - url += f"&publickey={captcha.pkey}" - url += "&surl=https://roblox-api.arkoselabs.com" - url += "&pageurl=https://www.roblox.com" - url += "&json=1" - - solve_req = await requests_async.post(url) - data = solve_req.json() - if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST": - raise IncorrectKeyError("The provided 2captcha api key was incorrect.") - if data['request'] == "ERROR_ZERO_BALANCE": - raise InsufficientCreditError("Insufficient credit in the 2captcha account.") - if data['request'] == "ERROR_NO_SLOT_AVAILABLE": - raise NoAvailableWorkersError("There are currently no available workers.") - task_id = data['request'] - - solution = None - while True: - await asyncio.sleep(5) - captcha_req = await requests_async.get(endpoint + f"/res.php" - f"?key={self.api_key}" - f"&id={task_id}" - f"&json=1&action=get") - captcha_data = captcha_req.json() - if captcha_data['request'] != "CAPCHA_NOT_READY": - solution = captcha_data['request'] - break - return solution From ac6a2392047c9be3d6bea65d9f8acaaa3a09629e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 15:48:57 -0500 Subject: [PATCH 382/518] Docs updated + badges in docs --- docs/client.html | 24 +- docs/extensions/anticaptcha.html | 315 -------------------------- docs/extensions/bots.html | 43 +++- docs/extensions/index.html | 10 - docs/extensions/prompt.html | 13 +- docs/extensions/twocaptcha.html | 240 -------------------- docs/groups.html | 375 +++++++++++++++++++++++-------- docs/index.html | 38 +++- docs/users.html | 6 +- docs/utilities/pages.html | 9 + docs/wall.html | 10 +- ro_py/__init__.py | 27 ++- 12 files changed, 393 insertions(+), 717 deletions(-) delete mode 100644 docs/extensions/anticaptcha.html delete mode 100644 docs/extensions/twocaptcha.html diff --git a/docs/client.html b/docs/client.html index f0c26f46..f320c77d 100644 --- a/docs/client.html +++ b/docs/client.html @@ -242,13 +242,7 @@

      Module ro_py.client

      username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - user = self.cso.cache.get(CacheType.Users, user_id) - if not user: - user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded - return user + return await self.get_user(user_id) else: raise UserDoesNotExistError @@ -516,13 +510,7 @@

      Parameters

      username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - user = self.cso.cache.get(CacheType.Users, user_id) - if not user: - user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded - return user + return await self.get_user(user_id) else: raise UserDoesNotExistError @@ -910,13 +898,7 @@

      Parameters

      username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - user = self.cso.cache.get(CacheType.Users, user_id) - if not user: - user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded - return user + return await self.get_user(user_id) else: raise UserDoesNotExistError
      diff --git a/docs/extensions/anticaptcha.html b/docs/extensions/anticaptcha.html deleted file mode 100644 index fb6835a6..00000000 --- a/docs/extensions/anticaptcha.html +++ /dev/null @@ -1,315 +0,0 @@ - - - - - - -ro_py.extensions.anticaptcha API documentation - - - - - - - - - - - - -
      -
      -
      -

      Module ro_py.extensions.anticaptcha

      -
      -
      -
      - -Expand source code - -
      from ro_py.utilities.errors import IncorrectKeyError, InsufficientCreditError, NoAvailableWorkersError
      -from ro_py.captcha import UnsolvedCaptcha
      -import requests_async
      -import asyncio
      -
      -endpoint = "https://2captcha.com"
      -
      -
      -class Task:
      -    def __init__(self):
      -        self.type = "FunCaptchaTaskProxyless"
      -        self.website_url = None
      -        self.website_public_key = None
      -        self.funcaptcha_api_js_subdomain = None
      -
      -    def get_raw(self):
      -        return {
      -            "type": self.type,
      -            "websiteURL": self.website_url,
      -            "websitePublicKey": self.website_public_key,
      -            "funcaptchaApiJSSubdomain": self.funcaptcha_api_js_subdomain
      -        }
      -
      -
      -class AntiCaptcha:
      -    def __init__(self, api_key):
      -        self.api_key = api_key
      -
      -    async def solve(self, captcha: UnsolvedCaptcha):
      -        task = Task()
      -        task.website_url = "https://roblox.com"
      -        task.website_public_key = captcha.pkey
      -        task.funcaptcha_api_js_subdomain = "https://roblox-api.arkoselabs.com"
      -
      -        data = {
      -            "clientKey": self.api_key,
      -            "task": task.get_raw()
      -        }
      -
      -        create_req = await requests_async.post('https://api.anti-captcha.com/createTask', json=data)
      -        create_res = create_req.json()
      -        if create_res['errorId'] == 1:
      -            raise IncorrectKeyError("The provided anit-captcha api key was incorrect.")
      -        if create_res['errorId'] == 2:
      -            raise NoAvailableWorkersError("There are currently no available workers.")
      -        if create_res['errorId'] == 10:
      -            raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
      -
      -        solution = None
      -        while True:
      -            await asyncio.sleep(5)
      -            check_data = {
      -                "clientKey": self.api_key,
      -                "taskId": create_res['taskId']
      -            }
      -            check_req = await requests_async.get("https://api.anti-captcha.com/getTaskResult", json=check_data)
      -            check_res = check_req.json()
      -            if check_res['status'] == "ready":
      -                solution = check_res['solution']['token']
      -                break
      -
      -        return solution
      -
      -
      -
      -
      -
      -
      -
      -
      -
      -

      Classes

      -
      -
      -class AntiCaptcha -(api_key) -
      -
      -
      -
      - -Expand source code - -
      class AntiCaptcha:
      -    def __init__(self, api_key):
      -        self.api_key = api_key
      -
      -    async def solve(self, captcha: UnsolvedCaptcha):
      -        task = Task()
      -        task.website_url = "https://roblox.com"
      -        task.website_public_key = captcha.pkey
      -        task.funcaptcha_api_js_subdomain = "https://roblox-api.arkoselabs.com"
      -
      -        data = {
      -            "clientKey": self.api_key,
      -            "task": task.get_raw()
      -        }
      -
      -        create_req = await requests_async.post('https://api.anti-captcha.com/createTask', json=data)
      -        create_res = create_req.json()
      -        if create_res['errorId'] == 1:
      -            raise IncorrectKeyError("The provided anit-captcha api key was incorrect.")
      -        if create_res['errorId'] == 2:
      -            raise NoAvailableWorkersError("There are currently no available workers.")
      -        if create_res['errorId'] == 10:
      -            raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
      -
      -        solution = None
      -        while True:
      -            await asyncio.sleep(5)
      -            check_data = {
      -                "clientKey": self.api_key,
      -                "taskId": create_res['taskId']
      -            }
      -            check_req = await requests_async.get("https://api.anti-captcha.com/getTaskResult", json=check_data)
      -            check_res = check_req.json()
      -            if check_res['status'] == "ready":
      -                solution = check_res['solution']['token']
      -                break
      -
      -        return solution
      -
      -

      Methods

      -
      -
      -async def solve(self, captcha: UnsolvedCaptcha) -
      -
      -
      -
      - -Expand source code - -
      async def solve(self, captcha: UnsolvedCaptcha):
      -    task = Task()
      -    task.website_url = "https://roblox.com"
      -    task.website_public_key = captcha.pkey
      -    task.funcaptcha_api_js_subdomain = "https://roblox-api.arkoselabs.com"
      -
      -    data = {
      -        "clientKey": self.api_key,
      -        "task": task.get_raw()
      -    }
      -
      -    create_req = await requests_async.post('https://api.anti-captcha.com/createTask', json=data)
      -    create_res = create_req.json()
      -    if create_res['errorId'] == 1:
      -        raise IncorrectKeyError("The provided anit-captcha api key was incorrect.")
      -    if create_res['errorId'] == 2:
      -        raise NoAvailableWorkersError("There are currently no available workers.")
      -    if create_res['errorId'] == 10:
      -        raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
      -
      -    solution = None
      -    while True:
      -        await asyncio.sleep(5)
      -        check_data = {
      -            "clientKey": self.api_key,
      -            "taskId": create_res['taskId']
      -        }
      -        check_req = await requests_async.get("https://api.anti-captcha.com/getTaskResult", json=check_data)
      -        check_res = check_req.json()
      -        if check_res['status'] == "ready":
      -            solution = check_res['solution']['token']
      -            break
      -
      -    return solution
      -
      -
      -
      -
      -
      -class Task -
      -
      -
      -
      - -Expand source code - -
      class Task:
      -    def __init__(self):
      -        self.type = "FunCaptchaTaskProxyless"
      -        self.website_url = None
      -        self.website_public_key = None
      -        self.funcaptcha_api_js_subdomain = None
      -
      -    def get_raw(self):
      -        return {
      -            "type": self.type,
      -            "websiteURL": self.website_url,
      -            "websitePublicKey": self.website_public_key,
      -            "funcaptchaApiJSSubdomain": self.funcaptcha_api_js_subdomain
      -        }
      -
      -

      Methods

      -
      -
      -def get_raw(self) -
      -
      -
      -
      - -Expand source code - -
      def get_raw(self):
      -    return {
      -        "type": self.type,
      -        "websiteURL": self.website_url,
      -        "websitePublicKey": self.website_public_key,
      -        "funcaptchaApiJSSubdomain": self.funcaptcha_api_js_subdomain
      -    }
      -
      -
      -
      -
      -
      -
      -
      - -
      - - - \ No newline at end of file diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index 05997337..a1fb77a2 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -119,14 +119,18 @@

      Module ro_py.extensions.bots

      help_string = help_string + "\n" + command + ": " + command.help[:24] return help_string - def run(self, token): + def run(self, token, background=False): self.keepgoing = True self.token_login(token) self.notifications.on_notification = self._on_notification self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) - while self.keepgoing: - sleep(1/32) + if not background: + while self.keepgoing: + sleep(1/32) + + def stop(self): + self.keepgoing = False async def _process_command(self, data, n_data): content = data["content"] @@ -237,14 +241,18 @@

      Parameters

      help_string = help_string + "\n" + command + ": " + command.help[:24] return help_string - def run(self, token): + def run(self, token, background=False): self.keepgoing = True self.token_login(token) self.notifications.on_notification = self._on_notification self.evtloop = self.cso.evtloop self.evtloop.run_until_complete(self._run()) - while self.keepgoing: - sleep(1/32) + if not background: + while self.keepgoing: + sleep(1/32) + + def stop(self): + self.keepgoing = False async def _process_command(self, data, n_data): content = data["content"] @@ -345,7 +353,7 @@

      Methods

      -def run(self, token) +def run(self, token, background=False)
      @@ -353,14 +361,28 @@

      Methods

      Expand source code -
      def run(self, token):
      +
      def run(self, token, background=False):
           self.keepgoing = True
           self.token_login(token)
           self.notifications.on_notification = self._on_notification
           self.evtloop = self.cso.evtloop
           self.evtloop.run_until_complete(self._run())
      -    while self.keepgoing:
      -        sleep(1/32)
      + if not background: + while self.keepgoing: + sleep(1/32)
      + +
      +
      +def stop(self) +
      +
      +
      +
      + +Expand source code + +
      def stop(self):
      +    self.keepgoing = False
      @@ -528,6 +550,7 @@

      command
    • event
    • run
    • +
    • stop
  • diff --git a/docs/extensions/index.html b/docs/extensions/index.html index 1a91a2d6..634fc31b 100644 --- a/docs/extensions/index.html +++ b/docs/extensions/index.html @@ -64,10 +64,6 @@

    Module ro_py.extensions

    Sub-modules

    -
    ro_py.extensions.anticaptcha
    -
    -
    -
    ro_py.extensions.bots

    This extension houses functions that allow generation of Bot objects, which interpret commands.

    @@ -76,10 +72,6 @@

    Sub-modules

    This extension houses functions that allow human verification prompts for interactive applications.

    -
    ro_py.extensions.twocaptcha
    -
    -
    -
    @@ -107,10 +99,8 @@

    Index

  • Sub-modules

  • diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html index 8e636f0c..1211008b 100644 --- a/docs/extensions/prompt.html +++ b/docs/extensions/prompt.html @@ -61,11 +61,14 @@

    Module ro_py.extensions.prompt

    """ -import wx -import wxasync -from wx import html2 -import pytweening -from wx.lib.embeddedimage import PyEmbeddedImage +try: + import wx + import wxasync + from wx import html2 + import pytweening + from wx.lib.embeddedimage import PyEmbeddedImage +except ModuleNotFoundError: + raise ModuleNotFoundError("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") icon_image = PyEmbeddedImage( b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B' diff --git a/docs/extensions/twocaptcha.html b/docs/extensions/twocaptcha.html deleted file mode 100644 index 369efed4..00000000 --- a/docs/extensions/twocaptcha.html +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - -ro_py.extensions.twocaptcha API documentation - - - - - - - - - - - - -
    -
    -
    -

    Module ro_py.extensions.twocaptcha

    -
    -
    -
    - -Expand source code - -
    from ro_py.utilities.errors import IncorrectKeyError, InsufficientCreditError, NoAvailableWorkersError
    -from ro_py.captcha import UnsolvedCaptcha
    -import requests_async
    -import asyncio
    -
    -endpoint = "https://2captcha.com"
    -
    -
    -class TwoCaptcha:
    -    # roblox-api.arkoselabs.com
    -    def __init__(self, api_key):
    -        self.api_key = api_key
    -
    -    async def solve(self, captcha: UnsolvedCaptcha):
    -        url = endpoint + "/in.php"
    -        url += f"?key={self.api_key}"
    -        url += "&method=funcaptcha"
    -        url += f"&publickey={captcha.pkey}"
    -        url += "&surl=https://roblox-api.arkoselabs.com"
    -        url += "&pageurl=https://www.roblox.com"
    -        url += "&json=1"
    -
    -        solve_req = await requests_async.post(url)
    -        data = solve_req.json()
    -        if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST":
    -            raise IncorrectKeyError("The provided 2captcha api key was incorrect.")
    -        if data['request'] == "ERROR_ZERO_BALANCE":
    -            raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
    -        if data['request'] == "ERROR_NO_SLOT_AVAILABLE":
    -            raise NoAvailableWorkersError("There are currently no available workers.")
    -        task_id = data['request']
    -
    -        solution = None
    -        while True:
    -            await asyncio.sleep(5)
    -            captcha_req = await requests_async.get(endpoint + f"/res.php"
    -                                                              f"?key={self.api_key}"
    -                                                              f"&id={task_id}"
    -                                                              f"&json=1&action=get")
    -            captcha_data = captcha_req.json()
    -            if captcha_data['request'] != "CAPCHA_NOT_READY":
    -                solution = captcha_data['request']
    -                break
    -        return solution
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class TwoCaptcha -(api_key) -
    -
    -
    -
    - -Expand source code - -
    class TwoCaptcha:
    -    # roblox-api.arkoselabs.com
    -    def __init__(self, api_key):
    -        self.api_key = api_key
    -
    -    async def solve(self, captcha: UnsolvedCaptcha):
    -        url = endpoint + "/in.php"
    -        url += f"?key={self.api_key}"
    -        url += "&method=funcaptcha"
    -        url += f"&publickey={captcha.pkey}"
    -        url += "&surl=https://roblox-api.arkoselabs.com"
    -        url += "&pageurl=https://www.roblox.com"
    -        url += "&json=1"
    -
    -        solve_req = await requests_async.post(url)
    -        data = solve_req.json()
    -        if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST":
    -            raise IncorrectKeyError("The provided 2captcha api key was incorrect.")
    -        if data['request'] == "ERROR_ZERO_BALANCE":
    -            raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
    -        if data['request'] == "ERROR_NO_SLOT_AVAILABLE":
    -            raise NoAvailableWorkersError("There are currently no available workers.")
    -        task_id = data['request']
    -
    -        solution = None
    -        while True:
    -            await asyncio.sleep(5)
    -            captcha_req = await requests_async.get(endpoint + f"/res.php"
    -                                                              f"?key={self.api_key}"
    -                                                              f"&id={task_id}"
    -                                                              f"&json=1&action=get")
    -            captcha_data = captcha_req.json()
    -            if captcha_data['request'] != "CAPCHA_NOT_READY":
    -                solution = captcha_data['request']
    -                break
    -        return solution
    -
    -

    Methods

    -
    -
    -async def solve(self, captcha: UnsolvedCaptcha) -
    -
    -
    -
    - -Expand source code - -
    async def solve(self, captcha: UnsolvedCaptcha):
    -    url = endpoint + "/in.php"
    -    url += f"?key={self.api_key}"
    -    url += "&method=funcaptcha"
    -    url += f"&publickey={captcha.pkey}"
    -    url += "&surl=https://roblox-api.arkoselabs.com"
    -    url += "&pageurl=https://www.roblox.com"
    -    url += "&json=1"
    -
    -    solve_req = await requests_async.post(url)
    -    data = solve_req.json()
    -    if data['request'] == "ERROR_WRONG_USER_KEY" or data['request'] == "ERROR_KEY_DOES_NOT_EXIST":
    -        raise IncorrectKeyError("The provided 2captcha api key was incorrect.")
    -    if data['request'] == "ERROR_ZERO_BALANCE":
    -        raise InsufficientCreditError("Insufficient credit in the 2captcha account.")
    -    if data['request'] == "ERROR_NO_SLOT_AVAILABLE":
    -        raise NoAvailableWorkersError("There are currently no available workers.")
    -    task_id = data['request']
    -
    -    solution = None
    -    while True:
    -        await asyncio.sleep(5)
    -        captcha_req = await requests_async.get(endpoint + f"/res.php"
    -                                                          f"?key={self.api_key}"
    -                                                          f"&id={task_id}"
    -                                                          f"&json=1&action=get")
    -        captcha_data = captcha_req.json()
    -        if captcha_data['request'] != "CAPCHA_NOT_READY":
    -            solution = captcha_data['request']
    -            break
    -    return solution
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - \ No newline at end of file diff --git a/docs/groups.html b/docs/groups.html index 44eca4a7..5e41fa75 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -62,6 +62,7 @@

    Module ro_py.groups

    import copy import iso8601 import asyncio + from ro_py.wall import Wall from ro_py.roles import Role from ro_py.users import PartialUser @@ -115,7 +116,7 @@

    Module ro_py.groups

    def member_handler(cso, data, args): members = [] for member in data: - members.append() + members.append(member) return members @@ -125,17 +126,31 @@

    Module ro_py.groups

    """ def __init__(self, cso, group_id): self.cso = cso + """Client Shared Object""" self.requests = cso.requests + "Requests object." self.id = group_id + "Group ID." self.wall = Wall(self.cso, self) + """Wall object.""" self.name = None + """Group name.""" self.description = None + """Group description.""" self.owner = None + """Group owner.""" self.member_count = None + """Group member count.""" self.is_builders_club_only = None + """True if the group is Builders Club (Premium) only. This seems to have been removed.""" self.public_entry_allowed = None + """Public entry allowed (private/public group)""" self.shout = None + """Current group shout (Shout)""" self.events = Events(cso, self) + """Events object.""" + self.is_locked = False + """True if this is a locked group.""" async def update(self): """ @@ -153,7 +168,8 @@

    Module ro_py.groups

    self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None - # self.is_locked = group_info["isLocked"] + if "isLocked" in group_info: + self.is_locked = group_info["isLocked"] async def update_shout(self, message): """ @@ -215,6 +231,29 @@

    Module ro_py.groups

    member = Member(self.cso, roblox_id, "", self, role) return member + async def get_member_by_username(self, name): + user = await self.cso.client.get_user_by_username(name) + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {name} was not found in group {self.id}") + + # Create data to return. + role = Role(self.cso, self, group_data['role']) + member = Member(self.cso, user.id, user.name, self, role) + return member + async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( cso=self.cso, @@ -240,12 +279,25 @@

    Module ro_py.groups

    return pages -class PartialGroup(Group): +class PartialGroup: """ - Represents a group with less information + Represents a group with less information. + + Different information will be present here in different circumstances. + If it was generated as a game owner, it might only contain an ID and a name. + If it was generated from, let's say, groups/v2/users/userid/groups/roles, it'll also contain a member count. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, cso, data): + self.cso = cso + self.requests = cso.requests + self.id = data["id"] + self.name = data["name"] + self.member_count = None + if "memberCount" in data: + self.member_count = data["memberCount"] + + async def expand(self): + return self.cso.client.get_group(self.id) class Member(PartialUser): @@ -424,29 +476,25 @@

    Module ro_py.groups

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - if asyncio.iscoroutinefunction(func): - await func(new_req) - else: - func(new_req) + asyncio.create_task(func(new_req)) async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts() - newest_wall_poster = current_wall_posts.data[0].poster.id + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + newest_wall_post = current_wall_posts.data[0].id while True: await asyncio.sleep(delay) - current_wall_posts = await self.group.wall.get_posts() + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) current_wall_posts = current_wall_posts.data - if current_wall_posts[0].poster.id != newest_wall_poster: + post = current_wall_posts[0] + if post.id != newest_wall_post: new_posts = [] for post in current_wall_posts: - if post.poster.id != newest_wall_poster: - new_posts.append(post) - newest_wall_poster = current_wall_posts[0].poster.id + if post.id == newest_wall_post: + break + new_posts.append(post) + newest_wall_post = current_wall_posts[0].id for new_post in new_posts: - if asyncio.iscoroutinefunction(func): - await func(new_post) - else: - func(new_post) + asyncio.create_task(func(new_post)) async def on_group_change(self, func: Callable, delay: int): await self.group.update() @@ -459,11 +507,7 @@

    Module ro_py.groups

    if getattr(self.group, attr) != value: has_changed = True if has_changed: - if asyncio.iscoroutinefunction(func): - await func(current_group, self.group) - else: - func(current_group, self.group) - current_group = copy.copy(self.group)
    + asyncio.create_task(func(current_group, self.group))
    @@ -501,7 +545,7 @@

    Functions

    def member_handler(cso, data, args):
         members = []
         for member in data:
    -        members.append()
    +        members.append(member)
         return members
    @@ -559,29 +603,25 @@

    Classes

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - if asyncio.iscoroutinefunction(func): - await func(new_req) - else: - func(new_req) + asyncio.create_task(func(new_req)) async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts() - newest_wall_poster = current_wall_posts.data[0].poster.id + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + newest_wall_post = current_wall_posts.data[0].id while True: await asyncio.sleep(delay) - current_wall_posts = await self.group.wall.get_posts() + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) current_wall_posts = current_wall_posts.data - if current_wall_posts[0].poster.id != newest_wall_poster: + post = current_wall_posts[0] + if post.id != newest_wall_post: new_posts = [] for post in current_wall_posts: - if post.poster.id != newest_wall_poster: - new_posts.append(post) - newest_wall_poster = current_wall_posts[0].poster.id + if post.id == newest_wall_post: + break + new_posts.append(post) + newest_wall_post = current_wall_posts[0].id for new_post in new_posts: - if asyncio.iscoroutinefunction(func): - await func(new_post) - else: - func(new_post) + asyncio.create_task(func(new_post)) async def on_group_change(self, func: Callable, delay: int): await self.group.update() @@ -594,11 +634,7 @@

    Classes

    if getattr(self.group, attr) != value: has_changed = True if has_changed: - if asyncio.iscoroutinefunction(func): - await func(current_group, self.group) - else: - func(current_group, self.group) - current_group = copy.copy(self.group)
    + asyncio.create_task(func(current_group, self.group))

    Methods

    @@ -661,11 +697,7 @@

    Parameters

    if getattr(self.group, attr) != value: has_changed = True if has_changed: - if asyncio.iscoroutinefunction(func): - await func(current_group, self.group) - else: - func(current_group, self.group) - current_group = copy.copy(self.group) + asyncio.create_task(func(current_group, self.group))
    @@ -691,10 +723,7 @@

    Parameters

    new_reqs.append(request) old_req = current_group_reqs[0].requester.id for new_req in new_reqs: - if asyncio.iscoroutinefunction(func): - await func(new_req) - else: - func(new_req)
    + asyncio.create_task(func(new_req))
    @@ -707,23 +736,22 @@

    Parameters

    Expand source code
    async def on_wall_post(self, func: Callable, delay: int):
    -    current_wall_posts = await self.group.wall.get_posts()
    -    newest_wall_poster = current_wall_posts.data[0].poster.id
    +    current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending)
    +    newest_wall_post = current_wall_posts.data[0].id
         while True:
             await asyncio.sleep(delay)
    -        current_wall_posts = await self.group.wall.get_posts()
    +        current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending)
             current_wall_posts = current_wall_posts.data
    -        if current_wall_posts[0].poster.id != newest_wall_poster:
    +        post = current_wall_posts[0]
    +        if post.id != newest_wall_post:
                 new_posts = []
                 for post in current_wall_posts:
    -                if post.poster.id != newest_wall_poster:
    -                    new_posts.append(post)
    -            newest_wall_poster = current_wall_posts[0].poster.id
    +                if post.id == newest_wall_post:
    +                    break
    +                new_posts.append(post)
    +            newest_wall_post = current_wall_posts[0].id
                 for new_post in new_posts:
    -                if asyncio.iscoroutinefunction(func):
    -                    await func(new_post)
    -                else:
    -                    func(new_post)
    + asyncio.create_task(func(new_post))
    @@ -744,17 +772,31 @@

    Parameters

    """ def __init__(self, cso, group_id): self.cso = cso + """Client Shared Object""" self.requests = cso.requests + "Requests object." self.id = group_id + "Group ID." self.wall = Wall(self.cso, self) + """Wall object.""" self.name = None + """Group name.""" self.description = None + """Group description.""" self.owner = None + """Group owner.""" self.member_count = None + """Group member count.""" self.is_builders_club_only = None + """True if the group is Builders Club (Premium) only. This seems to have been removed.""" self.public_entry_allowed = None + """Public entry allowed (private/public group)""" self.shout = None + """Current group shout (Shout)""" self.events = Events(cso, self) + """Events object.""" + self.is_locked = False + """True if this is a locked group.""" async def update(self): """ @@ -772,7 +814,8 @@

    Parameters

    self.shout = Shout(self.cso, group_info['shout']) else: self.shout = None - # self.is_locked = group_info["isLocked"] + if "isLocked" in group_info: + self.is_locked = group_info["isLocked"] async def update_shout(self, message): """ @@ -834,6 +877,29 @@

    Parameters

    member = Member(self.cso, roblox_id, "", self, role) return member + async def get_member_by_username(self, name): + user = await self.cso.client.get_user_by_username(name) + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {name} was not found in group {self.id}") + + # Create data to return. + role = Role(self.cso, self, group_data['role']) + member = Member(self.cso, user.id, user.name, self, role) + return member + async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( cso=self.cso, @@ -858,10 +924,61 @@

    Parameters

    await pages.get_page() return pages -

    Subclasses

    - +

    Instance variables

    +
    +
    var cso
    +
    +

    Client Shared Object

    +
    +
    var description
    +
    +

    Group description.

    +
    +
    var events
    +
    +

    Events object.

    +
    +
    var id
    +
    +

    Group ID.

    +
    +
    var is_builders_club_only
    +
    +

    True if the group is Builders Club (Premium) only. This seems to have been removed.

    +
    +
    var is_locked
    +
    +

    True if this is a locked group.

    +
    +
    var member_count
    +
    +

    Group member count.

    +
    +
    var name
    +
    +

    Group name.

    +
    +
    var owner
    +
    +

    Group owner.

    +
    +
    var public_entry_allowed
    +
    +

    Public entry allowed (private/public group)

    +
    +
    var requests
    +
    +

    Requests object.

    +
    +
    var shout
    +
    +

    Current group shout (Shout)

    +
    +
    var wall
    +
    +

    Wall object.

    +
    +

    Methods

    @@ -919,6 +1036,39 @@

    Methods

    return member
    +
    +async def get_member_by_username(self, name) +
    +
    +
    +
    + +Expand source code + +
    async def get_member_by_username(self, name):
    +    user = await self.cso.client.get_user_by_username(name)
    +    member_req = await self.requests.get(
    +        url=endpoint + f"/v2/users/{user.id}/groups/roles"
    +    )
    +    data = member_req.json()
    +
    +    # Find group in list.
    +    group_data = None
    +    for group in data['data']:
    +        if group['group']['id'] == self.id:
    +            group_data = group
    +            break
    +
    +    # Check if user is in group.
    +    if not group_data:
    +        raise NotFound(f"The user {name} was not found in group {self.id}")
    +
    +    # Create data to return.
    +    role = Role(self.cso, self, group_data['role'])
    +    member = Member(self.cso, user.id, user.name, self, role)
    +    return member
    +
    +
    async def get_members(self, sort_order=SortOrder.Ascending, limit=100)
    @@ -996,7 +1146,9 @@

    Returns

    if group_info.get('shout'): self.shout = Shout(self.cso, group_info['shout']) else: - self.shout = None + self.shout = None + if "isLocked" in group_info: + self.is_locked = group_info["isLocked"]
    @@ -1516,35 +1668,53 @@

    Inherited members

    class PartialGroup -(*args, **kwargs) +(cso, data)
    -

    Represents a group with less information

    +

    Represents a group with less information.

    +

    Different information will be present here in different circumstances. +If it was generated as a game owner, it might only contain an ID and a name. +If it was generated from, let's say, groups/v2/users/userid/groups/roles, it'll also contain a member count.

    Expand source code -
    class PartialGroup(Group):
    +
    class PartialGroup:
         """
    -    Represents a group with less information
    +    Represents a group with less information.
    +
    +    Different information will be present here in different circumstances.
    +    If it was generated as a game owner, it might only contain an ID and a name.
    +    If it was generated from, let's say, groups/v2/users/userid/groups/roles, it'll also contain a member count.
         """
    -    def __init__(self, *args, **kwargs):
    -        super().__init__(*args, **kwargs)
    + def __init__(self, cso, data): + self.cso = cso + self.requests = cso.requests + self.id = data["id"] + self.name = data["name"] + self.member_count = None + if "memberCount" in data: + self.member_count = data["memberCount"] + + async def expand(self): + return self.cso.client.get_group(self.id)
    -

    Ancestors

    - -

    Inherited members

    - +

    Methods

    +
    +
    +async def expand(self) +
    +
    +
    +
    + +Expand source code + +
    async def expand(self):
    +    return self.cso.client.get_group(self.id)
    +
    +
    +
    class Shout @@ -1606,13 +1776,27 @@

    Events<
  • Group

    -
      +
    • @@ -1636,6 +1820,9 @@

      Member<

    • PartialGroup

      +
    • Shout

      diff --git a/docs/index.html b/docs/index.html index 5b83f249..22e059d5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -56,9 +56,18 @@

      ro.py

      -

      ro.py is a powerful Python 3 wrapper for the Roblox Web API.

      +

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      +

      +ro.py Discord +ro.py PyPI +ro.py PyPI Downloads +ro.py PyPI License +ro.py GitHub Commit Activity +ro.py GitHub Last Commit +

      Information | +Discord | Requirements | Disclaimer | Documentation | @@ -76,15 +85,26 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API.< <img src="https://raw.githubusercontent.com/rbx-libdev/ro.py/main/resources/header.png" alt="ro.py" width="400" /> <br> </h1> -<h4 align="center">ro.py is a powerful Python 3 wrapper for the Roblox Web API.</h4> +<h4 align="center">ro.py is a powerful Python 3 wrapper for the Roblox Web API by <a href="https://github.com/jmkd3v">@jmkd3v</a> and <a href="https://github.com/iranathan">@iranathan</a>.</h4> + +<p align="center"> + <a href="https://j-mk.ml/ro.py"><img src="https://img.shields.io/discord/761603917490159676?style=flat-square&logo=discord" alt="ro.py Discord"/></a> + <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/v/ro-py?style=flat-square" alt="ro.py PyPI"/></a> + <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/dm/ro-py?style=flat-square" alt="ro.py PyPI Downloads"/></a> + <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/l/ro-py?style=flat-square" alt="ro.py PyPI License"/></a> + <a href="https://github.com/rbx-libdev/ro.py"><img src="https://img.shields.io/github/commit-activity/w/rbx-libdev/ro.py?style=flat-square" alt="ro.py GitHub Commit Activity"/></a> + <a href="https://github.com/rbx-libdev/ro.py"><img src="https://img.shields.io/github/last-commit/rbx-libdev/ro.py?style=flat-square" alt="ro.py GitHub Last Commit"/></a> +</p> + <p align="center"> - <a href="https://github.com/rbx-libdev/ro.py#information">Information</a> | - <a href="https://github.com/rbx-libdev/ro.py#requirements">Requirements</a> | - <a href="https://github.com/rbx-libdev/ro.py#disclaimer">Disclaimer</a> | - <a href="https://github.com/rbx-libdev/ro.py#documentation">Documentation</a> | - <a href="https://github.com/rbx-libdev/ro.py/tree/main/examples">Examples</a> | - <a href="https://github.com/rbx-libdev/ro.py#credits">Credits</a> | - <a href="https://github.com/rbx-libdev/ro.py/blob/main/LICENSE">License</a> + <a href="https://github.com/rbx-libdev/ro.py#information">Information</a> | + <a href="http://j-mk.ml/ro.py">Discord</a> | + <a href="https://github.com/rbx-libdev/ro.py#requirements">Requirements</a> | + <a href="https://github.com/rbx-libdev/ro.py#disclaimer">Disclaimer</a> | + <a href="https://github.com/rbx-libdev/ro.py#documentation">Documentation</a> | + <a href="https://github.com/rbx-libdev/ro.py/tree/main/examples">Examples</a> | + <a href="https://github.com/rbx-libdev/ro.py#credits">Credits</a> | + <a href="https://github.com/rbx-libdev/ro.py/blob/main/LICENSE">License</a> </p> """ diff --git a/docs/users.html b/docs/users.html index fd147cf3..ccb33627 100644 --- a/docs/users.html +++ b/docs/users.html @@ -165,7 +165,7 @@

      Module ro_py.users

      groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group)) return groups async def get_limiteds(self): @@ -512,7 +512,7 @@

      Parameters

      groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group)) return groups async def get_limiteds(self): @@ -675,7 +675,7 @@

      Methods

      groups = [] for group in data['data']: group = group['group'] - groups.append(PartialGroup(self.cso, group['id'], group['name'], group['memberCount'])) + groups.append(PartialGroup(self.cso, group)) return groups
      diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index 50a473bd..8a3141d2 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -121,6 +121,9 @@

      Module ro_py.utilities.pages

      if cursor: this_parameters["cursor"] = cursor + for name, value in self.parameters.items(): + this_parameters[name] = value + page_req = await self.requests.get( url=self.url, params=this_parameters @@ -257,6 +260,9 @@

      Instance variables

      if cursor: this_parameters["cursor"] = cursor + for name, value in self.parameters.items(): + this_parameters[name] = value + page_req = await self.requests.get( url=self.url, params=this_parameters @@ -328,6 +334,9 @@

      Methods

      if cursor: this_parameters["cursor"] = cursor + for name, value in self.parameters.items(): + this_parameters[name] = value + page_req = await self.requests.get( url=self.url, params=this_parameters diff --git a/docs/wall.html b/docs/wall.html index 6adc8c73..8bf96af4 100644 --- a/docs/wall.html +++ b/docs/wall.html @@ -75,7 +75,10 @@

      Module ro_py.wall

      self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username']) + if wall_data['poster']: + self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + else: + self.poster = None async def delete(self): wall_req = await self.requests.delete( @@ -284,7 +287,10 @@

      Methods

      self.body = wall_data['body'] self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) - self.poster = PartialUser(self.cso, wall_data['user']['userId'], wall_data['user']['username']) + if wall_data['poster']: + self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + else: + self.poster = None async def delete(self): wall_req = await self.requests.delete( diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 4c5eb415..d5d4dc5c 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -4,15 +4,26 @@ ro.py
  • -

    ro.py is a powerful Python 3 wrapper for the Roblox Web API.

    +

    ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

    + +

    + ro.py Discord + ro.py PyPI + ro.py PyPI Downloads + ro.py PyPI License + ro.py GitHub Commit Activity + ro.py GitHub Last Commit +

    +

    - Information | - Requirements | - Disclaimer | - Documentation | - Examples | - Credits | - License + Information | + Discord | + Requirements | + Disclaimer | + Documentation | + Examples | + Credits | + License

    """ From a47879c42f5428ce37dc0bb62fa0fe1ec8c51119 Mon Sep 17 00:00:00 2001 From: ira Date: Mon, 1 Feb 2021 00:24:55 +0100 Subject: [PATCH 383/518] Get audit log, and more. --- ro_py/assets.py | 16 ++++++++ ro_py/client.py | 13 +++--- ro_py/events.py | 1 + ro_py/groups.py | 105 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 128 insertions(+), 7 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index 9866c62f..c9b34ae6 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -13,6 +13,12 @@ endpoint = "https://api.roblox.com/" +class Reseller: + def __init__(self, user, user_asset): + self.user = user + self.user_asset = user_asset + + class Asset: """ Represents an asset. @@ -120,8 +126,18 @@ async def get_limited_resale_data(self): class UserAsset(Asset): def __init__(self, requests, asset_id, user_asset_id): super().__init__(requests, asset_id) + self.requests = requests self.user_asset_id = user_asset_id + async def get_resellers(self): + r = await self.requests.get( + url=f"https://economy.roblox.com/v1/assets/{self.id}/resellers?limit=10" + ) + data = r.json() + resellers = [] + for reseller in data['data']: + resellers.append(reseller(self.cso.client.get_user(reseller['seller']['id']))) + class Events: def __init__(self, cso, asset): diff --git a/ro_py/client.py b/ro_py/client.py index 0e64f86b..31f3fce7 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -146,7 +146,7 @@ async def get_self(self): data = self_req.json() return PartialUser(self.cso, data['UserId'], data['Username']) - async def get_user(self, user_id): + async def get_user(self, user_id, expand=True): """ Gets a Roblox user. @@ -158,12 +158,13 @@ async def get_user(self, user_id): user = self.cso.cache.get(CacheType.Users, user_id) if not user: user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + if expand: + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): """ Gets a Roblox user by their username.. @@ -186,7 +187,7 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id) + return await self.get_user(user_id, expand=expand) else: raise UserDoesNotExistError diff --git a/ro_py/events.py b/ro_py/events.py index 803477da..d545a73f 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -7,3 +7,4 @@ class EventTypes(enum.Enum): on_group_change = "on_group_change" on_asset_change = "on_asset_change" on_user_change = "on_user_change" + on_audit_log = "on_audit_log" diff --git a/ro_py/groups.py b/ro_py/groups.py index 64dda480..273677cd 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -4,6 +4,8 @@ """ import copy +from enum import Enum + import iso8601 import asyncio @@ -50,6 +52,66 @@ async def decline(self): return accept_req.status_code == 200 +class Actions(Enum): + delete_post = "deletePost" + remove_member = "removeMember" + accept_join_request = "acceptJoinRequest" + decline_join_request = "declineJoinRequest" + post_shout = "postShout" + change_rank = "changeRank" + buy_ad = "buyAd" + send_ally_request = "sendAllyRequest" + create_enemy = "createEnemy" + accept_ally_request = "acceptAllyRequest" + decline_ally_request = "declineAllyRequest" + delete_ally = "deleteAlly" + add_group_place = "addGroupPlace" + delete_group_place = "deleteGroupPlace" + create_items = "createItems" + configure_items = "configureItems" + spend_group_funds = "spendGroupFunds" + change_owner = "changeOwner" + delete = "delete" + adjust_currency_amounts = "adjustCurrencyAmounts" + abandon = "abandon" + claim = "claim" + Rename = "rename" + change_description = "changeDescription" + create_group_asset = "createGroupAsset" + upload_group_asset = "uploadGroupAsset" + configure_group_asset = "configureGroupAsset" + revert_group_asset = "revertGroupAsset" + create_group_developer_product = "createGroupDeveloperProduct" + configure_group_game = "configureGroupGame" + lock = "lock" + unlock = "unlock" + create_game_pass = "createGamePass" + create_badge = "createBadge" + configure_badge = "configureBadge" + save_place = "savePlace" + publish_place = "publishPlace" + invite_to_clan = "inviteToClan" + kick_from_clan = "kickFromClan" + cancel_clan_invite = "cancelClanInvite" + buy_clan = "buyClan" + + +class Action: + def __init__(self, cso, data, group): + self.group = group + self.actor = Member(cso, data['actor']['user']['userId'], data['actor']['user']['username'], group, Role(cso, group, data['actor']['role'])) + self.action = data['actionType'] + self.created = iso8601.parse_date(data['created']) + self.data = data['description'] + + +def action_handler(cso, data, args): + actions = [] + for action in data: + actions.append(Action(cso, action, args)) + return actions + + def join_request_handler(cso, data, args): join_requests = [] for request in data: @@ -219,6 +281,25 @@ async def get_members(self, sort_order=SortOrder.Ascending, limit=100): handler=member_handler, handler_args=self ) + + await pages.get_page() + return pages + + async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): + parameters = {} + if action_filter: + parameters['actionType'] = action_filter + + pages = Pages( + cso=self.cso, + url=endpoint + f"/v1/groups/{self.id}/audit-log", + handler=action_handler, + extra_parameters=parameters, + handler_args=self, + limit=limit, + sort_order=sort_order + ) + await pages.get_page() return pages @@ -263,7 +344,6 @@ class Member(PartialUser): """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) - self.requests = cso.requests self.role = role self.group = group @@ -405,6 +485,8 @@ def bind(self, func: Callable, event: EventTypes, delay: int = 15): return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: return asyncio.create_task(self.on_group_change(func, delay)) + if event == EventTypes.on_audit_log: + return asyncio.create_task(self.on_audit_log(func, delay)) async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() @@ -452,3 +534,24 @@ async def on_group_change(self, func: Callable, delay: int): has_changed = True if has_changed: asyncio.create_task(func(current_group, self.group)) + + """ + async def on_audit_log(self, func: Callable, delay: int): + audit_log = await self.group.get_audit_logs() + audit_log = audit_log.data[0] + while True: + await asyncio.sleep(delay) + new_audit = await self.group.get_audit_logs() + new_audits = [] + for audit in new_audit.data: + if audit.created == audit_log.created: + print(audit.created, audit_log.created, audit.created == audit_log.created) + break + else: + print(audit.created, audit_log.created) + new_audits.append(audit) + if len(new_audits) > 0: + audit_log = new_audit.data[0] + for new in new_audits: + asyncio.create_task(func(new)) + """ From d2763561701053154835d73f3bebb88313ae5131 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 31 Jan 2021 21:49:51 -0500 Subject: [PATCH 384/518] =?UTF-8?q?Dark=20mode=20resources=20=F0=9F=8C=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/ro.py modern test dark.pdn | Bin 0 -> 47905 bytes resources/ro.py modern test dark.png | Bin 0 -> 30032 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/ro.py modern test dark.pdn create mode 100644 resources/ro.py modern test dark.png diff --git a/resources/ro.py modern test dark.pdn b/resources/ro.py modern test dark.pdn new file mode 100644 index 0000000000000000000000000000000000000000..2702687578fc9621e4a69474a828b16ad19f9e25 GIT binary patch literal 47905 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 literal 0 HcmV?d00001 diff --git a/resources/ro.py modern test dark.png b/resources/ro.py modern test dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2d2a2f49b4a7ed7a20053ee01e7be8b609aa824b GIT binary patch literal 30032 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

    f)bzCUw0HEhGA_9AvDW;H&3*f<)`7NWZ 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 literal 0 HcmV?d00001 From bbbbb2aaae279c5a15b25dbb8b63fc08d4040036 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 1 Feb 2021 12:13:45 -0500 Subject: [PATCH 385/518] =?UTF-8?q?Dark=20theme=20docs=20=F0=9F=8C=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/accountinformation.html | 27 +- docs/accountsettings.html | 27 +- docs/assets.html | 99 +++++- docs/badges.html | 27 +- docs/captcha.html | 27 +- docs/catalog.html | 27 +- docs/chat.html | 27 +- docs/client.html | 70 +++-- docs/economy.html | 27 +- docs/events.html | 40 ++- docs/extensions/bots.html | 27 +- docs/extensions/index.html | 27 +- docs/extensions/prompt.html | 27 +- docs/gamepersistence.html | 27 +- docs/games.html | 27 +- docs/gender.html | 27 +- docs/groups.html | 523 +++++++++++++++++++++++++++++++- docs/index.html | 27 +- docs/notifications.html | 27 +- docs/robloxbadges.html | 27 +- docs/robloxdocs.html | 27 +- docs/robloxstatus.html | 27 +- docs/roles.html | 27 +- docs/thumbnails.html | 27 +- docs/trades.html | 27 +- docs/users.html | 27 +- docs/utilities/asset_type.html | 27 +- docs/utilities/cache.html | 27 +- docs/utilities/caseconvert.html | 27 +- docs/utilities/errors.html | 27 +- docs/utilities/index.html | 27 +- docs/utilities/pages.html | 27 +- docs/utilities/requests.html | 27 +- docs/wall.html | 27 +- 34 files changed, 1479 insertions(+), 63 deletions(-) diff --git a/docs/accountinformation.html b/docs/accountinformation.html index 5cd7aadc..e1874535 100644 --- a/docs/accountinformation.html +++ b/docs/accountinformation.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 9cfe5578..e728cdd1 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/assets.html b/docs/assets.html index 2ee54958..623faf3a 100644 --- a/docs/assets.html +++ b/docs/assets.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} @@ -69,6 +94,12 @@

    Module ro_py.assets

    endpoint = "https://api.roblox.com/" +class Reseller: + def __init__(self, user, user_asset): + self.user = user + self.user_asset = user_asset + + class Asset: """ Represents an asset. @@ -176,8 +207,18 @@

    Module ro_py.assets

    class UserAsset(Asset): def __init__(self, requests, asset_id, user_asset_id): super().__init__(requests, asset_id) + self.requests = requests self.user_asset_id = user_asset_id + async def get_resellers(self): + r = await self.requests.get( + url=f"https://economy.roblox.com/v1/assets/{self.id}/resellers?limit=10" + ) + data = r.json() + resellers = [] + for reseller in data['data']: + resellers.append(reseller(self.cso.client.get_user(reseller['seller']['id']))) + class Events: def __init__(self, cso, asset): @@ -530,6 +571,22 @@

    Methods

    +
    +class Reseller +(user, user_asset) +
    +
    +
    +
    + +Expand source code + +
    class Reseller:
    +    def __init__(self, user, user_asset):
    +        self.user = user
    +        self.user_asset = user_asset
    +
    +
    class UserAsset (requests, asset_id, user_asset_id) @@ -550,12 +607,44 @@

    Parameters

    class UserAsset(Asset):
         def __init__(self, requests, asset_id, user_asset_id):
             super().__init__(requests, asset_id)
    -        self.user_asset_id = user_asset_id
    + self.requests = requests + self.user_asset_id = user_asset_id + + async def get_resellers(self): + r = await self.requests.get( + url=f"https://economy.roblox.com/v1/assets/{self.id}/resellers?limit=10" + ) + data = r.json() + resellers = [] + for reseller in data['data']: + resellers.append(reseller(self.cso.client.get_user(reseller['seller']['id'])))

    Ancestors

    +

    Methods

    +
    +
    +async def get_resellers(self) +
    +
    +
    +
    + +Expand source code + +
    async def get_resellers(self):
    +    r = await self.requests.get(
    +        url=f"https://economy.roblox.com/v1/assets/{self.id}/resellers?limit=10"
    +    )
    +    data = r.json()
    +    resellers = []
    +    for reseller in data['data']:
    +        resellers.append(reseller(self.cso.client.get_user(reseller['seller']['id'])))
    +
    +
    +

    Inherited members

  • +

    Reseller

    +
  • +
  • UserAsset

    +
  • diff --git a/docs/badges.html b/docs/badges.html index 01397b20..950911c3 100644 --- a/docs/badges.html +++ b/docs/badges.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/captcha.html b/docs/captcha.html index 7500b49c..ba9410e4 100644 --- a/docs/captcha.html +++ b/docs/captcha.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/catalog.html b/docs/catalog.html index 523fa106..7b08793e 100644 --- a/docs/catalog.html +++ b/docs/catalog.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/chat.html b/docs/chat.html index 5705552c..5da93bc4 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/client.html b/docs/client.html index f320c77d..6e661168 100644 --- a/docs/client.html +++ b/docs/client.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} @@ -202,7 +227,7 @@

    Module ro_py.client

    data = self_req.json() return PartialUser(self.cso, data['UserId'], data['Username']) - async def get_user(self, user_id): + async def get_user(self, user_id, expand=True): """ Gets a Roblox user. @@ -214,12 +239,13 @@

    Module ro_py.client

    user = self.cso.cache.get(CacheType.Users, user_id) if not user: user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + if expand: + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): """ Gets a Roblox user by their username.. @@ -242,7 +268,7 @@

    Module ro_py.client

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id) + return await self.get_user(user_id, expand=expand) else: raise UserDoesNotExistError @@ -470,7 +496,7 @@

    Parameters

    data = self_req.json() return PartialUser(self.cso, data['UserId'], data['Username']) - async def get_user(self, user_id): + async def get_user(self, user_id, expand=True): """ Gets a Roblox user. @@ -482,12 +508,13 @@

    Parameters

    user = self.cso.cache.get(CacheType.Users, user_id) if not user: user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + if expand: + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): """ Gets a Roblox user by their username.. @@ -510,7 +537,7 @@

    Parameters

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id) + return await self.get_user(user_id, expand=expand) else: raise UserDoesNotExistError @@ -828,7 +855,7 @@

    Parameters

    -async def get_user(self, user_id) +async def get_user(self, user_id, expand=True)

    Gets a Roblox user.

    @@ -841,7 +868,7 @@

    Parameters

    Expand source code -
    async def get_user(self, user_id):
    +
    async def get_user(self, user_id, expand=True):
         """
         Gets a Roblox user.
     
    @@ -853,14 +880,15 @@ 

    Parameters

    user = self.cso.cache.get(CacheType.Users, user_id) if not user: user = PartialUser(self.cso, user_id) - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + if expand: + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user
    -async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False) +async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True)

    Gets a Roblox user by their username..

    @@ -875,7 +903,7 @@

    Parameters

    Expand source code -
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
    +
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True):
         """
         Gets a Roblox user by their username..
     
    @@ -898,7 +926,7 @@ 

    Parameters

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id) + return await self.get_user(user_id, expand=expand) else: raise UserDoesNotExistError
    diff --git a/docs/economy.html b/docs/economy.html index 8d76ca7f..3fddd8d3 100644 --- a/docs/economy.html +++ b/docs/economy.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/events.html b/docs/events.html index a1e4c0a3..9b007c5b 100644 --- a/docs/events.html +++ b/docs/events.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} @@ -61,7 +86,8 @@

    Module ro_py.events

    on_wall_post = "on_wall_post" on_group_change = "on_group_change" on_asset_change = "on_asset_change" - on_user_change = "on_user_change"
    + on_user_change = "on_user_change" + on_audit_log = "on_audit_log"
    @@ -88,7 +114,8 @@

    Classes

    on_wall_post = "on_wall_post" on_group_change = "on_group_change" on_asset_change = "on_asset_change" - on_user_change = "on_user_change"
    + on_user_change = "on_user_change" + on_audit_log = "on_audit_log"

    Ancestors

      @@ -100,6 +127,10 @@

      Class variables

      +
      var on_audit_log
      +
      +
      +
      var on_group_change
      @@ -141,8 +172,9 @@

      Index

      • EventTypes

        -
          +
          • on_asset_change
          • +
          • on_audit_log
          • on_group_change
          • on_join_request
          • on_user_change
          • diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index a1fb77a2..2292f5d1 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/extensions/index.html b/docs/extensions/index.html index 634fc31b..7bb36148 100644 --- a/docs/extensions/index.html +++ b/docs/extensions/index.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html index 1211008b..044551d5 100644 --- a/docs/extensions/prompt.html +++ b/docs/extensions/prompt.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/gamepersistence.html b/docs/gamepersistence.html index fdba1a92..09669a14 100644 --- a/docs/gamepersistence.html +++ b/docs/gamepersistence.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/games.html b/docs/games.html index 51d703fb..7b3c4b8e 100644 --- a/docs/games.html +++ b/docs/games.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/gender.html b/docs/gender.html index 1171dc02..10af881d 100644 --- a/docs/gender.html +++ b/docs/gender.html @@ -33,7 +33,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -41,6 +41,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} diff --git a/docs/groups.html b/docs/groups.html index 5e41fa75..d842e24a 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -32,7 +32,7 @@ width: 10px; } ::-webkit-scrollbar-track { -background: #f1f1f1; +background: #333; } ::-webkit-scrollbar-thumb { background: #888; @@ -40,6 +40,31 @@ ::-webkit-scrollbar-thumb:hover { background: #555; } +body { +background: #212121; +color: white; +} +a { +color: #0091EA; +} +a:hover { +color: #40C4FF; +} +.name { +background: #424242; +} +.name:hover { +background: #424242; +} +.ident { +color: #1DE9B6; +} +.admonition { +color: black; +} +* { +border-color: #424242 !important; +} @@ -60,6 +85,8 @@

            Module ro_py.groups

            """ import copy +from enum import Enum + import iso8601 import asyncio @@ -106,6 +133,66 @@

            Module ro_py.groups

            return accept_req.status_code == 200 +class Actions(Enum): + delete_post = "deletePost" + remove_member = "removeMember" + accept_join_request = "acceptJoinRequest" + decline_join_request = "declineJoinRequest" + post_shout = "postShout" + change_rank = "changeRank" + buy_ad = "buyAd" + send_ally_request = "sendAllyRequest" + create_enemy = "createEnemy" + accept_ally_request = "acceptAllyRequest" + decline_ally_request = "declineAllyRequest" + delete_ally = "deleteAlly" + add_group_place = "addGroupPlace" + delete_group_place = "deleteGroupPlace" + create_items = "createItems" + configure_items = "configureItems" + spend_group_funds = "spendGroupFunds" + change_owner = "changeOwner" + delete = "delete" + adjust_currency_amounts = "adjustCurrencyAmounts" + abandon = "abandon" + claim = "claim" + Rename = "rename" + change_description = "changeDescription" + create_group_asset = "createGroupAsset" + upload_group_asset = "uploadGroupAsset" + configure_group_asset = "configureGroupAsset" + revert_group_asset = "revertGroupAsset" + create_group_developer_product = "createGroupDeveloperProduct" + configure_group_game = "configureGroupGame" + lock = "lock" + unlock = "unlock" + create_game_pass = "createGamePass" + create_badge = "createBadge" + configure_badge = "configureBadge" + save_place = "savePlace" + publish_place = "publishPlace" + invite_to_clan = "inviteToClan" + kick_from_clan = "kickFromClan" + cancel_clan_invite = "cancelClanInvite" + buy_clan = "buyClan" + + +class Action: + def __init__(self, cso, data, group): + self.group = group + self.actor = Member(cso, data['actor']['user']['userId'], data['actor']['user']['username'], group, Role(cso, group, data['actor']['role'])) + self.action = data['actionType'] + self.created = iso8601.parse_date(data['created']) + self.data = data['description'] + + +def action_handler(cso, data, args): + actions = [] + for action in data: + actions.append(Action(cso, action, args)) + return actions + + def join_request_handler(cso, data, args): join_requests = [] for request in data: @@ -275,6 +362,25 @@

            Module ro_py.groups

            handler=member_handler, handler_args=self ) + + await pages.get_page() + return pages + + async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): + parameters = {} + if action_filter: + parameters['actionType'] = action_filter + + pages = Pages( + cso=self.cso, + url=endpoint + f"/v1/groups/{self.id}/audit-log", + handler=action_handler, + extra_parameters=parameters, + handler_args=self, + limit=limit, + sort_order=sort_order + ) + await pages.get_page() return pages @@ -319,7 +425,6 @@

            Module ro_py.groups

            """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) - self.requests = cso.requests self.role = role self.group = group @@ -461,6 +566,8 @@

            Module ro_py.groups

            return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: return asyncio.create_task(self.on_group_change(func, delay)) + if event == EventTypes.on_audit_log: + return asyncio.create_task(self.on_audit_log(func, delay)) async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() @@ -507,7 +614,28 @@

            Module ro_py.groups

            if getattr(self.group, attr) != value: has_changed = True if has_changed: - asyncio.create_task(func(current_group, self.group))
            + asyncio.create_task(func(current_group, self.group)) + + """ + async def on_audit_log(self, func: Callable, delay: int): + audit_log = await self.group.get_audit_logs() + audit_log = audit_log.data[0] + while True: + await asyncio.sleep(delay) + new_audit = await self.group.get_audit_logs() + new_audits = [] + for audit in new_audit.data: + if audit.created == audit_log.created: + print(audit.created, audit_log.created, audit.created == audit_log.created) + break + else: + print(audit.created, audit_log.created) + new_audits.append(audit) + if len(new_audits) > 0: + audit_log = new_audit.data[0] + for new in new_audits: + asyncio.create_task(func(new)) + """
    @@ -517,6 +645,22 @@

    Module ro_py.groups

    Functions

    +
    +def action_handler(cso, data, args) +
    +
    +
    +
    + +Expand source code + +
    def action_handler(cso, data, args):
    +    actions = []
    +    for action in data:
    +        actions.append(Action(cso, action, args))
    +    return actions
    +
    +
    def join_request_handler(cso, data, args)
    @@ -554,6 +698,250 @@

    Functions

    Classes

    +
    +class Action +(cso, data, group) +
    +
    +
    +
    + +Expand source code + +
    class Action:
    +    def __init__(self, cso, data, group):
    +        self.group = group
    +        self.actor = Member(cso, data['actor']['user']['userId'], data['actor']['user']['username'], group, Role(cso, group, data['actor']['role']))
    +        self.action = data['actionType']
    +        self.created = iso8601.parse_date(data['created'])
    +        self.data = data['description']
    +
    +
    +
    +class Actions +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
    +
    +

    An enumeration.

    +
    + +Expand source code + +
    class Actions(Enum):
    +    delete_post = "deletePost"
    +    remove_member = "removeMember"
    +    accept_join_request = "acceptJoinRequest"
    +    decline_join_request = "declineJoinRequest"
    +    post_shout = "postShout"
    +    change_rank = "changeRank"
    +    buy_ad = "buyAd"
    +    send_ally_request = "sendAllyRequest"
    +    create_enemy = "createEnemy"
    +    accept_ally_request = "acceptAllyRequest"
    +    decline_ally_request = "declineAllyRequest"
    +    delete_ally = "deleteAlly"
    +    add_group_place = "addGroupPlace"
    +    delete_group_place = "deleteGroupPlace"
    +    create_items = "createItems"
    +    configure_items = "configureItems"
    +    spend_group_funds = "spendGroupFunds"
    +    change_owner = "changeOwner"
    +    delete = "delete"
    +    adjust_currency_amounts = "adjustCurrencyAmounts"
    +    abandon = "abandon"
    +    claim = "claim"
    +    Rename = "rename"
    +    change_description = "changeDescription"
    +    create_group_asset = "createGroupAsset"
    +    upload_group_asset = "uploadGroupAsset"
    +    configure_group_asset = "configureGroupAsset"
    +    revert_group_asset = "revertGroupAsset"
    +    create_group_developer_product = "createGroupDeveloperProduct"
    +    configure_group_game = "configureGroupGame"
    +    lock = "lock"
    +    unlock = "unlock"
    +    create_game_pass = "createGamePass"
    +    create_badge = "createBadge"
    +    configure_badge = "configureBadge"
    +    save_place = "savePlace"
    +    publish_place = "publishPlace"
    +    invite_to_clan = "inviteToClan"
    +    kick_from_clan = "kickFromClan"
    +    cancel_clan_invite = "cancelClanInvite"
    +    buy_clan = "buyClan"
    +
    +

    Ancestors

    +
      +
    • enum.Enum
    • +
    +

    Class variables

    +
    +
    var Rename
    +
    +
    +
    +
    var abandon
    +
    +
    +
    +
    var accept_ally_request
    +
    +
    +
    +
    var accept_join_request
    +
    +
    +
    +
    var add_group_place
    +
    +
    +
    +
    var adjust_currency_amounts
    +
    +
    +
    +
    var buy_ad
    +
    +
    +
    +
    var buy_clan
    +
    +
    +
    +
    var cancel_clan_invite
    +
    +
    +
    +
    var change_description
    +
    +
    +
    +
    var change_owner
    +
    +
    +
    +
    var change_rank
    +
    +
    +
    +
    var claim
    +
    +
    +
    +
    var configure_badge
    +
    +
    +
    +
    var configure_group_asset
    +
    +
    +
    +
    var configure_group_game
    +
    +
    +
    +
    var configure_items
    +
    +
    +
    +
    var create_badge
    +
    +
    +
    +
    var create_enemy
    +
    +
    +
    +
    var create_game_pass
    +
    +
    +
    +
    var create_group_asset
    +
    +
    +
    +
    var create_group_developer_product
    +
    +
    +
    +
    var create_items
    +
    +
    +
    +
    var decline_ally_request
    +
    +
    +
    +
    var decline_join_request
    +
    +
    +
    +
    var delete
    +
    +
    +
    +
    var delete_ally
    +
    +
    +
    +
    var delete_group_place
    +
    +
    +
    +
    var delete_post
    +
    +
    +
    +
    var invite_to_clan
    +
    +
    +
    +
    var kick_from_clan
    +
    +
    +
    +
    var lock
    +
    +
    +
    +
    var post_shout
    +
    +
    +
    +
    var publish_place
    +
    +
    +
    +
    var remove_member
    +
    +
    +
    +
    var revert_group_asset
    +
    +
    +
    +
    var save_place
    +
    +
    +
    +
    var send_ally_request
    +
    +
    +
    +
    var spend_group_funds
    +
    +
    +
    +
    var unlock
    +
    +
    +
    +
    var upload_group_asset
    +
    +
    +
    +
    +
    class Events (cso, group) @@ -588,6 +976,8 @@

    Classes

    return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: return asyncio.create_task(self.on_group_change(func, delay)) + if event == EventTypes.on_audit_log: + return asyncio.create_task(self.on_audit_log(func, delay)) async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() @@ -634,7 +1024,28 @@

    Classes

    if getattr(self.group, attr) != value: has_changed = True if has_changed: - asyncio.create_task(func(current_group, self.group))
    + asyncio.create_task(func(current_group, self.group)) + + """ + async def on_audit_log(self, func: Callable, delay: int): + audit_log = await self.group.get_audit_logs() + audit_log = audit_log.data[0] + while True: + await asyncio.sleep(delay) + new_audit = await self.group.get_audit_logs() + new_audits = [] + for audit in new_audit.data: + if audit.created == audit_log.created: + print(audit.created, audit_log.created, audit.created == audit_log.created) + break + else: + print(audit.created, audit_log.created) + new_audits.append(audit) + if len(new_audits) > 0: + audit_log = new_audit.data[0] + for new in new_audits: + asyncio.create_task(func(new)) + """

    Methods

    @@ -674,7 +1085,9 @@

    Parameters

    if event == EventTypes.on_wall_post: return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: - return asyncio.create_task(self.on_group_change(func, delay)) + return asyncio.create_task(self.on_group_change(func, delay)) + if event == EventTypes.on_audit_log: + return asyncio.create_task(self.on_audit_log(func, delay))
    @@ -921,6 +1334,25 @@

    Parameters

    handler=member_handler, handler_args=self ) + + await pages.get_page() + return pages + + async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): + parameters = {} + if action_filter: + parameters['actionType'] = action_filter + + pages = Pages( + cso=self.cso, + url=endpoint + f"/v1/groups/{self.id}/audit-log", + handler=action_handler, + extra_parameters=parameters, + handler_args=self, + limit=limit, + sort_order=sort_order + ) + await pages.get_page() return pages
    @@ -981,6 +1413,34 @@

    Instance variables

    Methods

    +
    +async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100) +
    +
    +
    +
    + +Expand source code + +
    async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100):
    +    parameters = {}
    +    if action_filter:
    +        parameters['actionType'] = action_filter
    +
    +    pages = Pages(
    +        cso=self.cso,
    +        url=endpoint + f"/v1/groups/{self.id}/audit-log",
    +        handler=action_handler,
    +        extra_parameters=parameters,
    +        handler_args=self,
    +        limit=limit,
    +        sort_order=sort_order
    +    )
    +
    +    await pages.get_page()
    +    return pages
    +
    +
    async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100)
    @@ -1087,6 +1547,7 @@

    Methods

    handler=member_handler, handler_args=self ) + await pages.get_page() return pages @@ -1301,7 +1762,6 @@

    Parameters

    """ def __init__(self, cso, roblox_id, name, group, role): super().__init__(cso, roblox_id, name) - self.requests = cso.requests self.role = role self.group = group @@ -1759,6 +2219,7 @@

    Index

  • Functions

    @@ -1766,6 +2227,55 @@

    Index

  • Classes

  • @@ -504,6 +506,8 @@

    Parameters

    ---------- user_id ID of the user to generate the object from. + expand : bool + Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: @@ -524,6 +528,8 @@

    Parameters

    Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. + expand : bool + Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -629,7 +635,20 @@

    Parameters

    badge = Badge(self.cso, badge_id) self.cso.cache.set(CacheType.Assets, badge_id, badge) await badge.update() - return badge
    + return badge + + async def get_friend_requests(self): + friend_req = await self.requests.get( + url="https://friends.roblox.com/v1/user/friend-requests/count" + ) + return friend_req.json()["count"] + + async def get_captcha_metadata(self): + captcha_meta_req = await self.requests.get( + url="https://apis.roblox.com/captcha/v1/metadata" + ) + captcha_meta_raw = captcha_meta_req.json() + return CaptchaMetadata(captcha_meta_raw)

    Subclasses

      @@ -734,6 +753,39 @@

      Parameters

      return badge +
      +async def get_captcha_metadata(self) +
      +
      +
      +
      + +Expand source code + +
      async def get_captcha_metadata(self):
      +    captcha_meta_req = await self.requests.get(
      +        url="https://apis.roblox.com/captcha/v1/metadata"
      +    )
      +    captcha_meta_raw = captcha_meta_req.json()
      +    return CaptchaMetadata(captcha_meta_raw)
      +
      +
      +
      +async def get_friend_requests(self) +
      +
      +
      +
      + +Expand source code + +
      async def get_friend_requests(self):
      +    friend_req = await self.requests.get(
      +        url="https://friends.roblox.com/v1/user/friend-requests/count"
      +    )
      +    return friend_req.json()["count"]
      +
      +
      async def get_game_by_place_id(self, place_id)
      @@ -863,6 +915,8 @@

      Parameters

      user_id
      ID of the user to generate the object from.
      +
      expand : bool
      +
      Whether to automatically expand the data returned by the endpoint into Users.s
      @@ -876,6 +930,8 @@

      Parameters

      ---------- user_id ID of the user to generate the object from. + expand : bool + Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: @@ -898,6 +954,8 @@

      Parameters

      Name of the user to generate the object from.
      exclude_banned_users : bool
      Whether to exclude banned users in the request.
      +
      expand : bool
      +
      Whether to automatically expand the data returned by the endpoint into Users.
    @@ -913,6 +971,8 @@

    Parameters

    Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. + expand : bool + Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -1043,50 +1103,6 @@

    Returns

    -
    -class ClientSharedObject -(client) -
    -
    -

    This object is shared across most instances and classes for a particular client.

    -
    - -Expand source code - -
    class ClientSharedObject:
    -    """
    -    This object is shared across most instances and classes for a particular client.
    -    """
    -    def __init__(self, client):
    -        self.client = client
    -        """Client (parent) of this object."""
    -        self.cache = Cache()
    -        """Cache object to keep objects that don't need to be recreated."""
    -        self.requests = Requests()
    -        """Reqests object for all web requests."""
    -        self.evtloop = asyncio.new_event_loop()
    -        """Event loop for certain things."""
    -
    -

    Instance variables

    -
    -
    var cache
    -
    -

    Cache object to keep objects that don't need to be recreated.

    -
    -
    var client
    -
    -

    Client (parent) of this object.

    -
    -
    var evtloop
    -
    -

    Event loop for certain things.

    -
    -
    var requests
    -
    -

    Reqests object for all web requests.

    -
    -
    -
    @@ -1118,6 +1134,8 @@

    Client<
  • events
  • get_asset
  • get_badge
  • +
  • get_captcha_metadata
  • +
  • get_friend_requests
  • get_game_by_place_id
  • get_game_by_universe_id
  • get_group
  • @@ -1131,15 +1149,6 @@

    Client<
  • user_login
  • -
  • -

    ClientSharedObject

    - -
  • diff --git a/docs/events.html b/docs/events.html index 9b007c5b..4397f7fd 100644 --- a/docs/events.html +++ b/docs/events.html @@ -87,7 +87,8 @@

    Module ro_py.events

    on_group_change = "on_group_change" on_asset_change = "on_asset_change" on_user_change = "on_user_change" - on_audit_log = "on_audit_log"
    + on_audit_log = "on_audit_log" + on_trade_request = "on_trade_request"

    @@ -115,7 +116,8 @@

    Classes

    on_group_change = "on_group_change" on_asset_change = "on_asset_change" on_user_change = "on_user_change" - on_audit_log = "on_audit_log" + on_audit_log = "on_audit_log" + on_trade_request = "on_trade_request"

    Ancestors

    diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html index 044551d5..f13a2ba6 100644 --- a/docs/extensions/prompt.html +++ b/docs/extensions/prompt.html @@ -86,6 +86,7 @@

    Module ro_py.extensions.prompt

    """ +import sys try: import wx import wxasync @@ -93,7 +94,8 @@

    Module ro_py.extensions.prompt

    import pytweening from wx.lib.embeddedimage import PyEmbeddedImage except ModuleNotFoundError: - raise ModuleNotFoundError("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") + print("Please install wxPython, wxAsync and pytweening from pip to use the prompt extension.") + sys.exit() icon_image = PyEmbeddedImage( b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B' diff --git a/docs/games.html b/docs/games.html index 7b3c4b8e..9b4c68e3 100644 --- a/docs/games.html +++ b/docs/games.html @@ -85,6 +85,7 @@

    Module ro_py.games

    """ +from ro_py.utilities.clientobject import ClientObject from ro_py.groups import Group from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator @@ -106,7 +107,7 @@

    Module ro_py.games

    self.down_votes = votes_data["downVotes"] -class Game: +class Game(ClientObject): """ Represents a Roblox game universe. This class represents multiple game-related endpoints. @@ -289,7 +290,7 @@

    Classes

    Expand source code -
    class Game:
    +
    class Game(ClientObject):
         """
         Represents a Roblox game universe.
         This class represents multiple game-related endpoints.
    @@ -379,6 +380,10 @@ 

    Classes

    badges.append(Badge(self.cso, badge["id"])) return badges
    +

    Ancestors

    +

    Methods

    diff --git a/docs/groups.html b/docs/groups.html index d842e24a..eed84ad4 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -84,6 +84,7 @@

    Module ro_py.groups

    This file houses functions and classes that pertain to Roblox groups. """ + import copy from enum import Enum @@ -97,6 +98,7 @@

    Module ro_py.groups

    from typing import Tuple, Callable from ro_py.utilities.errors import NotFound from ro_py.utilities.pages import Pages, SortOrder +from ro_py.utilities.clientobject import ClientObject endpoint = "https://groups.roblox.com" @@ -122,13 +124,13 @@

    Module ro_py.groups

    async def accept(self): accept_req = await self.requests.post( - url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}" + url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}" ) return accept_req.status_code == 200 async def decline(self): accept_req = await self.requests.delete( - url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}" + url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}" ) return accept_req.status_code == 200 @@ -207,7 +209,7 @@

    Module ro_py.groups

    return members -class Group: +class Group(ClientObject): """ Represents a group. """ @@ -463,7 +465,7 @@

    Module ro_py.groups

    role_counter = -1 for group_role in roles: role_counter += 1 - if group_role.id == self.role.id: + if group_role.rank == self.role.rank: break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") @@ -528,7 +530,7 @@

    Module ro_py.groups

    roles = await self.group.get_roles() rank_role = None for role in roles: - if role.role == role_num: + if role.rank == role_num: rank_role = role break if not rank_role: @@ -1179,7 +1181,7 @@

    Parameters

    Expand source code -
    class Group:
    +
    class Group(ClientObject):
         """
         Represents a group.
         """
    @@ -1356,6 +1358,10 @@ 

    Parameters

    await pages.get_page() return pages
    +

    Ancestors

    +

    Instance variables

    var cso
    @@ -1674,13 +1680,13 @@

    Returns

    async def accept(self): accept_req = await self.requests.post( - url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}" + url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}" ) return accept_req.status_code == 200 async def decline(self): accept_req = await self.requests.delete( - url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}" + url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}" ) return accept_req.status_code == 200
    @@ -1697,7 +1703,7 @@

    Methods

    async def accept(self):
         accept_req = await self.requests.post(
    -        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}"
    +        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}"
         )
         return accept_req.status_code == 200
    @@ -1713,7 +1719,7 @@

    Methods

    async def decline(self):
         accept_req = await self.requests.delete(
    -        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}"
    +        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requester.id}"
         )
         return accept_req.status_code == 200
    @@ -1800,7 +1806,7 @@

    Parameters

    role_counter = -1 for group_role in roles: role_counter += 1 - if group_role.id == self.role.id: + if group_role.rank == self.role.rank: break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") @@ -1865,7 +1871,7 @@

    Parameters

    roles = await self.group.get_roles() rank_role = None for role in roles: - if role.role == role_num: + if role.rank == role_num: rank_role = role break if not rank_role: @@ -1917,7 +1923,7 @@

    Parameters

    role_counter = -1 for group_role in roles: role_counter += 1 - if group_role.id == self.role.id: + if group_role.rank == self.role.rank: break if not roles: raise NotFound(f"User {self.id} is not in group {self.group.id}") @@ -2068,7 +2074,7 @@

    Returns

    roles = await self.group.get_roles() rank_role = None for role in roles: - if role.role == role_num: + if role.rank == role_num: rank_role = role break if not rank_role: diff --git a/docs/index.html b/docs/index.html index f212a1fe..9edfeb7e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -238,7 +238,7 @@

    Sub-modules

    ro_py.utilities
    -

    This folder houses utilities that are used internally for ro.py.

    +

    This folder houses utilities that are used internally for ro.py …

    ro_py.wall
    diff --git a/docs/trades.html b/docs/trades.html index 1720a667..021355c9 100644 --- a/docs/trades.html +++ b/docs/trades.html @@ -84,18 +84,21 @@

    Module ro_py.trades

    This file houses functions and classes that pertain to Roblox trades and trading. """ +from typing import Callable from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset from ro_py.users import PartialUser +from ro_py.events import EventTypes import datetime import iso8601 +import asyncio import enum endpoint = "https://trades.roblox.com" -def trade_page_handler(requests, this_page) -> list: +def trade_page_handler(requests, this_page, args) -> list: trades_out = [] for raw_trade in this_page: trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) @@ -283,9 +286,10 @@

    Module ro_py.trades

    self.cso = cso self.requests = cso.requests self.get_self = get_self + self.events = Events(cso) self.TradeRequest = TradeRequest - async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: + async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = Pages( cso=self.cso, url=endpoint + f"/v1/trades/{trade_status_type}", @@ -293,6 +297,7 @@

    Module ro_py.trades

    limit=limit, handler=trade_page_handler ) + await trades.get_page() return trades async def send_trade(self, roblox_id, trade): @@ -341,7 +346,30 @@

    Module ro_py.trades

    data=data ) - return trade_req.status == 200
    + return trade_req.status == 200 + + +class Events: + def __init__(self, cso): + self.cso = cso + + def bind(self, event: EventTypes, func: Callable, delay=15): + if event == EventTypes.on_trade_request: + return asyncio.create_task(self.on_trade_request(func, delay)) + + async def on_trade_request(self, func: Callable, delay: int): + old_trades = await self.cso.client.trade.get_trades() + while True: + await asyncio.sleep(delay) + new_trades = await self.cso.client.trade.get_trades() + new_trade = [] + for trade in new_trades.data: + if trade.created == old_trades.data[0].created: + break + new_trade.append(trade) + old_trades = new_trades + for trade in new_trade: + asyncio.create_task(func(trade))
    @@ -352,7 +380,7 @@

    Module ro_py.trades

    Functions

    -def trade_page_handler(requests, this_page) ‑> list +def trade_page_handler(requests, this_page, args) ‑> list
    @@ -360,7 +388,7 @@

    Functions

    Expand source code -
    def trade_page_handler(requests, this_page) -> list:
    +
    def trade_page_handler(requests, this_page, args) -> list:
         trades_out = []
         for raw_trade in this_page:
             trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
    @@ -372,6 +400,80 @@ 

    Functions

    Classes

    +
    +class Events +(cso) +
    +
    +
    +
    + +Expand source code + +
    class Events:
    +    def __init__(self, cso):
    +        self.cso = cso
    +
    +    def bind(self, event: EventTypes, func: Callable, delay=15):
    +        if event == EventTypes.on_trade_request:
    +            return asyncio.create_task(self.on_trade_request(func, delay))
    +
    +    async def on_trade_request(self, func: Callable, delay: int):
    +        old_trades = await self.cso.client.trade.get_trades()
    +        while True:
    +            await asyncio.sleep(delay)
    +            new_trades = await self.cso.client.trade.get_trades()
    +            new_trade = []
    +            for trade in new_trades.data:
    +                if trade.created == old_trades.data[0].created:
    +                    break
    +                new_trade.append(trade)
    +            old_trades = new_trades
    +            for trade in new_trade:
    +                asyncio.create_task(func(trade))
    +
    +

    Methods

    +
    +
    +def bind(self, event: EventTypes, func: Callable, delay=15) +
    +
    +
    +
    + +Expand source code + +
    def bind(self, event: EventTypes, func: Callable, delay=15):
    +    if event == EventTypes.on_trade_request:
    +        return asyncio.create_task(self.on_trade_request(func, delay))
    +
    +
    +
    +async def on_trade_request(self, func: Callable, delay: int) +
    +
    +
    +
    + +Expand source code + +
    async def on_trade_request(self, func: Callable, delay: int):
    +    old_trades = await self.cso.client.trade.get_trades()
    +    while True:
    +        await asyncio.sleep(delay)
    +        new_trades = await self.cso.client.trade.get_trades()
    +        new_trade = []
    +        for trade in new_trades.data:
    +            if trade.created == old_trades.data[0].created:
    +                break
    +            new_trade.append(trade)
    +        old_trades = new_trades
    +        for trade in new_trade:
    +            asyncio.create_task(func(trade))
    +
    +
    +
    +
    class PartialTrade (cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool) @@ -884,9 +986,10 @@

    Class variables

    self.cso = cso self.requests = cso.requests self.get_self = get_self + self.events = Events(cso) self.TradeRequest = TradeRequest - async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: + async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = Pages( cso=self.cso, url=endpoint + f"/v1/trades/{trade_status_type}", @@ -894,6 +997,7 @@

    Class variables

    limit=limit, handler=trade_page_handler ) + await trades.get_page() return trades async def send_trade(self, roblox_id, trade): @@ -947,7 +1051,7 @@

    Class variables

    Methods

    -async def get_trades(self, trade_status_type: <TradeStatusType.Inbound: 'Inbound'>, sort_order=SortOrder.Ascending, limit=10) ‑> Pages +async def get_trades(self, trade_status_type='Inbound', sort_order=SortOrder.Ascending, limit=10) ‑> Pages
    @@ -955,7 +1059,7 @@

    Methods

    Expand source code -
    async def get_trades(self, trade_status_type: TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
    +
    async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages:
         trades = Pages(
             cso=self.cso,
             url=endpoint + f"/v1/trades/{trade_status_type}",
    @@ -963,6 +1067,7 @@ 

    Methods

    limit=limit, handler=trade_page_handler ) + await trades.get_page() return trades
    @@ -1065,6 +1170,13 @@

    Index

  • Classes

    • +

      Events

      + +
    • +
    • PartialTrade

      • accept
      • diff --git a/docs/users.html b/docs/users.html index b0eb0722..e3c17074 100644 --- a/docs/users.html +++ b/docs/users.html @@ -84,11 +84,13 @@

        Module ro_py.users

        This file houses functions and classes that pertain to Roblox users and profiles. """ + import copy from typing import List, Callable from ro_py.events import EventTypes from ro_py.robloxbadges import RobloxBadge from ro_py.thumbnails import UserThumbnailGenerator +from ro_py.utilities.clientobject import ClientObject from ro_py.utilities.pages import Pages from ro_py.assets import UserAsset import iso8601 @@ -170,15 +172,13 @@

        Module ro_py.users

        async def get_friends(self): """ Gets the user's friends. - :return: A list of User instances. + :return: List of Friend """ friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: - friends_list.append( - await self.cso.client.get_user(friend_raw["id"]) - ) + friends_list.append(Friend(self.cso, friend_raw)) return friends_list async def get_groups(self): @@ -216,7 +216,18 @@

        Module ro_py.users

        return status_req.json()["status"] -class User(PartialUser): +class Friend(PartialUser): + def __init__(self, cso, data): + super().__init__(cso, data["id"], data["name"]) + self.is_online = data["isOnline"] + self.is_deleted = data["isDeleted"] + self.description = data["description"] + self.created = iso8601.parse_date(data["created"]) + self.is_banned = data["isBanned"] + self.display_name = data["displayName"] + + +class User(PartialUser, ClientObject): """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. @@ -441,6 +452,46 @@

        Parameters

  • +
    +class Friend +(cso, data) +
    +
    +
    +
    + +Expand source code + +
    class Friend(PartialUser):
    +    def __init__(self, cso, data):
    +        super().__init__(cso, data["id"], data["name"])
    +        self.is_online = data["isOnline"]
    +        self.is_deleted = data["isDeleted"]
    +        self.description = data["description"]
    +        self.created = iso8601.parse_date(data["created"])
    +        self.is_banned = data["isBanned"]
    +        self.display_name = data["displayName"]
    +
    +

    Ancestors

    + +

    Inherited members

    + +
    class PartialUser (cso, roblox_id, roblox_name=None) @@ -517,15 +568,13 @@

    Parameters

    async def get_friends(self): """ Gets the user's friends. - :return: A list of User instances. + :return: List of Friend """ friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] friends_list = [] for friend_raw in friends_raw: - friends_list.append( - await self.cso.client.get_user(friend_raw["id"]) - ) + friends_list.append(Friend(self.cso, friend_raw)) return friends_list async def get_groups(self): @@ -565,6 +614,7 @@

    Parameters

    Subclasses

    Methods

    @@ -642,7 +692,7 @@

    Methods

    Gets the user's friends. -:return: A list of User instances.

    +:return: List of Friend

    Expand source code @@ -650,15 +700,13 @@

    Methods

    async def get_friends(self):
         """
         Gets the user's friends.
    -    :return: A list of User instances.
    +    :return: List of Friend
         """
         friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
         friends_raw = friends_req.json()["data"]
         friends_list = []
         for friend_raw in friends_raw:
    -        friends_list.append(
    -            await self.cso.client.get_user(friend_raw["id"])
    -        )
    +        friends_list.append(Friend(self.cso, friend_raw))
         return friends_list
    @@ -786,7 +834,7 @@

    Returns

    Can be initialized with either a user ID or a username.

    Parameters

    -
    cso : ClientSharedObject
    +
    cso : ro_py.client.ClientSharedObject
    ClientSharedObject.
    roblox_id : int
    The id of a user.
    @@ -801,7 +849,7 @@

    Parameters

    Expand source code -
    class User(PartialUser):
    +
    class User(PartialUser, ClientObject):
         """
         Represents a Roblox user and their profile.
         Can be initialized with either a user ID or a username.
    @@ -848,6 +896,7 @@ 

    Parameters

    Ancestors

    Methods

    @@ -927,6 +976,9 @@

    Events
  • +

    Friend

    +
  • +
  • PartialUser

    • expand
    • diff --git a/docs/utilities/clientobject.html b/docs/utilities/clientobject.html new file mode 100644 index 00000000..ae0d0e8d --- /dev/null +++ b/docs/utilities/clientobject.html @@ -0,0 +1,248 @@ + + + + + + +ro_py.utilities.clientobject API documentation + + + + + + + + + + + + +
      +
      +
      +

      Module ro_py.utilities.clientobject

      +
      +
      +
      + +Expand source code + +
      import asyncio
      +from ro_py.utilities.cache import Cache
      +from ro_py.utilities.requests import Requests
      +
      +
      +class ClientObject:
      +    """
      +    Every object that is grabbable with client.get_x inherits this object.
      +    """
      +    async def update(self):
      +        pass
      +
      +
      +class ClientSharedObject:
      +    """
      +    This object is shared across most instances and classes for a particular client.
      +    """
      +    def __init__(self, client):
      +        self.client = client
      +        """Client (parent) of this object."""
      +        self.cache = Cache()
      +        """Cache object to keep objects that don't need to be recreated."""
      +        self.requests = Requests()
      +        """Reqests object for all web requests."""
      +        self.evtloop = asyncio.new_event_loop()
      +        """Event loop for certain things."""
      +
      +
      +
      +
      +
      +
      +
      +
      +
      +

      Classes

      +
      +
      +class ClientObject +
      +
      +

      Every object that is grabbable with client.get_x inherits this object.

      +
      + +Expand source code + +
      class ClientObject:
      +    """
      +    Every object that is grabbable with client.get_x inherits this object.
      +    """
      +    async def update(self):
      +        pass
      +
      +

      Subclasses

      + +

      Methods

      +
      +
      +async def update(self) +
      +
      +
      +
      + +Expand source code + +
      async def update(self):
      +    pass
      +
      +
      +
      +
      +
      +class ClientSharedObject +(client) +
      +
      +

      This object is shared across most instances and classes for a particular client.

      +
      + +Expand source code + +
      class ClientSharedObject:
      +    """
      +    This object is shared across most instances and classes for a particular client.
      +    """
      +    def __init__(self, client):
      +        self.client = client
      +        """Client (parent) of this object."""
      +        self.cache = Cache()
      +        """Cache object to keep objects that don't need to be recreated."""
      +        self.requests = Requests()
      +        """Reqests object for all web requests."""
      +        self.evtloop = asyncio.new_event_loop()
      +        """Event loop for certain things."""
      +
      +

      Instance variables

      +
      +
      var cache
      +
      +

      Cache object to keep objects that don't need to be recreated.

      +
      +
      var client
      +
      +

      Client (parent) of this object.

      +
      +
      var evtloop
      +
      +

      Event loop for certain things.

      +
      +
      var requests
      +
      +

      Reqests object for all web requests.

      +
      +
      +
      +
      +
      +
      + +
      + + + \ No newline at end of file diff --git a/docs/utilities/errors.html b/docs/utilities/errors.html index fa04445b..b37a0ec6 100644 --- a/docs/utilities/errors.html +++ b/docs/utilities/errors.html @@ -89,48 +89,48 @@

      Module ro_py.utilities.errors

      """ -# The following are HTTP generic errors used by requests.py +# The following are HTTP generic errors used by utilities/requests.py class ApiError(Exception): """Called in requests when an API request fails with an error code that doesn't have an independent error.""" pass -class BadRequest(Exception): +class BadRequest(ApiError): """400 HTTP error""" pass -class Unauthorized(Exception): +class Unauthorized(ApiError): """401 HTTP error""" pass -class Forbidden(Exception): +class Forbidden(ApiError): """403 HTTP error""" pass -class NotFound(Exception): +class NotFound(ApiError): """404 HTTP error (also used for other things)""" pass -class Conflict(Exception): +class Conflict(ApiError): """409 HTTP error""" pass -class TooManyRequests(Exception): +class TooManyRequests(ApiError): """429 HTTP error""" pass -class InternalServerError(Exception): +class InternalServerError(ApiError): """500 HTTP error""" pass -class BadGateway(Exception): +class BadGateway(ApiError): """502 HTTP error""" pass @@ -226,6 +226,17 @@

      Ancestors

    • builtins.Exception
    • builtins.BaseException
    +

    Subclasses

    +
    class BadGateway @@ -237,12 +248,13 @@

    Ancestors

    Expand source code -
    class BadGateway(Exception):
    +
    class BadGateway(ApiError):
         """502 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -257,12 +269,13 @@

    Ancestors

    Expand source code -
    class BadRequest(Exception):
    +
    class BadRequest(ApiError):
         """400 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -296,12 +309,13 @@

    Ancestors

    Expand source code -
    class Conflict(Exception):
    +
    class Conflict(ApiError):
         """409 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -316,12 +330,13 @@

    Ancestors

    Expand source code -
    class Forbidden(Exception):
    +
    class Forbidden(ApiError):
         """403 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -395,12 +410,13 @@

    Ancestors

    Expand source code -
    class InternalServerError(Exception):
    +
    class InternalServerError(ApiError):
         """500 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -513,12 +529,13 @@

    Ancestors

    Expand source code -
    class NotFound(Exception):
    +
    class NotFound(ApiError):
         """404 HTTP error (also used for other things)"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -553,12 +570,13 @@

    Ancestors

    Expand source code -
    class TooManyRequests(Exception):
    +
    class TooManyRequests(ApiError):
         """429 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    @@ -573,12 +591,13 @@

    Ancestors

    Expand source code -
    class Unauthorized(Exception):
    +
    class Unauthorized(ApiError):
         """401 HTTP error"""
         pass

    Ancestors

      +
    • ApiError
    • builtins.Exception
    • builtins.BaseException
    diff --git a/docs/utilities/index.html b/docs/utilities/index.html index 15171d4a..581925ab 100644 --- a/docs/utilities/index.html +++ b/docs/utilities/index.html @@ -5,7 +5,7 @@ ro_py.utilities API documentation - + @@ -75,6 +75,13 @@

    Module ro_py.utilities

    This folder houses utilities that are used internally for ro.py.

    +
    +

    Warning

    +

    Files in this directory aren't meant for use outside of ro.py. +If you do rely on any of the utilities in this directory, please note that +your code may break as we frequently move and modify these utilities when +applicable.

    +
    Expand source code @@ -83,6 +90,12 @@

    Module ro_py.utilities

    This folder houses utilities that are used internally for ro.py. +.. warning:: + Files in this directory aren't meant for use outside of ro.py. + If you do rely on any of the utilities in this directory, please note that + your code may break as we frequently move and modify these utilities when + applicable. + """
  • @@ -101,6 +114,10 @@

    Sub-modules

    +
    ro_py.utilities.clientobject
    +
    +
    +
    ro_py.utilities.errors

    ro.py > errors.py …

    @@ -143,6 +160,7 @@

    Index

  • ro_py.utilities.asset_type
  • ro_py.utilities.cache
  • ro_py.utilities.caseconvert
  • +
  • ro_py.utilities.clientobject
  • ro_py.utilities.errors
  • ro_py.utilities.pages
  • ro_py.utilities.requests
  • diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index f57108fe..250517dc 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -158,7 +158,7 @@

    Module ro_py.utilities.pages

    data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ).data + ) async def previous(self): """ @@ -297,7 +297,7 @@

    Instance variables

    data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ).data + ) async def previous(self): """ @@ -371,7 +371,7 @@

    Methods

    data=page_req.json(), handler=self.handler, handler_args=self.handler_args - ).data
    + )
    diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 95dfb125..307847ac 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -79,7 +79,6 @@

    Module ro_py.utilities.requests

    Expand source code
    from ro_py.utilities.errors import ApiError, c_errors
    -from ro_py.captcha import CaptchaMetadata
     from json.decoder import JSONDecodeError
     import requests
     import httpx
    @@ -122,138 +121,82 @@ 

    Module ro_py.utilities.requests

    self.session.headers["User-Agent"] = "Roblox/WinInet" self.session.headers["Referer"] = "www.roblox.com" # Possibly useful for some things - async def get(self, *args, **kwargs): - """ - Essentially identical to requests_async.Session.get. - """ - + async def request(self, method, *args, **kwargs): quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) + this_request = await self.session.request(method, *args, **kwargs) + + method = method.lower() - get_request = await self.session.get(*args, **kwargs) + if doxcsrf and ((method == "post") or (method == "put") or (method == "patch") or (method == "delete")): + if "X-CSRF-TOKEN" in this_request.headers: + self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] + if this_request.status_code == 403: # Request failed, send it again + this_request = await self.session.post(*args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. - return get_request + return this_request try: - get_request_json = get_request.json() + this_request_json = this_request.json() except JSONDecodeError: - return get_request + return this_request - if isinstance(get_request_json, dict): + if isinstance(this_request_json, dict): try: - get_request_error = get_request_json["errors"] + get_request_error = this_request_json["errors"] except KeyError: - return get_request + return this_request else: - return get_request + return this_request if quickreturn: - return get_request - - raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") - - def back_post(self, *args, **kwargs): - kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) - kwargs["headers"] = kwargs.pop("headers", self.session.headers) + return this_request - post_request = requests.post(*args, **kwargs) + request_exception = status_code_error(this_request.status_code) + raise request_exception(f"[{this_request.status_code}] {get_request_error[0]['message']}") - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = requests.post(*args, **kwargs) + async def get(self, *args, **kwargs): + """ + Essentially identical to requests_async.Session.get. + """ - self.session.cookies = post_request.cookies - return post_request + return await self.request("GET", *args, **kwargs) async def post(self, *args, **kwargs): """ Essentially identical to requests_async.Session.post. """ - quickreturn = kwargs.pop("quickreturn", False) - doxcsrf = kwargs.pop("doxcsrf", True) - - post_request = await self.session.post(*args, **kwargs) - - if doxcsrf: - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) - - try: - post_request_json = post_request.json() - except JSONDecodeError: - return post_request - - if isinstance(post_request_json, dict): - try: - post_request_error = post_request_json["errors"] - except KeyError: - return post_request - else: - return post_request - - if quickreturn: - return post_request - - raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}") + return await self.request("post", *args, **kwargs) async def patch(self, *args, **kwargs): """ Essentially identical to requests_async.Session.patch. """ - patch_request = await self.session.patch(*args, **kwargs) - - if patch_request.status_code == 403: - if "X-CSRF-TOKEN" in patch_request.headers: - self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = await self.session.patch(*args, **kwargs) - - patch_request_json = patch_request.json() - - if isinstance(patch_request_json, dict): - try: - patch_request_error = patch_request_json["errors"] - except KeyError: - return patch_request - else: - return patch_request - - raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}") + return await self.request("patch", *args, **kwargs) async def delete(self, *args, **kwargs): """ Essentially identical to requests_async.Session.delete. """ - delete_request = await self.session.delete(*args, **kwargs) - - if delete_request.status_code == 403: - if "X-CSRF-TOKEN" in delete_request.headers: - self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] - delete_request = await self.session.delete(*args, **kwargs) + return await self.request("delete", *args, **kwargs) - delete_request_json = delete_request.json() + def back_post(self, *args, **kwargs): + kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + kwargs["headers"] = kwargs.pop("headers", self.session.headers) - if isinstance(delete_request_json, dict): - try: - delete_request_error = delete_request_json["errors"] - except KeyError: - return delete_request - else: - return delete_request + post_request = requests.post(*args, **kwargs) - raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}") + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) - async def get_captcha_metadata(self): - captcha_meta_req = await self.get( - url="https://apis.roblox.com/captcha/v1/metadata" - ) - captcha_meta_raw = captcha_meta_req.json() - return CaptchaMetadata(captcha_meta_raw)
    + self.session.cookies = post_request.cookies + return post_request
    @@ -337,138 +280,82 @@

    Ancestors

    self.session.headers["User-Agent"] = "Roblox/WinInet" self.session.headers["Referer"] = "www.roblox.com" # Possibly useful for some things - async def get(self, *args, **kwargs): - """ - Essentially identical to requests_async.Session.get. - """ - + async def request(self, method, *args, **kwargs): quickreturn = kwargs.pop("quickreturn", False) + doxcsrf = kwargs.pop("doxcsrf", True) + this_request = await self.session.request(method, *args, **kwargs) - get_request = await self.session.get(*args, **kwargs) + method = method.lower() + + if doxcsrf and ((method == "post") or (method == "put") or (method == "patch") or (method == "delete")): + if "X-CSRF-TOKEN" in this_request.headers: + self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] + if this_request.status_code == 403: # Request failed, send it again + this_request = await self.session.post(*args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. - return get_request + return this_request try: - get_request_json = get_request.json() + this_request_json = this_request.json() except JSONDecodeError: - return get_request + return this_request - if isinstance(get_request_json, dict): + if isinstance(this_request_json, dict): try: - get_request_error = get_request_json["errors"] + get_request_error = this_request_json["errors"] except KeyError: - return get_request + return this_request else: - return get_request + return this_request if quickreturn: - return get_request - - raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}") - - def back_post(self, *args, **kwargs): - kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) - kwargs["headers"] = kwargs.pop("headers", self.session.headers) + return this_request - post_request = requests.post(*args, **kwargs) + request_exception = status_code_error(this_request.status_code) + raise request_exception(f"[{this_request.status_code}] {get_request_error[0]['message']}") - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = requests.post(*args, **kwargs) + async def get(self, *args, **kwargs): + """ + Essentially identical to requests_async.Session.get. + """ - self.session.cookies = post_request.cookies - return post_request + return await self.request("GET", *args, **kwargs) async def post(self, *args, **kwargs): """ Essentially identical to requests_async.Session.post. """ - quickreturn = kwargs.pop("quickreturn", False) - doxcsrf = kwargs.pop("doxcsrf", True) - - post_request = await self.session.post(*args, **kwargs) - - if doxcsrf: - if post_request.status_code == 403: - if "X-CSRF-TOKEN" in post_request.headers: - self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] - post_request = await self.session.post(*args, **kwargs) - - try: - post_request_json = post_request.json() - except JSONDecodeError: - return post_request - - if isinstance(post_request_json, dict): - try: - post_request_error = post_request_json["errors"] - except KeyError: - return post_request - else: - return post_request - - if quickreturn: - return post_request - - raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}") + return await self.request("post", *args, **kwargs) async def patch(self, *args, **kwargs): """ Essentially identical to requests_async.Session.patch. """ - patch_request = await self.session.patch(*args, **kwargs) - - if patch_request.status_code == 403: - if "X-CSRF-TOKEN" in patch_request.headers: - self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = await self.session.patch(*args, **kwargs) - - patch_request_json = patch_request.json() - - if isinstance(patch_request_json, dict): - try: - patch_request_error = patch_request_json["errors"] - except KeyError: - return patch_request - else: - return patch_request - - raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}") + return await self.request("patch", *args, **kwargs) async def delete(self, *args, **kwargs): """ Essentially identical to requests_async.Session.delete. """ - delete_request = await self.session.delete(*args, **kwargs) - - if delete_request.status_code == 403: - if "X-CSRF-TOKEN" in delete_request.headers: - self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] - delete_request = await self.session.delete(*args, **kwargs) + return await self.request("delete", *args, **kwargs) - delete_request_json = delete_request.json() + def back_post(self, *args, **kwargs): + kwargs["cookies"] = kwargs.pop("cookies", self.session.cookies) + kwargs["headers"] = kwargs.pop("headers", self.session.headers) - if isinstance(delete_request_json, dict): - try: - delete_request_error = delete_request_json["errors"] - except KeyError: - return delete_request - else: - return delete_request + post_request = requests.post(*args, **kwargs) - raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}") + if "X-CSRF-TOKEN" in post_request.headers: + self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"] + post_request = requests.post(*args, **kwargs) - async def get_captcha_metadata(self): - captcha_meta_req = await self.get( - url="https://apis.roblox.com/captcha/v1/metadata" - ) - captcha_meta_raw = captcha_meta_req.json() - return CaptchaMetadata(captcha_meta_raw)
    + self.session.cookies = post_request.cookies + return post_request

    Instance variables

    @@ -516,24 +403,7 @@

    Methods

    Essentially identical to requests_async.Session.delete. """ - delete_request = await self.session.delete(*args, **kwargs) - - if delete_request.status_code == 403: - if "X-CSRF-TOKEN" in delete_request.headers: - self.session.headers['X-CSRF-TOKEN'] = delete_request.headers["X-CSRF-TOKEN"] - delete_request = await self.session.delete(*args, **kwargs) - - delete_request_json = delete_request.json() - - if isinstance(delete_request_json, dict): - try: - delete_request_error = delete_request_json["errors"] - except KeyError: - return delete_request - else: - return delete_request - - raise status_code_error(delete_request.status_code)(f"[{delete_request.status_code}] {delete_request_error[0]['message']}")
    + return await self.request("delete", *args, **kwargs)
    @@ -550,48 +420,7 @@

    Methods

    Essentially identical to requests_async.Session.get. """ - quickreturn = kwargs.pop("quickreturn", False) - - get_request = await self.session.get(*args, **kwargs) - - if kwargs.pop("stream", False): - # Skip request checking and just get on with it. - return get_request - - try: - get_request_json = get_request.json() - except JSONDecodeError: - return get_request - - if isinstance(get_request_json, dict): - try: - get_request_error = get_request_json["errors"] - except KeyError: - return get_request - else: - return get_request - - if quickreturn: - return get_request - - raise status_code_error(get_request.status_code)(f"[{get_request.status_code}] {get_request_error[0]['message']}")
    - - -
    -async def get_captcha_metadata(self) -
    -
    -
    -
    - -Expand source code - -
    async def get_captcha_metadata(self):
    -    captcha_meta_req = await self.get(
    -        url="https://apis.roblox.com/captcha/v1/metadata"
    -    )
    -    captcha_meta_raw = captcha_meta_req.json()
    -    return CaptchaMetadata(captcha_meta_raw)
    + return await self.request("GET", *args, **kwargs)
    @@ -608,24 +437,7 @@

    Methods

    Essentially identical to requests_async.Session.patch. """ - patch_request = await self.session.patch(*args, **kwargs) - - if patch_request.status_code == 403: - if "X-CSRF-TOKEN" in patch_request.headers: - self.session.headers['X-CSRF-TOKEN'] = patch_request.headers["X-CSRF-TOKEN"] - patch_request = await self.session.patch(*args, **kwargs) - - patch_request_json = patch_request.json() - - if isinstance(patch_request_json, dict): - try: - patch_request_error = patch_request_json["errors"] - except KeyError: - return patch_request - else: - return patch_request - - raise status_code_error(patch_request.status_code)(f"[{patch_request.status_code}] {patch_request_error[0]['message']}")
    + return await self.request("patch", *args, **kwargs)
    @@ -642,34 +454,53 @@

    Methods

    Essentially identical to requests_async.Session.post. """ + return await self.request("post", *args, **kwargs)
    + + +
    +async def request(self, method, *args, **kwargs) +
    +
    +
    +
    + +Expand source code + +
    async def request(self, method, *args, **kwargs):
         quickreturn = kwargs.pop("quickreturn", False)
         doxcsrf = kwargs.pop("doxcsrf", True)
    +    this_request = await self.session.request(method, *args, **kwargs)
     
    -    post_request = await self.session.post(*args, **kwargs)
    +    method = method.lower()
     
    -    if doxcsrf:
    -        if post_request.status_code == 403:
    -            if "X-CSRF-TOKEN" in post_request.headers:
    -                self.session.headers['X-CSRF-TOKEN'] = post_request.headers["X-CSRF-TOKEN"]
    -                post_request = await self.session.post(*args, **kwargs)
    +    if doxcsrf and ((method == "post") or (method == "put") or (method == "patch") or (method == "delete")):
    +        if "X-CSRF-TOKEN" in this_request.headers:
    +            self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"]
    +            if this_request.status_code == 403:  # Request failed, send it again
    +                this_request = await self.session.post(*args, **kwargs)
    +
    +    if kwargs.pop("stream", False):
    +        # Skip request checking and just get on with it.
    +        return this_request
     
         try:
    -        post_request_json = post_request.json()
    +        this_request_json = this_request.json()
         except JSONDecodeError:
    -        return post_request
    +        return this_request
     
    -    if isinstance(post_request_json, dict):
    +    if isinstance(this_request_json, dict):
             try:
    -            post_request_error = post_request_json["errors"]
    +            get_request_error = this_request_json["errors"]
             except KeyError:
    -            return post_request
    +            return this_request
         else:
    -        return post_request
    +        return this_request
     
         if quickreturn:
    -        return post_request
    +        return this_request
     
    -    raise status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}")
    + request_exception = status_code_error(this_request.status_code) + raise request_exception(f"[{this_request.status_code}] {get_request_error[0]['message']}")
    @@ -705,13 +536,13 @@

    Requests

    -
      + From f4757d625c75b799f99ef0e33892b18920d1a103 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 11 Feb 2021 18:47:11 -0500 Subject: [PATCH 422/518] Added Secure Sign Out (SSO) --- ro_py/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index 432a0473..f0ffcfbd 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -282,3 +282,8 @@ async def get_captcha_metadata(self): ) captcha_meta_raw = captcha_meta_req.json() return CaptchaMetadata(captcha_meta_raw) + + async def secure_sign_out(self): + await self.requests.post( + url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate" + ) From 4a6e170935f1dbebc201741b7c5ff401ac9f0f7e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 11 Feb 2021 19:37:40 -0500 Subject: [PATCH 423/518] Added comments --- ro_py/client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ro_py/client.py b/ro_py/client.py index f0ffcfbd..30ccb861 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -271,12 +271,19 @@ async def get_badge(self, badge_id): return badge async def get_friend_requests(self): + """ + Gets the amount of friend requests the client has. + """ friend_req = await self.requests.get( url="https://friends.roblox.com/v1/user/friend-requests/count" ) return friend_req.json()["count"] async def get_captcha_metadata(self): + """ + Grabs captcha metadata, which contains public keys. You can pass these to the prompt extension for GUI captcha + solving, + """ captcha_meta_req = await self.requests.get( url="https://apis.roblox.com/captcha/v1/metadata" ) @@ -284,6 +291,18 @@ async def get_captcha_metadata(self): return CaptchaMetadata(captcha_meta_raw) async def secure_sign_out(self): + """ + Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one. + + In the past, it was believed that Roblox would invalidate sessions automatically. This is not the case. + On the server, sessions are never invalidated unless a logout request is sent. In the browser, cookies expire + after 30 years. + + Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because + they would generate a new session token, and suggested that the user would "refresh their cookie" fairly + frequently as to avoid this. This isn't something you'll actually need to do, so this is left here as an + optional feature. + """ await self.requests.post( url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate" ) From 459ca54905c3eb4171d998ee198210c7219c821a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 11 Feb 2021 22:27:46 -0500 Subject: [PATCH 424/518] Preparation for new notification features --- ro_py/notifications.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ro_py/notifications.py b/ro_py/notifications.py index 88908ed1..7887d136 100644 --- a/ro_py/notifications.py +++ b/ro_py/notifications.py @@ -16,6 +16,27 @@ import json +class UnreadNotifications: + def __init__(self, data): + self.count = data["unreadNotifications"] + """Amount of unread notifications.""" + self.status_message = data["statusMessage"] + """Status message.""" + + +class RealtimeNotificationSettings: + def __init__(self, data): + self.primary_domain = data["primaryDomain"] + self.fallback_domain = data["fallbackDomain"] + + +class NotificationSettings: + def __init__(self, data): + self.notification_band_settings = data["notificationBandSettings"] + self.opted_out_notification_source_types = data["optedOutNotificationSourceTypes"] + self.opted_out_receiver_destination_types = data["optedOutReceiverDestinationTypes"] + + class Notification: """ Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client. @@ -66,6 +87,12 @@ def __init__(self, cso): self.wss_url = None self.connection = None + async def get_unread_notifications(self): + unread_req = await self.requests.get( + url="https://notifications.roblox.com/v2/stream-notifications/unread-count" + ) + return UnreadNotifications(unread_req.json()) + async def initialize(self): self.negotiate_request = await self.requests.get( url="https://realtime.roblox.com/notifications/negotiate" From 44877fc220aaa7ae5bf268fec56d895c3c2a26c7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 11 Feb 2021 22:32:36 -0500 Subject: [PATCH 425/518] =?UTF-8?q?Documented=20events=20(@iranathan=20?= =?UTF-8?q?=F0=9F=91=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/events.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ro_py/events.py b/ro_py/events.py index e0f00c72..49549937 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -1,3 +1,10 @@ +""" + +This file houses functions and classes that pertain to events and event handling with ro.py. Most methods that have +events actually don't reference content here, this doesn't contain much at the moment. + +""" + import enum From af5299958e63060e82cbf2313048b85f1d4e758c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 10:41:20 -0500 Subject: [PATCH 426/518] Moved to an enum for asset types --- ro_py/assets.py | 11 ++-- ro_py/utilities/asset_type.py | 105 ++++++++++++++++++---------------- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/ro_py/assets.py b/ro_py/assets.py index 7b80befd..e4647140 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -7,7 +7,7 @@ from ro_py.utilities.clientobject import ClientObject from ro_py.utilities.errors import NotLimitedError from ro_py.economy import LimitedResaleData -from ro_py.utilities.asset_type import asset_types +from ro_py.utilities.asset_type import AssetTypes import iso8601 import asyncio import copy @@ -27,8 +27,8 @@ class Asset(ClientObject): Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.utilities.clientobject.ClientSharedObject + CSO asset_id ID of the asset. """ @@ -76,7 +76,10 @@ async def update(self): self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] - self.asset_type_name = asset_types[self.asset_type_id] + for key, value in AssetTypes.member_map_.items(): + if value == self.asset_type_id: + self.asset_type_name = key + # if asset_info["Creator"]["CreatorType"] == "User": # self.creator = User(self.requests, asset_info["Creator"]["Id"]) # if asset_info["Creator"]["CreatorType"] == "Group": diff --git a/ro_py/utilities/asset_type.py b/ro_py/utilities/asset_type.py index 64510b83..35c68b9b 100644 --- a/ro_py/utilities/asset_type.py +++ b/ro_py/utilities/asset_type.py @@ -6,51 +6,60 @@ """ -asset_types = [ - None, - "Image", - "TeeShirt", - "Audio", - "Mesh", - "Lua", - "Hat", - "Place", - "Model", - "Shirt", - "Pants", - "Decal", - "Head", - "Face", - "Gear", - "Badge", - "Animation", - "Torso", - "RightArm", - "LeftArm", - "LeftLeg", - "RightLeg", - "Package", - "GamePass", - "Plugin", - "MeshPart", - "HairAccessory", - "FaceAccessory", - "NeckAccessory", - "ShoulderAccessory", - "FrontAccesory", - "BackAccessory", - "WaistAccessory", - "ClimbAnimation", - "DeathAnimation", - "FallAnimation", - "IdleAnimation", - "JumpAnimation", - "RunAnimation", - "SwimAnimation", - "WalkAnimation", - "PoseAnimation", - "EarAccessory", - "EyeAccessory", - "EmoteAnimation", - "Video" -] +from enum import IntEnum + + +class AssetTypes(IntEnum): + Image = 1 + TeeShirt = 2 + Audio = 3 + Mesh = 4 + Lua = 5 + Hat = 8 + Place = 9 + Model = 10 + + Shirt = 11 + Pants = 12 + + Decal = 13 + Head = 17 + Face = 18 + Gear = 19 + Badge = 21 + Animation = 24 + + Torso = 27 + RightArm = 28 + LeftArm = 29 + LeftLeg = 30 + RightLeg = 31 + Package = 32 + + GamePass = 34 + Plugin = 38 + MeshPart = 40 + + HairAccessory = 41 + FaceAccessory = 42 + NeckAccessory = 43 + ShoulderAccessory = 44 + FrontAccessory = 45 + BackAccessory = 46 + WaistAccessory = 47 + + ClimbAnimation = 48 + DeathAnimation = 49 + FallAnimation = 50 + IdleAnimation = 51 + JumpAnimation = 52 + RunAnimation = 53 + SwimAniation = 54 + WalkAnimation = 55 + PoseAnimation = 56 + + EarAccessory = 57 + EyeAccessory = 58 + + EmoteAnimation = 61 + Video = 62 From 878d9919b308c4766734f7aa619efb8121957c19 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 11:00:58 -0500 Subject: [PATCH 427/518] Added to_asset + super call to all other objects --- ro_py/assets.py | 1 + ro_py/badges.py | 1 + ro_py/games.py | 1 + ro_py/groups.py | 1 + ro_py/utilities/clientobject.py | 8 ++++++++ 5 files changed, 12 insertions(+) diff --git a/ro_py/assets.py b/ro_py/assets.py index e4647140..dc9d6737 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -34,6 +34,7 @@ class Asset(ClientObject): """ def __init__(self, cso, asset_id): + super().__init__() self.id = asset_id self.cso = cso self.requests = cso.requests diff --git a/ro_py/badges.py b/ro_py/badges.py index 3f1dc30e..4d520f27 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -31,6 +31,7 @@ class Badge(ClientObject): ID of the badge. """ def __init__(self, cso, badge_id): + super().__init__() self.id = badge_id self.cso = cso self.requests = cso.requests diff --git a/ro_py/games.py b/ro_py/games.py index 1c06d463..0bfef48e 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -32,6 +32,7 @@ class Game(ClientObject): This class represents multiple game-related endpoints. """ def __init__(self, cso, universe_id): + super().__init__() self.id = universe_id self.cso = cso self.requests = cso.requests diff --git a/ro_py/groups.py b/ro_py/groups.py index 16e4e5c7..3480fa2b 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -133,6 +133,7 @@ class Group(ClientObject): Represents a group. """ def __init__(self, cso, group_id): + super().__init__() self.cso = cso """Client Shared Object""" self.requests = cso.requests diff --git a/ro_py/utilities/clientobject.py b/ro_py/utilities/clientobject.py index 162e8a38..603d7800 100644 --- a/ro_py/utilities/clientobject.py +++ b/ro_py/utilities/clientobject.py @@ -7,6 +7,14 @@ class ClientObject: """ Every object that is grabbable with client.get_x inherits this object. """ + def __init__(self): + self.id = None + self.cso = None + self.requests = None + + async def to_asset(self): + return await self.cso.client.get_asset(self.id) + async def update(self): pass From e1bdc60962c32a6f019e41e3d73c45479de9b00c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 11:03:59 -0500 Subject: [PATCH 428/518] Update asset_type.py --- ro_py/utilities/asset_type.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ro_py/utilities/asset_type.py b/ro_py/utilities/asset_type.py index 35c68b9b..8ae0bdd5 100644 --- a/ro_py/utilities/asset_type.py +++ b/ro_py/utilities/asset_type.py @@ -38,6 +38,9 @@ class AssetTypes(IntEnum): GamePass = 34 Plugin = 38 + + Game = 39 # This isn't documented as far as I know, but this is the ID that is returned for game universes + MeshPart = 40 HairAccessory = 41 From 54aeae08a49c3d8f18ba4512661daefa42d3390a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 11:14:58 -0500 Subject: [PATCH 429/518] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ae4176cd..6fda838a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ build/ dist/ dist_old/ ro_py.egg-info/ -tests/ +jmkdev_tests/ ro_py_old/ other/ udocs/ From a3d7427ca6dde14f672a9f8b0727e3838551f97d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 12:46:47 -0800 Subject: [PATCH 430/518] Added sanity check + fixed bug --- ro_py/assets.py | 2 +- tests/sanitycheck.py | 70 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/sanitycheck.py diff --git a/ro_py/assets.py b/ro_py/assets.py index dc9d6737..97ec1727 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -77,7 +77,7 @@ async def update(self): self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] - for key, value in AssetTypes.member_map_.items(): + for key, value in AssetTypes._member_map_.items(): if value == self.asset_type_id: self.asset_type_name = key diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py new file mode 100644 index 00000000..41775d9c --- /dev/null +++ b/tests/sanitycheck.py @@ -0,0 +1,70 @@ +import asyncio +from ro_py import Client + +client = Client() + + +def i(name, thisobj): + assert name in thisobj.__dict__ + return True + + +async def main(): + user = await client.get_user(968108160) + i("id", user) + i("name", user) + i("description", user) + i("requests", user) + i("thumbnails", user) + i("display_name", user) + i("is_banned", user) + i("created", user) + i("cso", user) + await user.get_status() + await user.get_followings_count() + await user.update() + await user.get_groups() + await user.get_friends() + await user.get_followers_count() + await user.get_followings_count() + await user.get_roblox_badges() + + user = await client.get_user_by_username("John Doe") + i("id", user) + i("name", user) + i("description", user) + i("requests", user) + i("thumbnails", user) + i("display_name", user) + i("is_banned", user) + i("created", user) + i("cso", user) + await user.get_status() + await user.get_followings_count() + await user.update() + await user.get_groups() + await user.get_friends() + await user.get_followers_count() + await user.get_followings_count() + await user.get_roblox_badges() + + group = await client.get_group(1) + await group.update() + await group.get_roles() + await group.get_member_by_id(1179762) + + asset = await client.get_asset(5832204472) + await asset.update() + + badge = await client.get_badge(2124538588) + await badge.update() + + game = await client.get_game_by_universe_id(1732173541) + await game.update() + await game.get_votes() + await game.get_badges() + + print("Finished test.") + +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) From e5971c4e1d11cec1cc69a633013b1fb9ba95dc4f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 12:48:51 -0800 Subject: [PATCH 431/518] Sanity Check Action --- .github/workflows/sanitycheck.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/sanitycheck.yml diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml new file mode 100644 index 00000000..eaded0ff --- /dev/null +++ b/.github/workflows/sanitycheck.yml @@ -0,0 +1,21 @@ +name: Sanity Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Run a multi-line script + run: | + python3 setup.py install + python3 /tests/sanitycheck.py From aca20300cb4b733846b00672484b085944310aed Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 12:49:43 -0800 Subject: [PATCH 432/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index eaded0ff..2c96f1b8 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -17,5 +17,6 @@ jobs: - name: Run a multi-line script run: | + python3 -m pip install setuptools python3 setup.py install python3 /tests/sanitycheck.py From f866c31b91c44614818a2a1413ac750215c64383 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 12:51:01 -0800 Subject: [PATCH 433/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index 2c96f1b8..cbfb82fc 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -17,6 +17,6 @@ jobs: - name: Run a multi-line script run: | - python3 -m pip install setuptools - python3 setup.py install + sudo python3 -m pip install setuptools + sudo python3 setup.py install python3 /tests/sanitycheck.py From 058782f76d82252763df3fb8f26c75c86af46e9a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 12:51:50 -0800 Subject: [PATCH 434/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index cbfb82fc..6d74c899 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -19,4 +19,4 @@ jobs: run: | sudo python3 -m pip install setuptools sudo python3 setup.py install - python3 /tests/sanitycheck.py + python3 ./tests/sanitycheck.py From df21c07cbfffbcbd31c513c650e8b77c565744d0 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 13:24:59 -0800 Subject: [PATCH 435/518] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 355edfe2..06df4848 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,6 +14,8 @@ name: "CodeQL" on: push: branches: [ main ] + paths: + - 'ro_py/**' pull_request: # The branches below must be a subset of the branches above branches: [ main ] From 25ba48322120e132c06b202200f6dd2e15d1e5bb Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 13:27:04 -0800 Subject: [PATCH 436/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index 6d74c899..0849b36d 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -3,8 +3,9 @@ name: Sanity Check on: push: branches: [ main ] - pull_request: - branches: [ main ] + paths: + - 'ro_py/**' + - 'tests/** workflow_dispatch: From 33cf09d2207daf17bfb564eb7431d60b000ebfa3 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 13:28:19 -0800 Subject: [PATCH 437/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index 0849b36d..dff78116 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -5,7 +5,7 @@ on: branches: [ main ] paths: - 'ro_py/**' - - 'tests/** + - 'tests/**' workflow_dispatch: From cd84b2c24a785ee8371636feb98413d9b406eab1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 13:29:22 -0800 Subject: [PATCH 438/518] New user --- tests/sanitycheck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index 41775d9c..1bc16dd3 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -10,7 +10,7 @@ def i(name, thisobj): async def main(): - user = await client.get_user(968108160) + user = await client.get_user(2067807455) i("id", user) i("name", user) i("description", user) From 4ac642af4d9c86649e0cf4b6afe168b3bdf76c56 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 15:50:13 -0800 Subject: [PATCH 439/518] Improved sanity check --- tests/sanitycheck.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index 1bc16dd3..f5918d85 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -9,7 +9,7 @@ def i(name, thisobj): return True -async def main(): +async def client_test(): user = await client.get_user(2067807455) i("id", user) i("name", user) @@ -66,5 +66,10 @@ async def main(): print("Finished test.") + +async def main(): + await client_test() + + if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main()) From 602cf0a1e30d3de019dc20563a84a9108c67e8ad Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 15:56:33 -0800 Subject: [PATCH 440/518] Moved auth features + added logout --- ro_py/client.py | 157 ++++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 71 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 30ccb861..8c22f13b 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -53,77 +53,7 @@ def __init__(self, token: str = None): if token: self.token_login(token) - def token_login(self, token): - """ - Authenticates the client with a ROBLOSECURITY token. - - Parameters - ---------- - token : str - .ROBLOSECURITY token to authenticate with. - """ - self.requests.session.cookies[".ROBLOSECURITY"] = token - self.accountinformation = AccountInformation(self.cso) - self.accountsettings = AccountSettings(self.cso) - self.chat = ChatWrapper(self.cso) - self.trade = TradesWrapper(self.cso, self.get_self) - self.notifications = NotificationReceiver(self.cso) - - async def user_login(self, username, password, token=None): - """ - Authenticates the client with a username and password. - - Parameters - ---------- - username : str - Username to log in with. - password : str - Password to log in with. - token : str, optional - If you have already solved the captcha, pass it here. - - Returns - ------- - ro_py.captcha.UnsolvedCaptcha or request - """ - if token: - login_req = self.requests.back_post( - url="https://auth.roblox.com/v2/login", - json={ - "ctype": "Username", - "cvalue": username, - "password": password, - "captchaToken": token, - "captchaProvider": "PROVIDER_ARKOSE_LABS" - } - ) - return login_req - else: - login_req = await self.requests.post( - url="https://auth.roblox.com/v2/login", - json={ - "ctype": "Username", - "cvalue": username, - "password": password - }, - quickreturn=True - ) - if login_req.status_code == 200: - # If we're here, no captcha is required and we're already logged in, so we can return. - return - elif login_req.status_code == 403: - # A captcha is required, so we need to return the captcha to solve. - field_data = login_req.json()["errors"][0]["fieldData"] - captcha_req = await self.requests.post( - url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", - headers={ - "content-type": "application/x-www-form-urlencoded; charset=UTF-8" - }, - data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" - ) - captcha_json = captcha_req.json() - return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") - + # Grab objects async def get_self(self): self_req = await self.requests.get( url="https://roblox.com/my/profile" @@ -290,6 +220,79 @@ async def get_captcha_metadata(self): captcha_meta_raw = captcha_meta_req.json() return CaptchaMetadata(captcha_meta_raw) + # Login/logout + + def token_login(self, token): + """ + Authenticates the client with a ROBLOSECURITY token. + + Parameters + ---------- + token : str + .ROBLOSECURITY token to authenticate with. + """ + self.requests.session.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + self.chat = ChatWrapper(self.cso) + self.trade = TradesWrapper(self.cso, self.get_self) + self.notifications = NotificationReceiver(self.cso) + + async def user_login(self, username, password, token=None): + """ + Authenticates the client with a username and password. + + Parameters + ---------- + username : str + Username to log in with. + password : str + Password to log in with. + token : str, optional + If you have already solved the captcha, pass it here. + + Returns + ------- + ro_py.captcha.UnsolvedCaptcha or request + """ + if token: + login_req = self.requests.back_post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password, + "captchaToken": token, + "captchaProvider": "PROVIDER_ARKOSE_LABS" + } + ) + return login_req + else: + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password + }, + quickreturn=True + ) + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + async def secure_sign_out(self): """ Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one. @@ -306,3 +309,15 @@ async def secure_sign_out(self): await self.requests.post( url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate" ) + + async def logout(self): + """ + Logs out this user. + + This will invalidate your .ROBLOSECURITY token, unlike ro_py.client.secure_sign_out(). + Don't use this unless you plan to either never use this .ROBLOSECURITY token again. + + """ + await self.requests.post( + url="https://auth.roblox.com/v2/logout" + ) From da1c8443bf2bf0f57230c01d0f7c115ae44e63cd Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 21:26:25 -0500 Subject: [PATCH 441/518] Added new icons, added new enums, and fixed a bug --- resources/new studio icon small app.pdn | Bin 0 -> 88377 bytes resources/new studio icon small app.png | Bin 0 -> 44130 bytes resources/new studio icon small.pdn | Bin 0 -> 72875 bytes ro_py/utilities/asset_type.py | 16 +++++++++++++--- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 resources/new studio icon small app.pdn create mode 100644 resources/new studio icon small app.png create mode 100644 resources/new studio icon small.pdn diff --git a/resources/new studio icon small app.pdn b/resources/new studio icon small app.pdn new file mode 100644 index 0000000000000000000000000000000000000000..2aa87c184ac4dc206433d706d9c47b61de22cbf4 GIT binary patch literal 88377 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 K!`7`zO8*0xEuWtN literal 0 HcmV?d00001 diff --git a/resources/new studio icon small app.png b/resources/new studio icon small app.png new file mode 100644 index 0000000000000000000000000000000000000000..b2236d4907d2c5d0b5419a857f0d7a5261e1ad5c GIT binary patch literal 44130 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 literal 0 HcmV?d00001 diff --git a/resources/new studio icon small.pdn b/resources/new studio icon small.pdn new file mode 100644 index 0000000000000000000000000000000000000000..faa7161592265d43216e8d68e4d6b9809cec1cbd GIT binary patch literal 72875 zcmY&=*^aB)mSug))qO=uFg;RcNo&z}rniANgXs)5Hm&jB)bG_-)Qpq)Qd@iLgyV$5 zz?#jNV~lBEIWzzF|NO82eBWQ@^VU@V+PnMG{@1^0j{o<++Nx{X<$qobx1*Y7c=7Ll z&9|+(U%REfz<0Xw^FxKp;?j0pamqoh@n2EGdZUaCk&8T=QQ<)?lAeFvrID5Oj@&1}j4^E2TqNKP0b z`TN*6F$nvJQDnK-lit6gmk~Oer^XLecM{!lY0ION zE<`_1c0YFWF7p8=7t@S)i6N!?dOH31>GGjJ2~o<&Xxx6JORW6lt)=%)h*`Q3%MROK1zG?KF?H{?IRgO=!m|L)X&P=#>*9c2D3+*0N)>8U)4Vwo0?)oNFM=q^9_IMJ(&V{i@k<<9@yFOyGcvKsMAbJ~*@2$YE9 zE=9N2LC?3Krk@_YQP3c(Ky!~@?DV->Xc?07sosv;13{$VHtJpU8T-6QJ0$>@Tt z1bxV=-91S$k=JA><7FD*LxrFlP$Vi}-M8LzX|9nj{gk7K)|QyQ4V+<--{16CxYz7n zliH#qctbuG3G#jq$wKt>0BO@VLYk_D+7nxidfn}_WhL*&eTn6qZ~6JVlMo;G6dsV| ztzqVq9rHrberu8D2d_rFuu)$4YIyzu2c~gr+YC3i|~>9 znOcnBuW%w4Pw4k1g%6?mKZ*}TJlQVF12u^fWRLpz+_4pyV=Grmmvdjv&KKp!zwGw! zte+*d=+cqb+dI|^OsD%>j5eYkJ!CviXLNp_5q*=JAG%S<9D7ldS4L7!iDDbWr@nBB zbLXc_d7fszaI7L6jSVNtiIhhC`_M1R5T!i8e&S;b<FMrIJvoN$^sI}|w_u8?uzkAb-r0kUO1LM3CmGco?fWl2Ol8#NSbd}JrX=xAP2^t^ z1yP4(Ixy_^()X<~iKEV)Zv5rs+@&qvrg!|c5HS&iv0upW_#i0^8wbgExjM#j#M>EY~_Z!8Mm?54Fx$_0#g}#z7Z7gp2 zNf;J~%!yaya30cnead$060vqysK^P-GvXvcmba!eO)J6c<3)+2A4~%}$dnE4%xz&3 z$i3K^ZgG=RUQPB=0C9z>O?p+o7n4GFi&(79vO7@v+cF&wU-~pqFtg_ca`ky1@+E#? z<}$5N=jyYl70-)Fa$*^oMs?m9;*o}pPu-yaH`vv33!ZJ$CHRTDL%wvE;1UEp>nF7G ztrsTX>g_q#x6xFIVC5zM7y(&FHjdiA7*3QrxN)+2J*WqNZu{pvKt$F zPR57mjOM6z4;x>a%*c}$dS;4;U!iWQm*5=E$ne|a znptR=S2ul%dcNO8!_EAUO*(Dyk=B`Bj7K0CHC2a5bER7R4Zc9Wv-|G}_x2>(<`hEt zO&Yi_C^yadVwI?y9TVGucGud-8nvF@4H@xd(y4A-7Q5I5`{Sn&Q~XFT0+%nv`PTf5 znts8ty+2S3dCG%GGD?vN_n^TMdW^-a*m!>aP~R(;0-6v$Q(+LhxvNa$y;LSCq;S3u z?=@4t4f&=r=|mSVcfu6))Ylh@KNERAK~4L8C1R2g<~>O6Xd%d6V|4<63&EeJ zccO5@>vHi)L9)mXDUj62q8BQ@CjO4{Ylr+d)sGcu4%8IuhP6yxP{%qd9!B+wN_asj zcA%H)(4yWrWB$o-Wy8y-Wz%Rnt$d{n%cE63V&?J7T`2icEvUICb)TwPXQZW%fdn{< zi0{!AS$Em^I@lS@{`^Fj&@uN?d(w%VV{Nhk(jBOuT1srVw(*V3E^2 zFlY3cdrJO$uBjc`=+}HcLy)WHWPcH!+A=&8`%8p08MYfsoDQREn$sx$#24hE>6{3U zu_ftV52uVd74NG;64$FhZ~W=4s2_#e=;h(>6aRwx6$EmtSb$gL#;UN|@TP07Z@x{( z4INFM!6TRy1P0$7PDKDMcB}<}zmgy{@v-Zr+#^+OT=^wM ztm?n>9Lw+{y_TOpiUZZuT4Y99xZ`Z$mkvDk+2LaDR zG2>o=?0L$)Vt>rQ{h%R)}9@e|vp9vz2lkja9RhMhZwlVOmqR&W@X$;ZaV5+aqi8 z3%+20nVI#5KudmzyUG9Lc^@=y=pA_MFO4$GM7<5?>E+SvCz$pNb7$Cj%?$ol1mqtM zedSXzd*nC`*F^gGM9rCiGry$#9HU(?c~_j#xjX~Ne03_`&M^2)tHDceyZG*putahIqks$X);KQITm7P3 z8;F5`)p@*lp)o#O6g4Gl0^o#0!-5?^WaQK(Lc{(OTZ9ydG*o#?m*tt_u+OKe6+^$@ zh?q>wTvY1ubS$-h_$QU%>B;2=H(|%TP!vsN@LajLC z_2vb&c$NX*xb?vB_iB+45fME(Q-W8mmrJltD!;z{`(SVBiEs_C&=^O03*nMG9}>zQ zY5HMJO=L=Zat*?wLL9$cxdCyRSz#I%Y!4kr?qx01S=HF(uSS_Qphn{r#3)C`bRft< zO>XzuP@3auM1dQd!97)M>6&N$Nz}=ccq8)>ZKr~x$5}RphoIFk9h24Lw$m5+yGGYK zAYj-wJmGzs!hQd7A{PA%sk-tN0zzFlh>%frYskSRh}D6R&9@oZu(>@IEpo=SHW@L6qkE7q@^Wj8-#PGD|Hg^m zvLzidAo|a_9CTs2_MvjMe*Wm+HbW6^%>70`N4Lz8`Xj@dA>Cde?{T1xj@plkiw*d=;l{ZTi zuks-3y=noY(fWK6jqybP7HxlRp~N%1@gzlNL&E-qm^`X?B|R)g9kv@8cYm{|m0p*$ zK$=XTrH>Pd6z&PgB8g-iyn$=VZL*GB`~K%lP+iTp3gZ{q!GOQ}hg7lEo^yl1u? z@IRN%59W7Ejs~s!+g_ocz_AAK)MPKrdpaR8@t`4=}f>eWM3hP{kehlPVuD z%Q~j1?-!d#i*zQa+2=@E=xfg4I>}*Y`8A;+B5Q}#s&AvU)hrHo`RA73HGWn?UTu4G z5&$P%9t}^DCkv@&b9TrgA{kSDjL!≧5@&675=6(tA>%;3D=-VY|C!VJ%b1Vuw$8M*<|V9eK;q+o~_l8Sq@P(&&b zVdV$!87h?V7(Me9mX(q{b-p9_E8Ue(BQ_o!F&i(!AEBqL^!>C+fXneN-ATufUL$|^ z>gFWLd%nl0fbet)-}IGXeuwa)>;Gbz!IjmZq~Wv7Ep^P9Wv4v)1W76&MA{@i{uTck zwP`3*m#?mN`tV39B%AOrmZb0j!9GLcWUQH}2u7!5didIRz)bDPVsbEV%9ond_)~Q% z%g4_=_PI2h#mrxUn|z%=r>-$;cgGTWCw#yb1bHDk;w{Q2^^C8lUe&f(j5?=b)f#2r zr!*k>4;#U(a_63G=E<6?_efS)y&iXSHIZa)5fNg=5AoETF&;}#{~#seg=w_niDsck zf1QaG^kAaIPGbEkW`E^Ct2M`(RSBuCyvCgHgQ}xQzIM^?R>^>4jA6X`=UQOyve?TK zZ61d}Q%Qwvcu`8S8 z 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

      y+6YSl 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 Date: Fri, 12 Feb 2021 22:16:33 -0500 Subject: [PATCH 442/518] Added BaseAsset --- ro_py/utilities/baseasset.py | 7 +++++++ ro_py/utilities/clientobject.py | 7 ------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 ro_py/utilities/baseasset.py diff --git a/ro_py/utilities/baseasset.py b/ro_py/utilities/baseasset.py new file mode 100644 index 00000000..c5da3409 --- /dev/null +++ b/ro_py/utilities/baseasset.py @@ -0,0 +1,7 @@ +class BaseAsset: + def __init__(self): + self.id = None + self.cso = None + + async def to_asset(self): + return await self.cso.client.get_asset(self.id) diff --git a/ro_py/utilities/clientobject.py b/ro_py/utilities/clientobject.py index 603d7800..376faefa 100644 --- a/ro_py/utilities/clientobject.py +++ b/ro_py/utilities/clientobject.py @@ -7,13 +7,6 @@ class ClientObject: """ Every object that is grabbable with client.get_x inherits this object. """ - def __init__(self): - self.id = None - self.cso = None - self.requests = None - - async def to_asset(self): - return await self.cso.client.get_asset(self.id) async def update(self): pass From 4b13569fffdf1b950c07f070019a1b6388b2a65d Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 12 Feb 2021 22:19:05 -0500 Subject: [PATCH 443/518] BaseAsset mods --- ro_py/badges.py | 6 ++++-- ro_py/games.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ro_py/badges.py b/ro_py/badges.py index 4d520f27..073cc9ef 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -5,6 +5,7 @@ """ from ro_py.utilities.clientobject import ClientObject +from ro_py.utilities.baseasset import BaseAsset endpoint = "https://badges.roblox.com/" @@ -19,7 +20,7 @@ def __init__(self, past_date_awarded_count, awarded_count, win_rate_percentage): self.win_rate_percentage = win_rate_percentage -class Badge(ClientObject): +class Badge(ClientObject, BaseAsset): """ Represents a game-awarded badge. @@ -31,7 +32,8 @@ class Badge(ClientObject): ID of the badge. """ def __init__(self, cso, badge_id): - super().__init__() + ClientObject.__init__(self) + BaseAsset.__init__(self) self.id = badge_id self.cso = cso self.requests = cso.requests diff --git a/ro_py/games.py b/ro_py/games.py index 0bfef48e..73717402 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -5,6 +5,7 @@ """ from ro_py.utilities.clientobject import ClientObject +from ro_py.utilities.baseasset import BaseAsset from ro_py.groups import Group from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator @@ -118,8 +119,9 @@ async def get_badges(self): return badges -class Place: +class Place(ClientObject, BaseAsset): def __init__(self, requests, id): + super().__init__() self.requests = requests self.id = id pass From 9217794f8c5ffda40292fff9ecb2aefd9a5cdbdf Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 13 Feb 2021 10:23:35 -0500 Subject: [PATCH 444/518] roblox_id > user_id + comment fix --- ro_py/badges.py | 4 ++-- ro_py/groups.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ro_py/badges.py b/ro_py/badges.py index 073cc9ef..07771031 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -26,8 +26,8 @@ class Badge(ClientObject, BaseAsset): Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.utilities.clientobject.ClientSharedObject + ClientSharedObject. badge_id ID of the badge. """ diff --git a/ro_py/groups.py b/ro_py/groups.py index 3480fa2b..fb7a9357 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -217,10 +217,10 @@ async def get_roles(self): roles.append(Role(self.cso, self, role)) return roles - async def get_member_by_id(self, roblox_id): + async def get_member_by_id(self, user_id): # Get list of group user is in. member_req = await self.requests.get( - url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + url=endpoint + f"/v2/users/{user_id}/groups/roles" ) data = member_req.json() @@ -233,11 +233,11 @@ async def get_member_by_id(self, roblox_id): # Check if user is in group. if not group_data: - raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + raise NotFound(f"The user {user_id} was not found in group {self.id}") # Create data to return. role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, roblox_id, "", self, role) + member = Member(self.cso, user_id, "", self, role) return member async def get_member_by_username(self, name): From 09570c2740fb87e85b8a2ecc29c9f6e1d436348e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 13 Feb 2021 11:03:56 -0500 Subject: [PATCH 445/518] Fixed issue with request X-CSRF token handling --- ro_py/utilities/requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py index 1f265b12..d8843074 100644 --- a/ro_py/utilities/requests.py +++ b/ro_py/utilities/requests.py @@ -52,7 +52,7 @@ async def request(self, method, *args, **kwargs): if "X-CSRF-TOKEN" in this_request.headers: self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] if this_request.status_code == 403: # Request failed, send it again - this_request = await self.session.post(*args, **kwargs) + this_request = await self.session.request(method, *args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. From 5fb1454019af29f8ab151ef4f9456b5940b37b83 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 13 Feb 2021 11:41:21 -0500 Subject: [PATCH 446/518] Shout __call__ fixes --- ro_py/groups.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index fb7a9357..b0f58f69 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -26,13 +26,41 @@ class Shout: """ Represents a group shout. """ - def __init__(self, cso, shout_data): + def __init__(self, cso, group, shout_data): self.cso = cso + self.requests = cso.requests + self.group = group self.data = shout_data self.body = shout_data["body"] + self.created = iso8601.parse_date(shout_data["created"]) + self.updated = iso8601.parse_date(shout_data["created"]) + # TODO: Make this a PartialUser self.poster = None + def __str__(self): + return self.body + + async def __call__(self, message, replace=True): + """ + Updates the shout of the group. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + replace : bool + Whether to replace the shout with the new information returned from the server. + + """ + shout_req = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/status", + data={ + "message": message + } + ) + self.group.shout = Shout(self.cso, self.group, shout_req.json()) + class JoinRequest: def __init__(self, cso, data, group): @@ -174,7 +202,7 @@ async def update(self): self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] if group_info.get('shout'): - self.shout = Shout(self.cso, group_info['shout']) + self.shout = Shout(self.cso, self, group_info['shout']) else: self.shout = None if "isLocked" in group_info: @@ -183,6 +211,7 @@ async def update(self): async def update_shout(self, message): """ Updates the shout of the group. + DEPRECATED: Just call group.shout() Parameters ---------- @@ -193,13 +222,7 @@ async def update_shout(self, message): ------- int """ - shout_req = await self.requests.patch( - url=endpoint + f"/v1/groups/{self.id}/status", - data={ - "message": message - } - ) - return shout_req.status_code == 200 + return await self.shout(message) async def get_roles(self): """ From 62473f3b430e7458d75ee4981a0a663799d528e8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 13 Feb 2021 11:52:00 -0500 Subject: [PATCH 447/518] Small shout call modifications --- ro_py/groups.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index b0f58f69..6319ba4e 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -41,16 +41,20 @@ def __init__(self, cso, group, shout_data): def __str__(self): return self.body - async def __call__(self, message, replace=True): + async def __call__(self, message): """ Updates the shout of the group. + Please note that doing so will completely delete this Shout object and return a new Shout object. + The parent group's shout parameter will also be updated accordingly. Parameters ---------- message : str Message that will overwrite the current shout of a group. - replace : bool - Whether to replace the shout with the new information returned from the server. + + Returns + ------- + ro_py.groups.Shout """ shout_req = await self.requests.patch( @@ -60,6 +64,7 @@ async def __call__(self, message, replace=True): } ) self.group.shout = Shout(self.cso, self.group, shout_req.json()) + return self.group.shout class JoinRequest: From 125b5afbd8108d6f061f6b6cbb1c6b16079407e0 Mon Sep 17 00:00:00 2001 From: ira Date: Sun, 14 Feb 2021 01:19:45 +0100 Subject: [PATCH 448/518] Add filter, get_friend_requests + more. --- ro_py/client.py | 43 ++++++++++++++++++++++++++++++++++++------- ro_py/users.py | 17 +++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 8c22f13b..831be6bf 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -9,19 +9,27 @@ from ro_py.assets import Asset from ro_py.badges import Badge from ro_py.chat import ChatWrapper -from ro_py.users import PartialUser from ro_py.events import EventTypes from ro_py.trades import TradesWrapper from ro_py.captcha import CaptchaMetadata from ro_py.utilities.cache import CacheType from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings +from ro_py.utilities.pages import Pages, SortOrder +from ro_py.users import PartialUser, FriendRequest from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError +def friend_handler(cso, data, args): + friends = [] + for friend in data: + friends.append(FriendRequest(cso, friend)) + return friends + + class Client: """ Represents an authenticated Roblox client. @@ -53,6 +61,22 @@ def __init__(self, token: str = None): if token: self.token_login(token) + async def filter_text(self, text): + """ + Filters text. + + Parameters + ---------- + text : str + Text that will be filtered. + """ + filter_req = await self.requests.post( + url="https://develop.roblox.com/v1/gameUpdateNotifications/filter", + data=f'"{text}"' + ) + data = filter_req.json() + return data['filteredGameUpdateText'] + # Grab objects async def get_self(self): self_req = await self.requests.get( @@ -200,14 +224,19 @@ async def get_badge(self, badge_id): await badge.update() return badge - async def get_friend_requests(self): + async def get_friend_requests(self, sort_order=SortOrder.Ascending, limit=100): """ - Gets the amount of friend requests the client has. + Gets friend requests the client has. """ - friend_req = await self.requests.get( - url="https://friends.roblox.com/v1/user/friend-requests/count" + friends = Pages( + cso=self.cso, + url="https://friends.roblox.com/v1/my/friends/requests", + handler=friend_handler, + sort_order=sort_order, + limit=limit ) - return friend_req.json()["count"] + await friends.get_page() + return friends async def get_captcha_metadata(self): """ @@ -303,7 +332,7 @@ async def secure_sign_out(self): Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because they would generate a new session token, and suggested that the user would "refresh their cookie" fairly - frequently as to avoid this. This isn't something you'll actually need to do, so this is left here as an + frequently as to avoid this. This isn't something you'll actually need to do, therefore this is left here as an optional feature. """ await self.requests.post( diff --git a/ro_py/users.py b/ro_py/users.py index 47dde5ec..0552486d 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -146,6 +146,23 @@ def __init__(self, cso, data): self.display_name = data["displayName"] +class FriendRequest(Friend): + def __init__(self, cso, data): + super(FriendRequest, self).__init__(cso, data) + + async def accept(self): + accept_req = await self.cso.post( + url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request" + ) + return accept_req.status == 200 + + async def decline(self): + accept_req = await self.cso.post( + url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request" + ) + return accept_req.status == 200 + + class User(PartialUser, ClientObject): """ Represents a Roblox user and their profile. From ad53e9563eee4e0a4c57d5a55fd6c809c316af86 Mon Sep 17 00:00:00 2001 From: ira Date: Sun, 14 Feb 2021 12:54:00 +0100 Subject: [PATCH 449/518] Clean up trades + fixes. --- ro_py/client.py | 2 +- ro_py/trades.py | 51 +++++++++++++++++++++++-------------------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 831be6bf..41a65e98 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -264,7 +264,7 @@ def token_login(self, token): self.accountinformation = AccountInformation(self.cso) self.accountsettings = AccountSettings(self.cso) self.chat = ChatWrapper(self.cso) - self.trade = TradesWrapper(self.cso, self.get_self) + self.trade = TradesWrapper(self.cso) self.notifications = NotificationReceiver(self.cso) async def user_login(self, username, password, token=None): diff --git a/ro_py/trades.py b/ro_py/trades.py index 3fbb77ab..ff343c0b 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -3,11 +3,11 @@ This file houses functions and classes that pertain to Roblox trades and trading. """ -from typing import Callable +from typing import Callable, List from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset -from ro_py.users import PartialUser +from ro_py.users import PartialUser, User from ro_py.events import EventTypes import datetime import iso8601 @@ -20,20 +20,21 @@ def trade_page_handler(requests, this_page, args) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) + trades_out.append(PartialTrade(requests, raw_trade)) return trades_out class Trade: - def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool): - self.trade_id = trade_id - self.requests = requests + def __init__(self, cso, data, sender: User, send_items: List[Asset], receive_items: List[Asset]): + self.cso = cso + self.requests = cso.requests + self.trade_id = data['id'] self.sender = sender - self.receive_items = receive_items + self.created = iso8601.parse_date(data['created']) + self.expiration = iso8601.parse_date(data['expiration']) + self.status = data['status'] self.send_items = send_items - self.created = iso8601.parse_date(created) - self.expiration = iso8601.parse_date(expiration) - self.status = status + self.receive_items = receive_items async def accept(self) -> bool: """ @@ -57,14 +58,14 @@ async def decline(self) -> bool: class PartialTrade: - def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool): + def __init__(self, cso, data): self.cso = cso self.requests = cso.requests - self.trade_id = trade_id - self.user = user - self.created = iso8601.parse_date(created) - self.expiration = iso8601.parse_date(expiration) - self.status = status + self.trade_id = data['id'] + self.user = PartialUser(cso, data['user']['id'], data['user']['name']) + self.created = iso8601.parse_date(data['created']) + self.expiration = iso8601.parse_date(data['expiration']) + self.status = data['status'] async def accept(self) -> bool: """ @@ -100,27 +101,24 @@ async def expand(self) -> Trade: sender = await self.cso.client.get_user(data['user']['id']) await sender.update() - # load items that will be/have been sent and items that you will/have recieve(d) + # load items that will be/have been sent and items that you will/have receive(d) receive_items, send_items = [], [] for items_0 in data['offers'][0]['userAssets']: - item_0 = Asset(self.requests, items_0['assetId']) + item_0 = Asset(self.cso, items_0['assetId']) await item_0.update() receive_items.append(item_0) for items_1 in data['offers'][1]['userAssets']: - item_1 = Asset(self.requests, items_1['assetId']) + item_1 = Asset(self.cso, items_1['assetId']) await item_1.update() send_items.append(item_1) return Trade( self.cso, - self.trade_id, + data, sender, - receive_items, send_items, - data['created'], - data['expiration'], - data['status'] + receive_items ) @@ -201,10 +199,9 @@ class TradesWrapper: """ Represents the Roblox trades page. """ - def __init__(self, cso, get_self): + def __init__(self, cso): self.cso = cso self.requests = cso.requests - self.get_self = get_self self.events = Events(cso) self.TradeRequest = TradeRequest @@ -234,7 +231,7 @@ async def send_trade(self, roblox_id, trade): ------- int """ - me = await self.get_self() + me = await self.cso.client.get_self() data = { "offers": [ From a18157d8261319247a6f1cbb178c79569f34aabe Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 14 Feb 2021 11:43:33 -0500 Subject: [PATCH 450/518] get_trades uses enum now --- ro_py/trades.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 3fbb77ab..49e5cfc8 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -208,10 +208,10 @@ def __init__(self, cso, get_self): self.events = Events(cso) self.TradeRequest = TradeRequest - async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages: + async def get_trades(self, trade_status_type=TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = Pages( cso=self.cso, - url=endpoint + f"/v1/trades/{trade_status_type}", + url=endpoint + f"/v1/trades/{trade_status_type.value}", sort_order=sort_order, limit=limit, handler=trade_page_handler From f7dfa70fc6bf60103a2d383ede4e1c6921a3c54b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 14 Feb 2021 11:45:45 -0500 Subject: [PATCH 451/518] Update trades.py --- ro_py/trades.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 417a68eb..8321b516 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -64,7 +64,9 @@ def __init__(self, cso, data): self.trade_id = data['id'] self.user = PartialUser(cso, data['user']['id'], data['user']['name']) self.created = iso8601.parse_date(data['created']) - self.expiration = iso8601.parse_date(data['expiration']) + self.expiration = None + if "expiration" in data: + self.expiration = iso8601.parse_date(data['expiration']) self.status = data['status'] async def accept(self) -> bool: From 39b7db6cdc6a70e31c513709e0838d31b3eb93ab Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 14 Feb 2021 12:04:18 -0500 Subject: [PATCH 452/518] Update trades.py --- ro_py/trades.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index 8321b516..fdbbef1b 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -31,7 +31,6 @@ def __init__(self, cso, data, sender: User, send_items: List[Asset], receive_ite self.trade_id = data['id'] self.sender = sender self.created = iso8601.parse_date(data['created']) - self.expiration = iso8601.parse_date(data['expiration']) self.status = data['status'] self.send_items = send_items self.receive_items = receive_items From 6ae83a39e8cc466c7e2b6a23a21747592bd134c3 Mon Sep 17 00:00:00 2001 From: ira Date: Sun, 14 Feb 2021 20:11:59 +0100 Subject: [PATCH 453/518] fix get_limiteds --- ro_py/users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ro_py/users.py b/ro_py/users.py index 0552486d..2f03467c 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -120,11 +120,13 @@ async def get_limiteds(self): ------- list """ - return Pages( + limiteds = Pages( cso=self.cso, url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler ) + await limiteds.get_page() + return limiteds async def get_status(self): """ From 18e0199c63de9b469847b4c7f4933bbfeea0b244 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 14 Feb 2021 12:38:21 -0800 Subject: [PATCH 454/518] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba221da2..ad8da47b 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,11 @@ The docs are generated from docstrings in the code using pdoc3. ## Installation You can install ro.py from pip: ``` -pip install ro-py +pip3 install ro-py ``` If you want the latest bleeding-edge version, clone from git (you'll need [git-scm](https://git-scm.com/downloads) installed): ``` -pip install git+git://github.com/rbx-libdev/ro.py.git +pip3 install git+git://github.com/rbx-libdev/ro.py.git ``` Known issue: wxPython sometimes has trouble building on certain devices. I put wxPython last on the requirements so Python attempts to install it last, so you can safely ignore this error as everything else should be installed. From bb7c1c72e23aab1e2f0b73bcd9804edaad0fd17b Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 15 Feb 2021 11:06:32 -0500 Subject: [PATCH 455/518] Place now uses ClientSharedObject and Users now has profile_url You can access the link to a user's profile with PartialUser.profile_url. --- ro_py/games.py | 7 ++++--- ro_py/users.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index 73717402..073c1dfb 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -63,7 +63,7 @@ async def update(self): game_info = game_info["data"][0] self.name = game_info["name"] self.description = game_info["description"] - self.root_place = Place(self.requests, game_info["rootPlaceId"]) + self.root_place = Place(self.cso, game_info["rootPlaceId"]) if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: @@ -120,9 +120,10 @@ async def get_badges(self): class Place(ClientObject, BaseAsset): - def __init__(self, requests, id): + def __init__(self, cso, id): super().__init__() - self.requests = requests + self.cso = cso + self.requests = cso.requests self.id = id pass diff --git a/ro_py/users.py b/ro_py/users.py index 0552486d..90914346 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -31,6 +31,7 @@ def __init__(self, cso, roblox_id, roblox_name=None): self.requests = cso.requests self.id = roblox_id self.name = roblox_name + self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" async def expand(self): """ From ed5e909893607ae4f7eecd575706d2c88a92eb95 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 15 Feb 2021 11:41:11 -0500 Subject: [PATCH 456/518] Proper place information added --- ro_py/games.py | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/ro_py/games.py b/ro_py/games.py index 073c1dfb..4165b005 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -5,12 +5,13 @@ """ from ro_py.utilities.clientobject import ClientObject -from ro_py.utilities.baseasset import BaseAsset -from ro_py.groups import Group -from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator from ro_py.utilities.errors import GameJoinError +from ro_py.utilities.baseasset import BaseAsset from ro_py.utilities.cache import CacheType +from ro_py.users import PartialUser +from ro_py.groups import Group +from ro_py.badges import Badge import subprocess import json import os @@ -38,8 +39,8 @@ def __init__(self, cso, universe_id): self.cso = cso self.requests = cso.requests self.name = None + self.root_place_id = None self.description = None - self.root_place = None self.creator = None self.price = None self.allowed_gear_genres = None @@ -62,8 +63,8 @@ async def update(self): game_info = game_info_req.json() game_info = game_info["data"][0] self.name = game_info["name"] + self.root_place_id = game_info["rootPlaceId"] self.description = game_info["description"] - self.root_place = Place(self.cso, game_info["rootPlaceId"]) if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: @@ -83,6 +84,11 @@ async def update(self): self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] + async def get_root_place(self): + root_place = Place(self.cso, self.root_place_id, self) + await root_place.update() + return root_place + async def get_votes(self): """ Returns @@ -120,12 +126,35 @@ async def get_badges(self): class Place(ClientObject, BaseAsset): - def __init__(self, cso, id): + def __init__(self, cso, place_id, universe): super().__init__() self.cso = cso self.requests = cso.requests - self.id = id - pass + self.id = place_id + self.universe = universe + self.name = None + self.description = None + self.url = None + self.creator = None + self.is_playable = None + self.reason_prohibited = None + self.price = None + + async def update(self): + place_req = await self.requests.get( + url="https://games.roblox.com/v1/games/multiget-place-details", + params={ + "placeIds": self.id + } + ) + place_data = place_req.json()[0] + self.name = place_data["name"] + self.description = place_data["description"] + self.url = place_data["url"] + self.creator = PartialUser(self.cso, place_data["builderId"], place_data["builder"]) + self.is_playable = place_data["isPlayable"] + self.reason_prohibited = place_data["reasonProhibited"] + self.price = place_data["price"] async def join(self, launchtime=1609186776825, rloc="en_us", gloc="en_us", negotiate_url="https://www.roblox.com/Login/Negotiate.ashx"): @@ -191,4 +220,3 @@ async def join(self, launchtime=1609186776825, rloc="en_us", gloc="en_us", stderr=subprocess.PIPE ) return join_process.stdout, join_process.stderr - From 5216ab4439b86c0914893984cfce1daf43df0e1c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 15 Feb 2021 14:17:49 -0500 Subject: [PATCH 457/518] Update games.py --- ro_py/games.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ro_py/games.py b/ro_py/games.py index 4165b005..48f11d0e 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -45,6 +45,8 @@ def __init__(self, cso, universe_id): self.price = None self.allowed_gear_genres = None self.allowed_gear_categories = None + self.playing = None + self.visits = None self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None @@ -80,6 +82,8 @@ async def update(self): self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] + self.playing = game_info["playing"] + self.visits = game_info["visits"] self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] From 27dd112bae3d0d8f97ba819d625930d2fd961fe2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 15 Feb 2021 12:47:11 -0800 Subject: [PATCH 458/518] Update README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index ad8da47b..e9d83b33 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,31 @@ ro.py allows you to automate much of what you would do on the Roblox website and I’ve set up a small ro.py Discord server. It’s obviously very tiny, but some of you can be the first people to help found the server. If you need support for the library, you can ask your questions here if you need faster support. http://j-mk.ml/ro.py ## Get Started +To begin, first import the client, which is the most essential part of ro.py, and initialize it like so: +```py +from ro_py import Client +client = Client() +``` +Next, import `asyncio` at the top of your file: +```py +import asyncio +``` +Next, create an async method `main()` and run it with asyncio: +```py +async def main(): + # keep this empty for the next step! + +asyncio.get_event_loop().run_until_complete(main()) +``` +Next, read the [documentation for the Client](https://ro.py.jmksite.dev/client.html) to grab objects and interact with the API. + If you are looking for a full tutorial on ro.py, check out [the new DevForum article!](https://devforum.roblox.com/t/use-python-to-interact-with-the-roblox-api-with-ro-py/1006465) ## Requirements - httpx (for sending requests) - iso8601 (for parsing dates) - signalrcore (for recieving notifications) +- lxml (for parsing documentation, might be made optional soon) #### Previous Requirements - cachecontrol (for caching requests) From 2a41cbcbec1dd2e41d425aef2241755f997d6ea7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 13:05:05 -0500 Subject: [PATCH 459/518] Added project URLs + added URL utility --- ro_py/utilities/url.py | 8 ++++++++ setup_info.py | 6 ++++++ 2 files changed, 14 insertions(+) create mode 100644 ro_py/utilities/url.py diff --git a/ro_py/utilities/url.py b/ro_py/utilities/url.py new file mode 100644 index 00000000..a97bf80b --- /dev/null +++ b/ro_py/utilities/url.py @@ -0,0 +1,8 @@ +root_site = "sitetest1.roblox.com" + + +def url(path="www"): + if path: + return f"https://{path}.{root_site}/" + else: + return f"https://{root_site}" diff --git a/setup_info.py b/setup_info.py index 4c2fbc46..01feaacf 100644 --- a/setup_info.py +++ b/setup_info.py @@ -18,6 +18,12 @@ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ], + "project_urls": { + "Discord": "https://discord.gg/RJdW3gt9Ru", + "Issue Tracker": "https://github.com/rbx-libdev/ro.py/issues", + "GitHub": "https://github.com/rbx-libdev/ro.py/", + "Examples": "https://github.com/rbx-libdev/ro.py/tree/main/examples" + }, "python_requires": '>=3.6', "install_requires": [ "httpx", From 256614cab43724a5c95bf0aa34219d572792001f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 13:11:43 -0500 Subject: [PATCH 460/518] sitetest1 > roblox --- ro_py/utilities/url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/utilities/url.py b/ro_py/utilities/url.py index a97bf80b..c2116bed 100644 --- a/ro_py/utilities/url.py +++ b/ro_py/utilities/url.py @@ -1,4 +1,4 @@ -root_site = "sitetest1.roblox.com" +root_site = "roblox.com" def url(path="www"): From d99dd1e523e87ef8e95c889d1b922891f42b1cb0 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 13:14:34 -0500 Subject: [PATCH 461/518] Moved to the new URL utility --- ro_py/accountinformation.py | 3 ++- ro_py/accountsettings.py | 3 ++- ro_py/assets.py | 3 ++- ro_py/badges.py | 3 ++- ro_py/chat.py | 3 ++- ro_py/economy.py | 3 ++- ro_py/gamepersistence.py | 3 ++- ro_py/games.py | 3 ++- ro_py/groups.py | 3 ++- ro_py/roles.py | 5 ++--- ro_py/thumbnails.py | 3 ++- ro_py/trades.py | 4 ++-- ro_py/users.py | 11 ++++++----- ro_py/wall.py | 4 ++-- 14 files changed, 32 insertions(+), 22 deletions(-) diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py index 463d3cbd..e444ebe6 100644 --- a/ro_py/accountinformation.py +++ b/ro_py/accountinformation.py @@ -7,7 +7,8 @@ from datetime import datetime from ro_py.gender import RobloxGender -endpoint = "https://accountinformation.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("accountinformation") class AccountInformationMetadata: diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py index 4a7c238a..f026299c 100644 --- a/ro_py/accountsettings.py +++ b/ro_py/accountsettings.py @@ -6,7 +6,8 @@ import enum -endpoint = "https://accountsettings.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("accountsettings") class PrivacyLevel(enum.Enum): diff --git a/ro_py/assets.py b/ro_py/assets.py index 97ec1727..da9b2122 100644 --- a/ro_py/assets.py +++ b/ro_py/assets.py @@ -12,7 +12,8 @@ import asyncio import copy -endpoint = "https://api.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("api") class Reseller: diff --git a/ro_py/badges.py b/ro_py/badges.py index 07771031..0c78fd38 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -7,7 +7,8 @@ from ro_py.utilities.clientobject import ClientObject from ro_py.utilities.baseasset import BaseAsset -endpoint = "https://badges.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("badges") class BadgeStatistics: diff --git a/ro_py/chat.py b/ro_py/chat.py index 6e41e8c9..dda7bd3f 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -7,7 +7,8 @@ from ro_py.utilities.errors import ChatError from ro_py.users import PartialUser -endpoint = "https://chat.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("chat") class ChatSettings: diff --git a/ro_py/economy.py b/ro_py/economy.py index 35506e03..7e543a15 100644 --- a/ro_py/economy.py +++ b/ro_py/economy.py @@ -4,7 +4,8 @@ """ -endpoint = "https://economy.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("economy") class Currency: diff --git a/ro_py/gamepersistence.py b/ro_py/gamepersistence.py index edec6098..68f2d89b 100644 --- a/ro_py/gamepersistence.py +++ b/ro_py/gamepersistence.py @@ -8,7 +8,8 @@ from math import floor import re -endpoint = "http://gamepersistence.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("gamepersistence") class DataStore: diff --git a/ro_py/games.py b/ro_py/games.py index 48f11d0e..5e010151 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -16,7 +16,8 @@ import json import os -endpoint = "https://games.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("games") class Votes: diff --git a/ro_py/groups.py b/ro_py/groups.py index 6319ba4e..83ec7894 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -19,7 +19,8 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.utilities.clientobject import ClientObject -endpoint = "https://groups.roblox.com" +from ro_py.utilities.url import url +endpoint = url("groups") class Shout: diff --git a/ro_py/roles.py b/ro_py/roles.py index c46017ed..e2b636d1 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -5,9 +5,8 @@ """ -import enum - -endpoint = "https://groups.roblox.com" +from ro_py.utilities.url import url +endpoint = url("groups") class RolePermissions: diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py index dba25ae9..949ec758 100644 --- a/ro_py/thumbnails.py +++ b/ro_py/thumbnails.py @@ -7,7 +7,8 @@ from ro_py.utilities.errors import InvalidShotTypeError import enum -endpoint = "https://thumbnails.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("thumbnails") class ReturnPolicy(enum.Enum): diff --git a/ro_py/trades.py b/ro_py/trades.py index fdbbef1b..e65f3f3d 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -9,12 +9,12 @@ from ro_py.assets import Asset, UserAsset from ro_py.users import PartialUser, User from ro_py.events import EventTypes -import datetime import iso8601 import asyncio import enum -endpoint = "https://trades.roblox.com" +from ro_py.utilities.url import url +endpoint = url("trades") def trade_page_handler(requests, this_page, args) -> list: diff --git a/ro_py/users.py b/ro_py/users.py index 90914346..b38ab9d8 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -5,17 +5,18 @@ """ import copy +import iso8601 +import asyncio from typing import List, Callable +from ro_py.assets import UserAsset from ro_py.events import EventTypes +from ro_py.utilities.pages import Pages from ro_py.robloxbadges import RobloxBadge from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.clientobject import ClientObject -from ro_py.utilities.pages import Pages -from ro_py.assets import UserAsset -import iso8601 -import asyncio -endpoint = "https://users.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("users") def limited_handler(requests, data, args): diff --git a/ro_py/wall.py b/ro_py/wall.py index 2390ddf3..0060cc57 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -4,8 +4,8 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.users import PartialUser - -endpoint = "https://groups.roblox.com" +from ro_py.utilities.url import url +endpoint = url("groups") class WallPost: From c3719fb36829ce23f3010479c9d2bb828bfe85b5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 20:24:09 -0500 Subject: [PATCH 462/518] What the HELL HAPPENED HERE --- ro_py/client.py | 13 +++++-------- ro_py/users.py | 7 ++++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 41a65e98..a73decc8 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -16,9 +16,9 @@ from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings from ro_py.utilities.pages import Pages, SortOrder -from ro_py.users import PartialUser, FriendRequest from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation +from ro_py.users import PartialUser, FriendRequest, User from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError @@ -83,7 +83,7 @@ async def get_self(self): url="https://roblox.com/my/profile" ) data = self_req.json() - return PartialUser(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data) async def get_user(self, user_id, expand=True): """ @@ -94,15 +94,12 @@ async def get_user(self, user_id, expand=True): user_id ID of the user to generate the object from. expand : bool - Whether to automatically expand the data returned by the endpoint into Users.s + Whether to automatically expand the data returned by the endpoint into Users. """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = PartialUser(self.cso, user_id) - if expand: - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + user = User(self.cso, user_id) + self.cso.cache.set(CacheType.Users, user_id, user) return user async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): diff --git a/ro_py/users.py b/ro_py/users.py index b38ab9d8..2d54cd86 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -27,11 +27,12 @@ def limited_handler(requests, data, args): class PartialUser: - def __init__(self, cso, roblox_id, roblox_name=None): + def __init__(self, cso, data): self.cso = cso self.requests = cso.requests - self.id = roblox_id - self.name = roblox_name + self.id = data.get("id") or data.get("Id") or data.get("user_id") or data.get("UserId") or data.get("TargetId") + self.name = data.get("name") or data.get("Name") or data.get("username") or data.get("Username") + self.display_name = data.get("displayName") or data.get("DisplayName") self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" async def expand(self): From 062327dd58388c82a60037597083aaca68591c7e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 20:24:51 -0500 Subject: [PATCH 463/518] Revert "What the HELL HAPPENED HERE" This reverts commit c3719fb36829ce23f3010479c9d2bb828bfe85b5. --- ro_py/client.py | 13 ++++++++----- ro_py/users.py | 7 +++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index a73decc8..41a65e98 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -16,9 +16,9 @@ from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings from ro_py.utilities.pages import Pages, SortOrder +from ro_py.users import PartialUser, FriendRequest from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation -from ro_py.users import PartialUser, FriendRequest, User from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError @@ -83,7 +83,7 @@ async def get_self(self): url="https://roblox.com/my/profile" ) data = self_req.json() - return PartialUser(self.cso, data) + return PartialUser(self.cso, data['UserId'], data['Username']) async def get_user(self, user_id, expand=True): """ @@ -94,12 +94,15 @@ async def get_user(self, user_id, expand=True): user_id ID of the user to generate the object from. expand : bool - Whether to automatically expand the data returned by the endpoint into Users. + Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = User(self.cso, user_id) - self.cso.cache.set(CacheType.Users, user_id, user) + user = PartialUser(self.cso, user_id) + if expand: + expanded = await user.expand() + self.cso.cache.set(CacheType.Users, user_id, expanded) + return expanded return user async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): diff --git a/ro_py/users.py b/ro_py/users.py index 2d54cd86..b38ab9d8 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -27,12 +27,11 @@ def limited_handler(requests, data, args): class PartialUser: - def __init__(self, cso, data): + def __init__(self, cso, roblox_id, roblox_name=None): self.cso = cso self.requests = cso.requests - self.id = data.get("id") or data.get("Id") or data.get("user_id") or data.get("UserId") or data.get("TargetId") - self.name = data.get("name") or data.get("Name") or data.get("username") or data.get("Username") - self.display_name = data.get("displayName") or data.get("DisplayName") + self.id = roblox_id + self.name = roblox_name self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" async def expand(self): From a2ca5b39860afe69856c1130f3812101199dec6f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 20:39:12 -0500 Subject: [PATCH 464/518] =?UTF-8?q?=F0=9F=98=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/users.py | 59 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index b38ab9d8..e8289c7f 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -26,18 +26,20 @@ def limited_handler(requests, data, args): return assets -class PartialUser: - def __init__(self, cso, roblox_id, roblox_name=None): +class UserBase: + def __init__(self, cso, user_id): self.cso = cso self.requests = cso.requests - self.id = roblox_id - self.name = roblox_name + self.id = user_id self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" async def expand(self): """ - Updates some class values. - :return: Nothing + Expands into a full User object. + + Returns + ------ + ro_py.users.User """ user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() @@ -53,7 +55,10 @@ async def expand(self): async def get_roblox_badges(self) -> List[RobloxBadge]: """ Gets the user's roblox badges. - :return: A list of RobloxBadge instances + + Returns + ------- + List[ro_py.robloxbadges.RobloxBadge] """ roblox_badges_req = await self.requests.get( f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") @@ -65,7 +70,10 @@ async def get_roblox_badges(self) -> List[RobloxBadge]: async def get_friends_count(self) -> int: """ Gets the user's friends count. - :return: An integer + + Returns + ------- + int """ friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") friends_count = friends_count_req.json()["count"] @@ -74,7 +82,10 @@ async def get_friends_count(self) -> int: async def get_followers_count(self) -> int: """ Gets the user's followers count. - :return: An integer + + Returns + ------- + int """ followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") followers_count = followers_count_req.json()["count"] @@ -83,7 +94,10 @@ async def get_followers_count(self) -> int: async def get_followings_count(self) -> int: """ Gets the user's followings count. - :return: An integer + + Returns + ------- + int """ followings_count_req = await self.requests.get( f"https://friends.roblox.com/v1/users/{self.id}/followings/count") @@ -93,7 +107,10 @@ async def get_followings_count(self) -> int: async def get_friends(self): """ Gets the user's friends. - :return: List of Friend + + Returns + ------- + List[ro_py.users.Friend] """ friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") friends_raw = friends_req.json()["data"] @@ -103,6 +120,13 @@ async def get_friends(self): return friends_list async def get_groups(self): + """ + Gets the user's groups. + + Returns + ------- + List[ro_py.groups.PartialGroup] + """ from ro_py.groups import PartialGroup member_req = await self.requests.get( url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" @@ -120,7 +144,7 @@ async def get_limiteds(self): Returns ------- - list + bababooey """ return Pages( cso=self.cso, @@ -131,12 +155,21 @@ async def get_limiteds(self): async def get_status(self): """ Gets the user's status. - :return: A string + + Returns + ------- + str """ status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] +class PartialUser(UserBase): + def __init__(self, cso, user_id, username=None): + super().__init__(cso, user_id) + self.name = username + + class Friend(PartialUser): def __init__(self, cso, data): super().__init__(cso, data["id"], data["name"]) From 287fe68a1b69b37927fbfed4c11d448eb5545988 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 20:58:29 -0500 Subject: [PATCH 465/518] =?UTF-8?q?=E2=9C=8D=F0=9F=91=8D=F0=9F=A4=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/client.py | 20 +++++++----------- ro_py/users.py | 56 +++++++++++++++++++------------------------------ 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 41a65e98..0e253c1f 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -16,9 +16,9 @@ from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings from ro_py.utilities.pages import Pages, SortOrder -from ro_py.users import PartialUser, FriendRequest from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation +from ro_py.users import User, PartialUser, FriendRequest from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError @@ -83,9 +83,9 @@ async def get_self(self): url="https://roblox.com/my/profile" ) data = self_req.json() - return PartialUser(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data) - async def get_user(self, user_id, expand=True): + async def get_user(self, user_id): """ Gets a Roblox user. @@ -93,19 +93,15 @@ async def get_user(self, user_id, expand=True): ---------- user_id ID of the user to generate the object from. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = PartialUser(self.cso, user_id) - if expand: - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + user = User(self.cso, user_id) + self.cso.cache.set(CacheType.Users, user_id, user) + await user.update() return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): """ Gets a Roblox user by their username.. @@ -130,7 +126,7 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id, expand=expand) + return await self.get_user(user_id) else: raise UserDoesNotExistError diff --git a/ro_py/users.py b/ro_py/users.py index e8289c7f..72a8dd27 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -26,7 +26,7 @@ def limited_handler(requests, data, args): return assets -class UserBase: +class BaseUser: def __init__(self, cso, user_id): self.cso = cso self.requests = cso.requests @@ -41,16 +41,7 @@ async def expand(self): ------ ro_py.users.User """ - user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") - user_info = user_info_req.json() - description = user_info["description"] - created = iso8601.parse_date(user_info["created"]) - is_banned = user_info["isBanned"] - name = user_info["name"] - display_name = user_info["displayName"] - # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") - # self.has_premium = has_premium_req - return User(self.cso, self.id, name, description, created, is_banned, display_name) + return await self.cso.client.get_user(self.id) async def get_roblox_badges(self) -> List[RobloxBadge]: """ @@ -164,15 +155,17 @@ async def get_status(self): return status_req.json()["status"] -class PartialUser(UserBase): - def __init__(self, cso, user_id, username=None): - super().__init__(cso, user_id) - self.name = username +class PartialUser(BaseUser): + def __init__(self, cso, data): + self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId") + super().__init__(cso, self.id) + self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username") + self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name") class Friend(PartialUser): def __init__(self, cso, data): - super().__init__(cso, data["id"], data["name"]) + super().__init__(cso, data) self.is_online = data["isOnline"] self.is_deleted = data["isDeleted"] self.description = data["description"] @@ -198,35 +191,31 @@ async def decline(self): return accept_req.status == 200 -class User(PartialUser, ClientObject): +class User(BaseUser, ClientObject): """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. + I'm in so much pain + Parameters ---------- cso : ro_py.client.ClientSharedObject ClientSharedObject. - roblox_id : int + user_id : int The id of a user. - roblox_name : str - The name of the user. - description : str - The description of the user. - created : any - Time the user was created. """ - def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): - super().__init__(cso, roblox_id, roblox_name) + def __init__(self, cso, user_id): + super().__init__(cso, user_id) self.cso = cso - self.id = roblox_id - self.name = roblox_name - self.description = description - self.created = created - self.is_banned = banned - self.display_name = display_name - self.thumbnails = UserThumbnailGenerator(cso, roblox_id) + self.id = user_id + self.name = None + self.description = None + self.created = None + self.is_banned = None + self.display_name = None + self.thumbnails = UserThumbnailGenerator(cso, user_id) async def update(self): """ @@ -240,7 +229,6 @@ async def update(self): self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - return self # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req From 2f9a58e0ec853746518c5149eb7c582607924438 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 21:03:27 -0500 Subject: [PATCH 466/518] I'm going to bed. --- ro_py/groups.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 83ec7894..67df636d 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -135,7 +135,7 @@ class Actions(Enum): class Action: def __init__(self, cso, data, group): self.group = group - self.actor = Member(cso, data['actor']['user']['userId'], data['actor']['user']['username'], group, Role(cso, group, data['actor']['role'])) + self.actor = Member(cso, data['actor']) self.action = data['actionType'] self.created = iso8601.parse_date(data['created']) self.data = data['description'] @@ -365,17 +365,9 @@ class Member(PartialUser): ---------- cso : ro_py.utilities.requests.Requests Requests object to use for API requests. - roblox_id : int - The id of a user. - name : str - The name of the user. - group : ro_py.groups.Group - The group the user is in. - role : ro_py.roles.Role - The role the user has is the group. """ - def __init__(self, cso, roblox_id, name, group, role): - super().__init__(cso, roblox_id, name) + def __init__(self, cso, data): + super().__init__(cso, data) self.role = role self.group = group From b5a646cd0ee2a243f14d3a378899b205237449c0 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 21:55:21 -0500 Subject: [PATCH 467/518] =?UTF-8?q?=F0=9F=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ro_py/groups.py | 399 +++++++++++++++++++++---------------------- tests/sanitycheck.py | 2 +- 2 files changed, 200 insertions(+), 201 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 67df636d..93d18f06 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -132,20 +132,19 @@ class Actions(Enum): buy_clan = "buyClan" -class Action: - def __init__(self, cso, data, group): - self.group = group - self.actor = Member(cso, data['actor']) - self.action = data['actionType'] - self.created = iso8601.parse_date(data['created']) - self.data = data['description'] +# class Action: +# def __init__(self, cso, data, group): +# self.group = group +# self.actor = Member(cso, data['actor']) +# self.created = iso8601.parse_date(data['created']) +# self.data = data['description'] -def action_handler(cso, data, args): - actions = [] - for action in data: - actions.append(Action(cso, action, args)) - return actions +# def action_handler(cso, data, args): +# actions = [] +# for action in data: +# actions.append(Action(cso, action, args)) +# return actions def join_request_handler(cso, data, args): @@ -246,51 +245,51 @@ async def get_roles(self): roles.append(Role(self.cso, self, role)) return roles - async def get_member_by_id(self, user_id): - # Get list of group user is in. - member_req = await self.requests.get( - url=endpoint + f"/v2/users/{user_id}/groups/roles" - ) - data = member_req.json() - - # Find group in list. - group_data = None - for group in data['data']: - if group['group']['id'] == self.id: - group_data = group - break - - # Check if user is in group. - if not group_data: - raise NotFound(f"The user {user_id} was not found in group {self.id}") - - # Create data to return. - role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, user_id, "", self, role) - return member - - async def get_member_by_username(self, name): - user = await self.cso.client.get_user_by_username(name) - member_req = await self.requests.get( - url=endpoint + f"/v2/users/{user.id}/groups/roles" - ) - data = member_req.json() - - # Find group in list. - group_data = None - for group in data['data']: - if group['group']['id'] == self.id: - group_data = group - break - - # Check if user is in group. - if not group_data: - raise NotFound(f"The user {name} was not found in group {self.id}") - - # Create data to return. - role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, user.id, user.name, self, role) - return member + # async def get_member_by_id(self, user_id): + # # Get list of group user is in. + # member_req = await self.requests.get( + # url=endpoint + f"/v2/users/{user_id}/groups/roles" + # ) + # data = member_req.json() + # + # # Find group in list. + # group_data = None + # for group in data['data']: + # if group['group']['id'] == self.id: + # group_data = group + # break + # + # # Check if user is in group. + # if not group_data: + # raise NotFound(f"The user {user_id} was not found in group {self.id}") + # + # # Create data to return. + # role = Role(self.cso, self, group_data['role']) + # member = Member(self.cso, user_id, "", self, role) + # return member + + # async def get_member_by_username(self, name): + # user = await self.cso.client.get_user_by_username(name) + # member_req = await self.requests.get( + # url=endpoint + f"/v2/users/{user.id}/groups/roles" + # ) + # data = member_req.json() + # + # # Find group in list. + # group_data = None + # for group in data['data']: + # if group['group']['id'] == self.id: + # group_data = group + # break + # + # # Check if user is in group. + # if not group_data: + # raise NotFound(f"The user {name} was not found in group {self.id}") + # + # # Create data to return. + # role = Role(self.cso, self, group_data['role']) + # member = Member(self.cso, user.id, user.name, self, role) + # return member async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( @@ -317,23 +316,23 @@ async def get_members(self, sort_order=SortOrder.Ascending, limit=100): await pages.get_page() return pages - async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): - parameters = {} - if action_filter: - parameters['actionType'] = action_filter - - pages = Pages( - cso=self.cso, - url=endpoint + f"/v1/groups/{self.id}/audit-log", - handler=action_handler, - extra_parameters=parameters, - handler_args=self, - limit=limit, - sort_order=sort_order - ) - - await pages.get_page() - return pages + # async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): + # parameters = {} + # if action_filter: + # parameters['actionType'] = action_filter + # + # pages = Pages( + # cso=self.cso, + # url=endpoint + f"/v1/groups/{self.id}/audit-log", + # handler=action_handler, + # extra_parameters=parameters, + # handler_args=self, + # limit=limit, + # sort_order=sort_order + # ) + # + # await pages.get_page() + # return pages class PartialGroup: @@ -357,132 +356,132 @@ async def expand(self): return self.cso.client.get_group(self.id) -class Member(PartialUser): - """ - Represents a user in a group. - - Parameters - ---------- - cso : ro_py.utilities.requests.Requests - Requests object to use for API requests. - """ - def __init__(self, cso, data): - super().__init__(cso, data) - self.role = role - self.group = group - - async def update_role(self): - """ - Updates the role information of the user. - - Returns - ------- - ro_py.roles.Role - """ - member_req = await self.requests.get( - url=endpoint + f"/v2/users/{self.id}/groups/roles" - ) - data = member_req.json() - for role in data['data']: - if role['group']['id'] == self.group.id: - self.role = Role(self.cso, self.group, role['role']) - break - return self.role - - async def change_rank(self, num) -> Tuple[Role, Role]: - """ - Changes the users rank specified by a number. - If num is 1 the users role will go up by 1. - If num is -1 the users role will go down by 1. - - Parameters - ---------- - num : int - How much to change the rank by. - """ - await self.update_role() - roles = await self.group.get_roles() - old_role = copy.copy(self.role) - role_counter = -1 - for group_role in roles: - role_counter += 1 - if group_role.rank == self.role.rank: - break - if not roles: - raise NotFound(f"User {self.id} is not in group {self.group.id}") - await self.setrank(roles[role_counter + num].id) - self.role = roles[role_counter + num].id - return old_role, roles[role_counter + num] - - async def promote(self): - """ - Promotes the user. - - Returns - ------- - int - """ - return await self.change_rank(1) - - async def demote(self): - """ - Demotes the user. - - Returns - ------- - int - """ - return await self.change_rank(-1) - - async def setrank(self, rank): - """ - Sets the users role to specified role using rank id. - - Parameters - ---------- - rank : int - Rank id - - Returns - ------- - bool - """ - rank_request = await self.requests.patch( - url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}", - data={ - "roleId": rank - } - ) - return rank_request.status_code == 200 - - async def setrole(self, role_num): - """ - Sets the users role to specified role using role number (1-255). - - Parameters - ---------- - role_num : int - Role number (1-255) - - Returns - ------- - bool - """ - roles = await self.group.get_roles() - rank_role = None - for role in roles: - if role.rank == role_num: - rank_role = role - break - if not rank_role: - raise NotFound(f"Role {role_num} not found") - return await self.setrank(rank_role.id) - - async def exile(self): - exile_req = await self.requests.delete( - url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}" - ) - return exile_req.status_code == 200 +# class Member(PartialUser): +# """ +# Represents a user in a group. +# +# Parameters +# ---------- +# cso : ro_py.utilities.requests.Requests +# Requests object to use for API requests. +# """ +# def __init__(self, cso, data): +# super().__init__(cso, data) +# self.role = role +# self.group = group +# +# async def update_role(self): +# """ +# Updates the role information of the user. +# +# Returns +# ------- +# ro_py.roles.Role +# """ +# member_req = await self.requests.get( +# url=endpoint + f"/v2/users/{self.id}/groups/roles" +# ) +# data = member_req.json() +# for role in data['data']: +# if role['group']['id'] == self.group.id: +# self.role = Role(self.cso, self.group, role['role']) +# break +# return self.role +# +# async def change_rank(self, num) -> Tuple[Role, Role]: +# """ +# Changes the users rank specified by a number. +# If num is 1 the users role will go up by 1. +# If num is -1 the users role will go down by 1. +# +# Parameters +# ---------- +# num : int +# How much to change the rank by. +# """ +# await self.update_role() +# roles = await self.group.get_roles() +# old_role = copy.copy(self.role) +# role_counter = -1 +# for group_role in roles: +# role_counter += 1 +# if group_role.rank == self.role.rank: +# break +# if not roles: +# raise NotFound(f"User {self.id} is not in group {self.group.id}") +# await self.setrank(roles[role_counter + num].id) +# self.role = roles[role_counter + num].id +# return old_role, roles[role_counter + num] +# +# async def promote(self): +# """ +# Promotes the user. +# +# Returns +# ------- +# int +# """ +# return await self.change_rank(1) +# +# async def demote(self): +# """ +# Demotes the user. +# +# Returns +# ------- +# int +# """ +# return await self.change_rank(-1) +# +# async def setrank(self, rank): +# """ +# Sets the users role to specified role using rank id. +# +# Parameters +# ---------- +# rank : int +# Rank id +# +# Returns +# ------- +# bool +# """ +# rank_request = await self.requests.patch( +# url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}", +# data={ +# "roleId": rank +# } +# ) +# return rank_request.status_code == 200 +# +# async def setrole(self, role_num): +# """ +# Sets the users role to specified role using role number (1-255). +# +# Parameters +# ---------- +# role_num : int +# Role number (1-255) +# +# Returns +# ------- +# bool +# """ +# roles = await self.group.get_roles() +# rank_role = None +# for role in roles: +# if role.rank == role_num: +# rank_role = role +# break +# if not rank_role: +# raise NotFound(f"Role {role_num} not found") +# return await self.setrank(rank_role.id) +# +# async def exile(self): +# exile_req = await self.requests.delete( +# url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}" +# ) +# return exile_req.status_code == 200 class Events: diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index f5918d85..93c416a9 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -51,7 +51,7 @@ async def client_test(): group = await client.get_group(1) await group.update() await group.get_roles() - await group.get_member_by_id(1179762) + # await group.get_member_by_id(1179762) asset = await client.get_asset(5832204472) await asset.update() From 6de786913a29421d8649885a46d7b38173e979ae Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 18 Feb 2021 21:55:32 -0500 Subject: [PATCH 468/518] Update groups.py --- ro_py/groups.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index 93d18f06..981368db 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -136,6 +136,7 @@ class Actions(Enum): # def __init__(self, cso, data, group): # self.group = group # self.actor = Member(cso, data['actor']) +# self.action = data['actionType'] # self.created = iso8601.parse_date(data['created']) # self.data = data['description'] From d02fd8b0dd1d79f92ac444a235bd428b9119fee2 Mon Sep 17 00:00:00 2001 From: ira Date: Fri, 19 Feb 2021 10:58:26 +0100 Subject: [PATCH 469/518] clean users.py --- ro_py/users.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index ed526b2e..ffa27e05 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -41,14 +41,7 @@ async def expand(self): """ user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() - description = user_info["description"] - created = iso8601.parse_date(user_info["created"]) - is_banned = user_info["isBanned"] - name = user_info["name"] - display_name = user_info["displayName"] - # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") - # self.has_premium = has_premium_req - return User(self.cso, self.id, name, description, created, is_banned, display_name) + return User(self.cso, self.id, user_info) async def get_roblox_badges(self) -> List[RobloxBadge]: """ @@ -149,6 +142,12 @@ def __init__(self, cso, data): self.is_banned = data["isBanned"] self.display_name = data["displayName"] + async def remove_friend(self): + remove_req = await self.requests.post( + url=f"https://friends.roblox.com/v1/users/{self.id}/unfriend", + ) + return remove_req.status == 200 + class FriendRequest(Friend): def __init__(self, cso, data): @@ -186,15 +185,15 @@ class User(PartialUser, ClientObject): Time the user was created. """ - def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): - super().__init__(cso, roblox_id, roblox_name) + def __init__(self, cso, roblox_id, data): + super().__init__(cso, roblox_id, data['name']) self.cso = cso self.id = roblox_id - self.name = roblox_name - self.description = description - self.created = created - self.is_banned = banned - self.display_name = display_name + self.name = data['name'] + self.description = data['description'] + self.created = iso8601.parse_date(data["created"]) + self.is_banned = data["isBanned"] + self.display_name = data['displayName'] self.thumbnails = UserThumbnailGenerator(cso, roblox_id) async def update(self): From a82a7dd1619649670844c2ce7a29bcdfc8db552c Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 10:33:31 -0500 Subject: [PATCH 470/518] Revert "clean users.py" This reverts commit d02fd8b0dd1d79f92ac444a235bd428b9119fee2. --- ro_py/users.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/ro_py/users.py b/ro_py/users.py index ffa27e05..ed526b2e 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -41,7 +41,14 @@ async def expand(self): """ user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") user_info = user_info_req.json() - return User(self.cso, self.id, user_info) + description = user_info["description"] + created = iso8601.parse_date(user_info["created"]) + is_banned = user_info["isBanned"] + name = user_info["name"] + display_name = user_info["displayName"] + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req + return User(self.cso, self.id, name, description, created, is_banned, display_name) async def get_roblox_badges(self) -> List[RobloxBadge]: """ @@ -142,12 +149,6 @@ def __init__(self, cso, data): self.is_banned = data["isBanned"] self.display_name = data["displayName"] - async def remove_friend(self): - remove_req = await self.requests.post( - url=f"https://friends.roblox.com/v1/users/{self.id}/unfriend", - ) - return remove_req.status == 200 - class FriendRequest(Friend): def __init__(self, cso, data): @@ -185,15 +186,15 @@ class User(PartialUser, ClientObject): Time the user was created. """ - def __init__(self, cso, roblox_id, data): - super().__init__(cso, roblox_id, data['name']) + def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): + super().__init__(cso, roblox_id, roblox_name) self.cso = cso self.id = roblox_id - self.name = data['name'] - self.description = data['description'] - self.created = iso8601.parse_date(data["created"]) - self.is_banned = data["isBanned"] - self.display_name = data['displayName'] + self.name = roblox_name + self.description = description + self.created = created + self.is_banned = banned + self.display_name = display_name self.thumbnails = UserThumbnailGenerator(cso, roblox_id) async def update(self): From dff0edec454c6d9b4a28eee64dfc5d384bc81e5a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 16:24:59 -0500 Subject: [PATCH 471/518] Revert "Update groups.py" This reverts commit 6de786913a29421d8649885a46d7b38173e979ae. --- ro_py/groups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 981368db..93d18f06 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -136,7 +136,6 @@ class Actions(Enum): # def __init__(self, cso, data, group): # self.group = group # self.actor = Member(cso, data['actor']) -# self.action = data['actionType'] # self.created = iso8601.parse_date(data['created']) # self.data = data['description'] From 1ace9eba4de04bbae546d3fd896d6e9cfa8f3180 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 16:25:11 -0500 Subject: [PATCH 472/518] =?UTF-8?q?Revert=20"=F0=9F=8D=97"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b5a646cd0ee2a243f14d3a378899b205237449c0. --- ro_py/groups.py | 399 ++++++++++++++++++++++--------------------- tests/sanitycheck.py | 2 +- 2 files changed, 201 insertions(+), 200 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 93d18f06..67df636d 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -132,19 +132,20 @@ class Actions(Enum): buy_clan = "buyClan" -# class Action: -# def __init__(self, cso, data, group): -# self.group = group -# self.actor = Member(cso, data['actor']) -# self.created = iso8601.parse_date(data['created']) -# self.data = data['description'] +class Action: + def __init__(self, cso, data, group): + self.group = group + self.actor = Member(cso, data['actor']) + self.action = data['actionType'] + self.created = iso8601.parse_date(data['created']) + self.data = data['description'] -# def action_handler(cso, data, args): -# actions = [] -# for action in data: -# actions.append(Action(cso, action, args)) -# return actions +def action_handler(cso, data, args): + actions = [] + for action in data: + actions.append(Action(cso, action, args)) + return actions def join_request_handler(cso, data, args): @@ -245,51 +246,51 @@ async def get_roles(self): roles.append(Role(self.cso, self, role)) return roles - # async def get_member_by_id(self, user_id): - # # Get list of group user is in. - # member_req = await self.requests.get( - # url=endpoint + f"/v2/users/{user_id}/groups/roles" - # ) - # data = member_req.json() - # - # # Find group in list. - # group_data = None - # for group in data['data']: - # if group['group']['id'] == self.id: - # group_data = group - # break - # - # # Check if user is in group. - # if not group_data: - # raise NotFound(f"The user {user_id} was not found in group {self.id}") - # - # # Create data to return. - # role = Role(self.cso, self, group_data['role']) - # member = Member(self.cso, user_id, "", self, role) - # return member - - # async def get_member_by_username(self, name): - # user = await self.cso.client.get_user_by_username(name) - # member_req = await self.requests.get( - # url=endpoint + f"/v2/users/{user.id}/groups/roles" - # ) - # data = member_req.json() - # - # # Find group in list. - # group_data = None - # for group in data['data']: - # if group['group']['id'] == self.id: - # group_data = group - # break - # - # # Check if user is in group. - # if not group_data: - # raise NotFound(f"The user {name} was not found in group {self.id}") - # - # # Create data to return. - # role = Role(self.cso, self, group_data['role']) - # member = Member(self.cso, user.id, user.name, self, role) - # return member + async def get_member_by_id(self, user_id): + # Get list of group user is in. + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user_id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {user_id} was not found in group {self.id}") + + # Create data to return. + role = Role(self.cso, self, group_data['role']) + member = Member(self.cso, user_id, "", self, role) + return member + + async def get_member_by_username(self, name): + user = await self.cso.client.get_user_by_username(name) + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{user.id}/groups/roles" + ) + data = member_req.json() + + # Find group in list. + group_data = None + for group in data['data']: + if group['group']['id'] == self.id: + group_data = group + break + + # Check if user is in group. + if not group_data: + raise NotFound(f"The user {name} was not found in group {self.id}") + + # Create data to return. + role = Role(self.cso, self, group_data['role']) + member = Member(self.cso, user.id, user.name, self, role) + return member async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): pages = Pages( @@ -316,23 +317,23 @@ async def get_members(self, sort_order=SortOrder.Ascending, limit=100): await pages.get_page() return pages - # async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): - # parameters = {} - # if action_filter: - # parameters['actionType'] = action_filter - # - # pages = Pages( - # cso=self.cso, - # url=endpoint + f"/v1/groups/{self.id}/audit-log", - # handler=action_handler, - # extra_parameters=parameters, - # handler_args=self, - # limit=limit, - # sort_order=sort_order - # ) - # - # await pages.get_page() - # return pages + async def get_audit_logs(self, action_filter: Actions = None, sort_order=SortOrder.Ascending, limit=100): + parameters = {} + if action_filter: + parameters['actionType'] = action_filter + + pages = Pages( + cso=self.cso, + url=endpoint + f"/v1/groups/{self.id}/audit-log", + handler=action_handler, + extra_parameters=parameters, + handler_args=self, + limit=limit, + sort_order=sort_order + ) + + await pages.get_page() + return pages class PartialGroup: @@ -356,132 +357,132 @@ async def expand(self): return self.cso.client.get_group(self.id) -# class Member(PartialUser): -# """ -# Represents a user in a group. -# -# Parameters -# ---------- -# cso : ro_py.utilities.requests.Requests -# Requests object to use for API requests. -# """ -# def __init__(self, cso, data): -# super().__init__(cso, data) -# self.role = role -# self.group = group -# -# async def update_role(self): -# """ -# Updates the role information of the user. -# -# Returns -# ------- -# ro_py.roles.Role -# """ -# member_req = await self.requests.get( -# url=endpoint + f"/v2/users/{self.id}/groups/roles" -# ) -# data = member_req.json() -# for role in data['data']: -# if role['group']['id'] == self.group.id: -# self.role = Role(self.cso, self.group, role['role']) -# break -# return self.role -# -# async def change_rank(self, num) -> Tuple[Role, Role]: -# """ -# Changes the users rank specified by a number. -# If num is 1 the users role will go up by 1. -# If num is -1 the users role will go down by 1. -# -# Parameters -# ---------- -# num : int -# How much to change the rank by. -# """ -# await self.update_role() -# roles = await self.group.get_roles() -# old_role = copy.copy(self.role) -# role_counter = -1 -# for group_role in roles: -# role_counter += 1 -# if group_role.rank == self.role.rank: -# break -# if not roles: -# raise NotFound(f"User {self.id} is not in group {self.group.id}") -# await self.setrank(roles[role_counter + num].id) -# self.role = roles[role_counter + num].id -# return old_role, roles[role_counter + num] -# -# async def promote(self): -# """ -# Promotes the user. -# -# Returns -# ------- -# int -# """ -# return await self.change_rank(1) -# -# async def demote(self): -# """ -# Demotes the user. -# -# Returns -# ------- -# int -# """ -# return await self.change_rank(-1) -# -# async def setrank(self, rank): -# """ -# Sets the users role to specified role using rank id. -# -# Parameters -# ---------- -# rank : int -# Rank id -# -# Returns -# ------- -# bool -# """ -# rank_request = await self.requests.patch( -# url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}", -# data={ -# "roleId": rank -# } -# ) -# return rank_request.status_code == 200 -# -# async def setrole(self, role_num): -# """ -# Sets the users role to specified role using role number (1-255). -# -# Parameters -# ---------- -# role_num : int -# Role number (1-255) -# -# Returns -# ------- -# bool -# """ -# roles = await self.group.get_roles() -# rank_role = None -# for role in roles: -# if role.rank == role_num: -# rank_role = role -# break -# if not rank_role: -# raise NotFound(f"Role {role_num} not found") -# return await self.setrank(rank_role.id) -# -# async def exile(self): -# exile_req = await self.requests.delete( -# url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}" -# ) -# return exile_req.status_code == 200 +class Member(PartialUser): + """ + Represents a user in a group. + + Parameters + ---------- + cso : ro_py.utilities.requests.Requests + Requests object to use for API requests. + """ + def __init__(self, cso, data): + super().__init__(cso, data) + self.role = role + self.group = group + + async def update_role(self): + """ + Updates the role information of the user. + + Returns + ------- + ro_py.roles.Role + """ + member_req = await self.requests.get( + url=endpoint + f"/v2/users/{self.id}/groups/roles" + ) + data = member_req.json() + for role in data['data']: + if role['group']['id'] == self.group.id: + self.role = Role(self.cso, self.group, role['role']) + break + return self.role + + async def change_rank(self, num) -> Tuple[Role, Role]: + """ + Changes the users rank specified by a number. + If num is 1 the users role will go up by 1. + If num is -1 the users role will go down by 1. + + Parameters + ---------- + num : int + How much to change the rank by. + """ + await self.update_role() + roles = await self.group.get_roles() + old_role = copy.copy(self.role) + role_counter = -1 + for group_role in roles: + role_counter += 1 + if group_role.rank == self.role.rank: + break + if not roles: + raise NotFound(f"User {self.id} is not in group {self.group.id}") + await self.setrank(roles[role_counter + num].id) + self.role = roles[role_counter + num].id + return old_role, roles[role_counter + num] + + async def promote(self): + """ + Promotes the user. + + Returns + ------- + int + """ + return await self.change_rank(1) + + async def demote(self): + """ + Demotes the user. + + Returns + ------- + int + """ + return await self.change_rank(-1) + + async def setrank(self, rank): + """ + Sets the users role to specified role using rank id. + + Parameters + ---------- + rank : int + Rank id + + Returns + ------- + bool + """ + rank_request = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}", + data={ + "roleId": rank + } + ) + return rank_request.status_code == 200 + + async def setrole(self, role_num): + """ + Sets the users role to specified role using role number (1-255). + + Parameters + ---------- + role_num : int + Role number (1-255) + + Returns + ------- + bool + """ + roles = await self.group.get_roles() + rank_role = None + for role in roles: + if role.rank == role_num: + rank_role = role + break + if not rank_role: + raise NotFound(f"Role {role_num} not found") + return await self.setrank(rank_role.id) + + async def exile(self): + exile_req = await self.requests.delete( + url=endpoint + f"/v1/groups/{self.group.id}/users/{self.id}" + ) + return exile_req.status_code == 200 class Events: diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index 93c416a9..f5918d85 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -51,7 +51,7 @@ async def client_test(): group = await client.get_group(1) await group.update() await group.get_roles() - # await group.get_member_by_id(1179762) + await group.get_member_by_id(1179762) asset = await client.get_asset(5832204472) await asset.update() From 7f45870d5d2ee793206cfe85356faf4ebe9746a7 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 16:26:27 -0500 Subject: [PATCH 473/518] Revert "I'm going to bed." This reverts commit 2f9a58e0ec853746518c5149eb7c582607924438. --- ro_py/groups.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 67df636d..83ec7894 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -135,7 +135,7 @@ class Actions(Enum): class Action: def __init__(self, cso, data, group): self.group = group - self.actor = Member(cso, data['actor']) + self.actor = Member(cso, data['actor']['user']['userId'], data['actor']['user']['username'], group, Role(cso, group, data['actor']['role'])) self.action = data['actionType'] self.created = iso8601.parse_date(data['created']) self.data = data['description'] @@ -365,9 +365,17 @@ class Member(PartialUser): ---------- cso : ro_py.utilities.requests.Requests Requests object to use for API requests. + roblox_id : int + The id of a user. + name : str + The name of the user. + group : ro_py.groups.Group + The group the user is in. + role : ro_py.roles.Role + The role the user has is the group. """ - def __init__(self, cso, data): - super().__init__(cso, data) + def __init__(self, cso, roblox_id, name, group, role): + super().__init__(cso, roblox_id, name) self.role = role self.group = group From e5d97fec8aedef74f45cbddc227aa7b2e104cb63 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 16:29:24 -0500 Subject: [PATCH 474/518] Update groups.py --- ro_py/groups.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 83ec7894..67967f07 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -12,7 +12,7 @@ from ro_py.wall import Wall from ro_py.roles import Role -from ro_py.users import PartialUser +from ro_py.users import PartialUser, BaseUser from ro_py.events import EventTypes from typing import Tuple, Callable from ro_py.utilities.errors import NotFound @@ -72,7 +72,7 @@ class JoinRequest: def __init__(self, cso, data, group): self.requests = cso.requests self.group = group - self.requester = PartialUser(cso, data['requester']['userId'], data['requester']['username']) + self.requester = PartialUser(cso, data['requester']) self.created = iso8601.parse_date(data['created']) async def accept(self): @@ -357,7 +357,7 @@ async def expand(self): return self.cso.client.get_group(self.id) -class Member(PartialUser): +class Member(BaseUser): """ Represents a user in a group. @@ -365,7 +365,7 @@ class Member(PartialUser): ---------- cso : ro_py.utilities.requests.Requests Requests object to use for API requests. - roblox_id : int + user_id : int The id of a user. name : str The name of the user. @@ -374,8 +374,9 @@ class Member(PartialUser): role : ro_py.roles.Role The role the user has is the group. """ - def __init__(self, cso, roblox_id, name, group, role): - super().__init__(cso, roblox_id, name) + def __init__(self, cso, user_id, name, group, role): + super().__init__(cso, user_id) + self.name = name self.role = role self.group = group From ee7e46fd5303ec03e4c26da5f040ae9d08774c65 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 16:20:08 -0800 Subject: [PATCH 475/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9d83b33..7cb7cfbf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      - ro.py Discord + ro.py Discord ro.py PyPI ro.py PyPI Downloads ro.py PyPI License From 2376eb6f3ab4701c3ed297d511b3ef52a95982c2 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Fri, 19 Feb 2021 19:21:47 -0500 Subject: [PATCH 476/518] Update __init__.py --- ro_py/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index d5d4dc5c..0a56cd14 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -7,7 +7,7 @@

      ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

      - ro.py Discord + ro.py Discord ro.py PyPI ro.py PyPI Downloads ro.py PyPI License From c7f8a316ea0f44dd4dde045fca73eea4bd288bbe Mon Sep 17 00:00:00 2001 From: ira Date: Sat, 20 Feb 2021 14:52:58 +0100 Subject: [PATCH 477/518] add event handler --- ro_py/events.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ ro_py/groups.py | 2 -- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/ro_py/events.py b/ro_py/events.py index 49549937..fc8947aa 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -6,6 +6,9 @@ """ import enum +import time +import asyncio +from typing import Callable, Tuple class EventTypes(enum.Enum): @@ -16,3 +19,44 @@ class EventTypes(enum.Enum): on_user_change = "on_user_change" on_audit_log = "on_audit_log" on_trade_request = "on_trade_request" + + +class Event: + def __init__(self, event_id: str, func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15): + self.event_id = event_id + self.function = func + self.event_type = event_type + self.arguments = arguments + self.delay = delay + self.next_run = time.time() + delay + + def edit(self, arguments: Tuple = None, event_id: str = None, delay: int = None): + self.arguments = arguments if arguments else self.arguments + self.event_id = event_id if event_id else self.event_id + self.delay = delay if delay else self.delay + + +class EventHandler: + def __init__(self): + self.events = [] + self.running = False + + def add_event(self, event: Event): + self.events.append(event) + + def print_events(self): + text = "These are the current running events:" + for event in self.events: + text += f"\n{event.event_id}:\n Next run: {event.next_run}\n Times run per minute: {60 / event.delay}" + print(text) + + async def listen(self): + if not self.running: + self.running = True + while True: + # Limits delay to 1 second. + await asyncio.sleep(1) + for event in self.events: + if event.next_run <= time.time(): + asyncio.create_task(event.function(*event.arguments)) + event.next_run = time.time() + event.delay diff --git a/ro_py/groups.py b/ro_py/groups.py index 67967f07..3cec2b17 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -518,8 +518,6 @@ def bind(self, func: Callable, event: EventTypes, delay: int = 15): return asyncio.create_task(self.on_wall_post(func, delay)) if event == EventTypes.on_group_change: return asyncio.create_task(self.on_group_change(func, delay)) - if event == EventTypes.on_audit_log: - return asyncio.create_task(self.on_audit_log(func, delay)) async def on_join_request(self, func: Callable, delay: int): current_group_reqs = await self.group.get_join_requests() From 1cce729675863f4d50f953c3874d9758b03de776 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 24 Feb 2021 20:31:06 -0500 Subject: [PATCH 478/518] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7cb7cfbf..4f59441d 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,12 @@ The docs are generated from docstrings in the code using pdoc3. ## Installation You can install ro.py from pip: ``` -pip3 install ro-py +pip3 install ro.py ``` If you want the latest bleeding-edge version, clone from git (you'll need [git-scm](https://git-scm.com/downloads) installed): ``` pip3 install git+git://github.com/rbx-libdev/ro.py.git ``` -Known issue: wxPython sometimes has trouble building on certain devices. I put wxPython last on the requirements so Python attempts to install it last, so you can safely ignore this error as everything else should be installed. ## Contributors From 8f84e16bf1a24399696b6cb6472e3592e645e8b4 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 27 Feb 2021 16:04:16 -0500 Subject: [PATCH 479/518] Badge ownership + sanity check is faster --- ro_py/badges.py | 16 ++++++++++++++++ ro_py/users.py | 27 +++++++++++++++++++++++++++ tests/sanitycheck.py | 16 +++++++--------- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/ro_py/badges.py b/ro_py/badges.py index 0c78fd38..76af6325 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -62,3 +62,19 @@ async def update(self): statistics_info["awardedCount"], statistics_info["winRatePercentage"] ) + + async def owned_by(self, user): + """ + Checks if a user was awarded this badge and grabs the time that they were awarded it. + Functionally identical to ro_py.users.User.has_badge. + + Parameters + ---------- + user: ro_py.users.BaseUser + User to check badge ownership. + + Returns + ------- + tuple[bool, datetime.datetime] + """ + return await user.has_badge(self) diff --git a/ro_py/users.py b/ro_py/users.py index 72a8dd27..e48683ce 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,6 +7,7 @@ import copy import iso8601 import asyncio +from ro_py.badges import Badge from typing import List, Callable from ro_py.assets import UserAsset from ro_py.events import EventTypes @@ -154,6 +155,32 @@ async def get_status(self): status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") return status_req.json()["status"] + async def has_badge(self, badge: Badge): + """ + Checks if a user was awarded a badge and grabs the time that they were awarded it. + Functionally identical to ro_py.badges.Badge.owned_by. + + Parameters + ---------- + badge: ro_py.badges.Badge + Badge to check ownership of. + + Returns + ------- + tuple[bool, datetime.datetime] + """ + has_badge_req = await self.requests.get( + url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates", + params={ + "badgeIds": badge.id + } + ) + has_badge_data = has_badge_req.json()["data"] + if len(has_badge_data) >= 1: + return True, iso8601.parse_date(has_badge_data[0]["awardedDate"]) + else: + return False, None + class PartialUser(BaseUser): def __init__(self, cso, data): diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index f5918d85..cde1bd73 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -10,7 +10,7 @@ def i(name, thisobj): async def client_test(): - user = await client.get_user(2067807455) + user = await client.get_user(968108160) i("id", user) i("name", user) i("description", user) @@ -22,13 +22,18 @@ async def client_test(): i("cso", user) await user.get_status() await user.get_followings_count() - await user.update() await user.get_groups() await user.get_friends() await user.get_followers_count() await user.get_followings_count() await user.get_roblox_badges() + badge = await client.get_badge(14468882) + has_badge, badge_date = await badge.owned_by(user) + print(has_badge, badge_date) + has_badge, badge_date = await user.has_badge(badge) + print(has_badge, badge_date) + user = await client.get_user_by_username("John Doe") i("id", user) i("name", user) @@ -41,7 +46,6 @@ async def client_test(): i("cso", user) await user.get_status() await user.get_followings_count() - await user.update() await user.get_groups() await user.get_friends() await user.get_followers_count() @@ -49,18 +53,12 @@ async def client_test(): await user.get_roblox_badges() group = await client.get_group(1) - await group.update() await group.get_roles() await group.get_member_by_id(1179762) asset = await client.get_asset(5832204472) - await asset.update() - - badge = await client.get_badge(2124538588) - await badge.update() game = await client.get_game_by_universe_id(1732173541) - await game.update() await game.get_votes() await game.get_badges() From 7f5cc989d2e25f4d702550d7fef75f4dc8730372 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 27 Feb 2021 16:20:20 -0500 Subject: [PATCH 480/518] Bases implementation This is a large change that moves a lot of "base objects" and "partial objects" to a new "bases" folder. --- ro_py/badges.py | 2 +- ro_py/bases/__init__.py | 5 + ro_py/{utilities => bases}/baseasset.py | 0 ro_py/bases/basetrade.py | 78 +++++++++ ro_py/bases/baseuser.py | 179 +++++++++++++++++++++ ro_py/chat.py | 2 +- ro_py/client.py | 4 +- ro_py/friends.py | 30 ++++ ro_py/games.py | 4 +- ro_py/groups.py | 5 +- ro_py/trades.py | 69 +------- ro_py/users.py | 205 +----------------------- ro_py/wall.py | 2 +- 13 files changed, 306 insertions(+), 279 deletions(-) create mode 100644 ro_py/bases/__init__.py rename ro_py/{utilities => bases}/baseasset.py (100%) create mode 100644 ro_py/bases/basetrade.py create mode 100644 ro_py/bases/baseuser.py create mode 100644 ro_py/friends.py diff --git a/ro_py/badges.py b/ro_py/badges.py index 76af6325..3988e33a 100644 --- a/ro_py/badges.py +++ b/ro_py/badges.py @@ -5,7 +5,7 @@ """ from ro_py.utilities.clientobject import ClientObject -from ro_py.utilities.baseasset import BaseAsset +from ro_py.bases.baseasset import BaseAsset from ro_py.utilities.url import url endpoint = url("badges") diff --git a/ro_py/bases/__init__.py b/ro_py/bases/__init__.py new file mode 100644 index 00000000..77303963 --- /dev/null +++ b/ro_py/bases/__init__.py @@ -0,0 +1,5 @@ +""" + +This folder houses base/partial objects that other parts of ro.py inherit. + +""" diff --git a/ro_py/utilities/baseasset.py b/ro_py/bases/baseasset.py similarity index 100% rename from ro_py/utilities/baseasset.py rename to ro_py/bases/baseasset.py diff --git a/ro_py/bases/basetrade.py b/ro_py/bases/basetrade.py new file mode 100644 index 00000000..878b901a --- /dev/null +++ b/ro_py/bases/basetrade.py @@ -0,0 +1,78 @@ +from ro_py.assets import Asset +import iso8601 + +from ro_py.utilities.url import url +endpoint = url("trades") + + +class PartialTrade: + def __init__(self, cso, data): + from ro_py.bases.baseuser import PartialUser + self.cso = cso + self.requests = cso.requests + self.trade_id = data['id'] + self.user = PartialUser(cso, data['user']) + self.created = iso8601.parse_date(data['created']) + self.expiration = None + if "expiration" in data: + self.expiration = iso8601.parse_date(data['expiration']) + self.status = data['status'] + + async def accept(self) -> bool: + """ + accepts a trade requests + :returns: true/false + """ + accept_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/accept" + ) + return accept_req.status_code == 200 + + async def decline(self) -> bool: + """ + decline a trade requests + :returns: true/false + """ + decline_req = await self.requests.post( + url=endpoint + f"/v1/trades/{self.trade_id}/decline" + ) + return decline_req.status_code == 200 + + async def expand(self): + """ + Gets a more detailed trade request + + Returns + ------- + ro_py.trades.Trade + """ + + from ro_py.trades import Trade + expend_req = await self.requests.get( + url=endpoint + f"/v1/trades/{self.trade_id}" + ) + data = expend_req.json() + + # generate a user class and update it + sender = await self.cso.client.get_user(data['user']['id']) + await sender.update() + + # load items that will be/have been sent and items that you will/have receive(d) + receive_items, send_items = [], [] + for items_0 in data['offers'][0]['userAssets']: + item_0 = Asset(self.cso, items_0['assetId']) + await item_0.update() + receive_items.append(item_0) + + for items_1 in data['offers'][1]['userAssets']: + item_1 = Asset(self.cso, items_1['assetId']) + await item_1.update() + send_items.append(item_1) + + return Trade( + self.cso, + data, + sender, + send_items, + receive_items + ) diff --git a/ro_py/bases/baseuser.py b/ro_py/bases/baseuser.py new file mode 100644 index 00000000..505de691 --- /dev/null +++ b/ro_py/bases/baseuser.py @@ -0,0 +1,179 @@ +from ro_py.robloxbadges import RobloxBadge +from ro_py.utilities.pages import Pages +from ro_py.assets import UserAsset +from ro_py.badges import Badge +import iso8601 + +from ro_py.utilities.url import url +endpoint = url("users") + + +def limited_handler(requests, data, args): + assets = [] + for asset in data: + assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId'])) + return assets + + +class BaseUser: + def __init__(self, cso, user_id): + self.cso = cso + self.requests = cso.requests + self.id = user_id + self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" + + async def expand(self): + """ + Expands into a full User object. + + Returns + ------ + ro_py.users.User + """ + return await self.cso.client.get_user(self.id) + + async def get_roblox_badges(self) : + """ + Gets the user's roblox badges. + + Returns + ------- + List[ro_py.robloxbadges.RobloxBadge] + """ + roblox_badges_req = await self.requests.get( + f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") + roblox_badges = [] + for roblox_badge_data in roblox_badges_req.json(): + roblox_badges.append(RobloxBadge(roblox_badge_data)) + return roblox_badges + + async def get_friends_count(self) -> int: + """ + Gets the user's friends count. + + Returns + ------- + int + """ + friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") + friends_count = friends_count_req.json()["count"] + return friends_count + + async def get_followers_count(self) -> int: + """ + Gets the user's followers count. + + Returns + ------- + int + """ + followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") + followers_count = followers_count_req.json()["count"] + return followers_count + + async def get_followings_count(self) -> int: + """ + Gets the user's followings count. + + Returns + ------- + int + """ + followings_count_req = await self.requests.get( + f"https://friends.roblox.com/v1/users/{self.id}/followings/count") + followings_count = followings_count_req.json()["count"] + return followings_count + + async def get_friends(self): + """ + Gets the user's friends. + + Returns + ------- + List[ro_py.users.Friend] + """ + from ro_py.friends import Friend # Hacky circular import fix + friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") + friends_raw = friends_req.json()["data"] + friends_list = [] + for friend_raw in friends_raw: + friends_list.append(Friend(self.cso, friend_raw)) + return friends_list + + async def get_groups(self): + """ + Gets the user's groups. + + Returns + ------- + List[ro_py.groups.PartialGroup] + """ + from ro_py.groups import PartialGroup + member_req = await self.requests.get( + url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" + ) + data = member_req.json() + groups = [] + for group in data['data']: + group = group['group'] + groups.append(PartialGroup(self.cso, group)) + return groups + + async def get_limiteds(self): + """ + Gets all limiteds the user owns. + + Returns + ------- + bababooey + """ + return Pages( + cso=self.cso, + url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", + handler=limited_handler + ) + + async def get_status(self): + """ + Gets the user's status. + + Returns + ------- + str + """ + status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") + return status_req.json()["status"] + + async def has_badge(self, badge: Badge): + """ + Checks if a user was awarded a badge and grabs the time that they were awarded it. + Functionally identical to ro_py.badges.Badge.owned_by. + + Parameters + ---------- + badge: ro_py.badges.Badge + Badge to check ownership of. + + Returns + ------- + tuple[bool, datetime.datetime] + """ + has_badge_req = await self.requests.get( + url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates", + params={ + "badgeIds": badge.id + } + ) + has_badge_data = has_badge_req.json()["data"] + if len(has_badge_data) >= 1: + return True, iso8601.parse_date(has_badge_data[0]["awardedDate"]) + else: + return False, None + + +class PartialUser(BaseUser): + def __init__(self, cso, data): + self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId") + super().__init__(cso, self.id) + self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username") + self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name") diff --git a/ro_py/chat.py b/ro_py/chat.py index dda7bd3f..1d8f645f 100644 --- a/ro_py/chat.py +++ b/ro_py/chat.py @@ -5,7 +5,7 @@ """ from ro_py.utilities.errors import ChatError -from ro_py.users import PartialUser +from ro_py.bases.baseuser import PartialUser from ro_py.utilities.url import url endpoint = url("chat") diff --git a/ro_py/client.py b/ro_py/client.py index 0e253c1f..6fae926a 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -5,20 +5,22 @@ """ from ro_py.games import Game +from ro_py.users import User from ro_py.groups import Group from ro_py.assets import Asset from ro_py.badges import Badge from ro_py.chat import ChatWrapper from ro_py.events import EventTypes from ro_py.trades import TradesWrapper +from ro_py.friends import FriendRequest from ro_py.captcha import CaptchaMetadata from ro_py.utilities.cache import CacheType +from ro_py.bases.baseuser import PartialUser from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings from ro_py.utilities.pages import Pages, SortOrder from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation -from ro_py.users import User, PartialUser, FriendRequest from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError diff --git a/ro_py/friends.py b/ro_py/friends.py new file mode 100644 index 00000000..7f2eae00 --- /dev/null +++ b/ro_py/friends.py @@ -0,0 +1,30 @@ +import iso8601 +from ro_py.bases.baseuser import PartialUser + + +class Friend(PartialUser): + def __init__(self, cso, data): + super().__init__(cso, data) + self.is_online = data["isOnline"] + self.is_deleted = data["isDeleted"] + self.description = data["description"] + self.created = iso8601.parse_date(data["created"]) + self.is_banned = data["isBanned"] + self.display_name = data["displayName"] + + +class FriendRequest(Friend): + def __init__(self, cso, data): + super(FriendRequest, self).__init__(cso, data) + + async def accept(self): + accept_req = await self.cso.post( + url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request" + ) + return accept_req.status == 200 + + async def decline(self): + accept_req = await self.cso.post( + url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request" + ) + return accept_req.status == 200 \ No newline at end of file diff --git a/ro_py/games.py b/ro_py/games.py index 5e010151..a27c4ef1 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -7,9 +7,9 @@ from ro_py.utilities.clientobject import ClientObject from ro_py.thumbnails import GameThumbnailGenerator from ro_py.utilities.errors import GameJoinError -from ro_py.utilities.baseasset import BaseAsset +from ro_py.bases.baseuser import PartialUser +from ro_py.bases.baseasset import BaseAsset from ro_py.utilities.cache import CacheType -from ro_py.users import PartialUser from ro_py.groups import Group from ro_py.badges import Badge import subprocess diff --git a/ro_py/groups.py b/ro_py/groups.py index 67967f07..e440330f 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -12,10 +12,11 @@ from ro_py.wall import Wall from ro_py.roles import Role -from ro_py.users import PartialUser, BaseUser -from ro_py.events import EventTypes +from ro_py.users import BaseUser from typing import Tuple, Callable +from ro_py.events import EventTypes from ro_py.utilities.errors import NotFound +from ro_py.bases.baseuser import PartialUser from ro_py.utilities.pages import Pages, SortOrder from ro_py.utilities.clientobject import ClientObject diff --git a/ro_py/trades.py b/ro_py/trades.py index e65f3f3d..b69312f4 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -7,8 +7,8 @@ from ro_py.utilities.pages import Pages, SortOrder from ro_py.assets import Asset, UserAsset -from ro_py.users import PartialUser, User from ro_py.events import EventTypes +from ro_py.users import User import iso8601 import asyncio import enum @@ -56,73 +56,6 @@ async def decline(self) -> bool: return decline_req.status_code == 200 -class PartialTrade: - def __init__(self, cso, data): - self.cso = cso - self.requests = cso.requests - self.trade_id = data['id'] - self.user = PartialUser(cso, data['user']['id'], data['user']['name']) - self.created = iso8601.parse_date(data['created']) - self.expiration = None - if "expiration" in data: - self.expiration = iso8601.parse_date(data['expiration']) - self.status = data['status'] - - async def accept(self) -> bool: - """ - accepts a trade requests - :returns: true/false - """ - accept_req = await self.requests.post( - url=endpoint + f"/v1/trades/{self.trade_id}/accept" - ) - return accept_req.status_code == 200 - - async def decline(self) -> bool: - """ - decline a trade requests - :returns: true/false - """ - decline_req = await self.requests.post( - url=endpoint + f"/v1/trades/{self.trade_id}/decline" - ) - return decline_req.status_code == 200 - - async def expand(self) -> Trade: - """ - gets a more detailed trade request - :return: Trade class - """ - expend_req = await self.requests.get( - url=endpoint + f"/v1/trades/{self.trade_id}" - ) - data = expend_req.json() - - # generate a user class and update it - sender = await self.cso.client.get_user(data['user']['id']) - await sender.update() - - # load items that will be/have been sent and items that you will/have receive(d) - receive_items, send_items = [], [] - for items_0 in data['offers'][0]['userAssets']: - item_0 = Asset(self.cso, items_0['assetId']) - await item_0.update() - receive_items.append(item_0) - - for items_1 in data['offers'][1]['userAssets']: - item_1 = Asset(self.cso, items_1['assetId']) - await item_1.update() - send_items.append(item_1) - - return Trade( - self.cso, - data, - sender, - send_items, - receive_items - ) - - class TradeStatusType(enum.Enum): """ Represents a trade status type. diff --git a/ro_py/users.py b/ro_py/users.py index e48683ce..208e7f59 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -7,12 +7,9 @@ import copy import iso8601 import asyncio -from ro_py.badges import Badge -from typing import List, Callable -from ro_py.assets import UserAsset +from typing import Callable from ro_py.events import EventTypes -from ro_py.utilities.pages import Pages -from ro_py.robloxbadges import RobloxBadge +from ro_py.bases.baseuser import BaseUser from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.clientobject import ClientObject @@ -20,204 +17,6 @@ endpoint = url("users") -def limited_handler(requests, data, args): - assets = [] - for asset in data: - assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId'])) - return assets - - -class BaseUser: - def __init__(self, cso, user_id): - self.cso = cso - self.requests = cso.requests - self.id = user_id - self.profile_url = f"https://www.roblox.com/users/{self.id}/profile" - - async def expand(self): - """ - Expands into a full User object. - - Returns - ------ - ro_py.users.User - """ - return await self.cso.client.get_user(self.id) - - async def get_roblox_badges(self) -> List[RobloxBadge]: - """ - Gets the user's roblox badges. - - Returns - ------- - List[ro_py.robloxbadges.RobloxBadge] - """ - roblox_badges_req = await self.requests.get( - f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") - roblox_badges = [] - for roblox_badge_data in roblox_badges_req.json(): - roblox_badges.append(RobloxBadge(roblox_badge_data)) - return roblox_badges - - async def get_friends_count(self) -> int: - """ - Gets the user's friends count. - - Returns - ------- - int - """ - friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") - friends_count = friends_count_req.json()["count"] - return friends_count - - async def get_followers_count(self) -> int: - """ - Gets the user's followers count. - - Returns - ------- - int - """ - followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") - followers_count = followers_count_req.json()["count"] - return followers_count - - async def get_followings_count(self) -> int: - """ - Gets the user's followings count. - - Returns - ------- - int - """ - followings_count_req = await self.requests.get( - f"https://friends.roblox.com/v1/users/{self.id}/followings/count") - followings_count = followings_count_req.json()["count"] - return followings_count - - async def get_friends(self): - """ - Gets the user's friends. - - Returns - ------- - List[ro_py.users.Friend] - """ - friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") - friends_raw = friends_req.json()["data"] - friends_list = [] - for friend_raw in friends_raw: - friends_list.append(Friend(self.cso, friend_raw)) - return friends_list - - async def get_groups(self): - """ - Gets the user's groups. - - Returns - ------- - List[ro_py.groups.PartialGroup] - """ - from ro_py.groups import PartialGroup - member_req = await self.requests.get( - url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" - ) - data = member_req.json() - groups = [] - for group in data['data']: - group = group['group'] - groups.append(PartialGroup(self.cso, group)) - return groups - - async def get_limiteds(self): - """ - Gets all limiteds the user owns. - - Returns - ------- - bababooey - """ - return Pages( - cso=self.cso, - url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", - handler=limited_handler - ) - - async def get_status(self): - """ - Gets the user's status. - - Returns - ------- - str - """ - status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") - return status_req.json()["status"] - - async def has_badge(self, badge: Badge): - """ - Checks if a user was awarded a badge and grabs the time that they were awarded it. - Functionally identical to ro_py.badges.Badge.owned_by. - - Parameters - ---------- - badge: ro_py.badges.Badge - Badge to check ownership of. - - Returns - ------- - tuple[bool, datetime.datetime] - """ - has_badge_req = await self.requests.get( - url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates", - params={ - "badgeIds": badge.id - } - ) - has_badge_data = has_badge_req.json()["data"] - if len(has_badge_data) >= 1: - return True, iso8601.parse_date(has_badge_data[0]["awardedDate"]) - else: - return False, None - - -class PartialUser(BaseUser): - def __init__(self, cso, data): - self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId") - super().__init__(cso, self.id) - self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username") - self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name") - - -class Friend(PartialUser): - def __init__(self, cso, data): - super().__init__(cso, data) - self.is_online = data["isOnline"] - self.is_deleted = data["isDeleted"] - self.description = data["description"] - self.created = iso8601.parse_date(data["created"]) - self.is_banned = data["isBanned"] - self.display_name = data["displayName"] - - -class FriendRequest(Friend): - def __init__(self, cso, data): - super(FriendRequest, self).__init__(cso, data) - - async def accept(self): - accept_req = await self.cso.post( - url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request" - ) - return accept_req.status == 200 - - async def decline(self): - accept_req = await self.cso.post( - url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request" - ) - return accept_req.status == 200 - - class User(BaseUser, ClientObject): """ Represents a Roblox user and their profile. diff --git a/ro_py/wall.py b/ro_py/wall.py index 0060cc57..6f790322 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -1,8 +1,8 @@ import iso8601 from typing import List from ro_py.captcha import UnsolvedCaptcha +from ro_py.bases.baseuser import PartialUser from ro_py.utilities.pages import Pages, SortOrder -from ro_py.users import PartialUser from ro_py.utilities.url import url endpoint = url("groups") From bf0a1ff69def759b41afbfc4ab0252de27df0fb5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 27 Feb 2021 16:35:03 -0500 Subject: [PATCH 481/518] Trades base fixes + wall fix --- ro_py/trades.py | 1 + ro_py/wall.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ro_py/trades.py b/ro_py/trades.py index b69312f4..f8d7ff15 100644 --- a/ro_py/trades.py +++ b/ro_py/trades.py @@ -6,6 +6,7 @@ from typing import Callable, List from ro_py.utilities.pages import Pages, SortOrder +from ro_py.bases.basetrade import PartialTrade from ro_py.assets import Asset, UserAsset from ro_py.events import EventTypes from ro_py.users import User diff --git a/ro_py/wall.py b/ro_py/wall.py index 6f790322..9176d2c7 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -21,7 +21,7 @@ def __init__(self, cso, wall_data, group): self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) if wall_data['poster']: - self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + self.poster = PartialUser(self.cso, wall_data['poster']['user']) else: self.poster = None From b0aed39b554393c5947f814effee21116630668f Mon Sep 17 00:00:00 2001 From: ira Date: Sun, 28 Feb 2021 02:17:04 +0100 Subject: [PATCH 482/518] implement event handler into groups events + other fixes. --- ro_py/events.py | 12 ++- ro_py/groups.py | 129 ++++++++++++++++++++------------ ro_py/utilities/clientobject.py | 2 + ro_py/utilities/pages.py | 3 + 4 files changed, 95 insertions(+), 51 deletions(-) diff --git a/ro_py/events.py b/ro_py/events.py index fc8947aa..1bf66cb9 100644 --- a/ro_py/events.py +++ b/ro_py/events.py @@ -22,18 +22,17 @@ class EventTypes(enum.Enum): class Event: - def __init__(self, event_id: str, func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15): - self.event_id = event_id + def __init__(self, func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15): self.function = func self.event_type = event_type self.arguments = arguments self.delay = delay self.next_run = time.time() + delay - def edit(self, arguments: Tuple = None, event_id: str = None, delay: int = None): + def edit(self, arguments: Tuple = None, delay: int = None, func: Callable = None): self.arguments = arguments if arguments else self.arguments - self.event_id = event_id if event_id else self.event_id self.delay = delay if delay else self.delay + self.function = func if func else self.function class EventHandler: @@ -50,6 +49,9 @@ def print_events(self): text += f"\n{event.event_id}:\n Next run: {event.next_run}\n Times run per minute: {60 / event.delay}" print(text) + async def stop_event(self, event: Event): + self.events.remove(event) + async def listen(self): if not self.running: self.running = True @@ -58,5 +60,7 @@ async def listen(self): await asyncio.sleep(1) for event in self.events: if event.next_run <= time.time(): + if not isinstance(event.arguments[-1], Event): + event.arguments = (*event.arguments, event) asyncio.create_task(event.function(*event.arguments)) event.next_run = time.time() + event.delay diff --git a/ro_py/groups.py b/ro_py/groups.py index 96d220b6..adc559b9 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -12,6 +12,7 @@ from ro_py.wall import Wall from ro_py.roles import Role +from ro_py.events import Event from ro_py.users import BaseUser from typing import Tuple, Callable from ro_py.events import EventTypes @@ -35,7 +36,7 @@ def __init__(self, cso, group, shout_data): self.data = shout_data self.body = shout_data["body"] self.created = iso8601.parse_date(shout_data["created"]) - self.updated = iso8601.parse_date(shout_data["created"]) + self.updated = iso8601.parse_date(shout_data["updated"]) # TODO: Make this a PartialUser self.poster = None @@ -500,7 +501,7 @@ def __init__(self, cso, group): self.cso = cso self.group = group - def bind(self, func: Callable, event: EventTypes, delay: int = 15): + async def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -514,58 +515,92 @@ def bind(self, func: Callable, event: EventTypes, delay: int = 15): How many seconds between each poll. """ if event == EventTypes.on_join_request: - return asyncio.create_task(self.on_join_request(func, delay)) + event = Event(self.on_join_request, EventTypes.on_join_request, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_wall_post: - return asyncio.create_task(self.on_wall_post(func, delay)) + event = Event(self.on_wall_post, EventTypes.on_wall_post, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_group_change: - return asyncio.create_task(self.on_group_change(func, delay)) + event = Event(self.on_group_change, EventTypes.on_group_change, (func, None), delay) + self.cso.event_handler.add_event(event) + await self.cso.event_handler.listen() - async def on_join_request(self, func: Callable, delay: int): - current_group_reqs = await self.group.get_join_requests() - old_req = current_group_reqs.data.requester.id - while True: - await asyncio.sleep(delay) + async def on_join_request(self, func: Callable, old_req, event: Event): + if not old_req: current_group_reqs = await self.group.get_join_requests() - current_group_reqs = current_group_reqs.data - if current_group_reqs[0].requester.id != old_req: - new_reqs = [] - for request in current_group_reqs: - if request.requester.id != old_req: - new_reqs.append(request) - old_req = current_group_reqs[0].requester.id - for new_req in new_reqs: - asyncio.create_task(func(new_req)) - - async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - newest_wall_post = current_wall_posts.data[0].id - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs.data[0].requester.id + return event.edit(arguments=tuple(old_arguments)) + + current_group_reqs = await self.group.get_join_requests() + current_group_reqs = current_group_reqs.data + + if current_group_reqs[0].requester.id != old_req: + new_reqs = [] + + for request in current_group_reqs: + if request.requester.id == old_req: + break + new_reqs.append(request) + + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs[0].requester.id + event.edit(arguments=tuple(old_arguments)) + + for new_req in new_reqs: + asyncio.create_task(func(new_req)) + + async def on_wall_post(self, func: Callable, newest_wall_post, event: Event): + if not newest_wall_post: current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - current_wall_posts = current_wall_posts.data - post = current_wall_posts[0] - if post.id != newest_wall_post: - new_posts = [] - for post in current_wall_posts: - if post.id == newest_wall_post: - break - new_posts.append(post) - newest_wall_post = current_wall_posts[0].id - for new_post in new_posts: - asyncio.create_task(func(new_post)) - - async def on_group_change(self, func: Callable, delay: int): - await self.group.update() - current_group = copy.copy(self.group) - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts.data[0].id + return event.edit(arguments=tuple(old_arguments)) + + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + current_wall_posts = current_wall_posts.data + + post = current_wall_posts[0] + if post.id != newest_wall_post: + new_posts = [] + + for post in current_wall_posts: + if post.id == newest_wall_post: + break + new_posts.append(post) + + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts[0].id + event.edit(arguments=tuple(old_arguments)) + + for new_post in new_posts: + asyncio.create_task(func(new_post)) + + async def on_group_change(self, func: Callable, current_group, event: Event): + if not current_group: await self.group.update() - has_changed = False - for attr, value in current_group.__dict__.items(): - if getattr(self.group, attr) != value: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + return event.edit(arguments=tuple(old_arguments)) + + await self.group.update() + + has_changed = False + for attr, value in current_group.__dict__.items(): + other_value = getattr(self.group, attr) + if attr == "shout": + if str(value) != str(other_value): has_changed = True - if has_changed: - asyncio.create_task(func(current_group, self.group)) + else: + continue + if other_value != value: + has_changed = True + + if has_changed: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + event.edit(arguments=tuple(old_arguments)) + asyncio.create_task(func(current_group, self.group)) """ async def on_audit_log(self, func: Callable, delay: int): diff --git a/ro_py/utilities/clientobject.py b/ro_py/utilities/clientobject.py index 376faefa..318d6bbf 100644 --- a/ro_py/utilities/clientobject.py +++ b/ro_py/utilities/clientobject.py @@ -1,6 +1,7 @@ import asyncio from ro_py.utilities.cache import Cache from ro_py.utilities.requests import Requests +from ro_py.events import Event, EventHandler class ClientObject: @@ -25,3 +26,4 @@ def __init__(self, client): """Reqests object for all web requests.""" self.evtloop = asyncio.new_event_loop() """Event loop for certain things.""" + self.event_handler = EventHandler() diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 74046c90..70f64739 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -26,6 +26,9 @@ def __init__(self, requests, data, handler=None, handler_args=None): if handler: self.data = handler(requests, self.data, handler_args) + def __getitem__(self, key): + return self.data[key] + class Pages: """ From 22421403bfd3af807d1e1f7654d82465e339db37 Mon Sep 17 00:00:00 2001 From: ira Date: Sun, 28 Feb 2021 22:05:20 +0100 Subject: [PATCH 483/518] add asynchronous iteration to Pages --- ro_py/friends.py | 4 +-- ro_py/utilities/pages.py | 66 +++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/ro_py/friends.py b/ro_py/friends.py index 7f2eae00..e0eca2da 100644 --- a/ro_py/friends.py +++ b/ro_py/friends.py @@ -5,8 +5,8 @@ class Friend(PartialUser): def __init__(self, cso, data): super().__init__(cso, data) - self.is_online = data["isOnline"] - self.is_deleted = data["isDeleted"] + self.is_online = data.get('isOnline') + self.is_deleted = data.get('isDeleted') self.description = data["description"] self.created = iso8601.parse_date(data["created"]) self.is_banned = data["isBanned"] diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 70f64739..b9a9a941 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -11,23 +11,36 @@ class SortOrder(enum.Enum): class Page: - """ - Represents a single page from a Pages object. - """ - def __init__(self, requests, data, handler=None, handler_args=None): - self.previous_page_cursor = data["previousPageCursor"] - """Cursor to navigate to the previous page.""" - self.next_page_cursor = data["nextPageCursor"] - """Cursor to navigate to the next page.""" - - self.data = data["data"] - """Raw data from this page.""" - - if handler: - self.data = handler(requests, self.data, handler_args) + """ + Represents a single page from a Pages object. + """ - def __getitem__(self, key): - return self.data[key] + def __init__(self, cso, data, pages, handler=None, handler_args=None): + self.cso = cso + """Client shared object.""" + self.previous_page_cursor = data["previousPageCursor"] + """Cursor to navigate to the previous page.""" + self.next_page_cursor = data["nextPageCursor"] + """Cursor to navigate to the next page.""" + self.data = data["data"] + """Raw data from this page.""" + self.pages = pages + """Pages object for iteration.""" + + self.handler = handler + self.handler_args = handler_args + if handler: + self.data = handler(self.cso, self.data, handler_args) + + def update(self, data): + self.previous_page_cursor = data["previousPageCursor"] + self.next_page_cursor = data["nextPageCursor"] + self.data = data["data"] + if self.handler: + self.data = self.handler(self.cso, data["data"], self.handler_args) + + def __getitem__(self, key): + return self.data[key] class Pages: @@ -60,6 +73,21 @@ def __init__(self, cso, url, sort_order=SortOrder.Ascending, limit=10, extra_par """Current page number.""" self.handler_args = handler_args self.data = None + self.i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i == len(self.data.data): + if not self.data.next_page_cursor: + self.i = 0 + raise StopAsyncIteration + await self.next() + self.i = 0 + data = self.data.data[self.i] + self.i += 1 + return data async def get_page(self, cursor=None): """ @@ -76,9 +104,13 @@ async def get_page(self, cursor=None): url=self.url, params=this_parameters ) + if self.data: + self.data.update(page_req.json()) + return self.data = Page( - requests=self.cso, + cso=self.cso, data=page_req.json(), + pages=self, handler=self.handler, handler_args=self.handler_args ) From b992ec6b8c41777a4734a8bf8e15ad4dc85cca27 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 1 Mar 2021 11:21:59 -0500 Subject: [PATCH 484/518] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4f59441d..7132cec0 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ You'll need to install `wxPython`, `wxasync` and `pytweening` to use the prompts ## Disclaimer We are not responsible for any malicious use of this library. If you use this library in a way that violates the [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use) your account may be punished. +If you use code from ro.py in your own library, please credit us! We're working our hardest to deliver this library, and crediting us is the best way to help support the project. ## Documentation You can view documentation for ro.py at [ro.py.jmksite.dev](https://ro.py.jmksite.dev/). From fadcea2433392f5fa6d3f02362f91c7684c9741a Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 1 Mar 2021 11:22:14 -0500 Subject: [PATCH 485/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7132cec0..2e3f2be0 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ You'll need to install `wxPython`, `wxasync` and `pytweening` to use the prompts ## Disclaimer We are not responsible for any malicious use of this library. -If you use this library in a way that violates the [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use) your account may be punished. +If you use this library in a way that violates the [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use) your account may be punished. If you use code from ro.py in your own library, please credit us! We're working our hardest to deliver this library, and crediting us is the best way to help support the project. ## Documentation From 03b302c60327a8534cbe1ffe700cdf61d72762ed Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 1 Mar 2021 17:36:28 -0500 Subject: [PATCH 486/518] Prepare gamepasses --- ro_py/gamepasses.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ro_py/gamepasses.py diff --git a/ro_py/gamepasses.py b/ro_py/gamepasses.py new file mode 100644 index 00000000..5dd7939c --- /dev/null +++ b/ro_py/gamepasses.py @@ -0,0 +1,2 @@ +from ro_py.utilities.url import url +endpoint = url("inventory") From 08e00f55b492c571ec528ccdbebc5e3f19c282bc Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 2 Mar 2021 10:14:55 +0100 Subject: [PATCH 487/518] event updates. --- ro_py/groups.py | 5 +++-- ro_py/users.py | 48 +++++++++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index adc559b9..42ebbd1b 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -501,7 +501,7 @@ def __init__(self, cso, group): self.cso = cso self.group = group - async def bind(self, func: Callable, event: EventTypes, delay: int = 15): + def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -523,7 +523,7 @@ async def bind(self, func: Callable, event: EventTypes, delay: int = 15): if event == EventTypes.on_group_change: event = Event(self.on_group_change, EventTypes.on_group_change, (func, None), delay) self.cso.event_handler.add_event(event) - await self.cso.event_handler.listen() + asyncio.create_task(self.cso.event_handler.listen()) async def on_join_request(self, func: Callable, old_req, event: Event): if not old_req: @@ -586,6 +586,7 @@ async def on_group_change(self, func: Callable, current_group, event: Event): await self.group.update() has_changed = False + for attr, value in current_group.__dict__.items(): other_value = getattr(self.group, attr) if attr == "shout": diff --git a/ro_py/users.py b/ro_py/users.py index 208e7f59..b91dd769 100644 --- a/ro_py/users.py +++ b/ro_py/users.py @@ -8,7 +8,7 @@ import iso8601 import asyncio from typing import Callable -from ro_py.events import EventTypes +from ro_py.events import EventTypes, Event from ro_py.bases.baseuser import BaseUser from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.clientobject import ClientObject @@ -41,6 +41,7 @@ def __init__(self, cso, user_id): self.created = None self.is_banned = None self.display_name = None + self.events = Events(cso, self) self.thumbnails = UserThumbnailGenerator(cso, user_id) async def update(self): @@ -64,7 +65,7 @@ def __init__(self, cso, user): self.cso = cso self.user = user - def bind(self, func: Callable, event: str, delay: int = 15): + def bind(self, func: Callable, event_type: str, delay: int = 15): """ Binds an event to the provided function. @@ -72,26 +73,31 @@ def bind(self, func: Callable, event: str, delay: int = 15): ---------- func : function Function that will be called when the event fires. - event : ro_py.events.EventTypes + event_type : ro_py.events.EventTypes The name of the event. delay : int How many seconds between requests. """ - if event == EventTypes.on_user_change: - return asyncio.create_task(self.on_user_change(func, delay)) - - async def on_user_change(self, func: Callable, delay: int): - old_user = copy.copy(await self.user.update()) - while True: - await asyncio.sleep(delay) - new_user = await self.user.update() - has_changed = False - for attr, value in old_user.__dict__.items(): - if getattr(new_user, attr) != value: - has_changed = True - if has_changed: - if asyncio.iscoroutinefunction(func): - await func(old_user, new_user) - else: - func(old_user, new_user) - old_user = copy.copy(new_user) + if event_type == EventTypes.on_user_change: + event = Event(self.on_user_change, EventTypes.on_group_change, (func, None), delay) + self.cso.event_handler.add_event(event) + + async def on_user_change(self, func: Callable, old_user, event: Event): + if not old_user: + old_user = copy.copy(await self.user.update()) + old_arguments = list(event.arguments) + old_arguments[1] = old_user + return event.edit(arguments=tuple(old_arguments)) + + new_user = await self.user.update() + has_changed = False + + for attr, value in old_user.__dict__.items(): + if getattr(new_user, attr) != value: + has_changed = True + + if has_changed: + old_arguments = list(event.arguments) + old_arguments[1] = new_user + event.edit(arguments=tuple(old_arguments)) + return asyncio.create_task(func(old_user, new_user)) From ea365587d416600e9d95cfc58421384082ba6779 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 3 Mar 2021 12:01:41 -0500 Subject: [PATCH 488/518] Presence test --- ro_py/presence.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 ro_py/presence.py diff --git a/ro_py/presence.py b/ro_py/presence.py new file mode 100644 index 00000000..fbc3547a --- /dev/null +++ b/ro_py/presence.py @@ -0,0 +1,22 @@ +import iso8601 + + +class Presence: + def __init__(self, cso, user, data): + self.cso = cso + self.requests = cso.requests + + self.user = user + self.user_presence_type = data["userPresenceType"] + self.place_id = data["placeId"] + self.root_place_id = data["rootPlaceId"] + self.game_id = data["gameId"] + self.universe_id = data["universeId"] + self.last_location = data["lastLocation"] + self.last_online = iso8601.parse_date(data["lastOnline"]) + + async def get_game(self): + if self.universe_id: + return await self.cso.client.get_game_by_universe_id(self.universe_id) + else: + return None From c0f8779d2e54f5f84cdf69dfef54f6df9fba8302 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 4 Mar 2021 11:30:44 -0500 Subject: [PATCH 489/518] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e3f2be0..ddac4ee0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

      Information | - Discord | + Discord | Requirements | Disclaimer | Documentation | From cdbe5065c549ce4f18d012ec08d08c37bf95aad1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 4 Mar 2021 12:38:29 -0500 Subject: [PATCH 490/518] Fixed invite linnk --- ro_py/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/__init__.py b/ro_py/__init__.py index 0a56cd14..95dc04a9 100644 --- a/ro_py/__init__.py +++ b/ro_py/__init__.py @@ -17,7 +17,7 @@

      Information | - Discord | + Discord | Requirements | Disclaimer | Documentation | From 3a15e3d9fe24ce576adb7a6190205387b270fa13 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sat, 6 Mar 2021 21:37:17 -0500 Subject: [PATCH 491/518] Fixed link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ddac4ee0..8b1ca8ec 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ro.py is an object oriented, asynchronous wrapper for the Roblox Web API (and ot ro.py allows you to automate much of what you would do on the Roblox website and on other Roblox-related websites. ## Update: ro.py on Discord -I’ve set up a small ro.py Discord server. It’s obviously very tiny, but some of you can be the first people to help found the server. If you need support for the library, you can ask your questions here if you need faster support. http://j-mk.ml/ro.py +I’ve set up a small ro.py Discord server. It’s obviously very tiny, but some of you can be the first people to help found the server. If you need support for the library, you can ask your questions here if you need faster support. http://jmk.gg/ro.py ## Get Started To begin, first import the client, which is the most essential part of ro.py, and initialize it like so: From 5fa62473fb77f9ba3aba67d1da9fcc58576b296f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 7 Mar 2021 09:21:03 -0500 Subject: [PATCH 492/518] Fixed param docs + removed notifications --- ro_py/client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ro_py/client.py b/ro_py/client.py index 6fae926a..72c72d11 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -19,7 +19,7 @@ from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings from ro_py.utilities.pages import Pages, SortOrder -from ro_py.notifications import NotificationReceiver +# from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError @@ -113,8 +113,6 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -263,7 +261,8 @@ def token_login(self, token): self.accountsettings = AccountSettings(self.cso) self.chat = ChatWrapper(self.cso) self.trade = TradesWrapper(self.cso) - self.notifications = NotificationReceiver(self.cso) + # self.notifications = NotificationReceiver(self.cso) + self.notifications = None async def user_login(self, username, password, token=None): """ From 820fcaf59b86fcbcd59e7f2b9cb7d7ebab58c1e8 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 7 Mar 2021 14:44:38 -0500 Subject: [PATCH 493/518] Goodbye notifications + removed dependency --- ro_py/notifications.py | 146 ----------------------------------------- setup_info.py | 1 - 2 files changed, 147 deletions(-) delete mode 100644 ro_py/notifications.py diff --git a/ro_py/notifications.py b/ro_py/notifications.py deleted file mode 100644 index 7887d136..00000000 --- a/ro_py/notifications.py +++ /dev/null @@ -1,146 +0,0 @@ -""" - -This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger -notification menu on the Roblox web client. - -.. warning:: - This part of ro.py may have bugs and I don't recommend relying on it for daily use. - Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond - to Roblox chat messages, which is pretty neat. -""" - -from ro_py.utilities.caseconvert import to_snake_case - -from signalrcore.hub_connection_builder import HubConnectionBuilder -from urllib.parse import quote -import json - - -class UnreadNotifications: - def __init__(self, data): - self.count = data["unreadNotifications"] - """Amount of unread notifications.""" - self.status_message = data["statusMessage"] - """Status message.""" - - -class RealtimeNotificationSettings: - def __init__(self, data): - self.primary_domain = data["primaryDomain"] - self.fallback_domain = data["fallbackDomain"] - - -class NotificationSettings: - def __init__(self, data): - self.notification_band_settings = data["notificationBandSettings"] - self.opted_out_notification_source_types = data["optedOutNotificationSourceTypes"] - self.opted_out_receiver_destination_types = data["optedOutReceiverDestinationTypes"] - - -class Notification: - """ - Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client. - """ - - def __init__(self, notification_data): - self.identifier = notification_data["C"] - self.hub = notification_data["M"][0]["H"] - self.type = None - self.rtype = notification_data["M"][0]["M"] - self.atype = notification_data["M"][0]["A"][0] - self.raw_data = json.loads(notification_data["M"][0]["A"][1]) - self.data = None - - if isinstance(self.raw_data, dict): - self.data = {} - for key, value in self.raw_data.items(): - self.data[to_snake_case(key)] = value - - if "type" in self.data: - self.type = self.data["type"] - elif "Type" in self.data: - self.type = self.data["Type"] - - elif isinstance(self.raw_data, list): - self.data = [] - for value in self.raw_data: - self.data.append(value) - - if len(self.data) > 0: - if "type" in self.data[0]: - self.type = self.data[0]["type"] - elif "Type" in self.data[0]: - self.type = self.data[0]["Type"] - - -class NotificationReceiver: - """ - This object is used to receive notifications. - This should only be generated once per client as to not duplicate notifications. - """ - - def __init__(self, cso): - self.cso = cso - self.requests = cso.requests - self.evtloop = cso.evtloop - self.negotiate_request = None - self.wss_url = None - self.connection = None - - async def get_unread_notifications(self): - unread_req = await self.requests.get( - url="https://notifications.roblox.com/v2/stream-notifications/unread-count" - ) - return UnreadNotifications(unread_req.json()) - - async def initialize(self): - self.negotiate_request = await self.requests.get( - url="https://realtime.roblox.com/notifications/negotiate" - "?clientProtocol=1.5" - "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D", - cookies=self.requests.session.cookies - ) - self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \ - f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \ - f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D" - self.connection = HubConnectionBuilder() - self.connection.with_url( - self.wss_url, - options={ - "headers": { - "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};" - }, - "skip_negotiation": False - } - ) - - def on_message(_self, raw_notification): - """ - Internal callback when a message is received. - """ - try: - notification_json = json.loads(raw_notification) - except json.decoder.JSONDecodeError: - return - if len(notification_json) > 0: - notification = Notification(notification_json) - self.evtloop.run_until_complete(self.on_notification(notification)) - else: - return - - self.connection.with_automatic_reconnect({ - "type": "raw", - "keep_alive_interval": 10, - "reconnect_interval": 5, - "max_attempts": 5 - }).build() - - self.connection.hub.on_message = on_message - - self.connection.start() - - def close(self): - """ - Closes the connection and stops receiving notifications. - """ - self.connection.stop() diff --git a/setup_info.py b/setup_info.py index 01feaacf..7174c344 100644 --- a/setup_info.py +++ b/setup_info.py @@ -28,7 +28,6 @@ "install_requires": [ "httpx", "iso8601", - "signalrcore", "lxml" ] } From 6df5e9cc73e4189b5157442a5d038da7e6014bfe Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 7 Mar 2021 14:49:33 -0500 Subject: [PATCH 494/518] Updated documentation for v1.2.0 --- docs/accountinformation.html | 3 +- docs/accountsettings.html | 3 +- docs/assets.html | 38 +- docs/badges.html | 99 +++- docs/bases/baseasset.html | 174 +++++++ docs/bases/basetrade.html | 390 ++++++++++++++ docs/bases/baseuser.html | 865 +++++++++++++++++++++++++++++++ docs/bases/index.html | 143 +++++ docs/chat.html | 5 +- docs/client.html | 616 +++++++++++++++------- docs/economy.html | 3 +- docs/events.html | 229 +++++++- docs/extensions/bots.html | 5 + docs/friends.html | 294 +++++++++++ docs/gamepasses.html | 117 +++++ docs/gamepersistence.html | 3 +- docs/games.html | 163 +++++- docs/groups.html | 583 +++++++++++++-------- docs/index.html | 33 +- docs/notifications.html | 459 ---------------- docs/presence.html | 200 +++++++ docs/roles.html | 5 +- docs/thumbnails.html | 3 +- docs/trades.html | 318 ++---------- docs/users.html | 655 ++--------------------- docs/utilities/asset_type.html | 508 ++++++++++++++++-- docs/utilities/clientobject.html | 10 +- docs/utilities/index.html | 5 + docs/utilities/pages.html | 161 +++++- docs/utilities/requests.html | 6 +- docs/utilities/url.html | 147 ++++++ docs/wall.html | 10 +- 32 files changed, 4354 insertions(+), 1899 deletions(-) create mode 100644 docs/bases/baseasset.html create mode 100644 docs/bases/basetrade.html create mode 100644 docs/bases/baseuser.html create mode 100644 docs/bases/index.html create mode 100644 docs/friends.html create mode 100644 docs/gamepasses.html delete mode 100644 docs/notifications.html create mode 100644 docs/presence.html create mode 100644 docs/utilities/url.html diff --git a/docs/accountinformation.html b/docs/accountinformation.html index 6d34b55e..5443de8b 100644 --- a/docs/accountinformation.html +++ b/docs/accountinformation.html @@ -88,7 +88,8 @@

      Module ro_py.accountinformation

      from datetime import datetime from ro_py.gender import RobloxGender -endpoint = "https://accountinformation.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("accountinformation") class AccountInformationMetadata: diff --git a/docs/accountsettings.html b/docs/accountsettings.html index 1d1a46fb..e57ed231 100644 --- a/docs/accountsettings.html +++ b/docs/accountsettings.html @@ -87,7 +87,8 @@

      Module ro_py.accountsettings

      import enum -endpoint = "https://accountsettings.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("accountsettings") class PrivacyLevel(enum.Enum): diff --git a/docs/assets.html b/docs/assets.html index bd9a8e7f..33e0cad8 100644 --- a/docs/assets.html +++ b/docs/assets.html @@ -88,12 +88,13 @@

      Module ro_py.assets

      from ro_py.utilities.clientobject import ClientObject from ro_py.utilities.errors import NotLimitedError from ro_py.economy import LimitedResaleData -from ro_py.utilities.asset_type import asset_types +from ro_py.utilities.asset_type import AssetTypes import iso8601 import asyncio import copy -endpoint = "https://api.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("api") class Reseller: @@ -108,13 +109,14 @@

      Module ro_py.assets

      Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.utilities.clientobject.ClientSharedObject + CSO asset_id ID of the asset. """ def __init__(self, cso, asset_id): + super().__init__() self.id = asset_id self.cso = cso self.requests = cso.requests @@ -157,7 +159,10 @@

      Module ro_py.assets

      self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] - self.asset_type_name = asset_types[self.asset_type_id] + for key, value in AssetTypes._member_map_.items(): + if value == self.asset_type_id: + self.asset_type_name = key + # if asset_info["Creator"]["CreatorType"] == "User": # self.creator = User(self.requests, asset_info["Creator"]["Id"]) # if asset_info["Creator"]["CreatorType"] == "Group": @@ -266,8 +271,8 @@

      Classes

      Represents an asset.

      Parameters

      -
      requests : Requests
      -
      Requests object to use for API requests.
      +
      cso : ClientSharedObject
      +
      CSO
      asset_id
      ID of the asset.
      @@ -281,13 +286,14 @@

      Parameters

      Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.utilities.clientobject.ClientSharedObject + CSO asset_id ID of the asset. """ def __init__(self, cso, asset_id): + super().__init__() self.id = asset_id self.cso = cso self.requests = cso.requests @@ -330,7 +336,10 @@

      Parameters

      self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] - self.asset_type_name = asset_types[self.asset_type_id] + for key, value in AssetTypes._member_map_.items(): + if value == self.asset_type_id: + self.asset_type_name = key + # if asset_info["Creator"]["CreatorType"] == "User": # self.creator = User(self.requests, asset_info["Creator"]["Id"]) # if asset_info["Creator"]["CreatorType"] == "Group": @@ -477,7 +486,10 @@

      Returns

      self.name = asset_info["Name"] self.description = asset_info["Description"] self.asset_type_id = asset_info["AssetTypeId"] - self.asset_type_name = asset_types[self.asset_type_id] + for key, value in AssetTypes._member_map_.items(): + if value == self.asset_type_id: + self.asset_type_name = key + # if asset_info["Creator"]["CreatorType"] == "User": # self.creator = User(self.requests, asset_info["Creator"]["Id"]) # if asset_info["Creator"]["CreatorType"] == "Group": @@ -601,8 +613,8 @@

      Methods

      Represents an asset.

      Parameters

      -
      requests : Requests
      -
      Requests object to use for API requests.
      +
      cso : ClientSharedObject
      +
      CSO
      asset_id
      ID of the asset.
      diff --git a/docs/badges.html b/docs/badges.html index 5ca43e9f..036e92b9 100644 --- a/docs/badges.html +++ b/docs/badges.html @@ -86,8 +86,10 @@

      Module ro_py.badges

      """ from ro_py.utilities.clientobject import ClientObject +from ro_py.bases.baseasset import BaseAsset -endpoint = "https://badges.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("badges") class BadgeStatistics: @@ -100,18 +102,20 @@

      Module ro_py.badges

      self.win_rate_percentage = win_rate_percentage -class Badge(ClientObject): +class Badge(ClientObject, BaseAsset): """ Represents a game-awarded badge. Parameters ---------- - requests : ro_py.utilities.requests.Requests - Requests object to use for API requests. + cso : ro_py.utilities.clientobject.ClientSharedObject + ClientSharedObject. badge_id ID of the badge. """ def __init__(self, cso, badge_id): + ClientObject.__init__(self) + BaseAsset.__init__(self) self.id = badge_id self.cso = cso self.requests = cso.requests @@ -138,7 +142,23 @@

      Module ro_py.badges

      statistics_info["pastDayAwardedCount"], statistics_info["awardedCount"], statistics_info["winRatePercentage"] - ) + ) + + async def owned_by(self, user): + """ + Checks if a user was awarded this badge and grabs the time that they were awarded it. + Functionally identical to ro_py.users.User.has_badge. + + Parameters + ---------- + user: ro_py.users.BaseUser + User to check badge ownership. + + Returns + ------- + tuple[bool, datetime.datetime] + """ + return await user.has_badge(self)
    @@ -158,8 +178,8 @@

    Classes

    Represents a game-awarded badge.

    Parameters

    -
    requests : Requests
    -
    Requests object to use for API requests.
    +
    cso : ClientSharedObject
    +
    ClientSharedObject.
    badge_id
    ID of the badge.
    @@ -167,18 +187,20 @@

    Parameters

    Expand source code -
    class Badge(ClientObject):
    +
    class Badge(ClientObject, BaseAsset):
         """
         Represents a game-awarded badge.
     
         Parameters
         ----------
    -    requests : ro_py.utilities.requests.Requests
    -        Requests object to use for API requests.
    +    cso : ro_py.utilities.clientobject.ClientSharedObject
    +        ClientSharedObject.
         badge_id
             ID of the badge.
         """
         def __init__(self, cso, badge_id):
    +        ClientObject.__init__(self)
    +        BaseAsset.__init__(self)
             self.id = badge_id
             self.cso = cso
             self.requests = cso.requests
    @@ -205,14 +227,68 @@ 

    Parameters

    statistics_info["pastDayAwardedCount"], statistics_info["awardedCount"], statistics_info["winRatePercentage"] - )
    + ) + + async def owned_by(self, user): + """ + Checks if a user was awarded this badge and grabs the time that they were awarded it. + Functionally identical to ro_py.users.User.has_badge. + + Parameters + ---------- + user: ro_py.users.BaseUser + User to check badge ownership. + + Returns + ------- + tuple[bool, datetime.datetime] + """ + return await user.has_badge(self)

    Ancestors

    Methods

    +
    +async def owned_by(self, user) +
    +
    +

    Checks if a user was awarded this badge and grabs the time that they were awarded it. +Functionally identical to ro_py.users.User.has_badge.

    +

    Parameters

    +
    +
    user : ro_py.users.BaseUser
    +
    User to check badge ownership.
    +
    +

    Returns

    +
    +
    tuple[bool, datetime.datetime]
    +
     
    +
    +
    + +Expand source code + +
    async def owned_by(self, user):
    +    """
    +    Checks if a user was awarded this badge and grabs the time that they were awarded it.
    +    Functionally identical to ro_py.users.User.has_badge.
    +
    +    Parameters
    +    ----------
    +    user: ro_py.users.BaseUser
    +        User to check badge ownership.
    +
    +    Returns
    +    -------
    +    tuple[bool, datetime.datetime]
    +    """
    +    return await user.has_badge(self)
    +
    +
    async def update(self)
    @@ -287,6 +363,7 @@

    Index

  • Badge

  • diff --git a/docs/bases/baseasset.html b/docs/bases/baseasset.html new file mode 100644 index 00000000..a061bcd9 --- /dev/null +++ b/docs/bases/baseasset.html @@ -0,0 +1,174 @@ + + + + + + +ro_py.bases.baseasset API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.bases.baseasset

    +
    +
    +
    + +Expand source code + +
    class BaseAsset:
    +    def __init__(self):
    +        self.id = None
    +        self.cso = None
    +
    +    async def to_asset(self):
    +        return await self.cso.client.get_asset(self.id)
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class BaseAsset +
    +
    +
    +
    + +Expand source code + +
    class BaseAsset:
    +    def __init__(self):
    +        self.id = None
    +        self.cso = None
    +
    +    async def to_asset(self):
    +        return await self.cso.client.get_asset(self.id)
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +async def to_asset(self) +
    +
    +
    +
    + +Expand source code + +
    async def to_asset(self):
    +    return await self.cso.client.get_asset(self.id)
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/bases/basetrade.html b/docs/bases/basetrade.html new file mode 100644 index 00000000..41ea747b --- /dev/null +++ b/docs/bases/basetrade.html @@ -0,0 +1,390 @@ + + + + + + +ro_py.bases.basetrade API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.bases.basetrade

    +
    +
    +
    + +Expand source code + +
    from ro_py.assets import Asset
    +import iso8601
    +
    +from ro_py.utilities.url import url
    +endpoint = url("trades")
    +
    +
    +class PartialTrade:
    +    def __init__(self, cso, data):
    +        from ro_py.bases.baseuser import PartialUser
    +        self.cso = cso
    +        self.requests = cso.requests
    +        self.trade_id = data['id']
    +        self.user = PartialUser(cso, data['user'])
    +        self.created = iso8601.parse_date(data['created'])
    +        self.expiration = None
    +        if "expiration" in data:
    +            self.expiration = iso8601.parse_date(data['expiration'])
    +        self.status = data['status']
    +
    +    async def accept(self) -> bool:
    +        """
    +        accepts a trade requests
    +        :returns: true/false
    +        """
    +        accept_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +        )
    +        return accept_req.status_code == 200
    +
    +    async def decline(self) -> bool:
    +        """
    +        decline a trade requests
    +        :returns: true/false
    +        """
    +        decline_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +        )
    +        return decline_req.status_code == 200
    +
    +    async def expand(self):
    +        """
    +        Gets a more detailed trade request
    +
    +        Returns
    +        -------
    +        ro_py.trades.Trade
    +        """
    +
    +        from ro_py.trades import Trade
    +        expend_req = await self.requests.get(
    +            url=endpoint + f"/v1/trades/{self.trade_id}"
    +        )
    +        data = expend_req.json()
    +
    +        # generate a user class and update it
    +        sender = await self.cso.client.get_user(data['user']['id'])
    +        await sender.update()
    +
    +        # load items that will be/have been sent and items that you will/have receive(d)
    +        receive_items, send_items = [], []
    +        for items_0 in data['offers'][0]['userAssets']:
    +            item_0 = Asset(self.cso, items_0['assetId'])
    +            await item_0.update()
    +            receive_items.append(item_0)
    +
    +        for items_1 in data['offers'][1]['userAssets']:
    +            item_1 = Asset(self.cso, items_1['assetId'])
    +            await item_1.update()
    +            send_items.append(item_1)
    +
    +        return Trade(
    +            self.cso,
    +            data,
    +            sender,
    +            send_items,
    +            receive_items
    +        )
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class PartialTrade +(cso, data) +
    +
    +
    +
    + +Expand source code + +
    class PartialTrade:
    +    def __init__(self, cso, data):
    +        from ro_py.bases.baseuser import PartialUser
    +        self.cso = cso
    +        self.requests = cso.requests
    +        self.trade_id = data['id']
    +        self.user = PartialUser(cso, data['user'])
    +        self.created = iso8601.parse_date(data['created'])
    +        self.expiration = None
    +        if "expiration" in data:
    +            self.expiration = iso8601.parse_date(data['expiration'])
    +        self.status = data['status']
    +
    +    async def accept(self) -> bool:
    +        """
    +        accepts a trade requests
    +        :returns: true/false
    +        """
    +        accept_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +        )
    +        return accept_req.status_code == 200
    +
    +    async def decline(self) -> bool:
    +        """
    +        decline a trade requests
    +        :returns: true/false
    +        """
    +        decline_req = await self.requests.post(
    +            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +        )
    +        return decline_req.status_code == 200
    +
    +    async def expand(self):
    +        """
    +        Gets a more detailed trade request
    +
    +        Returns
    +        -------
    +        ro_py.trades.Trade
    +        """
    +
    +        from ro_py.trades import Trade
    +        expend_req = await self.requests.get(
    +            url=endpoint + f"/v1/trades/{self.trade_id}"
    +        )
    +        data = expend_req.json()
    +
    +        # generate a user class and update it
    +        sender = await self.cso.client.get_user(data['user']['id'])
    +        await sender.update()
    +
    +        # load items that will be/have been sent and items that you will/have receive(d)
    +        receive_items, send_items = [], []
    +        for items_0 in data['offers'][0]['userAssets']:
    +            item_0 = Asset(self.cso, items_0['assetId'])
    +            await item_0.update()
    +            receive_items.append(item_0)
    +
    +        for items_1 in data['offers'][1]['userAssets']:
    +            item_1 = Asset(self.cso, items_1['assetId'])
    +            await item_1.update()
    +            send_items.append(item_1)
    +
    +        return Trade(
    +            self.cso,
    +            data,
    +            sender,
    +            send_items,
    +            receive_items
    +        )
    +
    +

    Methods

    +
    +
    +async def accept(self) ‑> bool +
    +
    +

    accepts a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def accept(self) -> bool:
    +    """
    +    accepts a trade requests
    +    :returns: true/false
    +    """
    +    accept_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    +    )
    +    return accept_req.status_code == 200
    +
    +
    +
    +async def decline(self) ‑> bool +
    +
    +

    decline a trade requests +:returns: true/false

    +
    + +Expand source code + +
    async def decline(self) -> bool:
    +    """
    +    decline a trade requests
    +    :returns: true/false
    +    """
    +    decline_req = await self.requests.post(
    +        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    +    )
    +    return decline_req.status_code == 200
    +
    +
    +
    +async def expand(self) +
    +
    +

    Gets a more detailed trade request

    +

    Returns

    +
    +
    Trade
    +
     
    +
    +
    + +Expand source code + +
    async def expand(self):
    +    """
    +    Gets a more detailed trade request
    +
    +    Returns
    +    -------
    +    ro_py.trades.Trade
    +    """
    +
    +    from ro_py.trades import Trade
    +    expend_req = await self.requests.get(
    +        url=endpoint + f"/v1/trades/{self.trade_id}"
    +    )
    +    data = expend_req.json()
    +
    +    # generate a user class and update it
    +    sender = await self.cso.client.get_user(data['user']['id'])
    +    await sender.update()
    +
    +    # load items that will be/have been sent and items that you will/have receive(d)
    +    receive_items, send_items = [], []
    +    for items_0 in data['offers'][0]['userAssets']:
    +        item_0 = Asset(self.cso, items_0['assetId'])
    +        await item_0.update()
    +        receive_items.append(item_0)
    +
    +    for items_1 in data['offers'][1]['userAssets']:
    +        item_1 = Asset(self.cso, items_1['assetId'])
    +        await item_1.update()
    +        send_items.append(item_1)
    +
    +    return Trade(
    +        self.cso,
    +        data,
    +        sender,
    +        send_items,
    +        receive_items
    +    )
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/bases/baseuser.html b/docs/bases/baseuser.html new file mode 100644 index 00000000..971916a4 --- /dev/null +++ b/docs/bases/baseuser.html @@ -0,0 +1,865 @@ + + + + + + +ro_py.bases.baseuser API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.bases.baseuser

    +
    +
    +
    + +Expand source code + +
    from ro_py.robloxbadges import RobloxBadge
    +from ro_py.utilities.pages import Pages
    +from ro_py.assets import UserAsset
    +from ro_py.badges import Badge
    +import iso8601
    +
    +from ro_py.utilities.url import url
    +endpoint = url("users")
    +
    +
    +def limited_handler(requests, data, args):
    +    assets = []
    +    for asset in data:
    +        assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId']))
    +    return assets
    +
    +
    +class BaseUser:
    +    def __init__(self, cso, user_id):
    +        self.cso = cso
    +        self.requests = cso.requests
    +        self.id = user_id
    +        self.profile_url = f"https://www.roblox.com/users/{self.id}/profile"
    +
    +    async def expand(self):
    +        """
    +        Expands into a full User object.
    +
    +        Returns
    +        ------
    +        ro_py.users.User
    +        """
    +        return await self.cso.client.get_user(self.id)
    +
    +    async def get_roblox_badges(self) :
    +        """
    +        Gets the user's roblox badges.
    +
    +        Returns
    +        -------
    +        List[ro_py.robloxbadges.RobloxBadge]
    +        """
    +        roblox_badges_req = await self.requests.get(
    +            f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    +        roblox_badges = []
    +        for roblox_badge_data in roblox_badges_req.json():
    +            roblox_badges.append(RobloxBadge(roblox_badge_data))
    +        return roblox_badges
    +
    +    async def get_friends_count(self) -> int:
    +        """
    +        Gets the user's friends count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
    +        friends_count = friends_count_req.json()["count"]
    +        return friends_count
    +
    +    async def get_followers_count(self) -> int:
    +        """
    +        Gets the user's followers count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
    +        followers_count = followers_count_req.json()["count"]
    +        return followers_count
    +
    +    async def get_followings_count(self) -> int:
    +        """
    +        Gets the user's followings count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        followings_count_req = await self.requests.get(
    +            f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    +        followings_count = followings_count_req.json()["count"]
    +        return followings_count
    +
    +    async def get_friends(self):
    +        """
    +        Gets the user's friends.
    +
    +        Returns
    +        -------
    +        List[ro_py.users.Friend]
    +        """
    +        from ro_py.friends import Friend  # Hacky circular import fix
    +        friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
    +        friends_raw = friends_req.json()["data"]
    +        friends_list = []
    +        for friend_raw in friends_raw:
    +            friends_list.append(Friend(self.cso, friend_raw))
    +        return friends_list
    +
    +    async def get_groups(self):
    +        """
    +        Gets the user's groups.
    +
    +        Returns
    +        -------
    +        List[ro_py.groups.PartialGroup]
    +        """
    +        from ro_py.groups import PartialGroup
    +        member_req = await self.requests.get(
    +            url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
    +        )
    +        data = member_req.json()
    +        groups = []
    +        for group in data['data']:
    +            group = group['group']
    +            groups.append(PartialGroup(self.cso, group))
    +        return groups
    +
    +    async def get_limiteds(self):
    +        """
    +        Gets all limiteds the user owns.
    +
    +        Returns
    +        -------
    +        bababooey
    +        """
    +        return Pages(
    +            cso=self.cso,
    +            url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
    +            handler=limited_handler
    +        )
    +
    +    async def get_status(self):
    +        """
    +        Gets the user's status.
    +
    +        Returns
    +        -------
    +        str
    +        """
    +        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    +        return status_req.json()["status"]
    +
    +    async def has_badge(self, badge: Badge):
    +        """
    +        Checks if a user was awarded a badge and grabs the time that they were awarded it.
    +        Functionally identical to ro_py.badges.Badge.owned_by.
    +
    +        Parameters
    +        ----------
    +        badge: ro_py.badges.Badge
    +            Badge to check ownership of.
    +
    +        Returns
    +        -------
    +        tuple[bool, datetime.datetime]
    +        """
    +        has_badge_req = await self.requests.get(
    +            url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates",
    +            params={
    +                "badgeIds": badge.id
    +            }
    +        )
    +        has_badge_data = has_badge_req.json()["data"]
    +        if len(has_badge_data) >= 1:
    +            return True, iso8601.parse_date(has_badge_data[0]["awardedDate"])
    +        else:
    +            return False, None
    +
    +
    +class PartialUser(BaseUser):
    +    def __init__(self, cso, data):
    +        self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId")
    +        super().__init__(cso, self.id)
    +        self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username")
    +        self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name")
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def limited_handler(requests, data, args) +
    +
    +
    +
    + +Expand source code + +
    def limited_handler(requests, data, args):
    +    assets = []
    +    for asset in data:
    +        assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId']))
    +    return assets
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class BaseUser +(cso, user_id) +
    +
    +
    +
    + +Expand source code + +
    class BaseUser:
    +    def __init__(self, cso, user_id):
    +        self.cso = cso
    +        self.requests = cso.requests
    +        self.id = user_id
    +        self.profile_url = f"https://www.roblox.com/users/{self.id}/profile"
    +
    +    async def expand(self):
    +        """
    +        Expands into a full User object.
    +
    +        Returns
    +        ------
    +        ro_py.users.User
    +        """
    +        return await self.cso.client.get_user(self.id)
    +
    +    async def get_roblox_badges(self) :
    +        """
    +        Gets the user's roblox badges.
    +
    +        Returns
    +        -------
    +        List[ro_py.robloxbadges.RobloxBadge]
    +        """
    +        roblox_badges_req = await self.requests.get(
    +            f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    +        roblox_badges = []
    +        for roblox_badge_data in roblox_badges_req.json():
    +            roblox_badges.append(RobloxBadge(roblox_badge_data))
    +        return roblox_badges
    +
    +    async def get_friends_count(self) -> int:
    +        """
    +        Gets the user's friends count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
    +        friends_count = friends_count_req.json()["count"]
    +        return friends_count
    +
    +    async def get_followers_count(self) -> int:
    +        """
    +        Gets the user's followers count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
    +        followers_count = followers_count_req.json()["count"]
    +        return followers_count
    +
    +    async def get_followings_count(self) -> int:
    +        """
    +        Gets the user's followings count.
    +
    +        Returns
    +        -------
    +        int
    +        """
    +        followings_count_req = await self.requests.get(
    +            f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    +        followings_count = followings_count_req.json()["count"]
    +        return followings_count
    +
    +    async def get_friends(self):
    +        """
    +        Gets the user's friends.
    +
    +        Returns
    +        -------
    +        List[ro_py.users.Friend]
    +        """
    +        from ro_py.friends import Friend  # Hacky circular import fix
    +        friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
    +        friends_raw = friends_req.json()["data"]
    +        friends_list = []
    +        for friend_raw in friends_raw:
    +            friends_list.append(Friend(self.cso, friend_raw))
    +        return friends_list
    +
    +    async def get_groups(self):
    +        """
    +        Gets the user's groups.
    +
    +        Returns
    +        -------
    +        List[ro_py.groups.PartialGroup]
    +        """
    +        from ro_py.groups import PartialGroup
    +        member_req = await self.requests.get(
    +            url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
    +        )
    +        data = member_req.json()
    +        groups = []
    +        for group in data['data']:
    +            group = group['group']
    +            groups.append(PartialGroup(self.cso, group))
    +        return groups
    +
    +    async def get_limiteds(self):
    +        """
    +        Gets all limiteds the user owns.
    +
    +        Returns
    +        -------
    +        bababooey
    +        """
    +        return Pages(
    +            cso=self.cso,
    +            url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
    +            handler=limited_handler
    +        )
    +
    +    async def get_status(self):
    +        """
    +        Gets the user's status.
    +
    +        Returns
    +        -------
    +        str
    +        """
    +        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    +        return status_req.json()["status"]
    +
    +    async def has_badge(self, badge: Badge):
    +        """
    +        Checks if a user was awarded a badge and grabs the time that they were awarded it.
    +        Functionally identical to ro_py.badges.Badge.owned_by.
    +
    +        Parameters
    +        ----------
    +        badge: ro_py.badges.Badge
    +            Badge to check ownership of.
    +
    +        Returns
    +        -------
    +        tuple[bool, datetime.datetime]
    +        """
    +        has_badge_req = await self.requests.get(
    +            url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates",
    +            params={
    +                "badgeIds": badge.id
    +            }
    +        )
    +        has_badge_data = has_badge_req.json()["data"]
    +        if len(has_badge_data) >= 1:
    +            return True, iso8601.parse_date(has_badge_data[0]["awardedDate"])
    +        else:
    +            return False, None
    +
    +

    Subclasses

    + +

    Methods

    +
    +
    +async def expand(self) +
    +
    +

    Expands into a full User object.

    +

    Returns

    +
    +
    User
    +
     
    +
    +
    + +Expand source code + +
    async def expand(self):
    +    """
    +    Expands into a full User object.
    +
    +    Returns
    +    ------
    +    ro_py.users.User
    +    """
    +    return await self.cso.client.get_user(self.id)
    +
    +
    +
    +async def get_followers_count(self) ‑> int +
    +
    +

    Gets the user's followers count.

    +

    Returns

    +
    +
    int
    +
     
    +
    +
    + +Expand source code + +
    async def get_followers_count(self) -> int:
    +    """
    +    Gets the user's followers count.
    +
    +    Returns
    +    -------
    +    int
    +    """
    +    followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
    +    followers_count = followers_count_req.json()["count"]
    +    return followers_count
    +
    +
    +
    +async def get_followings_count(self) ‑> int +
    +
    +

    Gets the user's followings count.

    +

    Returns

    +
    +
    int
    +
     
    +
    +
    + +Expand source code + +
    async def get_followings_count(self) -> int:
    +    """
    +    Gets the user's followings count.
    +
    +    Returns
    +    -------
    +    int
    +    """
    +    followings_count_req = await self.requests.get(
    +        f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    +    followings_count = followings_count_req.json()["count"]
    +    return followings_count
    +
    +
    +
    +async def get_friends(self) +
    +
    +

    Gets the user's friends.

    +

    Returns

    +
    +
    List[ro_py.users.Friend]
    +
     
    +
    +
    + +Expand source code + +
    async def get_friends(self):
    +    """
    +    Gets the user's friends.
    +
    +    Returns
    +    -------
    +    List[ro_py.users.Friend]
    +    """
    +    from ro_py.friends import Friend  # Hacky circular import fix
    +    friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
    +    friends_raw = friends_req.json()["data"]
    +    friends_list = []
    +    for friend_raw in friends_raw:
    +        friends_list.append(Friend(self.cso, friend_raw))
    +    return friends_list
    +
    +
    +
    +async def get_friends_count(self) ‑> int +
    +
    +

    Gets the user's friends count.

    +

    Returns

    +
    +
    int
    +
     
    +
    +
    + +Expand source code + +
    async def get_friends_count(self) -> int:
    +    """
    +    Gets the user's friends count.
    +
    +    Returns
    +    -------
    +    int
    +    """
    +    friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
    +    friends_count = friends_count_req.json()["count"]
    +    return friends_count
    +
    +
    +
    +async def get_groups(self) +
    +
    +

    Gets the user's groups.

    +

    Returns

    +
    +
    List[PartialGroup]
    +
     
    +
    +
    + +Expand source code + +
    async def get_groups(self):
    +    """
    +    Gets the user's groups.
    +
    +    Returns
    +    -------
    +    List[ro_py.groups.PartialGroup]
    +    """
    +    from ro_py.groups import PartialGroup
    +    member_req = await self.requests.get(
    +        url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
    +    )
    +    data = member_req.json()
    +    groups = []
    +    for group in data['data']:
    +        group = group['group']
    +        groups.append(PartialGroup(self.cso, group))
    +    return groups
    +
    +
    +
    +async def get_limiteds(self) +
    +
    +

    Gets all limiteds the user owns.

    +

    Returns

    +
    +
    bababooey
    +
     
    +
    +
    + +Expand source code + +
    async def get_limiteds(self):
    +    """
    +    Gets all limiteds the user owns.
    +
    +    Returns
    +    -------
    +    bababooey
    +    """
    +    return Pages(
    +        cso=self.cso,
    +        url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
    +        handler=limited_handler
    +    )
    +
    +
    +
    +async def get_roblox_badges(self) +
    +
    +

    Gets the user's roblox badges.

    +

    Returns

    +
    +
    List[RobloxBadge]
    +
     
    +
    +
    + +Expand source code + +
    async def get_roblox_badges(self) :
    +    """
    +    Gets the user's roblox badges.
    +
    +    Returns
    +    -------
    +    List[ro_py.robloxbadges.RobloxBadge]
    +    """
    +    roblox_badges_req = await self.requests.get(
    +        f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    +    roblox_badges = []
    +    for roblox_badge_data in roblox_badges_req.json():
    +        roblox_badges.append(RobloxBadge(roblox_badge_data))
    +    return roblox_badges
    +
    +
    +
    +async def get_status(self) +
    +
    +

    Gets the user's status.

    +

    Returns

    +
    +
    str
    +
     
    +
    +
    + +Expand source code + +
    async def get_status(self):
    +    """
    +    Gets the user's status.
    +
    +    Returns
    +    -------
    +    str
    +    """
    +    status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    +    return status_req.json()["status"]
    +
    +
    +
    +async def has_badge(self, badge: Badge) +
    +
    +

    Checks if a user was awarded a badge and grabs the time that they were awarded it. +Functionally identical to ro_py.badges.Badge.owned_by.

    +

    Parameters

    +
    +
    badge : Badge
    +
    Badge to check ownership of.
    +
    +

    Returns

    +
    +
    tuple[bool, datetime.datetime]
    +
     
    +
    +
    + +Expand source code + +
    async def has_badge(self, badge: Badge):
    +    """
    +    Checks if a user was awarded a badge and grabs the time that they were awarded it.
    +    Functionally identical to ro_py.badges.Badge.owned_by.
    +
    +    Parameters
    +    ----------
    +    badge: ro_py.badges.Badge
    +        Badge to check ownership of.
    +
    +    Returns
    +    -------
    +    tuple[bool, datetime.datetime]
    +    """
    +    has_badge_req = await self.requests.get(
    +        url=url("badges") + f"v1/users/{self.id}/badges/awarded-dates",
    +        params={
    +            "badgeIds": badge.id
    +        }
    +    )
    +    has_badge_data = has_badge_req.json()["data"]
    +    if len(has_badge_data) >= 1:
    +        return True, iso8601.parse_date(has_badge_data[0]["awardedDate"])
    +    else:
    +        return False, None
    +
    +
    +
    +
    +
    +class PartialUser +(cso, data) +
    +
    +
    +
    + +Expand source code + +
    class PartialUser(BaseUser):
    +    def __init__(self, cso, data):
    +        self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId")
    +        super().__init__(cso, self.id)
    +        self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username")
    +        self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name")
    +
    +

    Ancestors

    + +

    Subclasses

    + +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/bases/index.html b/docs/bases/index.html new file mode 100644 index 00000000..2b3c0e80 --- /dev/null +++ b/docs/bases/index.html @@ -0,0 +1,143 @@ + + + + + + +ro_py.bases API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.bases

    +
    +
    +

    This folder houses base/partial objects that other parts of ro.py inherit.

    +
    + +Expand source code + +
    """
    +
    +This folder houses base/partial objects that other parts of ro.py inherit.
    +
    +"""
    +
    +
    +
    +

    Sub-modules

    +
    +
    ro_py.bases.baseasset
    +
    +
    +
    +
    ro_py.bases.basetrade
    +
    +
    +
    +
    ro_py.bases.baseuser
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/chat.html b/docs/chat.html index 34f09f56..229317a0 100644 --- a/docs/chat.html +++ b/docs/chat.html @@ -86,9 +86,10 @@

    Module ro_py.chat

    """ from ro_py.utilities.errors import ChatError -from ro_py.users import PartialUser +from ro_py.bases.baseuser import PartialUser -endpoint = "https://chat.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("chat") class ChatSettings: diff --git a/docs/client.html b/docs/client.html index d213a192..295fd2d4 100644 --- a/docs/client.html +++ b/docs/client.html @@ -86,23 +86,33 @@

    Module ro_py.client

    """ from ro_py.games import Game +from ro_py.users import User from ro_py.groups import Group from ro_py.assets import Asset from ro_py.badges import Badge from ro_py.chat import ChatWrapper -from ro_py.users import PartialUser from ro_py.events import EventTypes from ro_py.trades import TradesWrapper +from ro_py.friends import FriendRequest from ro_py.captcha import CaptchaMetadata from ro_py.utilities.cache import CacheType +from ro_py.bases.baseuser import PartialUser from ro_py.captcha import UnsolvedLoginCaptcha from ro_py.accountsettings import AccountSettings -from ro_py.notifications import NotificationReceiver +from ro_py.utilities.pages import Pages, SortOrder +# from ro_py.notifications import NotificationReceiver from ro_py.accountinformation import AccountInformation from ro_py.utilities.clientobject import ClientSharedObject from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError +def friend_handler(cso, data, args): + friends = [] + for friend in data: + friends.append(FriendRequest(cso, friend)) + return friends + + class Client: """ Represents an authenticated Roblox client. @@ -134,85 +144,31 @@

    Module ro_py.client

    if token: self.token_login(token) - def token_login(self, token): - """ - Authenticates the client with a ROBLOSECURITY token. - - Parameters - ---------- - token : str - .ROBLOSECURITY token to authenticate with. - """ - self.requests.session.cookies[".ROBLOSECURITY"] = token - self.accountinformation = AccountInformation(self.cso) - self.accountsettings = AccountSettings(self.cso) - self.chat = ChatWrapper(self.cso) - self.trade = TradesWrapper(self.cso, self.get_self) - self.notifications = NotificationReceiver(self.cso) - - async def user_login(self, username, password, token=None): + async def filter_text(self, text): """ - Authenticates the client with a username and password. + Filters text. Parameters ---------- - username : str - Username to log in with. - password : str - Password to log in with. - token : str, optional - If you have already solved the captcha, pass it here. - - Returns - ------- - ro_py.captcha.UnsolvedCaptcha or request + text : str + Text that will be filtered. """ - if token: - login_req = self.requests.back_post( - url="https://auth.roblox.com/v2/login", - json={ - "ctype": "Username", - "cvalue": username, - "password": password, - "captchaToken": token, - "captchaProvider": "PROVIDER_ARKOSE_LABS" - } - ) - return login_req - else: - login_req = await self.requests.post( - url="https://auth.roblox.com/v2/login", - json={ - "ctype": "Username", - "cvalue": username, - "password": password - }, - quickreturn=True - ) - if login_req.status_code == 200: - # If we're here, no captcha is required and we're already logged in, so we can return. - return - elif login_req.status_code == 403: - # A captcha is required, so we need to return the captcha to solve. - field_data = login_req.json()["errors"][0]["fieldData"] - captcha_req = await self.requests.post( - url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", - headers={ - "content-type": "application/x-www-form-urlencoded; charset=UTF-8" - }, - data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" - ) - captcha_json = captcha_req.json() - return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + filter_req = await self.requests.post( + url="https://develop.roblox.com/v1/gameUpdateNotifications/filter", + data=f'"{text}"' + ) + data = filter_req.json() + return data['filteredGameUpdateText'] + # Grab objects async def get_self(self): self_req = await self.requests.get( url="https://roblox.com/my/profile" ) data = self_req.json() - return PartialUser(self.cso, data['UserId'], data['Username']) + return PartialUser(self.cso, data) - async def get_user(self, user_id, expand=True): + async def get_user(self, user_id): """ Gets a Roblox user. @@ -220,19 +176,15 @@

    Module ro_py.client

    ---------- user_id ID of the user to generate the object from. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = PartialUser(self.cso, user_id) - if expand: - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + user = User(self.cso, user_id) + self.cso.cache.set(CacheType.Users, user_id, user) + await user.update() return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): """ Gets a Roblox user by their username.. @@ -242,8 +194,6 @@

    Module ro_py.client

    Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -257,7 +207,7 @@

    Module ro_py.client

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id, expand=expand) + return await self.get_user(user_id) else: raise UserDoesNotExistError @@ -351,74 +301,32 @@

    Module ro_py.client

    await badge.update() return badge - async def get_friend_requests(self): - friend_req = await self.requests.get( - url="https://friends.roblox.com/v1/user/friend-requests/count" + async def get_friend_requests(self, sort_order=SortOrder.Ascending, limit=100): + """ + Gets friend requests the client has. + """ + friends = Pages( + cso=self.cso, + url="https://friends.roblox.com/v1/my/friends/requests", + handler=friend_handler, + sort_order=sort_order, + limit=limit ) - return friend_req.json()["count"] + await friends.get_page() + return friends async def get_captcha_metadata(self): + """ + Grabs captcha metadata, which contains public keys. You can pass these to the prompt extension for GUI captcha + solving, + """ captcha_meta_req = await self.requests.get( url="https://apis.roblox.com/captcha/v1/metadata" ) captcha_meta_raw = captcha_meta_req.json() - return CaptchaMetadata(captcha_meta_raw) - -
    -
    -
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class Client -(token: str = None) -
    -
    -

    Represents an authenticated Roblox client.

    -

    Parameters

    -
    -
    token : str
    -
    Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -
    -
    - -Expand source code - -
    class Client:
    -    """
    -    Represents an authenticated Roblox client.
    -
    -    Parameters
    -    ----------
    -    token : str
    -        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    -    """
    -
    -    def __init__(self, token: str = None):
    -        self.cso = ClientSharedObject(self)
    -        """ClientSharedObject. Passed to each new object to share information."""
    -        self.requests = self.cso.requests
    -        """See self.cso.requests"""
    -        self.accountinformation = None
    -        """AccountInformation object. Only available for authenticated clients."""
    -        self.accountsettings = None
    -        """AccountSettings object. Only available for authenticated clients."""
    -        self.chat = None
    -        """ChatWrapper object. Only available for authenticated clients."""
    -        self.trade = None
    -        """TradesWrapper object. Only available for authenticated clients."""
    -        self.notifications = None
    -        """NotificationReceiver object. Only available for authenticated clients."""
    -        self.events = EventTypes
    -        """Types of events used for binding events to a function."""
    +        return CaptchaMetadata(captcha_meta_raw)
     
    -        if token:
    -            self.token_login(token)
    +    # Login/logout
     
         def token_login(self, token):
             """
    @@ -433,8 +341,9 @@ 

    Parameters

    self.accountinformation = AccountInformation(self.cso) self.accountsettings = AccountSettings(self.cso) self.chat = ChatWrapper(self.cso) - self.trade = TradesWrapper(self.cso, self.get_self) - self.notifications = NotificationReceiver(self.cso) + self.trade = TradesWrapper(self.cso) + # self.notifications = NotificationReceiver(self.cso) + self.notifications = None async def user_login(self, username, password, token=None): """ @@ -491,14 +400,135 @@

    Parameters

    captcha_json = captcha_req.json() return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + async def secure_sign_out(self): + """ + Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one. + + In the past, it was believed that Roblox would invalidate sessions automatically. This is not the case. + On the server, sessions are never invalidated unless a logout request is sent. In the browser, cookies expire + after 30 years. + + Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because + they would generate a new session token, and suggested that the user would "refresh their cookie" fairly + frequently as to avoid this. This isn't something you'll actually need to do, therefore this is left here as an + optional feature. + """ + await self.requests.post( + url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate" + ) + + async def logout(self): + """ + Logs out this user. + + This will invalidate your .ROBLOSECURITY token, unlike ro_py.client.secure_sign_out(). + Don't use this unless you plan to either never use this .ROBLOSECURITY token again. + + """ + await self.requests.post( + url="https://auth.roblox.com/v2/logout" + )
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def friend_handler(cso, data, args) +
    +
    +
    +
    + +Expand source code + +
    def friend_handler(cso, data, args):
    +    friends = []
    +    for friend in data:
    +        friends.append(FriendRequest(cso, friend))
    +    return friends
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Client +(token: str = None) +
    +
    +

    Represents an authenticated Roblox client.

    +

    Parameters

    +
    +
    token : str
    +
    Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    +
    +
    + +Expand source code + +
    class Client:
    +    """
    +    Represents an authenticated Roblox client.
    +
    +    Parameters
    +    ----------
    +    token : str
    +        Authentication token. You can take this from the .ROBLOSECURITY cookie in your browser.
    +    """
    +
    +    def __init__(self, token: str = None):
    +        self.cso = ClientSharedObject(self)
    +        """ClientSharedObject. Passed to each new object to share information."""
    +        self.requests = self.cso.requests
    +        """See self.cso.requests"""
    +        self.accountinformation = None
    +        """AccountInformation object. Only available for authenticated clients."""
    +        self.accountsettings = None
    +        """AccountSettings object. Only available for authenticated clients."""
    +        self.chat = None
    +        """ChatWrapper object. Only available for authenticated clients."""
    +        self.trade = None
    +        """TradesWrapper object. Only available for authenticated clients."""
    +        self.notifications = None
    +        """NotificationReceiver object. Only available for authenticated clients."""
    +        self.events = EventTypes
    +        """Types of events used for binding events to a function."""
    +
    +        if token:
    +            self.token_login(token)
    +
    +    async def filter_text(self, text):
    +        """
    +        Filters text.
    +
    +        Parameters
    +        ----------
    +        text : str
    +            Text that will be filtered.
    +        """
    +        filter_req = await self.requests.post(
    +            url="https://develop.roblox.com/v1/gameUpdateNotifications/filter",
    +            data=f'"{text}"'
    +        )
    +        data = filter_req.json()
    +        return data['filteredGameUpdateText']
    +
    +    # Grab objects
         async def get_self(self):
             self_req = await self.requests.get(
                 url="https://roblox.com/my/profile"
             )
             data = self_req.json()
    -        return PartialUser(self.cso, data['UserId'], data['Username'])
    +        return PartialUser(self.cso, data)
     
    -    async def get_user(self, user_id, expand=True):
    +    async def get_user(self, user_id):
             """
             Gets a Roblox user.
     
    @@ -506,19 +536,15 @@ 

    Parameters

    ---------- user_id ID of the user to generate the object from. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = PartialUser(self.cso, user_id) - if expand: - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + user = User(self.cso, user_id) + self.cso.cache.set(CacheType.Users, user_id, user) + await user.update() return user - async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True): + async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False): """ Gets a Roblox user by their username.. @@ -528,8 +554,6 @@

    Parameters

    Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -543,7 +567,7 @@

    Parameters

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id, expand=expand) + return await self.get_user(user_id) else: raise UserDoesNotExistError @@ -637,18 +661,133 @@

    Parameters

    await badge.update() return badge - async def get_friend_requests(self): - friend_req = await self.requests.get( - url="https://friends.roblox.com/v1/user/friend-requests/count" + async def get_friend_requests(self, sort_order=SortOrder.Ascending, limit=100): + """ + Gets friend requests the client has. + """ + friends = Pages( + cso=self.cso, + url="https://friends.roblox.com/v1/my/friends/requests", + handler=friend_handler, + sort_order=sort_order, + limit=limit ) - return friend_req.json()["count"] + await friends.get_page() + return friends async def get_captcha_metadata(self): + """ + Grabs captcha metadata, which contains public keys. You can pass these to the prompt extension for GUI captcha + solving, + """ captcha_meta_req = await self.requests.get( url="https://apis.roblox.com/captcha/v1/metadata" ) captcha_meta_raw = captcha_meta_req.json() - return CaptchaMetadata(captcha_meta_raw)
    + return CaptchaMetadata(captcha_meta_raw) + + # Login/logout + + def token_login(self, token): + """ + Authenticates the client with a ROBLOSECURITY token. + + Parameters + ---------- + token : str + .ROBLOSECURITY token to authenticate with. + """ + self.requests.session.cookies[".ROBLOSECURITY"] = token + self.accountinformation = AccountInformation(self.cso) + self.accountsettings = AccountSettings(self.cso) + self.chat = ChatWrapper(self.cso) + self.trade = TradesWrapper(self.cso) + # self.notifications = NotificationReceiver(self.cso) + self.notifications = None + + async def user_login(self, username, password, token=None): + """ + Authenticates the client with a username and password. + + Parameters + ---------- + username : str + Username to log in with. + password : str + Password to log in with. + token : str, optional + If you have already solved the captcha, pass it here. + + Returns + ------- + ro_py.captcha.UnsolvedCaptcha or request + """ + if token: + login_req = self.requests.back_post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password, + "captchaToken": token, + "captchaProvider": "PROVIDER_ARKOSE_LABS" + } + ) + return login_req + else: + login_req = await self.requests.post( + url="https://auth.roblox.com/v2/login", + json={ + "ctype": "Username", + "cvalue": username, + "password": password + }, + quickreturn=True + ) + if login_req.status_code == 200: + # If we're here, no captcha is required and we're already logged in, so we can return. + return + elif login_req.status_code == 403: + # A captcha is required, so we need to return the captcha to solve. + field_data = login_req.json()["errors"][0]["fieldData"] + captcha_req = await self.requests.post( + url="https://roblox-api.arkoselabs.com/fc/gt2/public_key/476068BF-9607-4799-B53D-966BE98E2B81", + headers={ + "content-type": "application/x-www-form-urlencoded; charset=UTF-8" + }, + data=f"public_key=476068BF-9607-4799-B53D-966BE98E2B81&data[blob]={field_data}" + ) + captcha_json = captcha_req.json() + return UnsolvedLoginCaptcha(captcha_json, "476068BF-9607-4799-B53D-966BE98E2B81") + + async def secure_sign_out(self): + """ + Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one. + + In the past, it was believed that Roblox would invalidate sessions automatically. This is not the case. + On the server, sessions are never invalidated unless a logout request is sent. In the browser, cookies expire + after 30 years. + + Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because + they would generate a new session token, and suggested that the user would "refresh their cookie" fairly + frequently as to avoid this. This isn't something you'll actually need to do, therefore this is left here as an + optional feature. + """ + await self.requests.post( + url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate" + ) + + async def logout(self): + """ + Logs out this user. + + This will invalidate your .ROBLOSECURITY token, unlike ro_py.client.secure_sign_out(). + Don't use this unless you plan to either never use this .ROBLOSECURITY token again. + + """ + await self.requests.post( + url="https://auth.roblox.com/v2/logout" + )

    Subclasses

      @@ -691,6 +830,37 @@

      Instance variables

    Methods

    +
    +async def filter_text(self, text) +
    +
    +

    Filters text.

    +

    Parameters

    +
    +
    text : str
    +
    Text that will be filtered.
    +
    +
    + +Expand source code + +
    async def filter_text(self, text):
    +    """
    +    Filters text.
    +
    +    Parameters
    +    ----------
    +    text : str
    +        Text that will be filtered.
    +    """
    +    filter_req = await self.requests.post(
    +        url="https://develop.roblox.com/v1/gameUpdateNotifications/filter",
    +        data=f'"{text}"'
    +    )
    +    data = filter_req.json()
    +    return data['filteredGameUpdateText']
    +
    +
    async def get_asset(self, asset_id)
    @@ -757,12 +927,17 @@

    Parameters

    async def get_captcha_metadata(self)
    -
    +

    Grabs captcha metadata, which contains public keys. You can pass these to the prompt extension for GUI captcha +solving,

    Expand source code
    async def get_captcha_metadata(self):
    +    """
    +    Grabs captcha metadata, which contains public keys. You can pass these to the prompt extension for GUI captcha
    +    solving,
    +    """
         captcha_meta_req = await self.requests.get(
             url="https://apis.roblox.com/captcha/v1/metadata"
         )
    @@ -771,19 +946,27 @@ 

    Parameters

    -async def get_friend_requests(self) +async def get_friend_requests(self, sort_order=SortOrder.Ascending, limit=100)
    -
    +

    Gets friend requests the client has.

    Expand source code -
    async def get_friend_requests(self):
    -    friend_req = await self.requests.get(
    -        url="https://friends.roblox.com/v1/user/friend-requests/count"
    +
    async def get_friend_requests(self, sort_order=SortOrder.Ascending, limit=100):
    +    """
    +    Gets friend requests the client has.
    +    """
    +    friends = Pages(
    +        cso=self.cso,
    +        url="https://friends.roblox.com/v1/my/friends/requests",
    +        handler=friend_handler,
    +        sort_order=sort_order,
    +        limit=limit
         )
    -    return friend_req.json()["count"]
    + await friends.get_page() + return friends
    @@ -903,11 +1086,11 @@

    Parameters

    url="https://roblox.com/my/profile" ) data = self_req.json() - return PartialUser(self.cso, data['UserId'], data['Username'])
    + return PartialUser(self.cso, data)
    -async def get_user(self, user_id, expand=True) +async def get_user(self, user_id)

    Gets a Roblox user.

    @@ -915,14 +1098,12 @@

    Parameters

    user_id
    ID of the user to generate the object from.
    -
    expand : bool
    -
    Whether to automatically expand the data returned by the endpoint into Users.s
    Expand source code -
    async def get_user(self, user_id, expand=True):
    +
    async def get_user(self, user_id):
         """
         Gets a Roblox user.
     
    @@ -930,21 +1111,17 @@ 

    Parameters

    ---------- user_id ID of the user to generate the object from. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users.s """ user = self.cso.cache.get(CacheType.Users, user_id) if not user: - user = PartialUser(self.cso, user_id) - if expand: - expanded = await user.expand() - self.cso.cache.set(CacheType.Users, user_id, expanded) - return expanded + user = User(self.cso, user_id) + self.cso.cache.set(CacheType.Users, user_id, user) + await user.update() return user
    -async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True) +async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False)

    Gets a Roblox user by their username..

    @@ -954,14 +1131,12 @@

    Parameters

    Name of the user to generate the object from.
    exclude_banned_users : bool
    Whether to exclude banned users in the request.
    -
    expand : bool
    -
    Whether to automatically expand the data returned by the endpoint into Users.
    Expand source code -
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False, expand=True):
    +
    async def get_user_by_username(self, user_name: str, exclude_banned_users: bool = False):
         """
         Gets a Roblox user by their username..
     
    @@ -971,8 +1146,6 @@ 

    Parameters

    Name of the user to generate the object from. exclude_banned_users : bool Whether to exclude banned users in the request. - expand : bool - Whether to automatically expand the data returned by the endpoint into Users. """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", @@ -986,11 +1159,69 @@

    Parameters

    username_data = username_req.json() if len(username_data["data"]) > 0: user_id = username_req.json()["data"][0]["id"] # TODO: make this a partialuser - return await self.get_user(user_id, expand=expand) + return await self.get_user(user_id) else: raise UserDoesNotExistError
    +
    +async def logout(self) +
    +
    +

    Logs out this user.

    +

    This will invalidate your .ROBLOSECURITY token, unlike ro_py.client.secure_sign_out(). +Don't use this unless you plan to either never use this .ROBLOSECURITY token again.

    +
    + +Expand source code + +
    async def logout(self):
    +    """
    +    Logs out this user.
    +
    +    This will invalidate your .ROBLOSECURITY token, unlike ro_py.client.secure_sign_out().
    +    Don't use this unless you plan to either never use this .ROBLOSECURITY token again.
    +
    +    """
    +    await self.requests.post(
    +        url="https://auth.roblox.com/v2/logout"
    +    )
    +
    +
    +
    +async def secure_sign_out(self) +
    +
    +

    Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one.

    +

    In the past, it was believed that Roblox would invalidate sessions automatically. This is not the case. +On the server, sessions are never invalidated unless a logout request is sent. In the browser, cookies expire +after 30 years.

    +

    Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because +they would generate a new session token, and suggested that the user would "refresh their cookie" fairly +frequently as to avoid this. This isn't something you'll actually need to do, therefore this is left here as an +optional feature.

    +
    + +Expand source code + +
    async def secure_sign_out(self):
    +    """
    +    Sends a Secure Sign Out (SSO) request. This invalidates all session tokens and generates a new one.
    +
    +    In the past, it was believed that Roblox would invalidate sessions automatically. This is not the case.
    +    On the server, sessions are never invalidated unless a logout request is sent. In the browser, cookies expire
    +    after 30 years.
    +
    +    Other Roblox API wrappers used to use SSO requests as a way to stop cookies from being invalidated, because
    +    they would generate a new session token, and suggested that the user would "refresh their cookie" fairly
    +    frequently as to avoid this. This isn't something you'll actually need to do, therefore this is left here as an
    +    optional feature.
    +    """
    +    await self.requests.post(
    +        url="https://www.roblox.com/authentication/signoutfromallsessionsandreauthenticate"
    +    )
    +
    +
    def token_login(self, token)
    @@ -1018,8 +1249,9 @@

    Parameters

    self.accountinformation = AccountInformation(self.cso) self.accountsettings = AccountSettings(self.cso) self.chat = ChatWrapper(self.cso) - self.trade = TradesWrapper(self.cso, self.get_self) - self.notifications = NotificationReceiver(self.cso)
    + self.trade = TradesWrapper(self.cso) + # self.notifications = NotificationReceiver(self.cso) + self.notifications = None
    @@ -1122,6 +1354,11 @@

    Index

  • ro_py
  • +
  • Functions

    + +
  • Classes

    • @@ -1132,6 +1369,7 @@

      Client<
    • chat
    • cso
    • events
    • +
    • filter_text
    • get_asset
    • get_badge
    • get_captcha_metadata
    • @@ -1142,8 +1380,10 @@

      Client<
    • get_self
    • get_user
    • get_user_by_username
    • +
    • logout
    • notifications
    • requests
    • +
    • secure_sign_out
    • token_login
    • trade
    • user_login
    • diff --git a/docs/economy.html b/docs/economy.html index 3fddd8d3..6ee05d10 100644 --- a/docs/economy.html +++ b/docs/economy.html @@ -85,7 +85,8 @@

      Module ro_py.economy

      """ -endpoint = "https://economy.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("economy") class Currency: diff --git a/docs/events.html b/docs/events.html index 4397f7fd..4b592e93 100644 --- a/docs/events.html +++ b/docs/events.html @@ -5,7 +5,8 @@ ro_py.events API documentation - + @@ -74,11 +75,23 @@

      Module ro_py.events

      +

      This file houses functions and classes that pertain to events and event handling with ro.py. Most methods that have +events actually don't reference content here, this doesn't contain much at the moment.

      Expand source code -
      import enum
      +
      """
      +
      +This file houses functions and classes that pertain to events and event handling with ro.py. Most methods that have
      +events actually don't reference content here, this doesn't contain much at the moment.
      +
      +"""
      +
      +import enum
      +import time
      +import asyncio
      +from typing import Callable, Tuple
       
       
       class EventTypes(enum.Enum):
      @@ -88,7 +101,52 @@ 

      Module ro_py.events

      on_asset_change = "on_asset_change" on_user_change = "on_user_change" on_audit_log = "on_audit_log" - on_trade_request = "on_trade_request"
      + on_trade_request = "on_trade_request" + + +class Event: + def __init__(self, func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15): + self.function = func + self.event_type = event_type + self.arguments = arguments + self.delay = delay + self.next_run = time.time() + delay + + def edit(self, arguments: Tuple = None, delay: int = None, func: Callable = None): + self.arguments = arguments if arguments else self.arguments + self.delay = delay if delay else self.delay + self.function = func if func else self.function + + +class EventHandler: + def __init__(self): + self.events = [] + self.running = False + + def add_event(self, event: Event): + self.events.append(event) + + def print_events(self): + text = "These are the current running events:" + for event in self.events: + text += f"\n{event.event_id}:\n Next run: {event.next_run}\n Times run per minute: {60 / event.delay}" + print(text) + + async def stop_event(self, event: Event): + self.events.remove(event) + + async def listen(self): + if not self.running: + self.running = True + while True: + # Limits delay to 1 second. + await asyncio.sleep(1) + for event in self.events: + if event.next_run <= time.time(): + if not isinstance(event.arguments[-1], Event): + event.arguments = (*event.arguments, event) + asyncio.create_task(event.function(*event.arguments)) + event.next_run = time.time() + event.delay
      @@ -100,6 +158,156 @@

      Module ro_py.events

      Classes

      +
      +class Event +(func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15) +
      +
      +
      +
      + +Expand source code + +
      class Event:
      +    def __init__(self, func: Callable, event_type: EventTypes, arguments: Tuple = (), delay: int = 15):
      +        self.function = func
      +        self.event_type = event_type
      +        self.arguments = arguments
      +        self.delay = delay
      +        self.next_run = time.time() + delay
      +
      +    def edit(self, arguments: Tuple = None, delay: int = None, func: Callable = None):
      +        self.arguments = arguments if arguments else self.arguments
      +        self.delay = delay if delay else self.delay
      +        self.function = func if func else self.function
      +
      +

      Methods

      +
      +
      +def edit(self, arguments: Tuple = None, delay: int = None, func: Callable = None) +
      +
      +
      +
      + +Expand source code + +
      def edit(self, arguments: Tuple = None, delay: int = None, func: Callable = None):
      +    self.arguments = arguments if arguments else self.arguments
      +    self.delay = delay if delay else self.delay
      +    self.function = func if func else self.function
      +
      +
      +
      +
      +
      +class EventHandler +
      +
      +
      +
      + +Expand source code + +
      class EventHandler:
      +    def __init__(self):
      +        self.events = []
      +        self.running = False
      +
      +    def add_event(self, event: Event):
      +        self.events.append(event)
      +
      +    def print_events(self):
      +        text = "These are the current running events:"
      +        for event in self.events:
      +            text += f"\n{event.event_id}:\n   Next run: {event.next_run}\n   Times run per minute: {60 / event.delay}"
      +        print(text)
      +
      +    async def stop_event(self, event: Event):
      +        self.events.remove(event)
      +
      +    async def listen(self):
      +        if not self.running:
      +            self.running = True
      +            while True:
      +                # Limits delay to 1 second.
      +                await asyncio.sleep(1)
      +                for event in self.events:
      +                    if event.next_run <= time.time():
      +                        if not isinstance(event.arguments[-1], Event):
      +                            event.arguments = (*event.arguments, event)
      +                        asyncio.create_task(event.function(*event.arguments))
      +                        event.next_run = time.time() + event.delay
      +
      +

      Methods

      +
      +
      +def add_event(self, event: Event) +
      +
      +
      +
      + +Expand source code + +
      def add_event(self, event: Event):
      +    self.events.append(event)
      +
      +
      +
      +async def listen(self) +
      +
      +
      +
      + +Expand source code + +
      async def listen(self):
      +    if not self.running:
      +        self.running = True
      +        while True:
      +            # Limits delay to 1 second.
      +            await asyncio.sleep(1)
      +            for event in self.events:
      +                if event.next_run <= time.time():
      +                    if not isinstance(event.arguments[-1], Event):
      +                        event.arguments = (*event.arguments, event)
      +                    asyncio.create_task(event.function(*event.arguments))
      +                    event.next_run = time.time() + event.delay
      +
      +
      +
      +def print_events(self) +
      +
      +
      +
      + +Expand source code + +
      def print_events(self):
      +    text = "These are the current running events:"
      +    for event in self.events:
      +        text += f"\n{event.event_id}:\n   Next run: {event.next_run}\n   Times run per minute: {60 / event.delay}"
      +    print(text)
      +
      +
      +
      +async def stop_event(self, event: Event) +
      +
      +
      +
      + +Expand source code + +
      async def stop_event(self, event: Event):
      +    self.events.remove(event)
      +
      +
      +
      +
      class EventTypes (value, names=None, *, module=None, qualname=None, type=None, start=1) @@ -177,6 +385,21 @@

      Index

    • Classes

      • +

        Event

        + +
      • +
      • +

        EventHandler

        + +
      • +
      • EventTypes

        • on_asset_change
        • diff --git a/docs/extensions/bots.html b/docs/extensions/bots.html index 2292f5d1..ddddca31 100644 --- a/docs/extensions/bots.html +++ b/docs/extensions/bots.html @@ -420,15 +420,20 @@

          Inherited members

        • chat
        • cso
        • events
        • +
        • filter_text
        • get_asset
        • get_badge
        • +
        • get_captcha_metadata
        • +
        • get_friend_requests
        • get_game_by_place_id
        • get_game_by_universe_id
        • get_group
        • get_user
        • get_user_by_username
        • +
        • logout
        • notifications
        • requests
        • +
        • secure_sign_out
        • token_login
        • trade
        • user_login
        • diff --git a/docs/friends.html b/docs/friends.html new file mode 100644 index 00000000..d84dda6e --- /dev/null +++ b/docs/friends.html @@ -0,0 +1,294 @@ + + + + + + +ro_py.friends API documentation + + + + + + + + + + + + +
          +
          +
          +

          Module ro_py.friends

          +
          +
          +
          + +Expand source code + +
          import iso8601
          +from ro_py.bases.baseuser import PartialUser
          +
          +
          +class Friend(PartialUser):
          +    def __init__(self, cso, data):
          +        super().__init__(cso, data)
          +        self.is_online = data.get('isOnline')
          +        self.is_deleted = data.get('isDeleted')
          +        self.description = data["description"]
          +        self.created = iso8601.parse_date(data["created"])
          +        self.is_banned = data["isBanned"]
          +        self.display_name = data["displayName"]
          +
          +
          +class FriendRequest(Friend):
          +    def __init__(self, cso, data):
          +        super(FriendRequest, self).__init__(cso, data)
          +
          +    async def accept(self):
          +        accept_req = await self.cso.post(
          +            url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request"
          +        )
          +        return accept_req.status == 200
          +
          +    async def decline(self):
          +        accept_req = await self.cso.post(
          +            url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request"
          +        )
          +        return accept_req.status == 200
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +

          Classes

          +
          +
          +class Friend +(cso, data) +
          +
          +
          +
          + +Expand source code + +
          class Friend(PartialUser):
          +    def __init__(self, cso, data):
          +        super().__init__(cso, data)
          +        self.is_online = data.get('isOnline')
          +        self.is_deleted = data.get('isDeleted')
          +        self.description = data["description"]
          +        self.created = iso8601.parse_date(data["created"])
          +        self.is_banned = data["isBanned"]
          +        self.display_name = data["displayName"]
          +
          +

          Ancestors

          + +

          Subclasses

          + +

          Inherited members

          + +
          +
          +class FriendRequest +(cso, data) +
          +
          +
          +
          + +Expand source code + +
          class FriendRequest(Friend):
          +    def __init__(self, cso, data):
          +        super(FriendRequest, self).__init__(cso, data)
          +
          +    async def accept(self):
          +        accept_req = await self.cso.post(
          +            url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request"
          +        )
          +        return accept_req.status == 200
          +
          +    async def decline(self):
          +        accept_req = await self.cso.post(
          +            url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request"
          +        )
          +        return accept_req.status == 200
          +
          +

          Ancestors

          + +

          Methods

          +
          +
          +async def accept(self) +
          +
          +
          +
          + +Expand source code + +
          async def accept(self):
          +    accept_req = await self.cso.post(
          +        url=f"https://friends.roblox.com/v1/users/{self.id}/accept-friend-request"
          +    )
          +    return accept_req.status == 200
          +
          +
          +
          +async def decline(self) +
          +
          +
          +
          + +Expand source code + +
          async def decline(self):
          +    accept_req = await self.cso.post(
          +        url=f"https://friends.roblox.com/v1/users/{self.id}/decline-friend-request"
          +    )
          +    return accept_req.status == 200
          +
          +
          +
          +

          Inherited members

          + +
          +
          +
          +
          + +
          + + + \ No newline at end of file diff --git a/docs/gamepasses.html b/docs/gamepasses.html new file mode 100644 index 00000000..ec3dc06c --- /dev/null +++ b/docs/gamepasses.html @@ -0,0 +1,117 @@ + + + + + + +ro_py.gamepasses API documentation + + + + + + + + + + + + +
          +
          +
          +

          Module ro_py.gamepasses

          +
          +
          +
          + +Expand source code + +
          from ro_py.utilities.url import url
          +endpoint = url("inventory")
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          +
          + +
          + + + \ No newline at end of file diff --git a/docs/gamepersistence.html b/docs/gamepersistence.html index 09669a14..5f74a3e1 100644 --- a/docs/gamepersistence.html +++ b/docs/gamepersistence.html @@ -89,7 +89,8 @@

          Module ro_py.gamepersistence

          from math import floor import re -endpoint = "http://gamepersistence.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("gamepersistence") class DataStore: diff --git a/docs/games.html b/docs/games.html index 9b4c68e3..54ed5ce0 100644 --- a/docs/games.html +++ b/docs/games.html @@ -86,16 +86,19 @@

          Module ro_py.games

          """ from ro_py.utilities.clientobject import ClientObject -from ro_py.groups import Group -from ro_py.badges import Badge from ro_py.thumbnails import GameThumbnailGenerator from ro_py.utilities.errors import GameJoinError +from ro_py.bases.baseuser import PartialUser +from ro_py.bases.baseasset import BaseAsset from ro_py.utilities.cache import CacheType +from ro_py.groups import Group +from ro_py.badges import Badge import subprocess import json import os -endpoint = "https://games.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("games") class Votes: @@ -113,16 +116,19 @@

          Module ro_py.games

          This class represents multiple game-related endpoints. """ def __init__(self, cso, universe_id): + super().__init__() self.id = universe_id self.cso = cso self.requests = cso.requests self.name = None + self.root_place_id = None self.description = None - self.root_place = None self.creator = None self.price = None self.allowed_gear_genres = None self.allowed_gear_categories = None + self.playing = None + self.visits = None self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None @@ -141,8 +147,8 @@

          Module ro_py.games

          game_info = game_info_req.json() game_info = game_info["data"][0] self.name = game_info["name"] + self.root_place_id = game_info["rootPlaceId"] self.description = game_info["description"] - self.root_place = Place(self.requests, game_info["rootPlaceId"]) if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: @@ -158,10 +164,17 @@

          Module ro_py.games

          self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] + self.playing = game_info["playing"] + self.visits = game_info["visits"] self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] + async def get_root_place(self): + root_place = Place(self.cso, self.root_place_id, self) + await root_place.update() + return root_place + async def get_votes(self): """ Returns @@ -198,11 +211,36 @@

          Module ro_py.games

          return badges -class Place: - def __init__(self, requests, id): - self.requests = requests - self.id = id - pass +class Place(ClientObject, BaseAsset): + def __init__(self, cso, place_id, universe): + super().__init__() + self.cso = cso + self.requests = cso.requests + self.id = place_id + self.universe = universe + self.name = None + self.description = None + self.url = None + self.creator = None + self.is_playable = None + self.reason_prohibited = None + self.price = None + + async def update(self): + place_req = await self.requests.get( + url="https://games.roblox.com/v1/games/multiget-place-details", + params={ + "placeIds": self.id + } + ) + place_data = place_req.json()[0] + self.name = place_data["name"] + self.description = place_data["description"] + self.url = place_data["url"] + self.creator = PartialUser(self.cso, place_data["builderId"], place_data["builder"]) + self.is_playable = place_data["isPlayable"] + self.reason_prohibited = place_data["reasonProhibited"] + self.price = place_data["price"] async def join(self, launchtime=1609186776825, rloc="en_us", gloc="en_us", negotiate_url="https://www.roblox.com/Login/Negotiate.ashx"): @@ -296,16 +334,19 @@

          Classes

          This class represents multiple game-related endpoints. """ def __init__(self, cso, universe_id): + super().__init__() self.id = universe_id self.cso = cso self.requests = cso.requests self.name = None + self.root_place_id = None self.description = None - self.root_place = None self.creator = None self.price = None self.allowed_gear_genres = None self.allowed_gear_categories = None + self.playing = None + self.visits = None self.max_players = None self.studio_access_to_apis_allowed = None self.create_vip_servers_allowed = None @@ -324,8 +365,8 @@

          Classes

          game_info = game_info_req.json() game_info = game_info["data"][0] self.name = game_info["name"] + self.root_place_id = game_info["rootPlaceId"] self.description = game_info["description"] - self.root_place = Place(self.requests, game_info["rootPlaceId"]) if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: @@ -341,10 +382,17 @@

          Classes

          self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] + self.playing = game_info["playing"] + self.visits = game_info["visits"] self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] + async def get_root_place(self): + root_place = Place(self.cso, self.root_place_id, self) + await root_place.update() + return root_place + async def get_votes(self): """ Returns @@ -415,6 +463,21 @@

          Methods

          return badges
          +
          +async def get_root_place(self) +
          +
          +
          +
          + +Expand source code + +
          async def get_root_place(self):
          +    root_place = Place(self.cso, self.root_place_id, self)
          +    await root_place.update()
          +    return root_place
          +
          +
          async def get_votes(self)
          @@ -468,8 +531,8 @@

          Methods

          game_info = game_info_req.json() game_info = game_info["data"][0] self.name = game_info["name"] + self.root_place_id = game_info["rootPlaceId"] self.description = game_info["description"] - self.root_place = Place(self.requests, game_info["rootPlaceId"]) if game_info["creator"]["type"] == "User": self.creator = self.cso.cache.get(CacheType.Users, game_info["creator"]["id"]) if not self.creator: @@ -485,6 +548,8 @@

          Methods

          self.price = game_info["price"] self.allowed_gear_genres = game_info["allowedGearGenres"] self.allowed_gear_categories = game_info["allowedGearCategories"] + self.playing = game_info["playing"] + self.visits = game_info["visits"] self.max_players = game_info["maxPlayers"] self.studio_access_to_apis_allowed = game_info["studioAccessToApisAllowed"] self.create_vip_servers_allowed = game_info["createVipServersAllowed"] @@ -494,19 +559,44 @@

          Methods

          class Place -(requests, id) +(cso, place_id, universe)
          -
          +

          Every object that is grabbable with client.get_x inherits this object.

          Expand source code -
          class Place:
          -    def __init__(self, requests, id):
          -        self.requests = requests
          -        self.id = id
          -        pass
          +
          class Place(ClientObject, BaseAsset):
          +    def __init__(self, cso, place_id, universe):
          +        super().__init__()
          +        self.cso = cso
          +        self.requests = cso.requests
          +        self.id = place_id
          +        self.universe = universe
          +        self.name = None
          +        self.description = None
          +        self.url = None
          +        self.creator = None
          +        self.is_playable = None
          +        self.reason_prohibited = None
          +        self.price = None
          +
          +    async def update(self):
          +        place_req = await self.requests.get(
          +            url="https://games.roblox.com/v1/games/multiget-place-details",
          +            params={
          +                "placeIds": self.id
          +            }
          +        )
          +        place_data = place_req.json()[0]
          +        self.name = place_data["name"]
          +        self.description = place_data["description"]
          +        self.url = place_data["url"]
          +        self.creator = PartialUser(self.cso, place_data["builderId"], place_data["builder"])
          +        self.is_playable = place_data["isPlayable"]
          +        self.reason_prohibited = place_data["reasonProhibited"]
          +        self.price = place_data["price"]
           
               async def join(self, launchtime=1609186776825, rloc="en_us", gloc="en_us",
                              negotiate_url="https://www.roblox.com/Login/Negotiate.ashx"):
          @@ -573,6 +663,11 @@ 

          Methods

          ) return join_process.stdout, join_process.stderr
          +

          Ancestors

          +

          Methods

          @@ -659,6 +754,32 @@

          Methods

          return join_process.stdout, join_process.stderr
          +
          +async def update(self) +
          +
          +
          +
          + +Expand source code + +
          async def update(self):
          +    place_req = await self.requests.get(
          +        url="https://games.roblox.com/v1/games/multiget-place-details",
          +        params={
          +            "placeIds": self.id
          +        }
          +    )
          +    place_data = place_req.json()[0]
          +    self.name = place_data["name"]
          +    self.description = place_data["description"]
          +    self.url = place_data["url"]
          +    self.creator = PartialUser(self.cso, place_data["builderId"], place_data["builder"])
          +    self.is_playable = place_data["isPlayable"]
          +    self.reason_prohibited = place_data["reasonProhibited"]
          +    self.price = place_data["price"]
          +
          +
    • @@ -705,6 +826,7 @@

      Index

      Game

      @@ -713,6 +835,7 @@

      GamePlace

    • diff --git a/docs/groups.html b/docs/groups.html index eed84ad4..a97d3e7c 100644 --- a/docs/groups.html +++ b/docs/groups.html @@ -93,33 +93,69 @@

      Module ro_py.groups

      from ro_py.wall import Wall from ro_py.roles import Role -from ro_py.users import PartialUser -from ro_py.events import EventTypes +from ro_py.events import Event +from ro_py.users import BaseUser from typing import Tuple, Callable +from ro_py.events import EventTypes from ro_py.utilities.errors import NotFound +from ro_py.bases.baseuser import PartialUser from ro_py.utilities.pages import Pages, SortOrder from ro_py.utilities.clientobject import ClientObject -endpoint = "https://groups.roblox.com" +from ro_py.utilities.url import url +endpoint = url("groups") class Shout: """ Represents a group shout. """ - def __init__(self, cso, shout_data): + def __init__(self, cso, group, shout_data): self.cso = cso + self.requests = cso.requests + self.group = group self.data = shout_data self.body = shout_data["body"] + self.created = iso8601.parse_date(shout_data["created"]) + self.updated = iso8601.parse_date(shout_data["updated"]) + # TODO: Make this a PartialUser self.poster = None + def __str__(self): + return self.body + + async def __call__(self, message): + """ + Updates the shout of the group. + Please note that doing so will completely delete this Shout object and return a new Shout object. + The parent group's shout parameter will also be updated accordingly. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + + Returns + ------- + ro_py.groups.Shout + + """ + shout_req = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/status", + data={ + "message": message + } + ) + self.group.shout = Shout(self.cso, self.group, shout_req.json()) + return self.group.shout + class JoinRequest: def __init__(self, cso, data, group): self.requests = cso.requests self.group = group - self.requester = PartialUser(cso, data['requester']['userId'], data['requester']['username']) + self.requester = PartialUser(cso, data['requester']) self.created = iso8601.parse_date(data['created']) async def accept(self): @@ -214,6 +250,7 @@

      Module ro_py.groups

      Represents a group. """ def __init__(self, cso, group_id): + super().__init__() self.cso = cso """Client Shared Object""" self.requests = cso.requests @@ -254,7 +291,7 @@

      Module ro_py.groups

      self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] if group_info.get('shout'): - self.shout = Shout(self.cso, group_info['shout']) + self.shout = Shout(self.cso, self, group_info['shout']) else: self.shout = None if "isLocked" in group_info: @@ -263,6 +300,7 @@

      Module ro_py.groups

      async def update_shout(self, message): """ Updates the shout of the group. + DEPRECATED: Just call group.shout() Parameters ---------- @@ -273,13 +311,7 @@

      Module ro_py.groups

      ------- int """ - shout_req = await self.requests.patch( - url=endpoint + f"/v1/groups/{self.id}/status", - data={ - "message": message - } - ) - return shout_req.status_code == 200 + return await self.shout(message) async def get_roles(self): """ @@ -297,10 +329,10 @@

      Module ro_py.groups

      roles.append(Role(self.cso, self, role)) return roles - async def get_member_by_id(self, roblox_id): + async def get_member_by_id(self, user_id): # Get list of group user is in. member_req = await self.requests.get( - url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + url=endpoint + f"/v2/users/{user_id}/groups/roles" ) data = member_req.json() @@ -313,11 +345,11 @@

      Module ro_py.groups

      # Check if user is in group. if not group_data: - raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + raise NotFound(f"The user {user_id} was not found in group {self.id}") # Create data to return. role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, roblox_id, "", self, role) + member = Member(self.cso, user_id, "", self, role) return member async def get_member_by_username(self, name): @@ -408,7 +440,7 @@

      Module ro_py.groups

      return self.cso.client.get_group(self.id) -class Member(PartialUser): +class Member(BaseUser): """ Represents a user in a group. @@ -416,7 +448,7 @@

      Module ro_py.groups

      ---------- cso : ro_py.utilities.requests.Requests Requests object to use for API requests. - roblox_id : int + user_id : int The id of a user. name : str The name of the user. @@ -425,8 +457,9 @@

      Module ro_py.groups

      role : ro_py.roles.Role The role the user has is the group. """ - def __init__(self, cso, roblox_id, name, group, role): - super().__init__(cso, roblox_id, name) + def __init__(self, cso, user_id, name, group, role): + super().__init__(cso, user_id) + self.name = name self.role = role self.group = group @@ -549,7 +582,7 @@

      Module ro_py.groups

      self.cso = cso self.group = group - def bind(self, func: Callable, event: EventTypes, delay: int = 15): + async def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -563,60 +596,92 @@

      Module ro_py.groups

      How many seconds between each poll. """ if event == EventTypes.on_join_request: - return asyncio.create_task(self.on_join_request(func, delay)) + event = Event(self.on_join_request, EventTypes.on_join_request, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_wall_post: - return asyncio.create_task(self.on_wall_post(func, delay)) + event = Event(self.on_wall_post, EventTypes.on_wall_post, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_group_change: - return asyncio.create_task(self.on_group_change(func, delay)) - if event == EventTypes.on_audit_log: - return asyncio.create_task(self.on_audit_log(func, delay)) + event = Event(self.on_group_change, EventTypes.on_group_change, (func, None), delay) + self.cso.event_handler.add_event(event) + await self.cso.event_handler.listen() - async def on_join_request(self, func: Callable, delay: int): - current_group_reqs = await self.group.get_join_requests() - old_req = current_group_reqs.data.requester.id - while True: - await asyncio.sleep(delay) + async def on_join_request(self, func: Callable, old_req, event: Event): + if not old_req: current_group_reqs = await self.group.get_join_requests() - current_group_reqs = current_group_reqs.data - if current_group_reqs[0].requester.id != old_req: - new_reqs = [] - for request in current_group_reqs: - if request.requester.id != old_req: - new_reqs.append(request) - old_req = current_group_reqs[0].requester.id - for new_req in new_reqs: - asyncio.create_task(func(new_req)) - - async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - newest_wall_post = current_wall_posts.data[0].id - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs.data[0].requester.id + return event.edit(arguments=tuple(old_arguments)) + + current_group_reqs = await self.group.get_join_requests() + current_group_reqs = current_group_reqs.data + + if current_group_reqs[0].requester.id != old_req: + new_reqs = [] + + for request in current_group_reqs: + if request.requester.id == old_req: + break + new_reqs.append(request) + + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs[0].requester.id + event.edit(arguments=tuple(old_arguments)) + + for new_req in new_reqs: + asyncio.create_task(func(new_req)) + + async def on_wall_post(self, func: Callable, newest_wall_post, event: Event): + if not newest_wall_post: current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - current_wall_posts = current_wall_posts.data - post = current_wall_posts[0] - if post.id != newest_wall_post: - new_posts = [] - for post in current_wall_posts: - if post.id == newest_wall_post: - break - new_posts.append(post) - newest_wall_post = current_wall_posts[0].id - for new_post in new_posts: - asyncio.create_task(func(new_post)) - - async def on_group_change(self, func: Callable, delay: int): - await self.group.update() - current_group = copy.copy(self.group) - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts.data[0].id + return event.edit(arguments=tuple(old_arguments)) + + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + current_wall_posts = current_wall_posts.data + + post = current_wall_posts[0] + if post.id != newest_wall_post: + new_posts = [] + + for post in current_wall_posts: + if post.id == newest_wall_post: + break + new_posts.append(post) + + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts[0].id + event.edit(arguments=tuple(old_arguments)) + + for new_post in new_posts: + asyncio.create_task(func(new_post)) + + async def on_group_change(self, func: Callable, current_group, event: Event): + if not current_group: await self.group.update() - has_changed = False - for attr, value in current_group.__dict__.items(): - if getattr(self.group, attr) != value: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + return event.edit(arguments=tuple(old_arguments)) + + await self.group.update() + + has_changed = False + for attr, value in current_group.__dict__.items(): + other_value = getattr(self.group, attr) + if attr == "shout": + if str(value) != str(other_value): has_changed = True - if has_changed: - asyncio.create_task(func(current_group, self.group)) + else: + continue + if other_value != value: + has_changed = True + + if has_changed: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + event.edit(arguments=tuple(old_arguments)) + asyncio.create_task(func(current_group, self.group)) """ async def on_audit_log(self, func: Callable, delay: int): @@ -959,7 +1024,7 @@

      Class variables

      self.cso = cso self.group = group - def bind(self, func: Callable, event: EventTypes, delay: int = 15): + async def bind(self, func: Callable, event: EventTypes, delay: int = 15): """ Binds a function to an event. @@ -973,60 +1038,92 @@

      Class variables

      How many seconds between each poll. """ if event == EventTypes.on_join_request: - return asyncio.create_task(self.on_join_request(func, delay)) + event = Event(self.on_join_request, EventTypes.on_join_request, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_wall_post: - return asyncio.create_task(self.on_wall_post(func, delay)) + event = Event(self.on_wall_post, EventTypes.on_wall_post, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_group_change: - return asyncio.create_task(self.on_group_change(func, delay)) - if event == EventTypes.on_audit_log: - return asyncio.create_task(self.on_audit_log(func, delay)) + event = Event(self.on_group_change, EventTypes.on_group_change, (func, None), delay) + self.cso.event_handler.add_event(event) + await self.cso.event_handler.listen() - async def on_join_request(self, func: Callable, delay: int): - current_group_reqs = await self.group.get_join_requests() - old_req = current_group_reqs.data.requester.id - while True: - await asyncio.sleep(delay) + async def on_join_request(self, func: Callable, old_req, event: Event): + if not old_req: current_group_reqs = await self.group.get_join_requests() - current_group_reqs = current_group_reqs.data - if current_group_reqs[0].requester.id != old_req: - new_reqs = [] - for request in current_group_reqs: - if request.requester.id != old_req: - new_reqs.append(request) - old_req = current_group_reqs[0].requester.id - for new_req in new_reqs: - asyncio.create_task(func(new_req)) - - async def on_wall_post(self, func: Callable, delay: int): - current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - newest_wall_post = current_wall_posts.data[0].id - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs.data[0].requester.id + return event.edit(arguments=tuple(old_arguments)) + + current_group_reqs = await self.group.get_join_requests() + current_group_reqs = current_group_reqs.data + + if current_group_reqs[0].requester.id != old_req: + new_reqs = [] + + for request in current_group_reqs: + if request.requester.id == old_req: + break + new_reqs.append(request) + + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs[0].requester.id + event.edit(arguments=tuple(old_arguments)) + + for new_req in new_reqs: + asyncio.create_task(func(new_req)) + + async def on_wall_post(self, func: Callable, newest_wall_post, event: Event): + if not newest_wall_post: current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) - current_wall_posts = current_wall_posts.data - post = current_wall_posts[0] - if post.id != newest_wall_post: - new_posts = [] - for post in current_wall_posts: - if post.id == newest_wall_post: - break - new_posts.append(post) - newest_wall_post = current_wall_posts[0].id - for new_post in new_posts: - asyncio.create_task(func(new_post)) - - async def on_group_change(self, func: Callable, delay: int): - await self.group.update() - current_group = copy.copy(self.group) - while True: - await asyncio.sleep(delay) + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts.data[0].id + return event.edit(arguments=tuple(old_arguments)) + + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + current_wall_posts = current_wall_posts.data + + post = current_wall_posts[0] + if post.id != newest_wall_post: + new_posts = [] + + for post in current_wall_posts: + if post.id == newest_wall_post: + break + new_posts.append(post) + + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts[0].id + event.edit(arguments=tuple(old_arguments)) + + for new_post in new_posts: + asyncio.create_task(func(new_post)) + + async def on_group_change(self, func: Callable, current_group, event: Event): + if not current_group: await self.group.update() - has_changed = False - for attr, value in current_group.__dict__.items(): - if getattr(self.group, attr) != value: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + return event.edit(arguments=tuple(old_arguments)) + + await self.group.update() + + has_changed = False + for attr, value in current_group.__dict__.items(): + other_value = getattr(self.group, attr) + if attr == "shout": + if str(value) != str(other_value): has_changed = True - if has_changed: - asyncio.create_task(func(current_group, self.group)) + else: + continue + if other_value != value: + has_changed = True + + if has_changed: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + event.edit(arguments=tuple(old_arguments)) + asyncio.create_task(func(current_group, self.group)) """ async def on_audit_log(self, func: Callable, delay: int): @@ -1052,7 +1149,7 @@

      Class variables

      Methods

      -def bind(self, func: Callable, event: EventTypes, delay: int = 15) +async def bind(self, func: Callable, event: EventTypes, delay: int = 15)

      Binds a function to an event.

      @@ -1069,7 +1166,7 @@

      Parameters

      Expand source code -
      def bind(self, func: Callable, event: EventTypes, delay: int = 15):
      +
      async def bind(self, func: Callable, event: EventTypes, delay: int = 15):
           """
           Binds a function to an event.
       
      @@ -1083,17 +1180,19 @@ 

      Parameters

      How many seconds between each poll. """ if event == EventTypes.on_join_request: - return asyncio.create_task(self.on_join_request(func, delay)) + event = Event(self.on_join_request, EventTypes.on_join_request, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_wall_post: - return asyncio.create_task(self.on_wall_post(func, delay)) + event = Event(self.on_wall_post, EventTypes.on_wall_post, (func, None), delay) + self.cso.event_handler.add_event(event) if event == EventTypes.on_group_change: - return asyncio.create_task(self.on_group_change(func, delay)) - if event == EventTypes.on_audit_log: - return asyncio.create_task(self.on_audit_log(func, delay))
      + event = Event(self.on_group_change, EventTypes.on_group_change, (func, None), delay) + self.cso.event_handler.add_event(event) + await self.cso.event_handler.listen()
      -async def on_group_change(self, func: Callable, delay: int) +async def on_group_change(self, func: Callable, current_group, event: Event)
      @@ -1101,22 +1200,35 @@

      Parameters

      Expand source code -
      async def on_group_change(self, func: Callable, delay: int):
      -    await self.group.update()
      -    current_group = copy.copy(self.group)
      -    while True:
      -        await asyncio.sleep(delay)
      +
      async def on_group_change(self, func: Callable, current_group, event: Event):
      +    if not current_group:
               await self.group.update()
      -        has_changed = False
      -        for attr, value in current_group.__dict__.items():
      -            if getattr(self.group, attr) != value:
      +        old_arguments = list(event.arguments)
      +        old_arguments[1] = copy.copy(self.group)
      +        return event.edit(arguments=tuple(old_arguments))
      +
      +    await self.group.update()
      +
      +    has_changed = False
      +    for attr, value in current_group.__dict__.items():
      +        other_value = getattr(self.group, attr)
      +        if attr == "shout":
      +            if str(value) != str(other_value):
                       has_changed = True
      -        if has_changed:
      -            asyncio.create_task(func(current_group, self.group))
      + else: + continue + if other_value != value: + has_changed = True + + if has_changed: + old_arguments = list(event.arguments) + old_arguments[1] = copy.copy(self.group) + event.edit(arguments=tuple(old_arguments)) + asyncio.create_task(func(current_group, self.group))
      -async def on_join_request(self, func: Callable, delay: int) +async def on_join_request(self, func: Callable, old_req, event: Event)
      @@ -1124,25 +1236,34 @@

      Parameters

      Expand source code -
      async def on_join_request(self, func: Callable, delay: int):
      -    current_group_reqs = await self.group.get_join_requests()
      -    old_req = current_group_reqs.data.requester.id
      -    while True:
      -        await asyncio.sleep(delay)
      +
      async def on_join_request(self, func: Callable, old_req, event: Event):
      +    if not old_req:
               current_group_reqs = await self.group.get_join_requests()
      -        current_group_reqs = current_group_reqs.data
      -        if current_group_reqs[0].requester.id != old_req:
      -            new_reqs = []
      -            for request in current_group_reqs:
      -                if request.requester.id != old_req:
      -                    new_reqs.append(request)
      -            old_req = current_group_reqs[0].requester.id
      -            for new_req in new_reqs:
      -                asyncio.create_task(func(new_req))
      + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs.data[0].requester.id + return event.edit(arguments=tuple(old_arguments)) + + current_group_reqs = await self.group.get_join_requests() + current_group_reqs = current_group_reqs.data + + if current_group_reqs[0].requester.id != old_req: + new_reqs = [] + + for request in current_group_reqs: + if request.requester.id == old_req: + break + new_reqs.append(request) + + old_arguments = list(event.arguments) + old_arguments[1] = current_group_reqs[0].requester.id + event.edit(arguments=tuple(old_arguments)) + + for new_req in new_reqs: + asyncio.create_task(func(new_req))
      -async def on_wall_post(self, func: Callable, delay: int) +async def on_wall_post(self, func: Callable, newest_wall_post, event: Event)
      @@ -1150,23 +1271,31 @@

      Parameters

      Expand source code -
      async def on_wall_post(self, func: Callable, delay: int):
      -    current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending)
      -    newest_wall_post = current_wall_posts.data[0].id
      -    while True:
      -        await asyncio.sleep(delay)
      +
      async def on_wall_post(self, func: Callable, newest_wall_post, event: Event):
      +    if not newest_wall_post:
               current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending)
      -        current_wall_posts = current_wall_posts.data
      -        post = current_wall_posts[0]
      -        if post.id != newest_wall_post:
      -            new_posts = []
      -            for post in current_wall_posts:
      -                if post.id == newest_wall_post:
      -                    break
      -                new_posts.append(post)
      -            newest_wall_post = current_wall_posts[0].id
      -            for new_post in new_posts:
      -                asyncio.create_task(func(new_post))
      + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts.data[0].id + return event.edit(arguments=tuple(old_arguments)) + + current_wall_posts = await self.group.wall.get_posts(sort_order=SortOrder.Descending) + current_wall_posts = current_wall_posts.data + + post = current_wall_posts[0] + if post.id != newest_wall_post: + new_posts = [] + + for post in current_wall_posts: + if post.id == newest_wall_post: + break + new_posts.append(post) + + old_arguments = list(event.arguments) + old_arguments[1] = current_wall_posts[0].id + event.edit(arguments=tuple(old_arguments)) + + for new_post in new_posts: + asyncio.create_task(func(new_post))
      @@ -1186,6 +1315,7 @@

      Parameters

      Represents a group. """ def __init__(self, cso, group_id): + super().__init__() self.cso = cso """Client Shared Object""" self.requests = cso.requests @@ -1226,7 +1356,7 @@

      Parameters

      self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] if group_info.get('shout'): - self.shout = Shout(self.cso, group_info['shout']) + self.shout = Shout(self.cso, self, group_info['shout']) else: self.shout = None if "isLocked" in group_info: @@ -1235,6 +1365,7 @@

      Parameters

      async def update_shout(self, message): """ Updates the shout of the group. + DEPRECATED: Just call group.shout() Parameters ---------- @@ -1245,13 +1376,7 @@

      Parameters

      ------- int """ - shout_req = await self.requests.patch( - url=endpoint + f"/v1/groups/{self.id}/status", - data={ - "message": message - } - ) - return shout_req.status_code == 200 + return await self.shout(message) async def get_roles(self): """ @@ -1269,10 +1394,10 @@

      Parameters

      roles.append(Role(self.cso, self, role)) return roles - async def get_member_by_id(self, roblox_id): + async def get_member_by_id(self, user_id): # Get list of group user is in. member_req = await self.requests.get( - url=endpoint + f"/v2/users/{roblox_id}/groups/roles" + url=endpoint + f"/v2/users/{user_id}/groups/roles" ) data = member_req.json() @@ -1285,11 +1410,11 @@

      Parameters

      # Check if user is in group. if not group_data: - raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + raise NotFound(f"The user {user_id} was not found in group {self.id}") # Create data to return. role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, roblox_id, "", self, role) + member = Member(self.cso, user_id, "", self, role) return member async def get_member_by_username(self, name): @@ -1470,7 +1595,7 @@

      Methods

      -async def get_member_by_id(self, roblox_id) +async def get_member_by_id(self, user_id)
      @@ -1478,10 +1603,10 @@

      Methods

      Expand source code -
      async def get_member_by_id(self, roblox_id):
      +
      async def get_member_by_id(self, user_id):
           # Get list of group user is in.
           member_req = await self.requests.get(
      -        url=endpoint + f"/v2/users/{roblox_id}/groups/roles"
      +        url=endpoint + f"/v2/users/{user_id}/groups/roles"
           )
           data = member_req.json()
       
      @@ -1494,11 +1619,11 @@ 

      Methods

      # Check if user is in group. if not group_data: - raise NotFound(f"The user {roblox_id} was not found in group {self.id}") + raise NotFound(f"The user {user_id} was not found in group {self.id}") # Create data to return. role = Role(self.cso, self, group_data['role']) - member = Member(self.cso, roblox_id, "", self, role) + member = Member(self.cso, user_id, "", self, role) return member
      @@ -1611,7 +1736,7 @@

      Returns

      self.is_builders_club_only = group_info["isBuildersClubOnly"] self.public_entry_allowed = group_info["publicEntryAllowed"] if group_info.get('shout'): - self.shout = Shout(self.cso, group_info['shout']) + self.shout = Shout(self.cso, self, group_info['shout']) else: self.shout = None if "isLocked" in group_info: @@ -1622,7 +1747,8 @@

      Returns

      async def update_shout(self, message)
  • -

    Updates the shout of the group.

    +

    Updates the shout of the group. +DEPRECATED: Just call group.shout()

    Parameters

    message : str
    @@ -1640,6 +1766,7 @@

    Returns

    async def update_shout(self, message):
         """
         Updates the shout of the group.
    +    DEPRECATED: Just call group.shout()
     
         Parameters
         ----------
    @@ -1650,13 +1777,7 @@ 

    Returns

    ------- int """ - shout_req = await self.requests.patch( - url=endpoint + f"/v1/groups/{self.id}/status", - data={ - "message": message - } - ) - return shout_req.status_code == 200
    + return await self.shout(message)
    @@ -1675,7 +1796,7 @@

    Returns

    def __init__(self, cso, data, group): self.requests = cso.requests self.group = group - self.requester = PartialUser(cso, data['requester']['userId'], data['requester']['username']) + self.requester = PartialUser(cso, data['requester']) self.created = iso8601.parse_date(data['created']) async def accept(self): @@ -1728,7 +1849,7 @@

    Methods

    class Member -(cso, roblox_id, name, group, role) +(cso, user_id, name, group, role)

    Represents a user in a group.

    @@ -1736,7 +1857,7 @@

    Parameters

    cso : Requests
    Requests object to use for API requests.
    -
    roblox_id : int
    +
    user_id : int
    The id of a user.
    name : str
    The name of the user.
    @@ -1749,7 +1870,7 @@

    Parameters

    Expand source code -
    class Member(PartialUser):
    +
    class Member(BaseUser):
         """
         Represents a user in a group.
     
    @@ -1757,7 +1878,7 @@ 

    Parameters

    ---------- cso : ro_py.utilities.requests.Requests Requests object to use for API requests. - roblox_id : int + user_id : int The id of a user. name : str The name of the user. @@ -1766,8 +1887,9 @@

    Parameters

    role : ro_py.roles.Role The role the user has is the group. """ - def __init__(self, cso, roblox_id, name, group, role): - super().__init__(cso, roblox_id, name) + def __init__(self, cso, user_id, name, group, role): + super().__init__(cso, user_id) + self.name = name self.role = role self.group = group @@ -1886,7 +2008,7 @@

    Parameters

    Ancestors

    Methods

    @@ -2118,16 +2240,18 @@

    Returns

    Inherited members

    @@ -2184,7 +2308,7 @@

    Methods

    class Shout -(cso, shout_data) +(cso, group, shout_data)

    Represents a group shout.

    @@ -2196,12 +2320,45 @@

    Methods

    """ Represents a group shout. """ - def __init__(self, cso, shout_data): + def __init__(self, cso, group, shout_data): self.cso = cso + self.requests = cso.requests + self.group = group self.data = shout_data self.body = shout_data["body"] + self.created = iso8601.parse_date(shout_data["created"]) + self.updated = iso8601.parse_date(shout_data["updated"]) + # TODO: Make this a PartialUser - self.poster = None
    + self.poster = None + + def __str__(self): + return self.body + + async def __call__(self, message): + """ + Updates the shout of the group. + Please note that doing so will completely delete this Shout object and return a new Shout object. + The parent group's shout parameter will also be updated accordingly. + + Parameters + ---------- + message : str + Message that will overwrite the current shout of a group. + + Returns + ------- + ro_py.groups.Shout + + """ + shout_req = await self.requests.patch( + url=endpoint + f"/v1/groups/{self.group.id}/status", + data={ + "message": message + } + ) + self.group.shout = Shout(self.cso, self.group, shout_req.json()) + return self.group.shout
    diff --git a/docs/index.html b/docs/index.html index 9edfeb7e..aedc7c9d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -83,7 +83,7 @@

    ro.py is a powerful Python 3 wrapper for the Roblox Web API by @jmkd3v and @iranathan.

    -ro.py Discord +ro.py Discord ro.py PyPI ro.py PyPI Downloads ro.py PyPI License @@ -92,7 +92,7 @@

    ro.py is a powerful Python 3 wrapper for the Roblox Web API b

    Information | -Discord | +Discord | Requirements | Disclaimer | Documentation | @@ -113,7 +113,7 @@

    ro.py is a powerful Python 3 wrapper for the Roblox Web API b <h4 align="center">ro.py is a powerful Python 3 wrapper for the Roblox Web API by <a href="https://github.com/jmkd3v">@jmkd3v</a> and <a href="https://github.com/iranathan">@iranathan</a>.</h4> <p align="center"> - <a href="https://j-mk.ml/ro.py"><img src="https://img.shields.io/discord/761603917490159676?style=flat-square&logo=discord" alt="ro.py Discord"/></a> + <a href="https://jmk.gg/ro.py"><img src="https://img.shields.io/discord/761603917490159676?style=flat-square&logo=discord" alt="ro.py Discord"/></a> <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/v/ro-py?style=flat-square" alt="ro.py PyPI"/></a> <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/dm/ro-py?style=flat-square" alt="ro.py PyPI Downloads"/></a> <a href="https://pypi.org/project/ro-py/"><img src="https://img.shields.io/pypi/l/ro-py?style=flat-square" alt="ro.py PyPI License"/></a> @@ -123,7 +123,7 @@

    ro.py is a powerful Python 3 wrapper for the Roblox Web API b <p align="center"> <a href="https://github.com/rbx-libdev/ro.py#information">Information</a> | - <a href="http://j-mk.ml/ro.py">Discord</a> | + <a href="http://jmk.gg/ro.py">Discord</a> | <a href="https://github.com/rbx-libdev/ro.py#requirements">Requirements</a> | <a href="https://github.com/rbx-libdev/ro.py#disclaimer">Disclaimer</a> | <a href="https://github.com/rbx-libdev/ro.py#documentation">Documentation</a> | @@ -156,6 +156,10 @@

    Sub-modules

    This file houses functions and classes that pertain to game-awarded badges.

    +
    ro_py.bases
    +
    +

    This folder houses base/partial objects that other parts of ro.py inherit.

    +
    ro_py.captcha

    This file houses functions and classes that pertain to the Roblox captcha.

    @@ -178,12 +182,21 @@

    Sub-modules

    ro_py.events
    -
    +

    This file houses functions and classes that pertain to events and event handling with ro.py. Most methods that have +events actually don't reference …

    ro_py.extensions

    This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement.

    +
    ro_py.friends
    +
    +
    +
    +
    ro_py.gamepasses
    +
    +
    +
    ro_py.gamepersistence

    This file houses functions used for tampering with Roblox Datastores

    @@ -201,10 +214,9 @@

    Sub-modules

    This file houses functions and classes that pertain to Roblox groups.

    -
    ro_py.notifications
    +
    ro_py.presence
    -

    This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger -notification menu on the Roblox web …

    +
    ro_py.robloxbadges
    @@ -270,6 +282,7 @@

    Index

  • ro_py.accountsettings
  • ro_py.assets
  • ro_py.badges
  • +
  • ro_py.bases
  • ro_py.captcha
  • ro_py.catalog
  • ro_py.chat
  • @@ -277,11 +290,13 @@

    Index

  • ro_py.economy
  • ro_py.events
  • ro_py.extensions
  • +
  • ro_py.friends
  • +
  • ro_py.gamepasses
  • ro_py.gamepersistence
  • ro_py.games
  • ro_py.gender
  • ro_py.groups
  • -
  • ro_py.notifications
  • +
  • ro_py.presence
  • ro_py.robloxbadges
  • ro_py.robloxdocs
  • ro_py.robloxstatus
  • diff --git a/docs/notifications.html b/docs/notifications.html deleted file mode 100644 index 75466b9f..00000000 --- a/docs/notifications.html +++ /dev/null @@ -1,459 +0,0 @@ - - - - - - -ro_py.notifications API documentation - - - - - - - - - - - - -
    -
    -
    -

    Module ro_py.notifications

    -
    -
    -

    This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger -notification menu on the Roblox web client.

    -
    -

    Warning

    -

    This part of ro.py may have bugs and I don't recommend relying on it for daily use. -Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond -to Roblox chat messages, which is pretty neat.

    -
    -
    - -Expand source code - -
    """
    -
    -This file houses functions and classes that pertain to Roblox notifications as you would see in the hamburger
    -notification menu on the Roblox web client.
    -
    -.. warning::
    -    This part of ro.py may have bugs and I don't recommend relying on it for daily use.
    -    Though it may contain bugs it's fairly reliable in my experience and is powerful enough to create bots that respond
    -    to Roblox chat messages, which is pretty neat.
    -"""
    -
    -from ro_py.utilities.caseconvert import to_snake_case
    -
    -from signalrcore.hub_connection_builder import HubConnectionBuilder
    -from urllib.parse import quote
    -import json
    -
    -
    -class Notification:
    -    """
    -    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
    -    """
    -
    -    def __init__(self, notification_data):
    -        self.identifier = notification_data["C"]
    -        self.hub = notification_data["M"][0]["H"]
    -        self.type = None
    -        self.rtype = notification_data["M"][0]["M"]
    -        self.atype = notification_data["M"][0]["A"][0]
    -        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
    -        self.data = None
    -
    -        if isinstance(self.raw_data, dict):
    -            self.data = {}
    -            for key, value in self.raw_data.items():
    -                self.data[to_snake_case(key)] = value
    -
    -            if "type" in self.data:
    -                self.type = self.data["type"]
    -            elif "Type" in self.data:
    -                self.type = self.data["Type"]
    -
    -        elif isinstance(self.raw_data, list):
    -            self.data = []
    -            for value in self.raw_data:
    -                self.data.append(value)
    -
    -            if len(self.data) > 0:
    -                if "type" in self.data[0]:
    -                    self.type = self.data[0]["type"]
    -                elif "Type" in self.data[0]:
    -                    self.type = self.data[0]["Type"]
    -
    -
    -class NotificationReceiver:
    -    """
    -    This object is used to receive notifications.
    -    This should only be generated once per client as to not duplicate notifications.
    -    """
    -
    -    def __init__(self, cso):
    -        self.cso = cso
    -        self.requests = cso.requests
    -        self.evtloop = cso.evtloop
    -        self.negotiate_request = None
    -        self.wss_url = None
    -        self.connection = None
    -
    -    async def initialize(self):
    -        self.negotiate_request = await self.requests.get(
    -            url="https://realtime.roblox.com/notifications/negotiate"
    -                "?clientProtocol=1.5"
    -                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
    -            cookies=self.requests.session.cookies
    -        )
    -        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
    -                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
    -                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
    -        self.connection = HubConnectionBuilder()
    -        self.connection.with_url(
    -            self.wss_url,
    -            options={
    -                "headers": {
    -                    "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};"
    -                },
    -                "skip_negotiation": False
    -            }
    -        )
    -
    -        def on_message(_self, raw_notification):
    -            """
    -            Internal callback when a message is received.
    -            """
    -            try:
    -                notification_json = json.loads(raw_notification)
    -            except json.decoder.JSONDecodeError:
    -                return
    -            if len(notification_json) > 0:
    -                notification = Notification(notification_json)
    -                self.evtloop.run_until_complete(self.on_notification(notification))
    -            else:
    -                return
    -
    -        self.connection.with_automatic_reconnect({
    -            "type": "raw",
    -            "keep_alive_interval": 10,
    -            "reconnect_interval": 5,
    -            "max_attempts": 5
    -        }).build()
    -
    -        self.connection.hub.on_message = on_message
    -
    -        self.connection.start()
    -
    -    def close(self):
    -        """
    -        Closes the connection and stops receiving notifications.
    -        """
    -        self.connection.stop()
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -

    Classes

    -
    -
    -class Notification -(notification_data) -
    -
    -

    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.

    -
    - -Expand source code - -
    class Notification:
    -    """
    -    Represents a Roblox notification as you would see in the notifications menu on the top of the Roblox web client.
    -    """
    -
    -    def __init__(self, notification_data):
    -        self.identifier = notification_data["C"]
    -        self.hub = notification_data["M"][0]["H"]
    -        self.type = None
    -        self.rtype = notification_data["M"][0]["M"]
    -        self.atype = notification_data["M"][0]["A"][0]
    -        self.raw_data = json.loads(notification_data["M"][0]["A"][1])
    -        self.data = None
    -
    -        if isinstance(self.raw_data, dict):
    -            self.data = {}
    -            for key, value in self.raw_data.items():
    -                self.data[to_snake_case(key)] = value
    -
    -            if "type" in self.data:
    -                self.type = self.data["type"]
    -            elif "Type" in self.data:
    -                self.type = self.data["Type"]
    -
    -        elif isinstance(self.raw_data, list):
    -            self.data = []
    -            for value in self.raw_data:
    -                self.data.append(value)
    -
    -            if len(self.data) > 0:
    -                if "type" in self.data[0]:
    -                    self.type = self.data[0]["type"]
    -                elif "Type" in self.data[0]:
    -                    self.type = self.data[0]["Type"]
    -
    -
    -
    -class NotificationReceiver -(cso) -
    -
    -

    This object is used to receive notifications. -This should only be generated once per client as to not duplicate notifications.

    -
    - -Expand source code - -
    class NotificationReceiver:
    -    """
    -    This object is used to receive notifications.
    -    This should only be generated once per client as to not duplicate notifications.
    -    """
    -
    -    def __init__(self, cso):
    -        self.cso = cso
    -        self.requests = cso.requests
    -        self.evtloop = cso.evtloop
    -        self.negotiate_request = None
    -        self.wss_url = None
    -        self.connection = None
    -
    -    async def initialize(self):
    -        self.negotiate_request = await self.requests.get(
    -            url="https://realtime.roblox.com/notifications/negotiate"
    -                "?clientProtocol=1.5"
    -                "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
    -            cookies=self.requests.session.cookies
    -        )
    -        self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
    -                       f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
    -                       f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
    -        self.connection = HubConnectionBuilder()
    -        self.connection.with_url(
    -            self.wss_url,
    -            options={
    -                "headers": {
    -                    "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};"
    -                },
    -                "skip_negotiation": False
    -            }
    -        )
    -
    -        def on_message(_self, raw_notification):
    -            """
    -            Internal callback when a message is received.
    -            """
    -            try:
    -                notification_json = json.loads(raw_notification)
    -            except json.decoder.JSONDecodeError:
    -                return
    -            if len(notification_json) > 0:
    -                notification = Notification(notification_json)
    -                self.evtloop.run_until_complete(self.on_notification(notification))
    -            else:
    -                return
    -
    -        self.connection.with_automatic_reconnect({
    -            "type": "raw",
    -            "keep_alive_interval": 10,
    -            "reconnect_interval": 5,
    -            "max_attempts": 5
    -        }).build()
    -
    -        self.connection.hub.on_message = on_message
    -
    -        self.connection.start()
    -
    -    def close(self):
    -        """
    -        Closes the connection and stops receiving notifications.
    -        """
    -        self.connection.stop()
    -
    -

    Methods

    -
    -
    -def close(self) -
    -
    -

    Closes the connection and stops receiving notifications.

    -
    - -Expand source code - -
    def close(self):
    -    """
    -    Closes the connection and stops receiving notifications.
    -    """
    -    self.connection.stop()
    -
    -
    -
    -async def initialize(self) -
    -
    -
    -
    - -Expand source code - -
    async def initialize(self):
    -    self.negotiate_request = await self.requests.get(
    -        url="https://realtime.roblox.com/notifications/negotiate"
    -            "?clientProtocol=1.5"
    -            "&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D",
    -        cookies=self.requests.session.cookies
    -    )
    -    self.wss_url = f"wss://realtime.roblox.com/notifications?transport=websockets" \
    -                   f"&connectionToken={quote(self.negotiate_request.json()['ConnectionToken'])}" \
    -                   f"&clientProtocol=1.5&connectionData=%5B%7B%22name%22%3A%22usernotificationhub%22%7D%5D"
    -    self.connection = HubConnectionBuilder()
    -    self.connection.with_url(
    -        self.wss_url,
    -        options={
    -            "headers": {
    -                "Cookie": f".ROBLOSECURITY={self.requests.session.cookies['.ROBLOSECURITY']};"
    -            },
    -            "skip_negotiation": False
    -        }
    -    )
    -
    -    def on_message(_self, raw_notification):
    -        """
    -        Internal callback when a message is received.
    -        """
    -        try:
    -            notification_json = json.loads(raw_notification)
    -        except json.decoder.JSONDecodeError:
    -            return
    -        if len(notification_json) > 0:
    -            notification = Notification(notification_json)
    -            self.evtloop.run_until_complete(self.on_notification(notification))
    -        else:
    -            return
    -
    -    self.connection.with_automatic_reconnect({
    -        "type": "raw",
    -        "keep_alive_interval": 10,
    -        "reconnect_interval": 5,
    -        "max_attempts": 5
    -    }).build()
    -
    -    self.connection.hub.on_message = on_message
    -
    -    self.connection.start()
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - \ No newline at end of file diff --git a/docs/presence.html b/docs/presence.html new file mode 100644 index 00000000..056dd892 --- /dev/null +++ b/docs/presence.html @@ -0,0 +1,200 @@ + + + + + + +ro_py.presence API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.presence

    +
    +
    +
    + +Expand source code + +
    import iso8601
    +
    +
    +class Presence:
    +    def __init__(self, cso, user, data):
    +        self.cso = cso
    +        self.requests = cso.requests
    +
    +        self.user = user
    +        self.user_presence_type = data["userPresenceType"]
    +        self.place_id = data["placeId"]
    +        self.root_place_id = data["rootPlaceId"]
    +        self.game_id = data["gameId"]
    +        self.universe_id = data["universeId"]
    +        self.last_location = data["lastLocation"]
    +        self.last_online = iso8601.parse_date(data["lastOnline"])
    +
    +    async def get_game(self):
    +        if self.universe_id:
    +            return await self.cso.client.get_game_by_universe_id(self.universe_id)
    +        else:
    +            return None
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class Presence +(cso, user, data) +
    +
    +
    +
    + +Expand source code + +
    class Presence:
    +    def __init__(self, cso, user, data):
    +        self.cso = cso
    +        self.requests = cso.requests
    +
    +        self.user = user
    +        self.user_presence_type = data["userPresenceType"]
    +        self.place_id = data["placeId"]
    +        self.root_place_id = data["rootPlaceId"]
    +        self.game_id = data["gameId"]
    +        self.universe_id = data["universeId"]
    +        self.last_location = data["lastLocation"]
    +        self.last_online = iso8601.parse_date(data["lastOnline"])
    +
    +    async def get_game(self):
    +        if self.universe_id:
    +            return await self.cso.client.get_game_by_universe_id(self.universe_id)
    +        else:
    +            return None
    +
    +

    Methods

    +
    +
    +async def get_game(self) +
    +
    +
    +
    + +Expand source code + +
    async def get_game(self):
    +    if self.universe_id:
    +        return await self.cso.client.get_game_by_universe_id(self.universe_id)
    +    else:
    +        return None
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/roles.html b/docs/roles.html index e04f859e..c0570412 100644 --- a/docs/roles.html +++ b/docs/roles.html @@ -86,9 +86,8 @@

    Module ro_py.roles

    """ -import enum - -endpoint = "https://groups.roblox.com" +from ro_py.utilities.url import url +endpoint = url("groups") class RolePermissions: diff --git a/docs/thumbnails.html b/docs/thumbnails.html index 82de8ded..5bf061e1 100644 --- a/docs/thumbnails.html +++ b/docs/thumbnails.html @@ -88,7 +88,8 @@

    Module ro_py.thumbnails

    from ro_py.utilities.errors import InvalidShotTypeError import enum -endpoint = "https://thumbnails.roblox.com/" +from ro_py.utilities.url import url +endpoint = url("thumbnails") class ReturnPolicy(enum.Enum): diff --git a/docs/trades.html b/docs/trades.html index 021355c9..7058d5de 100644 --- a/docs/trades.html +++ b/docs/trades.html @@ -84,68 +84,38 @@

    Module ro_py.trades

    This file houses functions and classes that pertain to Roblox trades and trading. """ -from typing import Callable +from typing import Callable, List from ro_py.utilities.pages import Pages, SortOrder +from ro_py.bases.basetrade import PartialTrade from ro_py.assets import Asset, UserAsset -from ro_py.users import PartialUser from ro_py.events import EventTypes -import datetime +from ro_py.users import User import iso8601 import asyncio import enum -endpoint = "https://trades.roblox.com" +from ro_py.utilities.url import url +endpoint = url("trades") def trade_page_handler(requests, this_page, args) -> list: trades_out = [] for raw_trade in this_page: - trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status'])) + trades_out.append(PartialTrade(requests, raw_trade)) return trades_out class Trade: - def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool): - self.trade_id = trade_id - self.requests = requests - self.sender = sender - self.receive_items = receive_items - self.send_items = send_items - self.created = iso8601.parse_date(created) - self.expiration = iso8601.parse_date(expiration) - self.status = status - - async def accept(self) -> bool: - """ - accepts a trade requests - :returns: true/false - """ - accept_req = await self.requests.post( - url=endpoint + f"/v1/trades/{self.trade_id}/accept" - ) - return accept_req.status_code == 200 - - async def decline(self) -> bool: - """ - decline a trade requests - :returns: true/false - """ - decline_req = await self.requests.post( - url=endpoint + f"/v1/trades/{self.trade_id}/decline" - ) - return decline_req.status_code == 200 - - -class PartialTrade: - def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool): + def __init__(self, cso, data, sender: User, send_items: List[Asset], receive_items: List[Asset]): self.cso = cso self.requests = cso.requests - self.trade_id = trade_id - self.user = user - self.created = iso8601.parse_date(created) - self.expiration = iso8601.parse_date(expiration) - self.status = status + self.trade_id = data['id'] + self.sender = sender + self.created = iso8601.parse_date(data['created']) + self.status = data['status'] + self.send_items = send_items + self.receive_items = receive_items async def accept(self) -> bool: """ @@ -167,43 +137,6 @@

    Module ro_py.trades

    ) return decline_req.status_code == 200 - async def expand(self) -> Trade: - """ - gets a more detailed trade request - :return: Trade class - """ - expend_req = await self.requests.get( - url=endpoint + f"/v1/trades/{self.trade_id}" - ) - data = expend_req.json() - - # generate a user class and update it - sender = await self.cso.client.get_user(data['user']['id']) - await sender.update() - - # load items that will be/have been sent and items that you will/have recieve(d) - receive_items, send_items = [], [] - for items_0 in data['offers'][0]['userAssets']: - item_0 = Asset(self.requests, items_0['assetId']) - await item_0.update() - receive_items.append(item_0) - - for items_1 in data['offers'][1]['userAssets']: - item_1 = Asset(self.requests, items_1['assetId']) - await item_1.update() - send_items.append(item_1) - - return Trade( - self.cso, - self.trade_id, - sender, - receive_items, - send_items, - data['created'], - data['expiration'], - data['status'] - ) - class TradeStatusType(enum.Enum): """ @@ -282,17 +215,16 @@

    Module ro_py.trades

    """ Represents the Roblox trades page. """ - def __init__(self, cso, get_self): + def __init__(self, cso): self.cso = cso self.requests = cso.requests - self.get_self = get_self self.events = Events(cso) self.TradeRequest = TradeRequest - async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages: + async def get_trades(self, trade_status_type=TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = Pages( cso=self.cso, - url=endpoint + f"/v1/trades/{trade_status_type}", + url=endpoint + f"/v1/trades/{trade_status_type.value}", sort_order=sort_order, limit=limit, handler=trade_page_handler @@ -315,7 +247,7 @@

    Module ro_py.trades

    ------- int """ - me = await self.get_self() + me = await self.cso.client.get_self() data = { "offers": [ @@ -391,7 +323,7 @@

    Functions

    def trade_page_handler(requests, this_page, args) -> list:
         trades_out = []
         for raw_trade in this_page:
    -        trades_out.append(PartialTrade(requests, raw_trade["id"], PartialUser(requests, raw_trade["user"]['id'], raw_trade['user']['name']), raw_trade['created'], raw_trade['expiration'], raw_trade['status']))
    +        trades_out.append(PartialTrade(requests, raw_trade))
         return trades_out
    @@ -474,180 +406,9 @@

    Methods

    -
    -class PartialTrade -(cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool) -
    -
    -
    -
    - -Expand source code - -
    class PartialTrade:
    -    def __init__(self, cso, trade_id: int, user: PartialUser, created: datetime.datetime, expiration: datetime.datetime, status: bool):
    -        self.cso = cso
    -        self.requests = cso.requests
    -        self.trade_id = trade_id
    -        self.user = user
    -        self.created = iso8601.parse_date(created)
    -        self.expiration = iso8601.parse_date(expiration)
    -        self.status = status
    -
    -    async def accept(self) -> bool:
    -        """
    -        accepts a trade requests
    -        :returns: true/false
    -        """
    -        accept_req = await self.requests.post(
    -            url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    -        )
    -        return accept_req.status_code == 200
    -
    -    async def decline(self) -> bool:
    -        """
    -        decline a trade requests
    -        :returns: true/false
    -        """
    -        decline_req = await self.requests.post(
    -            url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    -        )
    -        return decline_req.status_code == 200
    -
    -    async def expand(self) -> Trade:
    -        """
    -        gets a more detailed trade request
    -        :return: Trade class
    -        """
    -        expend_req = await self.requests.get(
    -            url=endpoint + f"/v1/trades/{self.trade_id}"
    -        )
    -        data = expend_req.json()
    -
    -        # generate a user class and update it
    -        sender = await self.cso.client.get_user(data['user']['id'])
    -        await sender.update()
    -
    -        # load items that will be/have been sent and items that you will/have recieve(d)
    -        receive_items, send_items = [], []
    -        for items_0 in data['offers'][0]['userAssets']:
    -            item_0 = Asset(self.requests, items_0['assetId'])
    -            await item_0.update()
    -            receive_items.append(item_0)
    -
    -        for items_1 in data['offers'][1]['userAssets']:
    -            item_1 = Asset(self.requests, items_1['assetId'])
    -            await item_1.update()
    -            send_items.append(item_1)
    -
    -        return Trade(
    -            self.cso,
    -            self.trade_id,
    -            sender,
    -            receive_items,
    -            send_items,
    -            data['created'],
    -            data['expiration'],
    -            data['status']
    -        )
    -
    -

    Methods

    -
    -
    -async def accept(self) ‑> bool -
    -
    -

    accepts a trade requests -:returns: true/false

    -
    - -Expand source code - -
    async def accept(self) -> bool:
    -    """
    -    accepts a trade requests
    -    :returns: true/false
    -    """
    -    accept_req = await self.requests.post(
    -        url=endpoint + f"/v1/trades/{self.trade_id}/accept"
    -    )
    -    return accept_req.status_code == 200
    -
    -
    -
    -async def decline(self) ‑> bool -
    -
    -

    decline a trade requests -:returns: true/false

    -
    - -Expand source code - -
    async def decline(self) -> bool:
    -    """
    -    decline a trade requests
    -    :returns: true/false
    -    """
    -    decline_req = await self.requests.post(
    -        url=endpoint + f"/v1/trades/{self.trade_id}/decline"
    -    )
    -    return decline_req.status_code == 200
    -
    -
    -
    -async def expand(self) ‑> Trade -
    -
    -

    gets a more detailed trade request -:return: Trade class

    -
    - -Expand source code - -
    async def expand(self) -> Trade:
    -    """
    -    gets a more detailed trade request
    -    :return: Trade class
    -    """
    -    expend_req = await self.requests.get(
    -        url=endpoint + f"/v1/trades/{self.trade_id}"
    -    )
    -    data = expend_req.json()
    -
    -    # generate a user class and update it
    -    sender = await self.cso.client.get_user(data['user']['id'])
    -    await sender.update()
    -
    -    # load items that will be/have been sent and items that you will/have recieve(d)
    -    receive_items, send_items = [], []
    -    for items_0 in data['offers'][0]['userAssets']:
    -        item_0 = Asset(self.requests, items_0['assetId'])
    -        await item_0.update()
    -        receive_items.append(item_0)
    -
    -    for items_1 in data['offers'][1]['userAssets']:
    -        item_1 = Asset(self.requests, items_1['assetId'])
    -        await item_1.update()
    -        send_items.append(item_1)
    -
    -    return Trade(
    -        self.cso,
    -        self.trade_id,
    -        sender,
    -        receive_items,
    -        send_items,
    -        data['created'],
    -        data['expiration'],
    -        data['status']
    -    )
    -
    -
    -
    -
    class Trade -(requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool) +(cso, data, sender: User, send_items: List[Asset], receive_items: List[Asset])
    @@ -656,15 +417,15 @@

    Methods

    Expand source code
    class Trade:
    -    def __init__(self, requests, trade_id: int, sender: PartialUser, receive_items, send_items, created: datetime.datetime, expiration: datetime.datetime, status: bool):
    -        self.trade_id = trade_id
    -        self.requests = requests
    +    def __init__(self, cso, data, sender: User, send_items: List[Asset], receive_items: List[Asset]):
    +        self.cso = cso
    +        self.requests = cso.requests
    +        self.trade_id = data['id']
             self.sender = sender
    -        self.receive_items = receive_items
    +        self.created = iso8601.parse_date(data['created'])
    +        self.status = data['status']
             self.send_items = send_items
    -        self.created = iso8601.parse_date(created)
    -        self.expiration = iso8601.parse_date(expiration)
    -        self.status = status
    +        self.receive_items = receive_items
     
         async def accept(self) -> bool:
             """
    @@ -970,7 +731,7 @@ 

    Class variables

    class TradesWrapper -(cso, get_self) +(cso)

    Represents the Roblox trades page.

    @@ -982,17 +743,16 @@

    Class variables

    """ Represents the Roblox trades page. """ - def __init__(self, cso, get_self): + def __init__(self, cso): self.cso = cso self.requests = cso.requests - self.get_self = get_self self.events = Events(cso) self.TradeRequest = TradeRequest - async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages: + async def get_trades(self, trade_status_type=TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages: trades = Pages( cso=self.cso, - url=endpoint + f"/v1/trades/{trade_status_type}", + url=endpoint + f"/v1/trades/{trade_status_type.value}", sort_order=sort_order, limit=limit, handler=trade_page_handler @@ -1015,7 +775,7 @@

    Class variables

    ------- int """ - me = await self.get_self() + me = await self.cso.client.get_self() data = { "offers": [ @@ -1051,7 +811,7 @@

    Class variables

    Methods

    -async def get_trades(self, trade_status_type='Inbound', sort_order=SortOrder.Ascending, limit=10) ‑> Pages +async def get_trades(self, trade_status_type=TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) ‑> Pages
    @@ -1059,10 +819,10 @@

    Methods

    Expand source code -
    async def get_trades(self, trade_status_type=TradeStatusType.Inbound.value, sort_order=SortOrder.Ascending, limit=10) -> Pages:
    +
    async def get_trades(self, trade_status_type=TradeStatusType.Inbound, sort_order=SortOrder.Ascending, limit=10) -> Pages:
         trades = Pages(
             cso=self.cso,
    -        url=endpoint + f"/v1/trades/{trade_status_type}",
    +        url=endpoint + f"/v1/trades/{trade_status_type.value}",
             sort_order=sort_order,
             limit=limit,
             handler=trade_page_handler
    @@ -1107,7 +867,7 @@ 

    Returns

    ------- int """ - me = await self.get_self() + me = await self.cso.client.get_self() data = { "offers": [ @@ -1177,14 +937,6 @@

    Events<
  • -

    PartialTrade

    - -
  • -
  • Trade

    • accept
    • diff --git a/docs/users.html b/docs/users.html index e3c17074..f7a757c8 100644 --- a/docs/users.html +++ b/docs/users.html @@ -86,176 +86,43 @@

      Module ro_py.users

      """ import copy -from typing import List, Callable +import iso8601 +import asyncio +from typing import Callable from ro_py.events import EventTypes -from ro_py.robloxbadges import RobloxBadge +from ro_py.bases.baseuser import BaseUser from ro_py.thumbnails import UserThumbnailGenerator from ro_py.utilities.clientobject import ClientObject -from ro_py.utilities.pages import Pages -from ro_py.assets import UserAsset -import iso8601 -import asyncio - -endpoint = "https://users.roblox.com/" - - -def limited_handler(requests, data, args): - assets = [] - for asset in data: - assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId'])) - return assets - - -class PartialUser: - def __init__(self, cso, roblox_id, roblox_name=None): - self.cso = cso - self.requests = cso.requests - self.id = roblox_id - self.name = roblox_name - - async def expand(self): - """ - Updates some class values. - :return: Nothing - """ - user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}") - user_info = user_info_req.json() - description = user_info["description"] - created = iso8601.parse_date(user_info["created"]) - is_banned = user_info["isBanned"] - name = user_info["name"] - display_name = user_info["displayName"] - # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") - # self.has_premium = has_premium_req - return User(self.cso, self.id, name, description, created, is_banned, display_name) - - async def get_roblox_badges(self) -> List[RobloxBadge]: - """ - Gets the user's roblox badges. - :return: A list of RobloxBadge instances - """ - roblox_badges_req = await self.requests.get( - f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges") - roblox_badges = [] - for roblox_badge_data in roblox_badges_req.json(): - roblox_badges.append(RobloxBadge(roblox_badge_data)) - return roblox_badges - - async def get_friends_count(self) -> int: - """ - Gets the user's friends count. - :return: An integer - """ - friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count") - friends_count = friends_count_req.json()["count"] - return friends_count - - async def get_followers_count(self) -> int: - """ - Gets the user's followers count. - :return: An integer - """ - followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count") - followers_count = followers_count_req.json()["count"] - return followers_count - - async def get_followings_count(self) -> int: - """ - Gets the user's followings count. - :return: An integer - """ - followings_count_req = await self.requests.get( - f"https://friends.roblox.com/v1/users/{self.id}/followings/count") - followings_count = followings_count_req.json()["count"] - return followings_count - - async def get_friends(self): - """ - Gets the user's friends. - :return: List of Friend - """ - friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends") - friends_raw = friends_req.json()["data"] - friends_list = [] - for friend_raw in friends_raw: - friends_list.append(Friend(self.cso, friend_raw)) - return friends_list - - async def get_groups(self): - from ro_py.groups import PartialGroup - member_req = await self.requests.get( - url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" - ) - data = member_req.json() - groups = [] - for group in data['data']: - group = group['group'] - groups.append(PartialGroup(self.cso, group)) - return groups - async def get_limiteds(self): - """ - Gets all limiteds the user owns. +from ro_py.utilities.url import url +endpoint = url("users") - Returns - ------- - list - """ - return Pages( - cso=self.cso, - url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", - handler=limited_handler - ) - async def get_status(self): - """ - Gets the user's status. - :return: A string - """ - status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status") - return status_req.json()["status"] - - -class Friend(PartialUser): - def __init__(self, cso, data): - super().__init__(cso, data["id"], data["name"]) - self.is_online = data["isOnline"] - self.is_deleted = data["isDeleted"] - self.description = data["description"] - self.created = iso8601.parse_date(data["created"]) - self.is_banned = data["isBanned"] - self.display_name = data["displayName"] - - -class User(PartialUser, ClientObject): +class User(BaseUser, ClientObject): """ Represents a Roblox user and their profile. Can be initialized with either a user ID or a username. + I'm in so much pain + Parameters ---------- cso : ro_py.client.ClientSharedObject ClientSharedObject. - roblox_id : int + user_id : int The id of a user. - roblox_name : str - The name of the user. - description : str - The description of the user. - created : any - Time the user was created. """ - def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name): - super().__init__(cso, roblox_id, roblox_name) + def __init__(self, cso, user_id): + super().__init__(cso, user_id) self.cso = cso - self.id = roblox_id - self.name = roblox_name - self.description = description - self.created = created - self.is_banned = banned - self.display_name = display_name - self.thumbnails = UserThumbnailGenerator(cso, roblox_id) + self.id = user_id + self.name = None + self.description = None + self.created = None + self.is_banned = None + self.display_name = None + self.thumbnails = UserThumbnailGenerator(cso, user_id) async def update(self): """ @@ -269,7 +136,6 @@

      Module ro_py.users

      self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - return self # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") # self.has_premium = has_premium_req @@ -317,25 +183,6 @@

      Module ro_py.users

      -

      Functions

      -
      -
      -def limited_handler(requests, data, args) -
      -
      -
      -
      - -Expand source code - -
      def limited_handler(requests, data, args):
      -    assets = []
      -    for asset in data:
      -        assets.append(UserAsset(requests, asset["assetId"], asset['userAssetId']))
      -    return assets
      -
      -
      -

      Classes

      @@ -452,432 +299,50 @@

      Parameters

  • -
    -class Friend -(cso, data) -
    -
    -
    -
    - -Expand source code - -
    class Friend(PartialUser):
    -    def __init__(self, cso, data):
    -        super().__init__(cso, data["id"], data["name"])
    -        self.is_online = data["isOnline"]
    -        self.is_deleted = data["isDeleted"]
    -        self.description = data["description"]
    -        self.created = iso8601.parse_date(data["created"])
    -        self.is_banned = data["isBanned"]
    -        self.display_name = data["displayName"]
    -
    -

    Ancestors

    - -

    Inherited members

    - -
    -
    -class PartialUser -(cso, roblox_id, roblox_name=None) -
    -
    -
    -
    - -Expand source code - -
    class PartialUser:
    -    def __init__(self, cso, roblox_id, roblox_name=None):
    -        self.cso = cso
    -        self.requests = cso.requests
    -        self.id = roblox_id
    -        self.name = roblox_name
    -
    -    async def expand(self):
    -        """
    -        Updates some class values.
    -        :return: Nothing
    -        """
    -        user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
    -        user_info = user_info_req.json()
    -        description = user_info["description"]
    -        created = iso8601.parse_date(user_info["created"])
    -        is_banned = user_info["isBanned"]
    -        name = user_info["name"]
    -        display_name = user_info["displayName"]
    -        # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
    -        # self.has_premium = has_premium_req
    -        return User(self.cso, self.id, name, description, created, is_banned, display_name)
    -
    -    async def get_roblox_badges(self) -> List[RobloxBadge]:
    -        """
    -        Gets the user's roblox badges.
    -        :return: A list of RobloxBadge instances
    -        """
    -        roblox_badges_req = await self.requests.get(
    -            f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    -        roblox_badges = []
    -        for roblox_badge_data in roblox_badges_req.json():
    -            roblox_badges.append(RobloxBadge(roblox_badge_data))
    -        return roblox_badges
    -
    -    async def get_friends_count(self) -> int:
    -        """
    -        Gets the user's friends count.
    -        :return: An integer
    -        """
    -        friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
    -        friends_count = friends_count_req.json()["count"]
    -        return friends_count
    -
    -    async def get_followers_count(self) -> int:
    -        """
    -        Gets the user's followers count.
    -        :return: An integer
    -        """
    -        followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
    -        followers_count = followers_count_req.json()["count"]
    -        return followers_count
    -
    -    async def get_followings_count(self) -> int:
    -        """
    -        Gets the user's followings count.
    -        :return: An integer
    -        """
    -        followings_count_req = await self.requests.get(
    -            f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    -        followings_count = followings_count_req.json()["count"]
    -        return followings_count
    -
    -    async def get_friends(self):
    -        """
    -        Gets the user's friends.
    -        :return: List of Friend
    -        """
    -        friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
    -        friends_raw = friends_req.json()["data"]
    -        friends_list = []
    -        for friend_raw in friends_raw:
    -            friends_list.append(Friend(self.cso, friend_raw))
    -        return friends_list
    -
    -    async def get_groups(self):
    -        from ro_py.groups import PartialGroup
    -        member_req = await self.requests.get(
    -            url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
    -        )
    -        data = member_req.json()
    -        groups = []
    -        for group in data['data']:
    -            group = group['group']
    -            groups.append(PartialGroup(self.cso, group))
    -        return groups
    -
    -    async def get_limiteds(self):
    -        """
    -        Gets all limiteds the user owns.
    -
    -        Returns
    -        -------
    -        list
    -        """
    -        return Pages(
    -            cso=self.cso,
    -            url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
    -            handler=limited_handler
    -        )
    -
    -    async def get_status(self):
    -        """
    -        Gets the user's status.
    -        :return: A string
    -        """
    -        status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    -        return status_req.json()["status"]
    -
    -

    Subclasses

    - -

    Methods

    -
    -
    -async def expand(self) -
    -
    -

    Updates some class values. -:return: Nothing

    -
    - -Expand source code - -
    async def expand(self):
    -    """
    -    Updates some class values.
    -    :return: Nothing
    -    """
    -    user_info_req = await self.requests.get(endpoint + f"v1/users/{self.id}")
    -    user_info = user_info_req.json()
    -    description = user_info["description"]
    -    created = iso8601.parse_date(user_info["created"])
    -    is_banned = user_info["isBanned"]
    -    name = user_info["name"]
    -    display_name = user_info["displayName"]
    -    # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership")
    -    # self.has_premium = has_premium_req
    -    return User(self.cso, self.id, name, description, created, is_banned, display_name)
    -
    -
    -
    -async def get_followers_count(self) ‑> int -
    -
    -

    Gets the user's followers count. -:return: An integer

    -
    - -Expand source code - -
    async def get_followers_count(self) -> int:
    -    """
    -    Gets the user's followers count.
    -    :return: An integer
    -    """
    -    followers_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/followers/count")
    -    followers_count = followers_count_req.json()["count"]
    -    return followers_count
    -
    -
    -
    -async def get_followings_count(self) ‑> int -
    -
    -

    Gets the user's followings count. -:return: An integer

    -
    - -Expand source code - -
    async def get_followings_count(self) -> int:
    -    """
    -    Gets the user's followings count.
    -    :return: An integer
    -    """
    -    followings_count_req = await self.requests.get(
    -        f"https://friends.roblox.com/v1/users/{self.id}/followings/count")
    -    followings_count = followings_count_req.json()["count"]
    -    return followings_count
    -
    -
    -
    -async def get_friends(self) -
    -
    -

    Gets the user's friends. -:return: List of Friend

    -
    - -Expand source code - -
    async def get_friends(self):
    -    """
    -    Gets the user's friends.
    -    :return: List of Friend
    -    """
    -    friends_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends")
    -    friends_raw = friends_req.json()["data"]
    -    friends_list = []
    -    for friend_raw in friends_raw:
    -        friends_list.append(Friend(self.cso, friend_raw))
    -    return friends_list
    -
    -
    -
    -async def get_friends_count(self) ‑> int -
    -
    -

    Gets the user's friends count. -:return: An integer

    -
    - -Expand source code - -
    async def get_friends_count(self) -> int:
    -    """
    -    Gets the user's friends count.
    -    :return: An integer
    -    """
    -    friends_count_req = await self.requests.get(f"https://friends.roblox.com/v1/users/{self.id}/friends/count")
    -    friends_count = friends_count_req.json()["count"]
    -    return friends_count
    -
    -
    -
    -async def get_groups(self) -
    -
    -
    -
    - -Expand source code - -
    async def get_groups(self):
    -    from ro_py.groups import PartialGroup
    -    member_req = await self.requests.get(
    -        url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles"
    -    )
    -    data = member_req.json()
    -    groups = []
    -    for group in data['data']:
    -        group = group['group']
    -        groups.append(PartialGroup(self.cso, group))
    -    return groups
    -
    -
    -
    -async def get_limiteds(self) -
    -
    -

    Gets all limiteds the user owns.

    -

    Returns

    -
    -
    list
    -
     
    -
    -
    - -Expand source code - -
    async def get_limiteds(self):
    -    """
    -    Gets all limiteds the user owns.
    -
    -    Returns
    -    -------
    -    list
    -    """
    -    return Pages(
    -        cso=self.cso,
    -        url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
    -        handler=limited_handler
    -    )
    -
    -
    -
    -async def get_roblox_badges(self) ‑> List[RobloxBadge] -
    -
    -

    Gets the user's roblox badges. -:return: A list of RobloxBadge instances

    -
    - -Expand source code - -
    async def get_roblox_badges(self) -> List[RobloxBadge]:
    -    """
    -    Gets the user's roblox badges.
    -    :return: A list of RobloxBadge instances
    -    """
    -    roblox_badges_req = await self.requests.get(
    -        f"https://accountinformation.roblox.com/v1/users/{self.id}/roblox-badges")
    -    roblox_badges = []
    -    for roblox_badge_data in roblox_badges_req.json():
    -        roblox_badges.append(RobloxBadge(roblox_badge_data))
    -    return roblox_badges
    -
    -
    -
    -async def get_status(self) -
    -
    -

    Gets the user's status. -:return: A string

    -
    - -Expand source code - -
    async def get_status(self):
    -    """
    -    Gets the user's status.
    -    :return: A string
    -    """
    -    status_req = await self.requests.get(endpoint + f"v1/users/{self.id}/status")
    -    return status_req.json()["status"]
    -
    -
    -
    -
    class User -(cso, roblox_id, roblox_name, description, created, banned, display_name) +(cso, user_id)

    Represents a Roblox user and their profile. Can be initialized with either a user ID or a username.

    +

    I'm in so much pain

    Parameters

    cso : ro_py.client.ClientSharedObject
    ClientSharedObject.
    -
    roblox_id : int
    +
    user_id : int
    The id of a user.
    -
    roblox_name : str
    -
    The name of the user.
    -
    description : str
    -
    The description of the user.
    -
    created : any
    -
    Time the user was created.
    Expand source code -
    class User(PartialUser, ClientObject):
    +
    class User(BaseUser, ClientObject):
         """
         Represents a Roblox user and their profile.
         Can be initialized with either a user ID or a username.
     
    +    I'm in so much pain
    +
         Parameters
         ----------
         cso : ro_py.client.ClientSharedObject
                 ClientSharedObject.
    -    roblox_id : int
    +    user_id : int
                 The id of a user.
    -    roblox_name : str
    -            The name of the user.
    -    description : str
    -            The description of the user.
    -    created : any
    -            Time the user was created.
         """
     
    -    def __init__(self, cso, roblox_id, roblox_name, description, created, banned, display_name):
    -        super().__init__(cso, roblox_id, roblox_name)
    +    def __init__(self, cso, user_id):
    +        super().__init__(cso, user_id)
             self.cso = cso
    -        self.id = roblox_id
    -        self.name = roblox_name
    -        self.description = description
    -        self.created = created
    -        self.is_banned = banned
    -        self.display_name = display_name
    -        self.thumbnails = UserThumbnailGenerator(cso, roblox_id)
    +        self.id = user_id
    +        self.name = None
    +        self.description = None
    +        self.created = None
    +        self.is_banned = None
    +        self.display_name = None
    +        self.thumbnails = UserThumbnailGenerator(cso, user_id)
     
         async def update(self):
             """
    @@ -891,11 +356,12 @@ 

    Parameters

    self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - return self
    + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req

    Ancestors

    Methods

    @@ -922,22 +388,25 @@

    Methods

    self.is_banned = user_info["isBanned"] self.name = user_info["name"] self.display_name = user_info["displayName"] - return self
    + # has_premium_req = requests.get(f"https://premiumfeatures.roblox.com/v1/users/{self.id}/validate-membership") + # self.has_premium = has_premium_req

    Inherited members

    @@ -961,11 +430,6 @@

    Index

  • ro_py
  • -
  • Functions

    - -
  • Classes

    • @@ -976,23 +440,6 @@

      Events

    • -

      Friend

      -
    • -
    • -

      PartialUser

      - -
    • -
    • User

      • update
      • diff --git a/docs/utilities/asset_type.html b/docs/utilities/asset_type.html index 142c54ab..bbaadf46 100644 --- a/docs/utilities/asset_type.html +++ b/docs/utilities/asset_type.html @@ -88,54 +88,76 @@

        Module ro_py.utilities.asset_type

        """ -asset_types = [ - None, - "Image", - "TeeShirt", - "Audio", - "Mesh", - "Lua", - "Hat", - "Place", - "Model", - "Shirt", - "Pants", - "Decal", - "Head", - "Face", - "Gear", - "Badge", - "Animation", - "Torso", - "RightArm", - "LeftArm", - "LeftLeg", - "RightLeg", - "Package", - "GamePass", - "Plugin", - "MeshPart", - "HairAccessory", - "FaceAccessory", - "NeckAccessory", - "ShoulderAccessory", - "FrontAccesory", - "BackAccessory", - "WaistAccessory", - "ClimbAnimation", - "DeathAnimation", - "FallAnimation", - "IdleAnimation", - "JumpAnimation", - "RunAnimation", - "SwimAnimation", - "WalkAnimation", - "PoseAnimation", - "EarAccessory", - "EyeAccessory", - "EmoteAnimation", - "Video" -]
        +from enum import IntEnum + + +class AssetTypes(IntEnum): + Image = 1 + TeeShirt = 2 + Audio = 3 + Mesh = 4 + Lua = 5 + HTML = 6 + Text = 7 + Hat = 8 + Place = 9 + Model = 10 + + Shirt = 11 + Pants = 12 + + Decal = 13 + Avatar = 16 + Head = 17 + Face = 18 + Gear = 19 + Badge = 21 + GroupEmblem = 22 + Animation = 24 + + Arms = 25 + Legs = 26 + Torso = 27 + RightArm = 28 + LeftArm = 29 + LeftLeg = 30 + RightLeg = 31 + Package = 32 + + YouTubeVideo = 33 + GamePass = 34 + App = 45 + Code = 37 + Plugin = 38 + + SolidModel = 39 # Fixed + MeshPart = 40 + + HairAccessory = 41 + FaceAccessory = 42 + NeckAccessory = 43 + ShoulderAccessory = 44 + FrontAccessory = 45 + BackAccessory = 46 + WaistAccessory = 47 + + ClimbAnimation = 48 + DeathAnimation = 49 + FallAnimation = 50 + IdleAnimation = 51 + JumpAnimation = 52 + RunAnimation = 53 + SwimAniation = 54 + WalkAnimation = 55 + PoseAnimation = 56 + EarAccessory = 57 + EyeAccessory = 58 + + LocalizationTableManifest = 59 + LocalizationTableTranslation = 60 + EmoteAnimation = 61 + Video = 62 + TexturePack = 63
        @@ -145,6 +167,329 @@

        Module ro_py.utilities.asset_type

        +

        Classes

        +
        +
        +class AssetTypes +(value, names=None, *, module=None, qualname=None, type=None, start=1) +
        +
        +

        An enumeration.

        +
        + +Expand source code + +
        class AssetTypes(IntEnum):
        +    Image = 1
        +    TeeShirt = 2
        +    Audio = 3
        +    Mesh = 4
        +    Lua = 5
        +    HTML = 6
        +    Text = 7
        +    Hat = 8
        +    Place = 9
        +    Model = 10
        +
        +    Shirt = 11
        +    Pants = 12
        +
        +    Decal = 13
        +    Avatar = 16
        +    Head = 17
        +    Face = 18
        +    Gear = 19
        +    Badge = 21
        +    GroupEmblem = 22
        +    Animation = 24
        +
        +    Arms = 25
        +    Legs = 26
        +    Torso = 27
        +    RightArm = 28
        +    LeftArm = 29
        +    LeftLeg = 30
        +    RightLeg = 31
        +    Package = 32
        +
        +    YouTubeVideo = 33
        +    GamePass = 34
        +    App = 45
        +    Code = 37
        +    Plugin = 38
        +
        +    SolidModel = 39  # Fixed
        +    MeshPart = 40
        +
        +    HairAccessory = 41
        +    FaceAccessory = 42
        +    NeckAccessory = 43
        +    ShoulderAccessory = 44
        +    FrontAccessory = 45
        +    BackAccessory = 46
        +    WaistAccessory = 47
        +
        +    ClimbAnimation = 48
        +    DeathAnimation = 49
        +    FallAnimation = 50
        +    IdleAnimation = 51
        +    JumpAnimation = 52
        +    RunAnimation = 53
        +    SwimAniation = 54
        +    WalkAnimation = 55
        +    PoseAnimation = 56
        +    EarAccessory = 57
        +    EyeAccessory = 58
        +
        +    LocalizationTableManifest = 59
        +    LocalizationTableTranslation = 60
        +    EmoteAnimation = 61
        +    Video = 62
        +    TexturePack = 63
        +
        +

        Ancestors

        +
          +
        • enum.IntEnum
        • +
        • builtins.int
        • +
        • enum.Enum
        • +
        +

        Class variables

        +
        +
        var Animation
        +
        +
        +
        +
        var App
        +
        +
        +
        +
        var Arms
        +
        +
        +
        +
        var Audio
        +
        +
        +
        +
        var Avatar
        +
        +
        +
        +
        var BackAccessory
        +
        +
        +
        +
        var Badge
        +
        +
        +
        +
        var ClimbAnimation
        +
        +
        +
        +
        var Code
        +
        +
        +
        +
        var DeathAnimation
        +
        +
        +
        +
        var Decal
        +
        +
        +
        +
        var EarAccessory
        +
        +
        +
        +
        var EmoteAnimation
        +
        +
        +
        +
        var EyeAccessory
        +
        +
        +
        +
        var Face
        +
        +
        +
        +
        var FaceAccessory
        +
        +
        +
        +
        var FallAnimation
        +
        +
        +
        +
        var FrontAccessory
        +
        +
        +
        +
        var GamePass
        +
        +
        +
        +
        var Gear
        +
        +
        +
        +
        var GroupEmblem
        +
        +
        +
        +
        var HTML
        +
        +
        +
        +
        var HairAccessory
        +
        +
        +
        +
        var Hat
        +
        +
        +
        +
        var Head
        +
        +
        +
        +
        var IdleAnimation
        +
        +
        +
        +
        var Image
        +
        +
        +
        +
        var JumpAnimation
        +
        +
        +
        +
        var LeftArm
        +
        +
        +
        +
        var LeftLeg
        +
        +
        +
        +
        var Legs
        +
        +
        +
        +
        var LocalizationTableManifest
        +
        +
        +
        +
        var LocalizationTableTranslation
        +
        +
        +
        +
        var Lua
        +
        +
        +
        +
        var Mesh
        +
        +
        +
        +
        var MeshPart
        +
        +
        +
        +
        var Model
        +
        +
        +
        +
        var NeckAccessory
        +
        +
        +
        +
        var Package
        +
        +
        +
        +
        var Pants
        +
        +
        +
        +
        var Place
        +
        +
        +
        +
        var Plugin
        +
        +
        +
        +
        var PoseAnimation
        +
        +
        +
        +
        var RightArm
        +
        +
        +
        +
        var RightLeg
        +
        +
        +
        +
        var RunAnimation
        +
        +
        +
        +
        var Shirt
        +
        +
        +
        +
        var ShoulderAccessory
        +
        +
        +
        +
        var SolidModel
        +
        +
        +
        +
        var SwimAniation
        +
        +
        +
        +
        var TeeShirt
        +
        +
        +
        +
        var Text
        +
        +
        +
        +
        var TexturePack
        +
        +
        +
        +
        var Torso
        +
        +
        +
        +
        var Video
        +
        +
        +
        +
        var WaistAccessory
        +
        +
        +
        +
        var WalkAnimation
        +
        +
        +
        +
        var YouTubeVideo
        +
        +
        +
        +
        +
        +
    • +
    • Classes

      + +
    diff --git a/docs/utilities/clientobject.html b/docs/utilities/clientobject.html index ae0d0e8d..3892e3fd 100644 --- a/docs/utilities/clientobject.html +++ b/docs/utilities/clientobject.html @@ -81,12 +81,14 @@

    Module ro_py.utilities.clientobject

    import asyncio
     from ro_py.utilities.cache import Cache
     from ro_py.utilities.requests import Requests
    +from ro_py.events import Event, EventHandler
     
     
     class ClientObject:
         """
         Every object that is grabbable with client.get_x inherits this object.
         """
    +
         async def update(self):
             pass
     
    @@ -103,7 +105,8 @@ 

    Module ro_py.utilities.clientobject

    self.requests = Requests() """Reqests object for all web requests.""" self.evtloop = asyncio.new_event_loop() - """Event loop for certain things."""
    + """Event loop for certain things.""" + self.event_handler = EventHandler()
    @@ -128,6 +131,7 @@

    Classes

    """ Every object that is grabbable with client.get_x inherits this object. """ + async def update(self): pass @@ -136,6 +140,7 @@

    Subclasses

  • Asset
  • Badge
  • Game
  • +
  • Place
  • Group
  • User
  • @@ -178,7 +183,8 @@

    Methods

    self.requests = Requests() """Reqests object for all web requests.""" self.evtloop = asyncio.new_event_loop() - """Event loop for certain things.""" + """Event loop for certain things.""" + self.event_handler = EventHandler()

    Instance variables

    diff --git a/docs/utilities/index.html b/docs/utilities/index.html index 581925ab..3d9f06da 100644 --- a/docs/utilities/index.html +++ b/docs/utilities/index.html @@ -130,6 +130,10 @@

    Sub-modules

    +
    ro_py.utilities.url
    +
    +
    +
    @@ -164,6 +168,7 @@

    Index

  • ro_py.utilities.errors
  • ro_py.utilities.pages
  • ro_py.utilities.requests
  • +
  • ro_py.utilities.url
  • diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html index 250517dc..b40103cc 100644 --- a/docs/utilities/pages.html +++ b/docs/utilities/pages.html @@ -91,20 +91,36 @@

    Module ro_py.utilities.pages

    class Page: - """ - Represents a single page from a Pages object. - """ - def __init__(self, requests, data, handler=None, handler_args=None): - self.previous_page_cursor = data["previousPageCursor"] - """Cursor to navigate to the previous page.""" - self.next_page_cursor = data["nextPageCursor"] - """Cursor to navigate to the next page.""" + """ + Represents a single page from a Pages object. + """ - self.data = data["data"] - """Raw data from this page.""" + def __init__(self, cso, data, pages, handler=None, handler_args=None): + self.cso = cso + """Client shared object.""" + self.previous_page_cursor = data["previousPageCursor"] + """Cursor to navigate to the previous page.""" + self.next_page_cursor = data["nextPageCursor"] + """Cursor to navigate to the next page.""" + self.data = data["data"] + """Raw data from this page.""" + self.pages = pages + """Pages object for iteration.""" - if handler: - self.data = handler(requests, self.data, handler_args) + self.handler = handler + self.handler_args = handler_args + if handler: + self.data = handler(self.cso, self.data, handler_args) + + def update(self, data): + self.previous_page_cursor = data["previousPageCursor"] + self.next_page_cursor = data["nextPageCursor"] + self.data = data["data"] + if self.handler: + self.data = self.handler(self.cso, data["data"], self.handler_args) + + def __getitem__(self, key): + return self.data[key] class Pages: @@ -137,6 +153,21 @@

    Module ro_py.utilities.pages

    """Current page number.""" self.handler_args = handler_args self.data = None + self.i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i == len(self.data.data): + if not self.data.next_page_cursor: + self.i = 0 + raise StopAsyncIteration + await self.next() + self.i = 0 + data = self.data.data[self.i] + self.i += 1 + return data async def get_page(self, cursor=None): """ @@ -153,9 +184,13 @@

    Module ro_py.utilities.pages

    url=self.url, params=this_parameters ) + if self.data: + self.data.update(page_req.json()) + return self.data = Page( - requests=self.cso, + cso=self.cso, data=page_req.json(), + pages=self, handler=self.handler, handler_args=self.handler_args ) @@ -190,7 +225,7 @@

    Classes

    class Page -(requests, data, handler=None, handler_args=None) +(cso, data, pages, handler=None, handler_args=None)

    Represents a single page from a Pages object.

    @@ -199,23 +234,43 @@

    Classes

    Expand source code
    class Page:
    -    """
    -    Represents a single page from a Pages object.
    -    """
    -    def __init__(self, requests, data, handler=None, handler_args=None):
    -        self.previous_page_cursor = data["previousPageCursor"]
    -        """Cursor to navigate to the previous page."""
    -        self.next_page_cursor = data["nextPageCursor"]
    -        """Cursor to navigate to the next page."""
    +        """
    +        Represents a single page from a Pages object.
    +        """
     
    -        self.data = data["data"]
    -        """Raw data from this page."""
    +        def __init__(self, cso, data, pages, handler=None, handler_args=None):
    +            self.cso = cso
    +            """Client shared object."""
    +            self.previous_page_cursor = data["previousPageCursor"]
    +            """Cursor to navigate to the previous page."""
    +            self.next_page_cursor = data["nextPageCursor"]
    +            """Cursor to navigate to the next page."""
    +            self.data = data["data"]
    +            """Raw data from this page."""
    +            self.pages = pages
    +            """Pages object for iteration."""
     
    -        if handler:
    -            self.data = handler(requests, self.data, handler_args)
    + self.handler = handler + self.handler_args = handler_args + if handler: + self.data = handler(self.cso, self.data, handler_args) + + def update(self, data): + self.previous_page_cursor = data["previousPageCursor"] + self.next_page_cursor = data["nextPageCursor"] + self.data = data["data"] + if self.handler: + self.data = self.handler(self.cso, data["data"], self.handler_args) + + def __getitem__(self, key): + return self.data[key]

    Instance variables

    +
    var cso
    +
    +

    Client shared object.

    +
    var data

    Raw data from this page.

    @@ -224,11 +279,35 @@

    Instance variables

    Cursor to navigate to the next page.

    +
    var pages
    +
    +

    Pages object for iteration.

    +
    var previous_page_cursor

    Cursor to navigate to the previous page.

    +

    Methods

    +
    +
    +def update(self, data) +
    +
    +
    +
    + +Expand source code + +
    def update(self, data):
    +    self.previous_page_cursor = data["previousPageCursor"]
    +    self.next_page_cursor = data["nextPageCursor"]
    +    self.data = data["data"]
    +    if self.handler:
    +        self.data = self.handler(self.cso, data["data"], self.handler_args)
    +
    +
    +
    class Pages @@ -276,6 +355,21 @@

    Instance variables

    """Current page number.""" self.handler_args = handler_args self.data = None + self.i = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self.i == len(self.data.data): + if not self.data.next_page_cursor: + self.i = 0 + raise StopAsyncIteration + await self.next() + self.i = 0 + data = self.data.data[self.i] + self.i += 1 + return data async def get_page(self, cursor=None): """ @@ -292,9 +386,13 @@

    Instance variables

    url=self.url, params=this_parameters ) + if self.data: + self.data.update(page_req.json()) + return self.data = Page( - requests=self.cso, + cso=self.cso, data=page_req.json(), + pages=self, handler=self.handler, handler_args=self.handler_args ) @@ -366,9 +464,13 @@

    Methods

    url=self.url, params=this_parameters ) + if self.data: + self.data.update(page_req.json()) + return self.data = Page( - requests=self.cso, + cso=self.cso, data=page_req.json(), + pages=self, handler=self.handler, handler_args=self.handler_args )
    @@ -471,9 +573,12 @@

    Index

  • Page

  • diff --git a/docs/utilities/requests.html b/docs/utilities/requests.html index 307847ac..5feb2c95 100644 --- a/docs/utilities/requests.html +++ b/docs/utilities/requests.html @@ -132,7 +132,7 @@

    Module ro_py.utilities.requests

    if "X-CSRF-TOKEN" in this_request.headers: self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] if this_request.status_code == 403: # Request failed, send it again - this_request = await self.session.post(*args, **kwargs) + this_request = await self.session.request(method, *args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. @@ -291,7 +291,7 @@

    Ancestors

    if "X-CSRF-TOKEN" in this_request.headers: self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] if this_request.status_code == 403: # Request failed, send it again - this_request = await self.session.post(*args, **kwargs) + this_request = await self.session.request(method, *args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. @@ -477,7 +477,7 @@

    Methods

    if "X-CSRF-TOKEN" in this_request.headers: self.session.headers['X-CSRF-TOKEN'] = this_request.headers["X-CSRF-TOKEN"] if this_request.status_code == 403: # Request failed, send it again - this_request = await self.session.post(*args, **kwargs) + this_request = await self.session.request(method, *args, **kwargs) if kwargs.pop("stream", False): # Skip request checking and just get on with it. diff --git a/docs/utilities/url.html b/docs/utilities/url.html new file mode 100644 index 00000000..20b4e824 --- /dev/null +++ b/docs/utilities/url.html @@ -0,0 +1,147 @@ + + + + + + +ro_py.utilities.url API documentation + + + + + + + + + + + + +
    +
    +
    +

    Module ro_py.utilities.url

    +
    +
    +
    + +Expand source code + +
    root_site = "roblox.com"
    +
    +
    +def url(path="www"):
    +    if path:
    +        return f"https://{path}.{root_site}/"
    +    else:
    +        return f"https://{root_site}"
    +
    +
    +
    +
    +
    +
    +
    +

    Functions

    +
    +
    +def url(path='www') +
    +
    +
    +
    + +Expand source code + +
    def url(path="www"):
    +    if path:
    +        return f"https://{path}.{root_site}/"
    +    else:
    +        return f"https://{root_site}"
    +
    +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/docs/wall.html b/docs/wall.html index e9994754..6cfca14a 100644 --- a/docs/wall.html +++ b/docs/wall.html @@ -81,11 +81,11 @@

    Module ro_py.wall

    import iso8601
     from typing import List
     from ro_py.captcha import UnsolvedCaptcha
    +from ro_py.bases.baseuser import PartialUser
     from ro_py.utilities.pages import Pages, SortOrder
    -from ro_py.users import PartialUser
     
    -
    -endpoint = "https://groups.roblox.com"
    +from ro_py.utilities.url import url
    +endpoint = url("groups")
     
     
     class WallPost:
    @@ -101,7 +101,7 @@ 

    Module ro_py.wall

    self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) if wall_data['poster']: - self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + self.poster = PartialUser(self.cso, wall_data['poster']['user']) else: self.poster = None @@ -313,7 +313,7 @@

    Methods

    self.created = iso8601.parse_date(wall_data['created']) self.updated = iso8601.parse_date(wall_data['updated']) if wall_data['poster']: - self.poster = PartialUser(self.cso, wall_data['poster']['user']['userId'], wall_data['poster']['user']['username']) + self.poster = PartialUser(self.cso, wall_data['poster']['user']) else: self.poster = None From 054f40a6d54fa6e46e3c005bef58bf62ea2dafc1 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Sun, 7 Mar 2021 14:50:11 -0500 Subject: [PATCH 495/518] =?UTF-8?q?=E2=9C=A8=20v1.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index 7174c344..5e79dea8 100644 --- a/setup_info.py +++ b/setup_info.py @@ -5,7 +5,7 @@ setup_info = { "name": "ro-py", - "version": "1.1.4", + "version": "1.2.0", "author": "jmkdev and iranathan", "author_email": "jmk@jmksite.dev", "description": "ro.py is a Python wrapper for the Roblox web API.", From 75eeacd762e810b6e1cc03ce73a4b7a9dc62ee1e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 9 Mar 2021 16:44:55 -0500 Subject: [PATCH 496/518] Fixed broken requirement(?) --- setup_info.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index 5e79dea8..ecf4afd9 100644 --- a/setup_info.py +++ b/setup_info.py @@ -28,6 +28,7 @@ "install_requires": [ "httpx", "iso8601", - "lxml" + "lxml", + "requests" ] } From 9a85e4ae4775f7551097fe9959e36be119b26c8e Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 9 Mar 2021 16:49:28 -0500 Subject: [PATCH 497/518] Update sanitycheck.yml --- .github/workflows/sanitycheck.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index dff78116..649d7846 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -7,8 +7,6 @@ on: - 'ro_py/**' - 'tests/**' - workflow_dispatch: - jobs: build: runs-on: ubuntu-latest From f3a615a61031f00cee274d2c3344b7a2ea38732f Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 9 Mar 2021 16:50:16 -0500 Subject: [PATCH 498/518] Revert "Update sanitycheck.yml" This reverts commit 9a85e4ae4775f7551097fe9959e36be119b26c8e. --- .github/workflows/sanitycheck.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sanitycheck.yml b/.github/workflows/sanitycheck.yml index 649d7846..dff78116 100644 --- a/.github/workflows/sanitycheck.yml +++ b/.github/workflows/sanitycheck.yml @@ -7,6 +7,8 @@ on: - 'ro_py/**' - 'tests/**' + workflow_dispatch: + jobs: build: runs-on: ubuntu-latest From 984545f61cfcc15ec7933640d2eec5ddd40c7851 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Tue, 9 Mar 2021 17:19:12 -0500 Subject: [PATCH 499/518] Updated version identifier(?) --- setup_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup_info.py b/setup_info.py index ecf4afd9..d2a36c55 100644 --- a/setup_info.py +++ b/setup_info.py @@ -5,7 +5,7 @@ setup_info = { "name": "ro-py", - "version": "1.2.0", + "version": "1.2.0.5", "author": "jmkdev and iranathan", "author_email": "jmk@jmksite.dev", "description": "ro.py is a Python wrapper for the Roblox web API.", From 355f6f045bb0f620641a5dabe11cd51b4f885e08 Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 16 Mar 2021 18:52:14 +0100 Subject: [PATCH 500/518] fix page iteration and member handler. --- ro_py/groups.py | 3 ++- ro_py/utilities/pages.py | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 42ebbd1b..881e22ec 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -160,7 +160,8 @@ def join_request_handler(cso, data, args): def member_handler(cso, data, args): members = [] for member in data: - members.append(member) + role = Role(cso, args, member['role']) + members.append(Member(cso, member['user']['userId'], member['user']['username'], args, role)) return members diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index b9a9a941..4f94201b 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -79,15 +79,27 @@ def __aiter__(self): return self async def __anext__(self): - if self.i == len(self.data.data): - if not self.data.next_page_cursor: - self.i = 0 - raise StopAsyncIteration - await self.next() + if not self.data.next_page_cursor: self.i = 0 - data = self.data.data[self.i] + raise StopAsyncIteration + if self.i == 0: + self.i += 1 + return self.data + await self.next() self.i += 1 - return data + return self.data + + async def objects(self): + """ + Yields the data of all pages. + """ + while True: + for data in self.data.data: + yield data + if not self.data.next_page_cursor: + break + else: + await self.next() async def get_page(self, cursor=None): """ From 7a05ec7a24199e5727e3454e9f16bd03684297fa Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 16 Mar 2021 19:07:03 +0100 Subject: [PATCH 501/518] Revert "fix page iteration and member handler." This reverts commit 355f6f045bb0f620641a5dabe11cd51b4f885e08. --- ro_py/groups.py | 3 +-- ro_py/utilities/pages.py | 26 +++++++------------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 881e22ec..42ebbd1b 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -160,8 +160,7 @@ def join_request_handler(cso, data, args): def member_handler(cso, data, args): members = [] for member in data: - role = Role(cso, args, member['role']) - members.append(Member(cso, member['user']['userId'], member['user']['username'], args, role)) + members.append(member) return members diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py index 4f94201b..b9a9a941 100644 --- a/ro_py/utilities/pages.py +++ b/ro_py/utilities/pages.py @@ -79,27 +79,15 @@ def __aiter__(self): return self async def __anext__(self): - if not self.data.next_page_cursor: + if self.i == len(self.data.data): + if not self.data.next_page_cursor: + self.i = 0 + raise StopAsyncIteration + await self.next() self.i = 0 - raise StopAsyncIteration - if self.i == 0: - self.i += 1 - return self.data - await self.next() + data = self.data.data[self.i] self.i += 1 - return self.data - - async def objects(self): - """ - Yields the data of all pages. - """ - while True: - for data in self.data.data: - yield data - if not self.data.next_page_cursor: - break - else: - await self.next() + return data async def get_page(self, cursor=None): """ From a135701122e766309949729af7b4aebc74e10bb8 Mon Sep 17 00:00:00 2001 From: ira Date: Tue, 16 Mar 2021 19:09:26 +0100 Subject: [PATCH 502/518] fix member handler --- ro_py/groups.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ro_py/groups.py b/ro_py/groups.py index 42ebbd1b..44758006 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -161,6 +161,8 @@ def member_handler(cso, data, args): members = [] for member in data: members.append(member) + role = Role(cso, args, member['role']) + members.append(Member(cso, member['user']['userId'], member['user']['username'], args, role)) return members From a44ba271ca953e70b021aa40f658482d7668709a Mon Sep 17 00:00:00 2001 From: ira Date: Thu, 18 Mar 2021 13:23:04 +0100 Subject: [PATCH 503/518] fix member handler --- ro_py/groups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 44758006..881e22ec 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -160,7 +160,6 @@ def join_request_handler(cso, data, args): def member_handler(cso, data, args): members = [] for member in data: - members.append(member) role = Role(cso, args, member['role']) members.append(Member(cso, member['user']['userId'], member['user']['username'], args, role)) return members From d00882046394b418100df44069bf0a46a54b2be1 Mon Sep 17 00:00:00 2001 From: jmkd3v Date: Fri, 19 Mar 2021 20:26:43 -0400 Subject: [PATCH 504/518] Modified endpoints used for pages + builder/builderId support --- ro_py/bases/baseuser.py | 8 ++++---- ro_py/games.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ro_py/bases/baseuser.py b/ro_py/bases/baseuser.py index 505de691..8b58cab6 100644 --- a/ro_py/bases/baseuser.py +++ b/ro_py/bases/baseuser.py @@ -110,7 +110,7 @@ async def get_groups(self): """ from ro_py.groups import PartialGroup member_req = await self.requests.get( - url=f"https://groups.roblox.com/v2/users/{self.id}/groups/roles" + url=f"{url('groups')}v2/users/{self.id}/groups/roles" ) data = member_req.json() groups = [] @@ -129,7 +129,7 @@ async def get_limiteds(self): """ return Pages( cso=self.cso, - url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", + url=f"{url('inventory')}v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", handler=limited_handler ) @@ -173,7 +173,7 @@ async def has_badge(self, badge: Badge): class PartialUser(BaseUser): def __init__(self, cso, data): - self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId") + self.id = data.get("id") or data.get("Id") or data.get("userId") or data.get("user_id") or data.get("UserId") or data.get("builderId") super().__init__(cso, self.id) - self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username") + self.name = data.get("name") or data.get("Name") or data.get("Username") or data.get("username") or data.get("builder") self.display_name = data.get("displayName") or data.get("DisplayName") or data.get("display_name") diff --git a/ro_py/games.py b/ro_py/games.py index a27c4ef1..40805334 100644 --- a/ro_py/games.py +++ b/ro_py/games.py @@ -156,7 +156,7 @@ async def update(self): self.name = place_data["name"] self.description = place_data["description"] self.url = place_data["url"] - self.creator = PartialUser(self.cso, place_data["builderId"], place_data["builder"]) + self.creator = PartialUser(self.cso, place_data) self.is_playable = place_data["isPlayable"] self.reason_prohibited = place_data["reasonProhibited"] self.price = place_data["price"] From 4d18650e496adeb1ad6f2455bd73a0732a88c805 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 7 Apr 2021 19:40:38 -0400 Subject: [PATCH 505/518] Updated index with new libraries --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8b1ca8ec..c2118d04 100644 --- a/README.md +++ b/README.md @@ -97,22 +97,22 @@ pip3 install git+git://github.com/rbx-libdev/ro.py.git ## Other Libraries ro.py not for you? Come check out these other libraries! -Name | Language | OOP | Async | Maintained | More Info | -------------------------------------------------------------|------------|---------|-------|------------|---------------------------------| -[ro.py](https://github.com/rbx-libdev/ro.py) | Python 3 | Yes | Yes | Yes | You are here! | -[robloxapi](https://github.com/iranathan/robloxapi) | Python 3 | Yes | Yes | No | Predecessor to ro.py. | -[robloxlib](https://github.com/NoahCristino/robloxlib) | Python 3 | Yes? | No | No | | -[pyblox](https://github.com/RbxAPI/Pyblox) | Python 3 | Partial | No | Yes | | -[bloxy](https://github.com/Visualizememe/bloxy) | Node.JS | Yes | Yes | Yes | | -[noblox.js](https://github.com/suufi/noblox.js) | Node.JS | No | Yes | Yes | | -[roblox.js](https://github.com/sentanos/roblox-js) | Node.JS | No | Yes? | No | Predecessor to noblox.js. | -[cblox](https://github.com/Meqolo/cblox) | C++ | Yes | No? | Yes | | -[robloxapi](https://github.com/gamenew09/RobloxAPI) | C# | Yes | Yes | Maybe | | -[roblox4j](https://github.com/PizzaCrust/Roblox4j) | Java | Yes | No? | No | | -[javablox](https://github.com/RbxAPI/Javablox) | Java | Yes | No? | No | | -robloxkt | Kotlin | ? | ? | No | I have no information on this. | -[KotlinRoblox](https://github.com/PizzaCrust/KotlinRoblox) | Kotlin | Yes? | No? | No | | -[rbx.lua](https://github.com/iiToxicity/rbx.lua) | Lua | N/A | No? | Yes? | | -robloxcomm | Lua | N/A | ? | ? | Again, no info on this or link. | -[tsblox](https://github.com/Dionysusnu/TSBlox) | TypeScript | Yes | Yes | No | | -roblophp | PHP | ? | ? | ? | Repo seems to be deleted. | +Name | Language | OOP | Async | Maintained | Developers | More Info | +------------------------------------------------------------|------------|---------|-------|------------|-------------------------------------------------------------------------------|---------------------------------| +[ro.py](https://github.com/rbx-libdev/ro.py) | Python 3 | Yes | Yes | Yes | [@jmkd3v](http://github.com/jmkd3v) [@iranathan](http://github.com/iranathan) | You are here! | +[robloxapi](https://github.com/iranathan/robloxapi) | Python 3 | Yes | Yes | No | [@iranathan](http://github.com/iranathan) | Predecessor to ro.py. | +[robloxlib](https://github.com/NoahCristino/robloxlib) | Python 3 | Yes? | No | No | [@NoahCristino](http://github.com/NoahCristino) | | +[pyblox](https://github.com/RbxAPI/Pyblox) | Python 3 | Partial | No | Yes | [@Sanjay-B](http://github.com/Sanjay-B) | | +[bloxy](https://github.com/Visualizememe/bloxy) | Node.JS | Yes | Yes | Yes | [@Visualizememe](http://github.com/Visualizememe) | | +[noblox.js](https://github.com/suufi/noblox.js) | Node.JS | No | Yes | Yes | [@suufi](http://github.com/suufi) | | +[roblox.js](https://github.com/sentanos/roblox-js) | Node.JS | No | Yes? | No | [@sentanos](http://github.com/sentanos) | Predecessor to noblox.js. | +[cblox](https://github.com/Meqolo/cblox) | C++ | Yes | No? | Yes | [@Meqolo](http://github.com/Meqolo) | | +[robloxapi](https://github.com/gamenew09/RobloxAPI) | C# | Yes | Yes | Maybe | [@gamenew09](http://github.com/gamenew09) | | +[roblox4j](https://github.com/PizzaCrust/Roblox4j) | Java | Yes | No? | No | [@PizzaCrust](http://github.com/PizzaCrust) | | +[javablox](https://github.com/RbxAPI/Javablox) | Java | Yes | No? | No | [@Pythonic-Rainbow](http://github.com/Pythonic-Rainbow) | | +robloxkt | Kotlin | ? | ? | No | ? | I have no information on this. | +[KotlinRoblox](https://github.com/PizzaCrust/KotlinRoblox) | Kotlin | Yes? | No? | No | [@Pythonic-Rainbow](http://github.com/Pythonic-Rainbow) | | +[rbx.lua](https://github.com/iiToxicity/rbx.lua) | Lua | N/A | No? | Yes? | [@iiToxicity](http://github.com/iiToxicity) | | +robloxcomm | Lua | N/A | ? | ? | ? | Again, no info on this or link. | +[tsblox](https://github.com/Dionysusnu/TSBlox) | TypeScript | Yes | Yes | No | [@Dionysusnu](http://github.com/Dionysusnu) | | +roblophp | PHP | ? | ? | ? | ? | Repo seems to be deleted. | From f69c77ad2095250d49e4bd849c710422d8c9f140 Mon Sep 17 00:00:00 2001 From: jmkd3v Date: Fri, 9 Apr 2021 20:20:35 -0400 Subject: [PATCH 506/518] Fixed really old bug with gender enum --- ro_py/gender.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/gender.py b/ro_py/gender.py index e1e2a70d..c972c199 100644 --- a/ro_py/gender.py +++ b/ro_py/gender.py @@ -13,5 +13,5 @@ class RobloxGender(enum.Enum): Represents the gender of the authenticated Roblox client. """ Other = 1 - Female = 2 - Male = 3 + Male = 2 + Female = 3 From 49904dfb24f24865ba82e362ef2689127c0318ab Mon Sep 17 00:00:00 2001 From: jmkdev Date: Mon, 12 Apr 2021 20:57:33 -0400 Subject: [PATCH 507/518] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c2118d04..9ae7e745 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ Welcome, and thank you for using ro.py! ro.py is an object oriented, asynchronous wrapper for the Roblox Web API (and other Roblox-related APIs) with many new and interesting features. ro.py allows you to automate much of what you would do on the Roblox website and on other Roblox-related websites. -## Update: ro.py on Discord -I’ve set up a small ro.py Discord server. It’s obviously very tiny, but some of you can be the first people to help found the server. If you need support for the library, you can ask your questions here if you need faster support. http://jmk.gg/ro.py +## Update: ro.py rewrite +We are currently working on a complete ro.py rewrite, and as such we are not accepting feature requests until the rewrite branch is on par with the main branch. +If you'd like to give suggestions on ro.py design decisions, you can join the ro.py Discord. ## Get Started To begin, first import the client, which is the most essential part of ro.py, and initialize it like so: From 71e052e438a5552e55b29a2528ac52eeeeba86eb Mon Sep 17 00:00:00 2001 From: Zamdie <74075188+Zamdie@users.noreply.github.com> Date: Wed, 16 Jun 2021 22:24:22 +0200 Subject: [PATCH 508/518] Add Shout.poster --- ro_py/groups.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 881e22ec..1ac608bf 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -38,8 +38,7 @@ def __init__(self, cso, group, shout_data): self.created = iso8601.parse_date(shout_data["created"]) self.updated = iso8601.parse_date(shout_data["updated"]) - # TODO: Make this a PartialUser - self.poster = None + self.poster = PartialUser(cso, shout_data["poster"]) def __str__(self): return self.body From 6ecb53309dc7eb9c834b325e0b8ef7d03e1ac0e8 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Mon, 21 Jun 2021 20:37:58 +0200 Subject: [PATCH 509/518] Added get_groups_role --- ro_py/bases/baseuser.py | 22 ++++++++++++++++++++++ ro_py/roles.py | 2 +- tests/sanitycheck.py | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/ro_py/bases/baseuser.py b/ro_py/bases/baseuser.py index 8b58cab6..08cfa21c 100644 --- a/ro_py/bases/baseuser.py +++ b/ro_py/bases/baseuser.py @@ -119,6 +119,28 @@ async def get_groups(self): groups.append(PartialGroup(self.cso, group)) return groups + async def get_groups_role(self): + """ + Gets the user's groups. + + Returns + ------- + List[ro_py.roles.Role] + """ + from ro_py.groups import PartialGroup + from ro_py.roles import Role + member_req = await self.requests.get( + url=f"{url('groups')}v2/users/{self.id}/groups/roles" + ) + data = member_req.json() + roles = [] + for group in data['data']: + group = group['group'] + PartialGroup(self.cso, group) + role = group['role'] + Role(self.cso, group, role) + return roles + async def get_limiteds(self): """ Gets all limiteds the user owns. diff --git a/ro_py/roles.py b/ro_py/roles.py index e2b636d1..1b11eece 100644 --- a/ro_py/roles.py +++ b/ro_py/roles.py @@ -69,7 +69,7 @@ class Role: ---------- requests : ro_py.utilities.requests.Requests Requests object to use for API requests. - group : ro_py.groups.Group + group : ro_py.groups.Group or ro_py.groups.PartialGroup Group the role belongs to. role_data : dict Dictionary containing role information. diff --git a/tests/sanitycheck.py b/tests/sanitycheck.py index cde1bd73..864989c2 100644 --- a/tests/sanitycheck.py +++ b/tests/sanitycheck.py @@ -23,6 +23,7 @@ async def client_test(): await user.get_status() await user.get_followings_count() await user.get_groups() + await user.get_groups_role() await user.get_friends() await user.get_followers_count() await user.get_followings_count() From 0915c59e7d3ba882cb5da904a003b33979ce6077 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Mon, 21 Jun 2021 21:12:59 +0200 Subject: [PATCH 510/518] Added BaseUser.get_groups_role --- ro_py/bases/baseuser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ro_py/bases/baseuser.py b/ro_py/bases/baseuser.py index 08cfa21c..088c38d4 100644 --- a/ro_py/bases/baseuser.py +++ b/ro_py/bases/baseuser.py @@ -135,10 +135,10 @@ async def get_groups_role(self): data = member_req.json() roles = [] for group in data['data']: - group = group['group'] - PartialGroup(self.cso, group) role = group['role'] - Role(self.cso, group, role) + group = group['group'] + partial_group = PartialGroup(self.cso, group) + roles.append(Role(self.cso, partial_group, role)) return roles async def get_limiteds(self): From 36c4bfa32a4cb8b143ebf385ca67231cdd406f10 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:50:38 +0200 Subject: [PATCH 511/518] fixed wallPost --- ro_py/wall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ro_py/wall.py b/ro_py/wall.py index 9176d2c7..6804c69c 100644 --- a/ro_py/wall.py +++ b/ro_py/wall.py @@ -27,9 +27,9 @@ def __init__(self, cso, wall_data, group): async def delete(self): wall_req = await self.requests.delete( - url=endpoint + f"/v1/groups/{self.id}/wall/posts/{self.id}" + url=endpoint + f"/v1/groups/{self.group.id}/wall/posts/{self.id}" ) - return wall_req.status == 200 + return wall_req.status_code == 200 def wall_post_handler(requests, this_page, args) -> List[WallPost]: From 77ddc1b6a224a41bd0530501ae4d3c3fb0b9a9a1 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Wed, 23 Jun 2021 10:57:25 +0200 Subject: [PATCH 512/518] Fixed filter_wall example --- examples/filter_wall.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/filter_wall.py b/examples/filter_wall.py index 6e4a0f55..c0b6bfda 100644 --- a/examples/filter_wall.py +++ b/examples/filter_wall.py @@ -18,3 +18,4 @@ async def main(): if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main()) + asyncio.get_event_loop().run_forever() From a4f2a5e0bdb055e177525f537e6471ba47f616b0 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Thu, 24 Jun 2021 21:14:37 +0200 Subject: [PATCH 513/518] Fixed documentation of examples --- examples/filter_wall.py | 2 +- examples/wall_commands.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/filter_wall.py b/examples/filter_wall.py index c0b6bfda..ceebbeef 100644 --- a/examples/filter_wall.py +++ b/examples/filter_wall.py @@ -14,7 +14,7 @@ async def on_wall_post(post): async def main(): group = await client.get_group(group_id) - group.events.bind(client.events.on_wall_post, on_wall_post) + group.events.bind(on_wall_post, client.events.on_wall_post) if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/wall_commands.py b/examples/wall_commands.py index dca8ed19..375a89f3 100644 --- a/examples/wall_commands.py +++ b/examples/wall_commands.py @@ -60,8 +60,9 @@ async def on_wall_post(post): async def main(): client.group = await client.get_group(group_id) - await client.group.events.bind(on_wall_post, client.events.on_wall_post) + client.group.events.bind(on_wall_post, client.events.on_wall_post) if __name__ == '__main__': asyncio.get_event_loop().run_until_complete(main()) + asyncio.get_event_loop().run_forever() From 0da79492ab040e392d7142cba52a61ab3d841e03 Mon Sep 17 00:00:00 2001 From: Boegie19 <34578426+Boegie19@users.noreply.github.com> Date: Fri, 2 Jul 2021 23:10:54 +0200 Subject: [PATCH 514/518] await group.expend --- ro_py/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/groups.py b/ro_py/groups.py index 1ac608bf..07136bc3 100644 --- a/ro_py/groups.py +++ b/ro_py/groups.py @@ -356,7 +356,7 @@ def __init__(self, cso, data): self.member_count = data["memberCount"] async def expand(self): - return self.cso.client.get_group(self.id) + return await self.cso.client.get_group(self.id) class Member(BaseUser): From cdd71a295b4d602c95c3a7e6590d60945ec43506 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Wed, 21 Jul 2021 11:31:32 -0400 Subject: [PATCH 515/518] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9ae7e745..ae3cec9b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ ro.py Discord ro.py PyPI ro.py PyPI Downloads + ro.py PyPI Downloads alt ro.py PyPI License ro.py GitHub Commit Activity ro.py GitHub Last Commit From a45d5e90498c4d486270e91b6f093a127d904e74 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 19 Aug 2021 14:08:34 -0400 Subject: [PATCH 516/518] Create LICENSE --- LICENSE | 695 ++------------------------------------------------------ 1 file changed, 21 insertions(+), 674 deletions(-) diff --git a/LICENSE b/LICENSE index e62ec04c..c91960f3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,21 @@ -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. +MIT License + +Copyright (c) 2021 Roblox Library Development + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 8f9daf2c1010f6a4d65bace3b8c0e155caf558c5 Mon Sep 17 00:00:00 2001 From: jmkdev Date: Thu, 19 Aug 2021 15:05:54 -0400 Subject: [PATCH 517/518] Added mkdocs deploy --- .github/workflows/manual.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/manual.yml diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml new file mode 100644 index 00000000..cdc4f97e --- /dev/null +++ b/.github/workflows/manual.yml @@ -0,0 +1,15 @@ +name: MkDocs Deploy +on: + push: + branches: + - rewrite +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: 3.x + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force From 65571b0779e2bad20c2b549b2ed782215d41cf33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zm=CE=BBdie?= <74075188+zmadie@users.noreply.github.com> Date: Sat, 10 Dec 2022 03:08:34 +0000 Subject: [PATCH 518/518] Update client.py (#87) --- ro_py/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ro_py/client.py b/ro_py/client.py index 72c72d11..2cdb9db6 100644 --- a/ro_py/client.py +++ b/ro_py/client.py @@ -116,7 +116,7 @@ async def get_user_by_username(self, user_name: str, exclude_banned_users: bool """ username_req = await self.requests.post( url="https://users.roblox.com/v1/usernames/users", - data={ + json={ "usernames": [ user_name ],