diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..1cbd5f9f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**If your feature is related to implementing a new Roblox API, provide information here.** +API URL: https://users.roblox.com/v1/users/{userId} +Documented: Yes/No + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 355edfe2..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# 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 diff --git a/.github/workflows/dev-docs.yml b/.github/workflows/dev-docs.yml new file mode 100644 index 00000000..d8b07dbd --- /dev/null +++ b/.github/workflows/dev-docs.yml @@ -0,0 +1,41 @@ +name: MkDocs Deploy (dev) + +on: + workflow_dispatch: + inputs: + name: + description: "Deploy docs (dev)" + push: + branches: + - "main" + paths: + - "roblox/**" + - "docs/**" + - ".github/workflows/dev-docs.yml" + - "mkdocs.yml" + +jobs: + mkdocs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10.9" + + - name: Install dependencies + run: | + pip install mkdocs-material==9.5.7 mkdocs-section-index==0.3.8 mkdocstrings==0.24.0 mkdocstrings-python==1.8.0 mkdocs-literate-nav==0.6.1 mkdocs-gen-files==0.5.0 mkdocs-autorefs==0.5.0 mike==2.0.0 + - name: Configure Git user + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + - name: Deploy docs + run: | + mike deploy dev --push diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml new file mode 100644 index 00000000..4942f7c7 --- /dev/null +++ b/.github/workflows/release-docs.yml @@ -0,0 +1,37 @@ +name: MkDocs Deploy (release) + +on: + release: + types: [published] + workflow_dispatch: + inputs: + name: + description: "Deploy docs (release)" + +jobs: + mkdocs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.10.9" + + - name: Install dependencies + run: | + pip install mkdocs-material==9.5.7 mkdocs-section-index==0.3.8 mkdocstrings==0.24.0 mkdocstrings-python==1.8.0 mkdocs-literate-nav==0.6.1 mkdocs-gen-files==0.5.0 mkdocs-autorefs==0.5.0 mike==2.0.0 + + - name: Configure Git user + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + - name: Deploy docs + run: | + mike deploy ${{ github.event.release.tag_name }} --push + mike alias ${{ github.event.release.tag_name }} latest --update-aliases --push diff --git a/.gitignore b/.gitignore index 1036f4b5..7a988136 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea/ __pycache__/ +venv/ *.pyc *.pyo *.bak @@ -9,11 +10,10 @@ build/ dist/ dist_old/ ro_py.egg-info/ -tests/ +jmkdev_tests/ ro_py_old/ other/ udocs/ -docstemplate/ build.* docsbuild.bat -chat.py +*.egg-info diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..76bddd31 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at jmk@jmksite.dev. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/LICENSE b/LICENSE index e62ec04c..25fd1be0 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 jmkdev + +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. diff --git a/README.md b/README.md index 99bb5544..21aacf5b 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,26 @@ -

- ro.py -
-

-

ro.py is a powerful Python 3 wrapper for the Roblox Web API.

-

- Information | - Requirements | - Disclaimer | - Documentation | - Examples | - Credits | - License -

+ro.pyDocsDiscord -## Information -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. +## Overview +ro.py is an asynchronous, object-oriented wrapper for the Roblox web API. -## Requirements -- 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) - -## 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. - -## Documentation -You can view documentation for ro.py at [ro.py.jmksite.dev](https://ro.py.jmksite.dev/). -If something's missing from docs, feel free to dive into the code and read the docstrings as most things are documented there. -The docs are generated from docstrings in the code using pdoc3. +## Features +- **Asynchronous**: ro.py works well with asynchronous frameworks like [FastAPI](https://fastapi.tiangolo.com/) and +[discord.py](https://github.com/Rapptz/discord.py). +- **Easy**: ro.py's client-based model is intuitive and easy to learn. + It abstracts away API requests and leaves you with simple objects that represent data on the Roblox platform. +- **Flexible**: ro.py's Requests object allows you to extend ro.py beyond what we've already implemented. ## Installation -You can install ro.py from pip: +To install the latest stable version of ro.py, run the following command: ``` -pip install ro-py +python3 -m pip install roblox ``` -If you want the latest bleeding-edge version, clone from git: + +To install the latest **unstable** version of ro.py, install [git-scm](https://git-scm.com/downloads) and run the following: ``` -pip install git+git://github.com/rbx-libdev/ro.py.git +python3 -m pip install git+https://github.com/ro-py/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 -[@jmkdev](https://github.com/iranathan) - maintainer -[@nsg-mfd](https://github.com/nsg-mfd) - helped with endpoints -## 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. | +## Support +- Learn how to use ro.py in the docs: https://ro.py.jmk.gg/dev/tutorials/ +- The [RoAPI Discord server](https://discord.gg/a69neqaNZ5) provides support for ro.py in the `#ro.py-support` channel. diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 6deca327..00000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -ro.py.jmksite.dev diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 00000000..1702bd16 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,15 @@ +--- +search: + exclude: true +--- + +* [Overview](index.md) +* [Tutorials](tutorials/index.md) + * [Get Started](tutorials/get-started.md) + * [Authentication](tutorials/authentication.md) + * [Error Handling](tutorials/error-handling.md) + * [Thumbnails](tutorials/thumbnails.md) + * [Pagination](tutorials/pagination.md) + * [ROBLOSECURITY](tutorials/roblosecurity.md) + * [Bases](tutorials/bases.md) +* [Code Reference](reference/) \ No newline at end of file diff --git a/docs/accountinformation.html b/docs/accountinformation.html deleted file mode 100644 index cbb23870..00000000 --- a/docs/accountinformation.html +++ /dev/null @@ -1,621 +0,0 @@ - - - - - - -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.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
-            }
-        )
-
-
-
-
-
-
-
-
-
-

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.

-

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

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

Grabs the user's birthdate.

-

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) -
-
-

Gets the user's gender.

-

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) -
-
-

Sets the user's birthdate.

-

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) -
-
-

Sets the user's gender.

-

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())
-
-
-
-
-
-class AccountInformationMetadata -(metadata_raw) -
-
-

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
-
-

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 -(promotion_raw) -
-
-

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
-
-

Link to the user's Facebook page.

-
-
var promotion_channels_visibility_privacy
-
-

Visibility of promotion channels.

-
-
var twitch
-
-

Link to the user's Twitch page.

-
-
var twitter
-
-

Link to the user's Twitter page.

-
-
var youtube
-
-

Link to the user's YouTube page.

-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/accountsettings.html b/docs/accountsettings.html deleted file mode 100644 index 8c0e4a2f..00000000 --- a/docs/accountsettings.html +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - -ro_py.accountsettings API documentation - - - - - - - - - - - - -
-
-
-

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]
-
-
-
-
-
-
-
-
-
-

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.

-

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

-
-
-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: dict) -
-
-

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 - -
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"]
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/assets.html b/docs/assets.html deleted file mode 100644 index c7d8788d..00000000 --- a/docs/assets.html +++ /dev/null @@ -1,547 +0,0 @@ - - - - - - -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.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, cso, asset_id):
-        self.id = asset_id
-        self.cso = cso
-        self.requests = cso.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"])
-        # 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"]
-
-    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.")
-
-
-class UserAsset(Asset):
-    def __init__(self, requests, asset_id, user_asset_id):
-        super().__init__(requests, asset_id)
-        self.user_asset_id = user_asset_id
-
-
-class Events:
-    def __init__(self, cso):
-        self.cso = cso
-
-    async def bind(self, func, event, delay=15):
-        pass
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Asset -(cso, asset_id) -
-
-

Represents an asset.

-

Parameters

-
-
requests : Requests
-
Requests object to use for API requests.
-
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, cso, asset_id):
-        self.id = asset_id
-        self.cso = cso
-        self.requests = cso.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"])
-        # 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"]
-
-    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

-
-
-async def get_limited_resale_data(self) -
-
-

Gets the limited resale data

-

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) -
-
-

Gets the remaining amount of this asset. (used for Limited U items)

-

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"]
-
-
-
-
-
-class Events -(cso) -
-
-
-
- -Expand source code - -
class Events:
-    def __init__(self, cso):
-        self.cso = cso
-
-    async def bind(self, func, event, delay=15):
-        pass
-
-

Methods

-
-
-async def bind(self, func, event, delay=15) -
-
-
-
- -Expand source code - -
async def bind(self, func, event, delay=15):
-    pass
-
-
-
-
-
-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.
-
-
- -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

- -

Inherited members

- -
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/assets/logo-wordmark.svg b/docs/assets/logo-wordmark.svg new file mode 100644 index 00000000..380cfe4d --- /dev/null +++ b/docs/assets/logo-wordmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..cccc1c66 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/assets/screenshots/ChromeCookie.png b/docs/assets/screenshots/ChromeCookie.png new file mode 100644 index 00000000..2e5fdef9 Binary files /dev/null and b/docs/assets/screenshots/ChromeCookie.png differ diff --git a/docs/assets/screenshots/ChromeDevTools.png b/docs/assets/screenshots/ChromeDevTools.png new file mode 100644 index 00000000..02390130 Binary files /dev/null and b/docs/assets/screenshots/ChromeDevTools.png differ diff --git a/docs/assets/screenshots/FirefoxCookie.jpeg b/docs/assets/screenshots/FirefoxCookie.jpeg new file mode 100644 index 00000000..ec47abca Binary files /dev/null and b/docs/assets/screenshots/FirefoxCookie.jpeg differ diff --git a/docs/badges.html b/docs/badges.html deleted file mode 100644 index 01397b20..00000000 --- a/docs/badges.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - -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.
-
-    Parameters
-    ----------
-    requests : ro_py.utilities.requests.Requests
-        Requests object to use for API requests.
-    badge_id
-        ID of the badge.
-    """
-    def __init__(self, cso, badge_id):
-        self.id = badge_id
-        self.cso = cso
-        self.requests = cso.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"]
-        )
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Badge -(cso, badge_id) -
-
-

Represents a game-awarded badge.

-

Parameters

-
-
requests : Requests
-
Requests object to use for API requests.
-
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, cso, badge_id):
-        self.id = badge_id
-        self.cso = cso
-        self.requests = cso.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

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

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"]
-    )
-
-
-
-
-
-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/captcha.html b/docs/captcha.html deleted file mode 100644 index 7500b49c..00000000 --- a/docs/captcha.html +++ /dev/null @@ -1,194 +0,0 @@ - - - - - - -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 UnsolvedLoginCaptcha:
-    def __init__(self, data, pkey):
-        self.pkey = 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.pkey = 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"]
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class CaptchaMetadata -(data) -
-
-
-
- -Expand source code - -
class CaptchaMetadata:
-    def __init__(self, data):
-        self.fun_captcha_public_keys = data["funCaptchaPublicKeys"]
-
-
-
-class UnsolvedCaptcha -(pkey) -
-
-
-
- -Expand source code - -
class UnsolvedCaptcha:
-    def __init__(self, pkey):
-        self.pkey = 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.pkey = 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"]
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/catalog.html b/docs/catalog.html deleted file mode 100644 index 523fa106..00000000 --- a/docs/catalog.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - -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):
-    """
-    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"
-
-
-
-
-
-
-
-
-
-

Classes

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

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
  • -
-

Class variables

-
-
var amazon
-
-
-
-
var android
-
-
-
-
var fire
-
-
-
-
var google_play
-
-
-
-
var idevice
-
-
-
-
var ios
-
-
-
-
var iphone
-
-
-
-
var xbox
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/chat.html b/docs/chat.html deleted file mode 100644 index cfb43d2c..00000000 --- a/docs/chat.html +++ /dev/null @@ -1,678 +0,0 @@ - - - - - - -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, cso, conversation_id):
-        self.cso = cso
-        self.requests = cso.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, cso, conversation_id=None, raw=False, raw_data=None):
-        self.cso = cso
-        self.requests = cso.requests
-        self.raw = raw
-        self.id = None
-        self.title = None
-        self.initiator = None
-        self.type = None
-        self.typing = ConversationTyping(self.cso, conversation_id)
-
-        if self.raw:
-            data = raw_data
-            self.id = data["id"]
-            self.title = data["title"]
-            self.initiator = User(self.cso, data["initiator"]["targetId"])
-            self.type = data["conversationType"]
-            self.typing = ConversationTyping(self.cso, 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"]
-
-    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
-
-
-
-
-
-
-
-
-
-

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) -
-
-

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

-
-
-async def get_conversation(self, conversation_id) -
-
-

Gets a conversation by the conversation ID.

-

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
-
-
-
-
-
-class Conversation -(cso, conversation_id=None, raw=False, raw_data=None) -
-
-
-
- -Expand source code - -
class Conversation:
-    def __init__(self, cso, conversation_id=None, raw=False, raw_data=None):
-        self.cso = cso
-        self.requests = cso.requests
-        self.raw = raw
-        self.id = None
-        self.title = None
-        self.initiator = None
-        self.type = None
-        self.typing = ConversationTyping(self.cso, conversation_id)
-
-        if self.raw:
-            data = raw_data
-            self.id = data["id"]
-            self.title = data["title"]
-            self.initiator = User(self.cso, data["initiator"]["targetId"])
-            self.type = data["conversationType"]
-            self.typing = ConversationTyping(self.cso, 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"]
-
-    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

-
-
-async def get_message(self, message_id) -
-
-
-
- -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"]
-
-
-
-
-
-class ConversationTyping -(cso, conversation_id) -
-
-
-
- -Expand source code - -
class ConversationTyping:
-    def __init__(self, cso, conversation_id):
-        self.cso = cso
-        self.requests = cso.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 -(requests, message_id, conversation_id) -
-
-

Represents a single message in a chat conversation.

-

Parameters

-
-
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.
-
-
- -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

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

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"]
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/client.html b/docs/client.html deleted file mode 100644 index a82cbe30..00000000 --- a/docs/client.html +++ /dev/null @@ -1,1125 +0,0 @@ - - - - - - -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.events import EventTypes
-from ro_py.trades import TradesWrapper
-from ro_py.utilities.requests import Requests
-from ro_py.accountsettings import AccountSettings
-from ro_py.utilities.cache import Cache, CacheType
-from ro_py.accountinformation import AccountInformation
-from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError
-from ro_py.captcha import UnsolvedLoginCaptcha
-
-import logging
-
-
-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."""
-
-
-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"""
-
-        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."""
-        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):
-        """
-        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_self(self):
-        self_req = await self.requests.get(
-            url="https://roblox.com/my/profile"
-        )
-        data = self_req.json()
-        return User(self.cso, data['UserId'], data['Username'])
-
-    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.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()
-        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.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()
-            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.cso.cache.get(CacheType.Groups, group_id)
-        if not group:
-            group = Group(self.cso, group_id)
-            self.cso.cache.set(CacheType.Groups, group_id, group)
-            await group.update()
-        return group
-
-    async def get_game_by_universe_id(self, universe_id):
-        """
-        Gets a Roblox game.
-
-        Parameters
-        ----------
-        universe_id
-            ID of the game to generate the object from.
-        """
-        game = self.cso.cache.get(CacheType.Games, universe_id)
-        if not game:
-            game = Game(self.cso, universe_id)
-            self.cso.cache.set(CacheType.Games, universe_id, game)
-            await game.update()
-        return game
-
-    async def get_game_by_place_id(self, place_id):
-        """
-        Gets a Roblox game by one of it's place's Plaece IDs.
-
-        Parameters
-        ----------
-        place_id
-            ID of the place to generate the object from.
-        """
-        place_req = await self.requests.get(
-            url="https://games.roblox.com/v1/games/multiget-place-details",
-            params={
-                "placeIds": place_id
-            }
-        )
-        place_data = place_req.json()
-
-        try:
-            place_details = place_data[0]
-        except IndexError:
-            raise InvalidPlaceIDError("Invalid place ID.")
-
-        universe_id = place_details["universeId"]
-
-        return await self.get_game_by_universe_id(universe_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.cso.cache.get(CacheType.Assets, asset_id)
-        if not asset:
-            asset = Asset(self.cso, asset_id)
-            self.cso.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.cso.cache.get(CacheType.Assets, badge_id)
-        if not badge:
-            badge = Badge(self.cso, badge_id)
-            self.cso.cache.set(CacheType.Assets, badge_id, badge)
-            await badge.update()
-        return badge
-
-
-
-
-
-
-
-
-
-

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"""
-
-        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."""
-        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):
-        """
-        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_self(self):
-        self_req = await self.requests.get(
-            url="https://roblox.com/my/profile"
-        )
-        data = self_req.json()
-        return User(self.cso, data['UserId'], data['Username'])
-
-    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.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()
-        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.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()
-            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.cso.cache.get(CacheType.Groups, group_id)
-        if not group:
-            group = Group(self.cso, group_id)
-            self.cso.cache.set(CacheType.Groups, group_id, group)
-            await group.update()
-        return group
-
-    async def get_game_by_universe_id(self, universe_id):
-        """
-        Gets a Roblox game.
-
-        Parameters
-        ----------
-        universe_id
-            ID of the game to generate the object from.
-        """
-        game = self.cso.cache.get(CacheType.Games, universe_id)
-        if not game:
-            game = Game(self.cso, universe_id)
-            self.cso.cache.set(CacheType.Games, universe_id, game)
-            await game.update()
-        return game
-
-    async def get_game_by_place_id(self, place_id):
-        """
-        Gets a Roblox game by one of it's place's Plaece IDs.
-
-        Parameters
-        ----------
-        place_id
-            ID of the place to generate the object from.
-        """
-        place_req = await self.requests.get(
-            url="https://games.roblox.com/v1/games/multiget-place-details",
-            params={
-                "placeIds": place_id
-            }
-        )
-        place_data = place_req.json()
-
-        try:
-            place_details = place_data[0]
-        except IndexError:
-            raise InvalidPlaceIDError("Invalid place ID.")
-
-        universe_id = place_details["universeId"]
-
-        return await self.get_game_by_universe_id(universe_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.cso.cache.get(CacheType.Assets, asset_id)
-        if not asset:
-            asset = Asset(self.cso, asset_id)
-            self.cso.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.cso.cache.get(CacheType.Assets, badge_id)
-        if not badge:
-            badge = Badge(self.cso, badge_id)
-            self.cso.cache.set(CacheType.Assets, badge_id, badge)
-            await badge.update()
-        return badge
-
-

Subclasses

- -

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 cso
-
-

ClientSharedObject. Passed to each new object to share information.

-
-
var events
-
-

Types of events used for binding events to a function.

-
-
var requests
-
-

See self.cso.requests

-
-
var trade
-
-

TradesWrapper object. Only available for authenticated clients.

-
-
-

Methods

-
-
-async def get_asset(self, asset_id) -
-
-

Gets a Roblox asset.

-

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.cso.cache.get(CacheType.Assets, asset_id)
-    if not asset:
-        asset = Asset(self.cso, asset_id)
-        self.cso.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.
-
-
- -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.cso.cache.get(CacheType.Assets, badge_id)
-    if not badge:
-        badge = Badge(self.cso, badge_id)
-        self.cso.cache.set(CacheType.Assets, badge_id, badge)
-        await badge.update()
-    return badge
-
-
-
-async def get_game_by_place_id(self, place_id) -
-
-

Gets a Roblox game by one of it's place's Plaece IDs.

-

Parameters

-
-
place_id
-
ID of the place to generate the object from.
-
-
- -Expand source code - -
async def get_game_by_place_id(self, place_id):
-    """
-    Gets a Roblox game by one of it's place's Plaece IDs.
-
-    Parameters
-    ----------
-    place_id
-        ID of the place to generate the object from.
-    """
-    place_req = await self.requests.get(
-        url="https://games.roblox.com/v1/games/multiget-place-details",
-        params={
-            "placeIds": place_id
-        }
-    )
-    place_data = place_req.json()
-
-    try:
-        place_details = place_data[0]
-    except IndexError:
-        raise InvalidPlaceIDError("Invalid place ID.")
-
-    universe_id = place_details["universeId"]
-
-    return await self.get_game_by_universe_id(universe_id)
-
-
-
-async def get_game_by_universe_id(self, universe_id) -
-
-

Gets a Roblox game.

-

Parameters

-
-
universe_id
-
ID of the game to generate the object from.
-
-
- -Expand source code - -
async def get_game_by_universe_id(self, universe_id):
-    """
-    Gets a Roblox game.
-
-    Parameters
-    ----------
-    universe_id
-        ID of the game to generate the object from.
-    """
-    game = self.cso.cache.get(CacheType.Games, universe_id)
-    if not game:
-        game = Game(self.cso, universe_id)
-        self.cso.cache.set(CacheType.Games, universe_id, game)
-        await game.update()
-    return game
-
-
-
-async def get_group(self, group_id) -
-
-

Gets a Roblox group.

-

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.cso.cache.get(CacheType.Groups, group_id)
-    if not group:
-        group = Group(self.cso, group_id)
-        self.cso.cache.set(CacheType.Groups, group_id, group)
-        await group.update()
-    return group
-
-
-
-async def get_self(self) -
-
-
-
- -Expand source code - -
async def get_self(self):
-    self_req = await self.requests.get(
-        url="https://roblox.com/my/profile"
-    )
-    data = self_req.json()
-    return User(self.cso, data['UserId'], data['Username'])
-
-
-
-async def get_user(self, user_id) -
-
-

Gets a Roblox user.

-

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.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()
-    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.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()
-        return user
-    else:
-        raise UserDoesNotExistError
-
-
-
-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
-
-
-
-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",
-            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")
-
-
-
-
-
-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."""
-
-

Instance variables

-
-
var cache
-
-

Cache object to keep objects that don't need to be recreated.

-
-
var client
-
-

Client (parent) of this object.

-
-
var requests
-
-

Reqests object for all web requests.

-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/economy.html b/docs/economy.html deleted file mode 100644 index 8d76ca7f..00000000 --- a/docs/economy.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - -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/events.html b/docs/events.html deleted file mode 100644 index 02f637d2..00000000 --- a/docs/events.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - -ro_py.events API documentation - - - - - - - - - - - - -
-
-
-

Module ro_py.events

-
-
-
- -Expand source code - -
import enum
-
-
-class EventTypes(enum.Enum):
-    on_join_request = "on_join_request"
-    on_wall_post = "on_wall_post"
-    on_shout_update = "on_shout_update"
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class EventTypes -(value, names=None, *, module=None, qualname=None, type=None, start=1) -
-
-

An enumeration.

-
- -Expand source code - -
class EventTypes(enum.Enum):
-    on_join_request = "on_join_request"
-    on_wall_post = "on_wall_post"
-    on_shout_update = "on_shout_update"
-
-

Ancestors

-
    -
  • enum.Enum
  • -
-

Class variables

-
-
var on_join_request
-
-
-
-
var on_shout_update
-
-
-
-
var on_wall_post
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file 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 deleted file mode 100644 index a4d8bfcf..00000000 --- a/docs/extensions/bots.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - -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.
-
-
- -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 deleted file mode 100644 index 1a91a2d6..00000000 --- a/docs/extensions/index.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - -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.anticaptcha
-
-
-
-
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.

-
-
ro_py.extensions.twocaptcha
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/extensions/prompt.html b/docs/extensions/prompt.html deleted file mode 100644 index 8e636f0c..00000000 --- a/docs/extensions/prompt.html +++ /dev/null @@ -1,1043 +0,0 @@ - - - - - - -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
-import wxasync
-from wx import html2
-import pytweening
-from wx.lib.embeddedimage import PyEmbeddedImage
-
-icon_image = PyEmbeddedImage(
-    b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B'
-    b'AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAhaSURBVGhDxZl5bFRVFMbPfTNtQRTF'
-    b'uOAWVKKIylqptCBiNBQ1MURjQv8Qt8RoxCWKawTFGLdo4oIaY2LUGCUmBo3BAKJgtRTFQi3u'
-    b'KBA1LhEXqMK0nb7n79w7M05f36zt1C+9c++7b/re+c757rnLGCkBQbvMkyq5XHxZw+UGyldm'
-    b'guyzN/8nFEWgftGqC/Z1j1q55YrTa7jcKCNlgnRKkv/+juuPKOtob6T+FkJJ6iFDQQIYP5xq'
-    b'K+WG1kfmriQKZ0tcVhOFmHj0xikBpVsSPO1rWi3U66k3SbXsNONoVRDFEJhovNingd+7mcsZ'
-    b'kEhA4hUZJk2Y/B/0SUomRvEpPbKHvs9pfZgitJno/EI9qFAfFkK9icXFi9dMpX2l65IleHy3'
-    b'NTYNjUIPRUl1UwxCi0u91MgtvGUlpLYGHbKWsiTYKrMpB/OtAaOYCLyK8fMDPylB4P/MRy1R'
-    b'+Jko3C3D5Z6ih7CSTRcl6MtPvL2N1nrqD6m/IEJ/U5eEvAQwfj+qDuPFxyoBr2qY+D2JZRC4'
-    b'DgIHcm8TWekE6/lSoG9Njx9tdxE/IztofUxRQprhvoFQF3VeFCIwRYzZRDOG5/m24c9LMB5m'
-    b'QqINEvMZqK9ajw4EaoVGRgkpuniikW9ptVKvo/6Ieoc5VXrt/SwUInCtV1WzzO/5zxHISfxk'
-    b'15ogMOe2Lmg0+G4VOj+nsK9KQDYhl+H20vclrXRCaCM6P1AXHMRn2gdkAePFxKrmGBNcZCZZ'
-    b'j9xJ5u8qKh0UC32nziaaENQxRvaDUC3RvoF6BeOngyTQjALuyhkBvD+C6jNS6LFIxnWmoFkp'
-    b'6E1qzq9DSnt40NMM6GuGbE5WZ+no/Fva8vltPII/JvA1qfcFxuuA1inqetcj9+GtX23YhwJq'
-    b'kvPp6nwEZnjxakwKaSiFIMnINd5NROp4M5mUGMj9ZKShg8t8zfkIoP9o4xXMCQzoqlE0l7oe'
-    b'eY4obEGnlYdGOil/8NkeSQCvHkB1Wlj7YfhEgTEyn+/PJgo6Au4kvH7+3DYIcFLtIOq/5orA'
-    b'yeT7o6L03wdECAKa7B6Yvmh1NSRW4ZkVpNXKwhFoNlNyp9GZJl7NvdwSSkOjwFiZzoRwaapr'
-    b'MXm7c1DTahhO/x/oR67XzBI0XixcpMxipHQoUfgSET1ZsSg4/e/is10v+xHACF3j1BbSfzbc'
-    b'OqnmGJq3ux55lAG9k7mh8FRZKpx82nGUkoh8/Cno/8iC+g9B0yr/dzUOmMTD/0B9N5KrN1Pv'
-    b'tdEYRkkv3gYCR8DKRxFFQPXPawrrPxuaVk28SufHJXU3rRFIvCnfSx1vmEzIL2FcPI+0dD2T'
-    b'vQ0qDUreLRyb7SeIIkD+L837GTjOQVVqVWnmSi+Lrm2Ul81ENkNGyBvyYNnjQ63tZcbXFJpC'
-    b'HwKE/yCqKaXovw+cPNa2PDzHNsKw6/tAxpYtI+cY1b9OYhbhCEwwnje6VP1bsFcgpeoastV1'
-    b'9AeLPh3WDXalWQ6ctRn5KMIEzjCx0vWvMIbRFQQ7aX7jeiIxDu+P6b8tKQJO/2pYZgArwgRK'
-    b'yv/ZYEbWagPL63yL6gb0z1o8dVUK1NJee6qhRzwZZAigfz0lKF//apUx2xtufUcTZj8EW2x1'
-    b'TlnGK5z+t6D/v2wrhcxwgsBZePG98gkAY3T/tIOds57SreN6Y3fnru2fPNOUDDRv+PI688GF'
-    b'ZSVSHT375DYIPOw6HLIlhP4HvKCvYSycxHMuY9f2InNDe82Bh3+Mc+aRRhVL7f42LNxCUDdr'
-    b'/tI9cQj2URdf/JpWZes/A1anuqzQfbMu8sBwBge53zymEsV7HUThmZLnAbVSz5HEnvT1gSXw'
-    b'45iRh1JN0pcPKiDk9yR0nTSGq0WuUx5CQj+kNF0c3HfbcMBu28pCOpiT0P8hZeX/IpBaJy0k'
-    b'CuMx4jfEcG9qTVMcnJXv288Q0gRmDYL+c8Kuk2JVusu7Txbs0a6X0HRrUdtPp3/1bIu9DsGb'
-    b'fvNqrc+QCnk/Dbf9jM+rP2zDuURBB4huP/U3hvzQSPnyI59f2OsQPGOCw6knDrr++4HtJzqi'
-    b'cT9SGg6J9eyslhcc0E5qqv9O2wpBHzgZzxysYa40/N4ePZqcTPMq22HkbmLxZ97x4CIUqX+F'
-    b'EkD/paSEgcFG2pg7iMKReHU78ng051hQ47vtyilS/wol0KjrGBfdykNneqKgsl3seuQJZtiv'
-    b'Iw/FnP71EFc3QpFQq1eQq5uZgv70yETkbM0YsK8cIXtA7MUuJwrTUtq+y90JwQljE9/5x7Yi'
-    b'kMkBLMKOJt/V0pzNnD0TX42H0AhCY71m10h5TupKhRev1sz0bhCYxtYFjfhP3mZAN9rT6DR0'
-    b'WZiQhRB4ynX0R2QSa7h1Le4PjsfgOi7P4un11Cd4sWqrVtWxm/QGRkgjzsBuYgm+nM3OVCTT'
-    b'wiOH2azvLONFUgcBt5aNQCSBMIgOhgcn8rAGLomQJXYcXvQ0Ki5CpRNKHdNvozkNErsh8SSr'
-    b'p4X2kFLlk7S/Q0+EwF7qSBRFIAwIjcDYk7EXqVlCtSyhjzLIAib2+L3YtJz63e0eCCyFwGgs'
-    b'2kwkjrAEErIc45vcN6NRFoEwIDQKi3XBPItyJoTs2or5RZO/i1AOQqnst5v7GoVtkLgWES2z'
-    b'NxNyNQSete0cGBQCYUBoNBbrEUo6IZzqefGR4qG4iISgmc/v6VoOgSYI6NBtIQpTmQH0kCxz'
-    b'hBKFihDIxgUP/Sa7fm8fg8HTuFRCM7B+HAOYPZbLcCo7QtFLuxES70LifL77OGUCBPL+cFVx'
-    b'AmEQHXQVjGX8TOdSM5zWY+M1+8eTiU7dsNdvuHJugnR6Hsa/pf+TD0NOIAwIIZngJG0yu80h'
-    b'AbxAFN5wdwtB5F91LAlTEJXvrgAAAABJRU5ErkJggg=='
-)
-
-
-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(icon_image.GetIcon())
-
-        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(icon_image.GetIcon())
-
-        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
-
-
-
-
-
-
-
-

Functions

-
-
-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
-
Client object to authenticate.
-
-

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) -
-
-

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) -
-
-
-
- -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 -(warn_on_cancel_callback=False, loop=None) -
-
-

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(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
  • -
  • 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 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(icon_image.GetIcon())
-
-        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) -
-
-

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(icon_image.GetIcon())
-
-        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
  • -
  • 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

-
-
-async def login_click(self, event) -
-
-
-
- -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()
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/extensions/twocaptcha.html b/docs/extensions/twocaptcha.html deleted file mode 100644 index 825e17ee..00000000 --- a/docs/extensions/twocaptcha.html +++ /dev/null @@ -1,237 +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"
-        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.")
-        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?key={self.api_key}&id={task_id}&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"
-        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.")
-        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?key={self.api_key}&id={task_id}&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"
-    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.")
-    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?key={self.api_key}&id={task_id}&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/gamepersistence.html b/docs/gamepersistence.html deleted file mode 100644 index fdba1a92..00000000 --- a/docs/gamepersistence.html +++ /dev/null @@ -1,1138 +0,0 @@ - - - - - - -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.
-    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
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class DataStore -(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). -This is only available for authenticated clients, and games that they own.

-

Parameters

-
-
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.
-
-
- -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

-
-
-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:
-        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) -
-
-

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.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
-
 
-
-
- -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) -
-
-

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:
-        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) -
-
-

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 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) -
-
-

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
-
 
-
-
- -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']
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/games.html b/docs/games.html deleted file mode 100644 index b3c34ab7..00000000 --- a/docs/games.html +++ /dev/null @@ -1,701 +0,0 @@ - - - - - - -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
-from ro_py.thumbnails import GameThumbnailGenerator
-from ro_py.utilities.errors import GameJoinError
-from ro_py.utilities.cache import CacheType
-import subprocess
-import json
-import os
-
-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, cso, universe_id):
-        self.id = universe_id
-        self.cso = cso
-        self.requests = cso.requests
-        self.name = 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.max_players = None
-        self.studio_access_to_apis_allowed = None
-        self.create_vip_servers_allowed = None
-        self.thumbnails = GameThumbnailGenerator(self.requests, self.id)
-
-    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"]
-        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:
-                self.creator = User(self.cso, 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":
-            self.creator = self.cso.cache.get(CacheType.Groups, game_info["creator"]["id"])
-            if not self.creator:
-                self.creator = Group(self.cso, game_info["creator"]["id"])
-                self.cso.cache.set(CacheType.Groups, game_info["creator"]["id"], self.creator)
-                await self.creator.update()
-        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):
-        """
-        Returns
-        -------
-        ro_py.games.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.cso, badge["id"]))
-        return badges
-
-
-class Place:
-    def __init__(self, requests, id):
-        self.requests = requests
-        self.id = id
-        pass
-
-    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.
-        """
-        local_app_data = os.getenv('LocalAppData')
-        roblox_appdata_path = local_app_data + "\\Roblox"
-        roblox_launcher = None
-
-        app_storage = roblox_appdata_path + "\\LocalStorage"
-        app_versions = roblox_appdata_path + "\\Versions"
-
-        with open(app_storage + "\\appStorage.json") as app_storage_file:
-            app_storage_data = json.load(app_storage_file)
-        browser_tracker_id = app_storage_data["BrowserTrackerId"]
-
-        for directory in os.listdir(app_versions):
-            dir_path = app_versions + "\\" + directory
-            if os.path.isdir(dir_path):
-                if os.path.isfile(dir_path + "\\" + "RobloxPlayerBeta.exe"):
-                    roblox_launcher = dir_path + "\\" + "RobloxPlayerBeta.exe"
-
-        if not roblox_launcher:
-            raise GameJoinError("Couldn't find RobloxPlayerBeta.exe.")
-
-        ticket_req = self.requests.back_post(url="https://auth.roblox.com/v1/authentication-ticket/")
-        auth_ticket = ticket_req.headers["rbx-authentication-ticket"]
-
-        launch_url = "https://assetgame.roblox.com/game/PlaceLauncher.ashx" \
-                     "?request=RequestGame" \
-                     f"&browserTrackerId={browser_tracker_id}" \
-                     f"&placeId={self.id}" \
-                     "&isPlayTogetherGame=false"
-        join_parameters = [
-            roblox_launcher,
-            "--play",
-            "-a",
-            negotiate_url,
-            "-t",
-            auth_ticket,
-            "-j",
-            launch_url,
-            "-b",
-            browser_tracker_id,
-            "--launchtime=" + str(launchtime),
-            "--rloc",
-            rloc,
-            "--gloc",
-            gloc
-        ]
-        join_process = subprocess.run(
-            args=join_parameters,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE
-        )
-        return join_process.stdout, join_process.stderr
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Game -(cso, 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, cso, universe_id):
-        self.id = universe_id
-        self.cso = cso
-        self.requests = cso.requests
-        self.name = 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.max_players = None
-        self.studio_access_to_apis_allowed = None
-        self.create_vip_servers_allowed = None
-        self.thumbnails = GameThumbnailGenerator(self.requests, self.id)
-
-    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"]
-        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:
-                self.creator = User(self.cso, 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":
-            self.creator = self.cso.cache.get(CacheType.Groups, game_info["creator"]["id"])
-            if not self.creator:
-                self.creator = Group(self.cso, game_info["creator"]["id"])
-                self.cso.cache.set(CacheType.Groups, game_info["creator"]["id"], self.creator)
-                await self.creator.update()
-        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):
-        """
-        Returns
-        -------
-        ro_py.games.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.cso, badge["id"]))
-        return badges
-
-

Methods

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

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.cso, badge["id"]))
-    return badges
-
-
-
-async def get_votes(self) -
-
-

Returns

-
-
Votes
-
 
-
-
- -Expand source code - -
async def get_votes(self):
-    """
-    Returns
-    -------
-    ro_py.games.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"]
-    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:
-            self.creator = User(self.cso, 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":
-        self.creator = self.cso.cache.get(CacheType.Groups, game_info["creator"]["id"])
-        if not self.creator:
-            self.creator = Group(self.cso, game_info["creator"]["id"])
-            self.cso.cache.set(CacheType.Groups, game_info["creator"]["id"], self.creator)
-            await self.creator.update()
-    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) -
-
-
-
- -Expand source code - -
class Place:
-    def __init__(self, requests, id):
-        self.requests = requests
-        self.id = id
-        pass
-
-    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.
-        """
-        local_app_data = os.getenv('LocalAppData')
-        roblox_appdata_path = local_app_data + "\\Roblox"
-        roblox_launcher = None
-
-        app_storage = roblox_appdata_path + "\\LocalStorage"
-        app_versions = roblox_appdata_path + "\\Versions"
-
-        with open(app_storage + "\\appStorage.json") as app_storage_file:
-            app_storage_data = json.load(app_storage_file)
-        browser_tracker_id = app_storage_data["BrowserTrackerId"]
-
-        for directory in os.listdir(app_versions):
-            dir_path = app_versions + "\\" + directory
-            if os.path.isdir(dir_path):
-                if os.path.isfile(dir_path + "\\" + "RobloxPlayerBeta.exe"):
-                    roblox_launcher = dir_path + "\\" + "RobloxPlayerBeta.exe"
-
-        if not roblox_launcher:
-            raise GameJoinError("Couldn't find RobloxPlayerBeta.exe.")
-
-        ticket_req = self.requests.back_post(url="https://auth.roblox.com/v1/authentication-ticket/")
-        auth_ticket = ticket_req.headers["rbx-authentication-ticket"]
-
-        launch_url = "https://assetgame.roblox.com/game/PlaceLauncher.ashx" \
-                     "?request=RequestGame" \
-                     f"&browserTrackerId={browser_tracker_id}" \
-                     f"&placeId={self.id}" \
-                     "&isPlayTogetherGame=false"
-        join_parameters = [
-            roblox_launcher,
-            "--play",
-            "-a",
-            negotiate_url,
-            "-t",
-            auth_ticket,
-            "-j",
-            launch_url,
-            "-b",
-            browser_tracker_id,
-            "--launchtime=" + str(launchtime),
-            "--rloc",
-            rloc,
-            "--gloc",
-            gloc
-        ]
-        join_process = subprocess.run(
-            args=join_parameters,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE
-        )
-        return join_process.stdout, join_process.stderr
-
-

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.

-
-
- -Expand source code - -
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.
-    """
-    local_app_data = os.getenv('LocalAppData')
-    roblox_appdata_path = local_app_data + "\\Roblox"
-    roblox_launcher = None
-
-    app_storage = roblox_appdata_path + "\\LocalStorage"
-    app_versions = roblox_appdata_path + "\\Versions"
-
-    with open(app_storage + "\\appStorage.json") as app_storage_file:
-        app_storage_data = json.load(app_storage_file)
-    browser_tracker_id = app_storage_data["BrowserTrackerId"]
-
-    for directory in os.listdir(app_versions):
-        dir_path = app_versions + "\\" + directory
-        if os.path.isdir(dir_path):
-            if os.path.isfile(dir_path + "\\" + "RobloxPlayerBeta.exe"):
-                roblox_launcher = dir_path + "\\" + "RobloxPlayerBeta.exe"
-
-    if not roblox_launcher:
-        raise GameJoinError("Couldn't find RobloxPlayerBeta.exe.")
-
-    ticket_req = self.requests.back_post(url="https://auth.roblox.com/v1/authentication-ticket/")
-    auth_ticket = ticket_req.headers["rbx-authentication-ticket"]
-
-    launch_url = "https://assetgame.roblox.com/game/PlaceLauncher.ashx" \
-                 "?request=RequestGame" \
-                 f"&browserTrackerId={browser_tracker_id}" \
-                 f"&placeId={self.id}" \
-                 "&isPlayTogetherGame=false"
-    join_parameters = [
-        roblox_launcher,
-        "--play",
-        "-a",
-        negotiate_url,
-        "-t",
-        auth_ticket,
-        "-j",
-        launch_url,
-        "-b",
-        browser_tracker_id,
-        "--launchtime=" + str(launchtime),
-        "--rloc",
-        rloc,
-        "--gloc",
-        gloc
-    ]
-    join_process = subprocess.run(
-        args=join_parameters,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE
-    )
-    return join_process.stdout, join_process.stderr
-
-
-
-
-
-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/gender.html b/docs/gender.html deleted file mode 100644 index 1171dc02..00000000 --- a/docs/gender.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - -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/groups.html b/docs/groups.html deleted file mode 100644 index d07b01c8..00000000 --- a/docs/groups.html +++ /dev/null @@ -1,1598 +0,0 @@ - - - - - - -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.
-
-"""
-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.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, cso, shout_data):
-        self.body = shout_data["body"]
-        self.poster = User(cso, shout_data["poster"]["userId"], shout_data['poster']['username'])
-
-
-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.created = iso8601.parse_date(data['created'])
-
-    async def accept(self):
-        accept_req = await self.requests.post(
-            url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.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}"
-        )
-        return accept_req.status_code == 200
-
-
-def join_request_handler(cso, data, args):
-    join_requests = []
-    for request in data:
-        join_requests.append(JoinRequest(cso, request, args))
-    return join_requests
-
-
-def member_handler(cso, data, args):
-    members = []
-    for member in data:
-        members.append()
-    return members
-
-
-class Group:
-    """
-    Represents a group.
-    """
-    def __init__(self, cso, group_id):
-        self.cso = cso
-        self.requests = cso.requests
-        self.id = group_id
-        self.wall = Wall(self.cso, self)
-        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.events = Events(cso, self)
-
-    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.cso, 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"])
-        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.cso, self, role))
-        return roles
-
-    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.cso, self, group_data['role'])
-        member = Member(self.cso, roblox_id, "", self, role)
-        return await member.update()
-
-    async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100):
-        pages = Pages(
-            cso=self.cso,
-            url=endpoint + f"/v1/groups/{self.id}/join-requests",
-            sort_order=sort_order,
-            limit=limit,
-            handler=join_request_handler,
-            handler_args=self
-        )
-        await pages.get_page()
-        return pages
-
-    async def get_members(self, sort_order=SortOrder.Ascending, limit=100):
-        pages = Pages(
-            cso=self.cso,
-            url=endpoint + f"/v1/groups/{self.id}/users?limit=100&sortOrder=Desc",
-            sort_order=sort_order,
-            limit=limit,
-            handler=member_handler,
-            handler_args=self
-        )
-        await pages.get_page()
-        return pages
-
-
-class PartialGroup(Group):
-    """
-    Represents a group with less information
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-
-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, cso, roblox_id, name, group, role):
-        super().__init__(cso, roblox_id, name)
-        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.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.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.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 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:
-    def __init__(self, cso, group):
-        self.cso = cso
-        self.group = group
-
-    async def bind(self, func, event, delay=15):
-        """
-        Binds a function to an event.
-
-        Parameters
-        ----------
-        func : function
-                Function that will be bound to the event.
-        event : str
-                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):
-        current_group_reqs = await self.group.get_join_requests()
-        old_req = current_group_reqs.data.requester.id
-        while True:
-            await asyncio.sleep(delay)
-            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:
-                    await func(new_req)
-
-    async def on_wall_post(self, func, delay):
-        current_wall_posts = await self.group.wall.get_posts()
-        newest_wall_poster = current_wall_posts.data[0].poster.id
-        while True:
-            await asyncio.sleep(delay)
-            current_wall_posts = await self.group.wall.get_posts()
-            current_wall_posts = current_wall_posts.data
-            if current_wall_posts[0].poster.id != newest_wall_poster:
-                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
-                for new_post in new_posts:
-                    await func(new_post)
-
-    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)
-
-
-
-
-
-
-
-

Functions

-
-
-def join_request_handler(cso, data, args) -
-
-
-
- -Expand source code - -
def join_request_handler(cso, data, args):
-    join_requests = []
-    for request in data:
-        join_requests.append(JoinRequest(cso, request, args))
-    return join_requests
-
-
-
-def member_handler(cso, data, args) -
-
-
-
- -Expand source code - -
def member_handler(cso, data, args):
-    members = []
-    for member in data:
-        members.append()
-    return members
-
-
-
-
-
-

Classes

-
-
-class Events -(cso, group) -
-
-
-
- -Expand source code - -
class Events:
-    def __init__(self, cso, group):
-        self.cso = cso
-        self.group = group
-
-    async def bind(self, func, event, delay=15):
-        """
-        Binds a function to an event.
-
-        Parameters
-        ----------
-        func : function
-                Function that will be bound to the event.
-        event : str
-                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):
-        current_group_reqs = await self.group.get_join_requests()
-        old_req = current_group_reqs.data.requester.id
-        while True:
-            await asyncio.sleep(delay)
-            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:
-                    await func(new_req)
-
-    async def on_wall_post(self, func, delay):
-        current_wall_posts = await self.group.wall.get_posts()
-        newest_wall_poster = current_wall_posts.data[0].poster.id
-        while True:
-            await asyncio.sleep(delay)
-            current_wall_posts = await self.group.wall.get_posts()
-            current_wall_posts = current_wall_posts.data
-            if current_wall_posts[0].poster.id != newest_wall_poster:
-                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
-                for new_post in new_posts:
-                    await func(new_post)
-
-    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)
-
-

Methods

-
-
-async def bind(self, func, event, delay=15) -
-
-

Binds a function to an event.

-

Parameters

-
-
func : function
-
Function that will be bound to the event.
-
event : str
-
Event that will be bound to the function.
-
delay : int
-
How many seconds between each poll.
-
-
- -Expand source code - -
async def bind(self, func, event, delay=15):
-    """
-    Binds a function to an event.
-
-    Parameters
-    ----------
-    func : function
-            Function that will be bound to the event.
-    event : str
-            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) -
-
-
-
- -Expand source code - -
async def on_join_request(self, func, delay):
-    current_group_reqs = await self.group.get_join_requests()
-    old_req = current_group_reqs.data.requester.id
-    while True:
-        await asyncio.sleep(delay)
-        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:
-                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)
-
-
-
-async def on_wall_post(self, func, delay) -
-
-
-
- -Expand source code - -
async def on_wall_post(self, func, delay):
-    current_wall_posts = await self.group.wall.get_posts()
-    newest_wall_poster = current_wall_posts.data[0].poster.id
-    while True:
-        await asyncio.sleep(delay)
-        current_wall_posts = await self.group.wall.get_posts()
-        current_wall_posts = current_wall_posts.data
-        if current_wall_posts[0].poster.id != newest_wall_poster:
-            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
-            for new_post in new_posts:
-                await func(new_post)
-
-
-
-
-
-class Group -(cso, group_id) -
-
-

Represents a group.

-
- -Expand source code - -
class Group:
-    """
-    Represents a group.
-    """
-    def __init__(self, cso, group_id):
-        self.cso = cso
-        self.requests = cso.requests
-        self.id = group_id
-        self.wall = Wall(self.cso, self)
-        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.events = Events(cso, self)
-
-    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.cso, 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"])
-        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.cso, self, role))
-        return roles
-
-    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.cso, self, group_data['role'])
-        member = Member(self.cso, roblox_id, "", self, role)
-        return await member.update()
-
-    async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100):
-        pages = Pages(
-            cso=self.cso,
-            url=endpoint + f"/v1/groups/{self.id}/join-requests",
-            sort_order=sort_order,
-            limit=limit,
-            handler=join_request_handler,
-            handler_args=self
-        )
-        await pages.get_page()
-        return pages
-
-    async def get_members(self, sort_order=SortOrder.Ascending, limit=100):
-        pages = Pages(
-            cso=self.cso,
-            url=endpoint + f"/v1/groups/{self.id}/users?limit=100&sortOrder=Desc",
-            sort_order=sort_order,
-            limit=limit,
-            handler=member_handler,
-            handler_args=self
-        )
-        await pages.get_page()
-        return pages
-
-

Subclasses

- -

Methods

-
-
-async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100) -
-
-
-
- -Expand source code - -
async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100):
-    pages = Pages(
-        cso=self.cso,
-        url=endpoint + f"/v1/groups/{self.id}/join-requests",
-        sort_order=sort_order,
-        limit=limit,
-        handler=join_request_handler,
-        handler_args=self
-    )
-    await pages.get_page()
-    return pages
-
-
-
-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.cso, self, group_data['role'])
-    member = Member(self.cso, roblox_id, "", self, role)
-    return await member.update()
-
-
-
-async def get_members(self, sort_order=SortOrder.Ascending, limit=100) -
-
-
-
- -Expand source code - -
async def get_members(self, sort_order=SortOrder.Ascending, limit=100):
-    pages = Pages(
-        cso=self.cso,
-        url=endpoint + f"/v1/groups/{self.id}/users?limit=100&sortOrder=Desc",
-        sort_order=sort_order,
-        limit=limit,
-        handler=member_handler,
-        handler_args=self
-    )
-    await pages.get_page()
-    return pages
-
-
-
-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"
-    )
-    roles = []
-    for role in role_req.json()['roles']:
-        roles.append(Role(self.cso, self, role))
-    return roles
-
-
-
-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.cso, 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"])
-    else:
-        self.shout = None
-
-
-
-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",
-        data={
-            "message": message
-        }
-    )
-    return shout_req.status_code == 200
-
-
-
-
-
-class JoinRequest -(cso, data, group) -
-
-
-
- -Expand source code - -
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.created = iso8601.parse_date(data['created'])
-
-    async def accept(self):
-        accept_req = await self.requests.post(
-            url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.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}"
-        )
-        return accept_req.status_code == 200
-
-

Methods

-
-
-async def accept(self) -
-
-
-
- -Expand source code - -
async def accept(self):
-    accept_req = await self.requests.post(
-        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}"
-    )
-    return accept_req.status_code == 200
-
-
-
-async def decline(self) -
-
-
-
- -Expand source code - -
async def decline(self):
-    accept_req = await self.requests.delete(
-        url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.id}"
-    )
-    return accept_req.status_code == 200
-
-
-
-
-
-class Member -(cso, roblox_id, name, group, role) -
-
-

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, cso, roblox_id, name, group, role):
-        super().__init__(cso, roblox_id, name)
-        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.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.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.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 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
-
-

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):
-    """
-    Demotes the user.
-
-    Returns
-    -------
-    int
-    """
-    return await self.change_rank(-1)
-
-
-
-async def exile(self) -
-
-
-
- -Expand source code - -
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
-
-
-
-async def promote(self) -
-
-

Promotes the user.

-

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) -
-
-

Sets the users role to specified role using rank id.

-

Parameters

-
-
rank : int
-
Rank id
-
-

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.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
-
 
-
-
- -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) -
-
-

Updates the role information of the user.

-

Returns

-
-
Role
-
 
-
-
- -Expand source code - -
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.requests, self.group, role['role'])
-            break
-    return self.role
-
-
-
-

Inherited members

- -
-
-class PartialGroup -(*args, **kwargs) -
-
-

Represents a group with less information

-
- -Expand source code - -
class PartialGroup(Group):
-    """
-    Represents a group with less information
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-

Ancestors

- -

Inherited members

- -
-
-class Shout -(cso, shout_data) -
-
-

Represents a group shout.

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

Package ro_py

-
-
-

-ro.py -
-

-

ro.py is a powerful Python 3 wrapper for the Roblox Web API.

-

-Information | -Requirements | -Disclaimer | -Documentation | -Examples | -Credits | -License -

-
- -Expand source code - -
"""
-
-<h1 align="center">
-    <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>
-<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>
-</p>
-
-"""
-
-
-
-

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 settings.

-
-
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.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.

-
-
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.events
-
-
-
-
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

-
-
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.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) -I don't know if this is really that useful, but I â€Ļ

-
-
ro_py.roles
-
-

This file contains classes and functions related to Roblox roles.

-
-
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.

-
-
ro_py.wall
-
-
-
-
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..6fbdfe45 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,26 @@ +

+ ro.py +

+ ro.py is an asynchronous, object-oriented wrapper for the Roblox web API. +

+ +## Features +- **Easy**: ro.py's client-based model is intuitive and easy to learn. + It abstracts away API requests and leaves you with simple objects that represent data on the Roblox platform. +- **Asynchronous**: ro.py works well with asynchronous frameworks like [FastAPI](https://fastapi.tiangolo.com/) and +[discord.py](https://github.com/Rapptz/discord.py). +- **Flexible**: ro.py's Requests object allows you to extend ro.py beyond what we've already implemented. + +## Installation +To install the latest stable version of ro.py, run the following command: +``` +python3 -m pip install roblox +``` + +To install the latest **unstable** version of ro.py, install [git-scm](https://git-scm.com/downloads) and run the following: +``` +python3 -m pip install git+https://github.com/ro-py/ro.py.git +``` + +## Support +The [RoAPI Discord server](https://discord.gg/a69neqaNZ5) provides support for ro.py in the `#ro.py-support` channel. diff --git a/docs/notifications.html b/docs/notifications.html deleted file mode 100644 index a31abe61..00000000 --- a/docs/notifications.html +++ /dev/null @@ -1,523 +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_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()
-
-
-
-
-
-
-
-
-
-

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.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

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

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()
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 00000000..85bffa30 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block announce %} + + For updates and support, join the + + {% include ".icons/fontawesome/brands/discord.svg" %} + + RoAPI Discord + +{% endblock %} \ No newline at end of file diff --git a/docs/robloxbadges.html b/docs/robloxbadges.html deleted file mode 100644 index ac1f8e20..00000000 --- a/docs/robloxbadges.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - -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/robloxdocs.html b/docs/robloxdocs.html deleted file mode 100644 index a464e676..00000000 --- a/docs/robloxdocs.html +++ /dev/null @@ -1,466 +0,0 @@ - - - - - - -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/robloxstatus.html b/docs/robloxstatus.html deleted file mode 100644 index 0df46930..00000000 --- a/docs/robloxstatus.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - -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/roles.html b/docs/roles.html deleted file mode 100644 index 2f6a7b92..00000000 --- a/docs/roles.html +++ /dev/null @@ -1,676 +0,0 @@ - - - - - - -ro_py.roles API documentation - - - - - - - - - - - - -
-
-
-

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:
-    """
-    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, cso, group, role_data):
-        self.cso = cso
-        self.requests = cso.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
-
-
-
-
-
-
-
-

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,
-        "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 -(cso, group, role_data) -
-
-

Represents a role

-

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
-
-    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, cso, group, role_data):
-        self.cso = cso
-        self.requests = cso.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

-
-
-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={
-            "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 : 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": {}
-    }
-
-    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 -
-
-

Represents role permissions.

-
- -Expand source code - -
class RolePermissions:
-    """
-    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
-
-

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 diff --git a/docs/scripts/gen_ref_pages.py b/docs/scripts/gen_ref_pages.py new file mode 100644 index 00000000..cfdd64cb --- /dev/null +++ b/docs/scripts/gen_ref_pages.py @@ -0,0 +1,36 @@ +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.Nav() + +src = Path(__file__).parent.parent.parent / "roblox" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + if not parts: + print(f"skipping {module_path.parts}") + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: roblox.{ident}") + + mkdocs_gen_files.set_edit_path(full_doc_path, path) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/stylesheets/main.css b/docs/stylesheets/main.css new file mode 100644 index 00000000..10efe803 --- /dev/null +++ b/docs/stylesheets/main.css @@ -0,0 +1,107 @@ +:root { + /* font-size: 125% !important; */ +} + +[data-md-color-scheme=slate] { + --md-code-bg-color: hsl(0 0% 100% / 1.5%); + --md-default-bg-color: hsl(0 0% 6%); + + --md-footer-bg-color: hsl(0 0% 12% / 75%); + --md-footer-bg-color--dark: transparent; + +} + +[data-md-color-scheme=slate] .md-header, +[data-md-color-scheme=slate] .md-tabs { + background: hsl(0 0% 10% / 75%); +} + +[data-md-color-scheme=slate] .md-typeset table { + --md-default-bg-color: hsl(0 0% 7.5%); + --md-typeset-table-color: hsl(0 0% 95% / 3%); +} + +[data-md-color-scheme=slate] .md-typeset details { + background: hsl(0 0% 100% / 2%); +} + +[data-md-color-scheme=default] { + --md-footer-bg-color: hsl(0 0% 97%); + --md-footer-bg-color--dark: transparent; + --md-footer-fg-color: hsl(0 0% 0%); + --md-footer-fg-color--lighter: hsl(0 0% 33%); + --md-footer-fg-color--light: hsl(0 0% 0%); + --md-code-bg-color: hsl(0 0% 0% / 2.33%); +} + +[data-md-color-scheme=default] .md-header, +[data-md-color-scheme=default] .md-tabs { + background: hsl(0 0% 100% / 75%); +} + +[data-md-color-scheme=default] .md-search__form { + background: hsl(0 0% 0% / 5%); +} + +[data-md-color-scheme=default] .md-search__form:hover { + background: hsl(0 0% 0% / 10%); +} + +[data-md-color-scheme=default] .md-typeset details { + background: hsl(0 0% 0% / 0.5%); +} + +.md-header, .md-tabs { + backdrop-filter: blur(32px); + -webkit-backdrop-filter: blur(32px); +} + +.md-typeset table { + border-radius: .3rem !important; +} + +.md-banner strong { + white-space: nowrap; +} + +.md-banner a { + color: inherit; + font-size: 0.8rem; +} + +.md-typeset .admonition, .md-typeset details { + border-width: 1px; + border-radius: .3rem !important; + overflow: clip; +} + +.md-typeset details { + border: none; +} + +.admonition-title { + border-radius: 0rem !important; +} + +.md-header--shadow { + box-shadow: 0 0 .2rem #0000001a, 0 .2rem .4rem #0001; +} + +.md-search__form { + border-radius: .15rem !important; +} +[data-md-toggle=search]:checked~.md-header .md-search__form { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.doc-contents .quote .highlight { + margin: 0; + margin-left: -.6rem; + margin-right: -.6rem; + --md-code-bg-color: transparent; +} + +.doc-contents .quote .highlighttable { + margin: 0; +} diff --git a/docs/thumbnails.html b/docs/thumbnails.html deleted file mode 100644 index fa99b3c1..00000000 --- a/docs/thumbnails.html +++ /dev/null @@ -1,868 +0,0 @@ - - - - - - -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
-import enum
-
-endpoint = "https://thumbnails.roblox.com/"
-
-
-class ReturnPolicy(enum.Enum):
-    place_holder = "PlaceHolder"
-    auto_generated = "AutoGenerated"
-    force_auto_generated = "ForceAutoGenerated"
-
-
-class ThumbnailType(enum.Enum):
-    avatar_full_body = 0
-    avatar_bust = 1
-    avatar_headshot = 2
-
-
-class ThumbnailSize(enum.Enum):
-    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"
-
-
-class ThumbnailFormat(enum.Enum):
-    format_png = "Png"
-    format_jpg = "Jpeg"
-    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
-        self.id = 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):
-        """
-        Gets a full body, bust, or headshot image of the user.
-
-        Parameters
-        ----------
-        shot_type : ro_py.thumbnails.ThumbnailType
-            Type of shot.
-        size : ro_py.thumbnails.ThumbnailSize
-            The thumbnail size.
-        file_format : ro_py.thumbnails.ThumbnailFormat
-            The thumbnail format
-        is_circular : bool
-            The circle thumbnail output parameter.
-
-        Returns
-        -------
-        Image URL
-        """
-
-        shot_type = shot_type.value
-        file_format = file_format.value
-        size = size.value
-
-        shot_endpoint = endpoint + "v1/users/"
-        if shot_type == 0:
-            shot_endpoint = shot_endpoint + "avatar"
-        elif shot_type == 1:
-            shot_endpoint = shot_endpoint + "avatar-bust"
-        elif shot_type == 2:
-            shot_endpoint = shot_endpoint + "avatar-headshot"
-        else:
-            raise InvalidShotTypeError("Invalid shot type.")
-        shot_req = await self.requests.get(
-            url=shot_endpoint,
-            params={
-                "userIds": [self.id],
-                "size": size,
-                "format": file_format,
-                "isCircular": is_circular
-            }
-        )
-        return shot_req.json()["data"][0]["imageUrl"]
-
-
-"""
-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
-"""
-
-
-
-
-
-
-
-
-
-

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(enum.Enum):
-    place_holder = "PlaceHolder"
-    auto_generated = "AutoGenerated"
-    force_auto_generated = "ForceAutoGenerated"
-
-

Ancestors

-
    -
  • enum.Enum
  • -
-

Class variables

-
-
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.

-
- -Expand source code - -
class ThumbnailFormat(enum.Enum):
-    format_png = "Png"
-    format_jpg = "Jpeg"
-    format_jpeg = "Jpeg"
-
-

Ancestors

-
    -
  • enum.Enum
  • -
-

Class variables

-
-
var format_jpeg
-
-
-
-
var format_jpg
-
-
-
-
var format_png
-
-
-
-
-
-
-class ThumbnailSize -(value, names=None, *, module=None, qualname=None, type=None, start=1) -
-
-

An enumeration.

-
- -Expand source code - -
class ThumbnailSize(enum.Enum):
-    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"
-
-

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.

-
- -Expand source code - -
class ThumbnailType(enum.Enum):
-    avatar_full_body = 0
-    avatar_bust = 1
-    avatar_headshot = 2
-
-

Ancestors

-
    -
  • enum.Enum
  • -
-

Class variables

-
-
var avatar_bust
-
-
-
-
var avatar_full_body
-
-
-
-
var avatar_headshot
-
-
-
-
-
-
-class UserThumbnailGenerator -(requests, id) -
-
-
-
- -Expand source code - -
class UserThumbnailGenerator:
-    def __init__(self, requests, id):
-        self.requests = requests
-        self.id = 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):
-        """
-        Gets a full body, bust, or headshot image of the user.
-
-        Parameters
-        ----------
-        shot_type : ro_py.thumbnails.ThumbnailType
-            Type of shot.
-        size : ro_py.thumbnails.ThumbnailSize
-            The thumbnail size.
-        file_format : ro_py.thumbnails.ThumbnailFormat
-            The thumbnail format
-        is_circular : bool
-            The circle thumbnail output parameter.
-
-        Returns
-        -------
-        Image URL
-        """
-
-        shot_type = shot_type.value
-        file_format = file_format.value
-        size = size.value
-
-        shot_endpoint = endpoint + "v1/users/"
-        if shot_type == 0:
-            shot_endpoint = shot_endpoint + "avatar"
-        elif shot_type == 1:
-            shot_endpoint = shot_endpoint + "avatar-bust"
-        elif shot_type == 2:
-            shot_endpoint = shot_endpoint + "avatar-headshot"
-        else:
-            raise InvalidShotTypeError("Invalid shot type.")
-        shot_req = await self.requests.get(
-            url=shot_endpoint,
-            params={
-                "userIds": [self.id],
-                "size": size,
-                "format": file_format,
-                "isCircular": is_circular
-            }
-        )
-        return shot_req.json()["data"][0]["imageUrl"]
-
-

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 full body, bust, or headshot image of the user.

-

Parameters

-
-
shot_type : ThumbnailType
-
Type of shot.
-
size : ThumbnailSize
-
The thumbnail size.
-
file_format : ThumbnailFormat
-
The thumbnail format
-
is_circular : bool
-
The circle thumbnail output parameter.
-
-

Returns

-
-
Image URL
-
 
-
-
- -Expand source code - -
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 full body, bust, or headshot image of the user.
-
-    Parameters
-    ----------
-    shot_type : ro_py.thumbnails.ThumbnailType
-        Type of shot.
-    size : ro_py.thumbnails.ThumbnailSize
-        The thumbnail size.
-    file_format : ro_py.thumbnails.ThumbnailFormat
-        The thumbnail format
-    is_circular : bool
-        The circle thumbnail output parameter.
-
-    Returns
-    -------
-    Image URL
-    """
-
-    shot_type = shot_type.value
-    file_format = file_format.value
-    size = size.value
-
-    shot_endpoint = endpoint + "v1/users/"
-    if shot_type == 0:
-        shot_endpoint = shot_endpoint + "avatar"
-    elif shot_type == 1:
-        shot_endpoint = shot_endpoint + "avatar-bust"
-    elif shot_type == 2:
-        shot_endpoint = shot_endpoint + "avatar-headshot"
-    else:
-        raise InvalidShotTypeError("Invalid shot type.")
-    shot_req = await self.requests.get(
-        url=shot_endpoint,
-        params={
-            "userIds": [self.id],
-            "size": size,
-            "format": file_format,
-            "isCircular": is_circular
-        }
-    )
-    return shot_req.json()["data"][0]["imageUrl"]
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/trades.html b/docs/trades.html deleted file mode 100644 index 093d0346..00000000 --- a/docs/trades.html +++ /dev/null @@ -1,1065 +0,0 @@ - - - - - - -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.assets import Asset, UserAsset
-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_date(created)
-        self.experation = 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, 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 TradeRequest:
-    def __init__(self):
-        self.recieve_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
-        """Robux that will be recieved when the trade is accepted."""
-
-    def request_item(self, asset: UserAsset):
-        """
-        Appends asset to self.recieve_asset.
-
-        Parameters
-        ----------
-        asset : ro_py.assets.UserAsset
-        """
-        self.recieve_asset.append(asset)
-
-    def send_item(self, asset: UserAsset):
-        """
-        Appends asset to self.send_asset.
-
-        Parameters
-        ----------
-        asset : ro_py.assets.UserAsset
-        """
-        self.send_asset.append(asset)
-
-    def request_robux(self, robux: int):
-        """
-        Sets self.request_robux to robux
-
-        Parameters
-        ----------
-        robux : int
-        """
-        self.recieve_robux = robux
-
-    def send_robux(self, robux: int):
-        """
-        Sets self.send_robux to robux
-
-        Parameters
-        ----------
-        robux : int
-        """
-        self.send_robux = robux
-
-
-class TradesWrapper:
-    """
-    Represents the Roblox trades page.
-    """
-    def __init__(self, requests, get_self):
-        self.requests = 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,
-            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, roblox_id, trade):
-        """
-        Sends a trade request.
-
-        Parameters
-        ----------
-        roblox_id : int
-                User who will recieve the trade.
-        trade : ro_py.trades.TradeRequest
-                Trade that will be sent to the user.
-
-        Returns
-        -------
-        int
-        """
-        me = await self.get_self()
-
-        data = {
-            "offers": [
-                {
-                    "userId": roblox_id,
-                    "userAssetIds": [],
-                    "robux": None
-                },
-                {
-                    "userId": me.id,
-                    "userAssetIds": [],
-                    "robux": None
-                }
-            ]
-        }
-
-        for asset in trade.send_asset:
-            data['offers'][1]['userAssetIds'].append(asset.user_asset_id)
-
-        for asset in trade.recieve_asset:
-            data['offers'][0]['userAssetIds'].append(asset.user_asset_id)
-
-        data['offers'][0]['robux'] = trade.recieve_robux
-        data['offers'][1]['robux'] = trade.send_robux
-
-        trade_req = await self.requests.post(
-            url=endpoint + "/v1/trades/send",
-            data=data
-        )
-
-        return trade_req.status == 200
-
-
-
-
-
-
-
-

Functions

-
-
-def trade_page_handler(requests, this_page) ‑> list -
-
-
-
- -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
-
-
-
-
-
-

Classes

-
-
-class PartialTrade -(requests, trade_id: int, user: PartialUser, created, expiration, status: bool) -
-
-
-
- -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

-
-
-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: int, sender: PartialUser, recieve_items, send_items, created, expiration, status: bool) -
-
-
-
- -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_date(created)
-        self.experation = 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
-
-

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 TradeRequest -
-
-
-
- -Expand source code - -
class TradeRequest:
-    def __init__(self):
-        self.recieve_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
-        """Robux that will be recieved when the trade is accepted."""
-
-    def request_item(self, asset: UserAsset):
-        """
-        Appends asset to self.recieve_asset.
-
-        Parameters
-        ----------
-        asset : ro_py.assets.UserAsset
-        """
-        self.recieve_asset.append(asset)
-
-    def send_item(self, asset: UserAsset):
-        """
-        Appends asset to self.send_asset.
-
-        Parameters
-        ----------
-        asset : ro_py.assets.UserAsset
-        """
-        self.send_asset.append(asset)
-
-    def request_robux(self, robux: int):
-        """
-        Sets self.request_robux to robux
-
-        Parameters
-        ----------
-        robux : int
-        """
-        self.recieve_robux = robux
-
-    def send_robux(self, robux: int):
-        """
-        Sets self.send_robux to robux
-
-        Parameters
-        ----------
-        robux : int
-        """
-        self.send_robux = robux
-
-

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.

-
- -Expand source code - -
def send_robux(self, robux: int):
-    """
-    Sets self.send_robux to robux
-
-    Parameters
-    ----------
-    robux : int
-    """
-    self.send_robux = robux
-
-
-
-

Methods

-
-
-def request_item(self, asset: UserAsset) -
-
-

Appends asset to self.recieve_asset.

-

Parameters

-
-
asset : UserAsset
-
 
-
-
- -Expand source code - -
def request_item(self, asset: UserAsset):
-    """
-    Appends asset to self.recieve_asset.
-
-    Parameters
-    ----------
-    asset : ro_py.assets.UserAsset
-    """
-    self.recieve_asset.append(asset)
-
-
-
-def request_robux(self, robux: int) -
-
-

Sets self.request_robux to robux

-

Parameters

-
-
robux : int
-
 
-
-
- -Expand source code - -
def request_robux(self, robux: int):
-    """
-    Sets self.request_robux to robux
-
-    Parameters
-    ----------
-    robux : int
-    """
-    self.recieve_robux = robux
-
-
-
-def send_item(self, asset: UserAsset) -
-
-

Appends asset to self.send_asset.

-

Parameters

-
-
asset : UserAsset
-
 
-
-
- -Expand source code - -
def send_item(self, asset: UserAsset):
-    """
-    Appends asset to self.send_asset.
-
-    Parameters
-    ----------
-    asset : ro_py.assets.UserAsset
-    """
-    self.send_asset.append(asset)
-
-
-
-
-
-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, get_self) -
-
-

Represents the Roblox trades page.

-
- -Expand source code - -
class TradesWrapper:
-    """
-    Represents the Roblox trades page.
-    """
-    def __init__(self, requests, get_self):
-        self.requests = 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,
-            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, roblox_id, trade):
-        """
-        Sends a trade request.
-
-        Parameters
-        ----------
-        roblox_id : int
-                User who will recieve the trade.
-        trade : ro_py.trades.TradeRequest
-                Trade that will be sent to the user.
-
-        Returns
-        -------
-        int
-        """
-        me = await self.get_self()
-
-        data = {
-            "offers": [
-                {
-                    "userId": roblox_id,
-                    "userAssetIds": [],
-                    "robux": None
-                },
-                {
-                    "userId": me.id,
-                    "userAssetIds": [],
-                    "robux": None
-                }
-            ]
-        }
-
-        for asset in trade.send_asset:
-            data['offers'][1]['userAssetIds'].append(asset.user_asset_id)
-
-        for asset in trade.recieve_asset:
-            data['offers'][0]['userAssetIds'].append(asset.user_asset_id)
-
-        data['offers'][0]['robux'] = trade.recieve_robux
-        data['offers'][1]['robux'] = trade.send_robux
-
-        trade_req = await self.requests.post(
-            url=endpoint + "/v1/trades/send",
-            data=data
-        )
-
-        return trade_req.status == 200
-
-

Methods

-
-
-async def get_trades(self, trade_status_type: <TradeStatusType.Inbound: 'Inbound'>, sort_order=SortOrder.Ascending, limit=10) ‑> Pages -
-
-
-
- -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, roblox_id, trade) -
-
-

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
-
 
-
-
- -Expand source code - -
async def send_trade(self, roblox_id, trade):
-    """
-    Sends a trade request.
-
-    Parameters
-    ----------
-    roblox_id : int
-            User who will recieve the trade.
-    trade : ro_py.trades.TradeRequest
-            Trade that will be sent to the user.
-
-    Returns
-    -------
-    int
-    """
-    me = await self.get_self()
-
-    data = {
-        "offers": [
-            {
-                "userId": roblox_id,
-                "userAssetIds": [],
-                "robux": None
-            },
-            {
-                "userId": me.id,
-                "userAssetIds": [],
-                "robux": None
-            }
-        ]
-    }
-
-    for asset in trade.send_asset:
-        data['offers'][1]['userAssetIds'].append(asset.user_asset_id)
-
-    for asset in trade.recieve_asset:
-        data['offers'][0]['userAssetIds'].append(asset.user_asset_id)
-
-    data['offers'][0]['robux'] = trade.recieve_robux
-    data['offers'][1]['robux'] = trade.send_robux
-
-    trade_req = await self.requests.post(
-        url=endpoint + "/v1/trades/send",
-        data=data
-    )
-
-    return trade_req.status == 200
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/tutorials/authentication.md b/docs/tutorials/authentication.md new file mode 100644 index 00000000..6573b367 --- /dev/null +++ b/docs/tutorials/authentication.md @@ -0,0 +1,70 @@ +# Authentication +To authenticate our client, we need our .ROBLOSECURITY token. To learn about why we need this and how to get it, +please see [ROBLOSECURITY](./roblosecurity.md). + +Once we have our token, we can add it to our client by passing it as the first parameter. +Use the following code and replace `TOKEN` with the .ROBLOSECURITY token grabbed earlier to authenticate your client. +```python +from roblox import Client +client = Client("TOKEN") +``` + +To test your token, replace the code in `main()` with the following: +```python +user = await client.get_authenticated_user() +print("ID:", user.id) +print("Name:", user.name) +``` +If this raises an error, or the name and ID differ from what is expected, follow the instructions and try again. +The issue with this structure is that it is not secure. It's easy to slip up and copy your code and accidentally send +someone your token, and it makes it harder to collaborate on code with others. + +# Using a .env file +To solve this problem, we'll create a separate file called `.env` which will contain our token. + +Your file should look like this, where TOKEN is the .ROBLOSECURITY token you grabbed earlier. +```dotenv title=".env" +ROBLOXTOKEN=TOKEN +``` +Place it in the same folder as your application's main file. + +Your file structure should look like this: +```sh +. +├─ .env +└─ main.py +``` + +Next, install the [python-dotenv](https://github.com/theskumar/python-dotenv) library with the following command: +``` +$ pip install python-dotenv +``` +Then, add these lines to the top of your code: +```python +import os +from dotenv import load_dotenv +``` +After that, replace the code where you generate your client with this: +```python +load_dotenv() +client = Client(os.getenv("ROBLOXTOKEN")) +``` +Test it with `get_authenticated_user` and you should be all set! +!!! abstract "Finished code" + ```python title="main.py" + import asyncio + import os + from dotenv import load_dotenv + from roblox import Client + + load_dotenv() + + client = Client(os.getenv("ROBLOXTOKEN")) + + async def main(): + user = await client.get_authenticated_user() + print("ID:", user.id) + print("Name:", user.name) + + asyncio.get_event_loop().run_until_complete(main()) + ``` diff --git a/docs/tutorials/bases.md b/docs/tutorials/bases.md new file mode 100644 index 00000000..fc7dd01a --- /dev/null +++ b/docs/tutorials/bases.md @@ -0,0 +1,38 @@ +# Bases +Let's say you want to use ro.py to fetch the username history of a user, and you already know their user ID. You could do this: +```py +user = await client.get_user(968108160) +async for username in user.username_history(): + print(username) +``` +This code works, but it has an issue: we're sending an unnecessary request to Roblox. + +To explain why, let's take a look at what ro.py is doing behind the scenes in this code. +- First, we call `await client.get_user(2067807455)`. ro.py asks Roblox for information about the user with the ID 2067807455 and returns it as a User object. +- Next, we iterate through `user.username_history`. ro.py asks Roblox for the username history for user 2067807455 and returns it to you. + +In this code, we call `await client.get_user()`, but we don't use any user information, like `user.name` or `user.description`. We don't need to make this request! + +ro.py lets you skip the "information request" with the `client.get_base_TYPE` methods. We can use the `client.get_base_user()` function to improve this code: +```py +user = client.get_base_user(2067807455) # no await! +async for username in user.username_history(): + print(username) +``` + +!!! hint + In ro.py, all functions you `await` or paginators you iterate through with `async for` make at least one request internally. Notice how you need to `await` the `get_user` function, but not the `get_base_user` function! + +This works for other Roblox types as well, like groups and assets. For example, this code kicks a user from a group with only 1 request: +```py +group = client.get_base_group(9695397) +user = client.get_base_user(2067807455) +await group.kick_user(user) +``` + +There's another technique we can use to optimize this example further. For functions that accept only one type, like `kick_user` which always accepts a user, ro.py accepts bare IDs: +```py +group = client.get_base_group(9695397) +await group.kick_user(2067807455) +``` + diff --git a/docs/tutorials/error-handling.md b/docs/tutorials/error-handling.md new file mode 100644 index 00000000..d220573c --- /dev/null +++ b/docs/tutorials/error-handling.md @@ -0,0 +1,95 @@ +# Error handling +You can import ro.py exceptions from the `roblox.utilities.exceptions` module or from the main `roblox` module: + +```py +from roblox.utilities.exceptions import InternalServerError +# or +from roblox import InternalServerError +``` + +## Client errors +All of the `Client.get_TYPE()` methods, like `get_user()` and `get_group()`, raise their own exceptions. + +| Method | Exception | +|---------------------------------|--------------------| +| `client.get_asset()` | `AssetNotFound` | +| `client.get_badge()` | `BadgeNotFound` | +| `client.get_group()` | `GroupNotFound` | +| `client.get_place()` | `PlaceNotFound` | +| `client.get_plugin()` | `PluginNotFound` | +| `client.get_universe()` | `UniverseNotFound` | +| `client.get_user()` | `UserNotFound` | +| `client.get_user_by_username()` | `UserNotFound` | + +Here is an example of catching one of these exceptions: +```python +try: + user = await client.get_user_by_username("InvalidUsername!!!") +except UserNotFound: + print("Invalid username!") +``` + +All of these exceptions are subclasses of `ItemNotFound`, which you can use as a catch-all. + +## HTTP errors +When Roblox returns an error, ro.py raises an HTTP exception. + +For example, if we try to post a group shout to a group that we don't the necessary permissions in, Roblox stops us and returns a +`401 Unauthorized` error: +```python +group = await client.get_group(1) +await group.update_shout("Shout!") +``` +This code will raise an error like this: +```pytb +roblox.utilities.exceptions.Unauthorized: 401 Unauthorized: https://groups.roblox.com/v1/groups/1/status. + +Errors: + 0: Authorization has been denied for this request. +``` +You can catch this error as follows:: +```python +group = await client.get_group(1) +try: + await group.update_shout("Shout!") + print("Shout updated.") +except Unauthorized: + print("Not allowed to shout.") +``` + +These are the different types of exceptions raised depending on the HTTP error code Roblox returns: + +| HTTP status code | Exception | +|------------------|-----------------------| +| 400 | `BadRequest` | +| 401 | `Unauthorized` | +| 403 | `Forbidden` | +| 429 | `TooManyRequests` | +| 500 | `InternalServerError` | + +All of these exceptions are subclasses of the `HTTPException` error, which you can use as a catch-all. For other unrecognized error codes, ro.py will fallback to the default `HTTPException`. + +### Getting more error information +For all HTTP exceptions, ro.py exposes a `response` attribute so you can get the response information: +```python +group = await client.get_group(1) +try: + await group.update_shout("Shout!") + print("Shout updated.") +except Unauthorized as exception: + print("Not allowed to shout.") + print("URL:", exception.response.url) +``` +Roblox also returns extra error data, which is what you see in the default error message. +We can access this with the `.errors` attribute, which is a list of [`ResponseError`][roblox.utilities.exceptions.ResponseError]: +```python +group = await client.get_group(1) +try: + await group.update_shout("Shout!") + print("Shout updated.") +except Unauthorized as exception: + print("Not allowed to shout.") + if len(exception.errors) > 0: + error = exception.errors[0] + print("Reason:", error.message) +``` \ No newline at end of file diff --git a/docs/tutorials/get-started.md b/docs/tutorials/get-started.md new file mode 100644 index 00000000..25b384ce --- /dev/null +++ b/docs/tutorials/get-started.md @@ -0,0 +1,60 @@ +# Get started + +At the beginning of every ro.py application is the client. The client represents a Roblox session, and it's your gateway to everything in ro.py. + +To initialize a client, import it from the `roblox` module: +```python title="main.py" +from roblox import Client +client = Client() +``` + +We can use the client to get information from Roblox by calling `await client.get_TYPE()`, where `TYPE` is a Roblox datatype, like a user or group. + +There's a problem, though: if we run the following code... +```python title="main.py" +from roblox import Client +client = Client() +await client.get_user(1) +``` +...it'll raise an error like this: +```pytb + File "...", line 1 +SyntaxError: 'await' outside function +``` + +This is because ro.py, like many Python libraries, is based on [asyncio](https://docs.python.org/3/library/asyncio.html), a builtin Python library that allows for concurrent code. In the case of ro.py, this means your app can do something, like process Discord bot commands, while ro.py waits for Roblox to respond, saving tons of time and preventing one slow function from slowing down the whole program. Neat! + +This means we need to wrap our code in an **asynchronous function** and then run it with `asyncio.run`, like so: + +```python title="main.py" +import asyncio +from roblox import Client +client = Client() + +async def main(): + user = await client.get_user(1) + +asyncio.run(main()) +``` + +This is the basic structure of every simple ro.py application. More complicated apps might not work like this - for example, in a Discord bot, another library might already be handling the asyncio part for you - but for simple scripts, this is what you'll be doing. + +Now the error is gone, but our code doesn't do anything yet. Let's try printing out some information about this user. Add these lines to the end of your main function: + +```python title="main.py" +print("Name:", user.name) +print("Display Name:", user.display_name) +print("Description:", user.description) +``` + +Great! We now have a program that prints out a user's name, display name, and description. This same basic concept works +for other kinds of objects on Roblox, like groups. Try replacing the code in your main function with this: +```python +group = await client.get_group(1) +print("Name:", group.name) +print("Description:", group.description) +``` + +To see a list of everything you can do with the client, see [`Client`][roblox.client.Client] in the Code Reference. + +So far, we've been using ro.py **unauthenticated**. Basically, we aren't logged in to Roblox, which means we can't perform any actions, like updating our description, or access any sensitive information, like which game our friend is playing right now. Your next mission, if you choose to accept it, is [authenticating your client](./authentication.md). diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 00000000..62a2051b --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,6 @@ +This tutorial is intended for people building **standalone applications**. It expects basic Python knowledge but will +explain almost everything you need to know to build. Make sure to read through the entire page instead of skimming it to +ensure you don't miss anything important! + +If at any point you are struggling to understand what to do, join the [RoAPI Discord](https://discord.gg/N8yUdkSJwA) for +help and support. diff --git a/docs/tutorials/pagination.md b/docs/tutorials/pagination.md new file mode 100644 index 00000000..d2b1ee08 --- /dev/null +++ b/docs/tutorials/pagination.md @@ -0,0 +1,89 @@ +# Pagination +Certain Roblox endpoints are paginated. This means that going through their data is kind of like flipping through the +pages of a book - you start at page 1 and then you can move forwards or backwards until you reach the start or the end. + +This can be annoying when all you want is "every member in a group" or "the last 10 posts on a group wall", so ro.py +abstracts this away into an iterator that you can use to loop over your data. + +As an example, the [`Client.user_search()`][roblox.client.Client.user_search] function takes in a keyword (like "builderman") and returns a [`PageIterator`][roblox.utilities.iterators.PageIterator] +which you can loop through to get the search results. + +## Looping through items +A simple `async for` can loop through the data no problem: +```python +async for user in client.user_search("builderman"): + print(user.name) +``` +We can limit the amount of items returned using the `max_items` argument: +```python +async for user in client.user_search("builderman", max_items=10): + print(user.name) +``` +We can also use `.items()`: +```python +async for user in client.user_search("builderman").items(10): + print(user.name) +``` + +## Looping through pages +If we want to instead loop through each *page*, we can use `.pages()`: +```python +async for page in client.user_search("builderman").pages(): + print("Page:") + for user in page: + print(f"\t{user.name}") +``` +The size of this page depends on the value of the `page_size` argument. It can be either 10, 25, 50 or 100. +Higher values mean you send less requests to get the same amount of data, however these requests will usually take +longer. + +```python +async for page in client.user_search("builderman", page_size=100).pages(): + print(f"Page with {len(page)} items:") + for user in page: + print(f"\t{user.name}") +``` + +## Flattening into a list +If we want to turn all of this data into one list, we can use [`flatten()`][roblox.utilities.iterators.PageIterator.flatten]. Be careful, as this isn't ideal for large +sets of data and may use more memory. Because we turn this iterator into a list, we can use a normal for loop now: +```python +for user in await client.user_search("boatbomber").flatten(): + print(user.name) +``` +We can limit the amount of items in this list using the `max_items` argument: +```python +for user in await client.user_search("builderman", max_items=10).flatten(): + print(user.name) +``` +We can also pass the value directly to `.flatten()`: +```python +for user in await client.user_search("builderman").flatten(10): + print(user.name) +``` +As the result is just a normal list, we can store it in a variable: +```python +users = await client.user_search("builderman").flatten(10) +print(f"{len(users)} items:") +for user in users: + print(f"\t{user.name}") +``` + +## But what about other things? +Iterators aren't *just* used for searching for users. There are also various other things that use this same concept, +including group wall posts. In this example, we get the first 10 posts on the "Official Group of Roblox" group: +```python +group = await client.get_group(1200769) +async for post in group.get_wall_posts(max_items=10): + print(post) +``` +If instead we want the *last* 10 posts (as in the most recent posts) we can use the `sort_order` argument: +```python +group = await client.get_group(1200769) +async for post in group.get_wall_posts(sort_order=SortOrder.Descending, max_items=10): + print(post) +``` +The `SortOrder` object can be imported like this: +```python +from roblox.utilities.iterators import SortOrder +``` diff --git a/docs/tutorials/roblosecurity.md b/docs/tutorials/roblosecurity.md new file mode 100644 index 00000000..093b0003 --- /dev/null +++ b/docs/tutorials/roblosecurity.md @@ -0,0 +1,52 @@ +# ROBLOSECURITY + +When you log in on the Roblox website, you create a new session with a special identifier linked to it, and that token is stored on your computer as a cookie. +Every single time your computer asks Roblox to do anything - for example, "give me the name of this user" - your computer also gives this token to Roblox, and it can look and see if that token is valid. + +Let's say you're asking Roblox to give you a list of your friends. It'll look at that token and know who you are, and can use that to give you your friends list. +When you log out, that token is invalidated. Even if the client holds on to the token, it won't be valid after logging out. + +This token is called the `.ROBLOSECURITY` token and you will need one to do anything that you need to be logged in to do +on Roblox, including: +- getting information about yourself (name, description, ID, etc) +- changing avatar +- getting friends list +- playing games + +!!! danger + You may have heard of this token before and have been told that you should never, under any circumstances, share + this token with anyone - and this is true! This token does give an attacker access to your Roblox account. However, + this doesn't mean they gain access to *everything* - over time, more and more things are being locked behind other + verification methods, like 2-step verification. + We recommend using an alternate account with only the permissions it needs to limit the destruction an attacker can + do. [Always enable 2-step verification!](https://en.help.roblox.com/hc/articles/212459863) + +The best way to authenticate your ro.py application is to log in to Roblox on the website and then taking the +.ROBLOSECURITY token from there. + +!!! warning + Pressing the "Log out" button on the Roblox website invalidates your token, so you should not press this button + after grabbing your token. Instead, consider using a private or incognito window and closing it when you are done. + +To grab your .ROBLOSECURITY cookie, log into your account on the Roblox website and follow the instructions below. + +=== "Chrome/Chromium-based" + You can access the cookie by going to https://www.roblox.com/, pressing the padlock icon next to the URL in your + browser, clicking the arrow next to `roblox.com`, opening up the "Cookies" folder, clicking ".ROBLOSECURITY", + clicking on the "Content" text once, pressing ++control+a++, and then pressing ++control+c++ + (make sure **not** to double-click this field as you won't select the entire value!) + + ![](../assets/screenshots/ChromeCookie.png){: style="width: 400px"} + + Alternatively, you can access the cookie by going to https://www.roblox.com/, pressing ++control+shift+i++ to access + the Developer Tools, navigating to the "Application" tab, opening up the arrow next to "Cookies" on the sidebar on + the left, clicking the `https://www.roblox.com` item underneath the Cookies button, and then copying the + .ROBLOSECURITY token by double-clicking on the value and then hitting ++control+c++. + + ![](../assets/screenshots/ChromeDevTools.png){: style="height: 436px"} +=== "Firefox" + You can access the cookie by going to https://www.roblox.com/ and pressing ++shift+f9++, + pressing the "Storage" tab button on the top, opening up the "Cookies" section in the sidebar on the left, + clicking the `https://www.roblox.com` item underneath it, + and then copying the .ROBLOSECURITY token by double-clicking on the value and then hitting ++control+c++. + ![](../assets/screenshots/FirefoxCookie.jpeg) diff --git a/docs/tutorials/thumbnails.md b/docs/tutorials/thumbnails.md new file mode 100644 index 00000000..7f185a7c --- /dev/null +++ b/docs/tutorials/thumbnails.md @@ -0,0 +1,157 @@ +# Thumbnails +The `client.thumbnails` attribute is a `ThumbnailProvider` object which you can use to generate thumbnails. +Below is a list of item types on Roblox and methods you can use to generate their thumbnails. + +## Users +To generate avatar thumbnails, use the [`get_user_avatar_thumbnails()`][roblox.thumbnails.ThumbnailProvider.get_user_avatar_thumbnails] method. +The `type` parameter is an [`AvatarThumbnailType`][roblox.thumbnails.AvatarThumbnailType] +object, which you can import from `roblox` or from `roblox.thumbnails`. +Do note that the `size` parameter only allows certain sizes - see the docs for more details. + +```python +user = await client.get_user(2067807455) +user_thumbnails = await client.thumbnails.get_user_avatar_thumbnails( + users=[user], + type=AvatarThumbnailType.full_body, + size=(420, 420) +) + +if len(user_thumbnails) > 0: + user_thumbnail = user_thumbnails[0] + print(user_thumbnail.image_url) +``` + +`thumbnails` is a list of [`Thumbnail`][roblox.thumbnails.Thumbnail] objects. +We can read the first thumbnail (if it exists) and print out its URL. + +### 3D thumbnails +To generate 3D avatar thumbnails, use the [`get_user_avatar_thumbnail_3d()`][roblox.thumbnails.ThumbnailProvider.get_user_avatar_thumbnail_3d] method +and call [`get_3d_data()`][roblox.thumbnails.Thumbnail.get_3d_data] on the resulting thumbnail. + +```python +user = await client.get_user(1) +user_3d_thumbnail = await client.thumbnails.get_user_avatar_thumbnail_3d(user) +user_3d_data = await user_3d_thumbnail.get_3d_data() +print("OBJ:", user_3d_data.obj.get_url()) +print("MTL:", user_3d_data.mtl.get_url()) +print("Textures:") +for texture in user_3d_data.textures: + print(texture.get_url()) +``` +`threed_data` is a [`ThreeDThumbnail`][roblox.threedthumbnails.ThreeDThumbnail] +object. + +## Groups +To generate group icons, use the +[`get_group_icons()`][roblox.thumbnails.ThumbnailProvider.get_group_icons] method. +```python +group = await client.get_group(9695397) +group_icons = await client.thumbnails.get_group_icons( + groups=[group], + size=(150, 150) +) +if len(group_icons) > 0: + group_icon = group_icons[0] + print(group_icon.image_url) +``` + +## Assets +To generate asset thumbnails, use the +[`get_asset_thumbnails()`][roblox.thumbnails.ThumbnailProvider.get_asset_thumbnails] +method. +```python +asset = await client.get_asset(8100249026) +asset_thumbnails = await client.thumbnails.get_asset_thumbnails( + assets=[asset], + size=(420, 420) +) +if len(asset_thumbnails) > 0: + asset_thumbnail = asset_thumbnails[0] + print(asset_thumbnail.image_url) +``` + +### 3D thumbnails +!!! note + Not all assets support 3D thumbnails. Most "catalog" assets do, excluding "classic faces", which have no 3D representation. + +To generate 3D asset thumbnails, use the [`get_asset_thumbnail_3d()`][roblox.thumbnails.ThumbnailProvider.get_asset_thumbnail_3d] +method and call [`get_3d_data()`][roblox.thumbnails.Thumbnail.get_3d_data] on the resulting thumbnail. +```python +asset = await client.get_asset(151784320) +asset_3d_thumbnail = await client.thumbnails.get_asset_thumbnail_3d(asset) +asset_3d_data = await asset_3d_thumbnail.get_3d_data() +print("OBJ:", asset_3d_data.obj.get_url()) +print("MTL:", asset_3d_data.mtl.get_url()) +print("Textures:") +for texture in asset_3d_data.textures: + print(texture.get_url()) +``` + +## Places +To generate place icons, use the [`get_place_icons()`][roblox.thumbnails.ThumbnailProvider.get_place_icons] method. +```python +place = await client.get_place(8100260845) +place_thumbnails = await client.thumbnails.get_place_icons( + places=[place], + size=(512, 512) +) +if len(place_thumbnails) > 0: + place_thumbnail = place_thumbnails[0] + print(place_thumbnail.image_url) +``` + +## Universes +### Icons +To generate universe icons, use the[`get_universe_icons()`][roblox.thumbnails.ThumbnailProvider.get_universe_icons] method. +```python +universe = await client.get_universe(3118067569) +universe_icons = await client.thumbnails.get_universe_icons( + universes=[universe], + size=(512, 512) +) +if len(universe_icons) > 0: + universe_icon = universe_icons[0] + print(universe_icon.image_url) +``` +### Thumbnails +To generate universe thumbnails, use the [`get_universe_thumbnails()`][roblox.thumbnails.ThumbnailProvider.get_universe_thumbnails] method. +Because each universe can have multiple thumbnails, this method behaves differently. +```python +universe = await client.get_universe(3118067569) +universes_thumbnails = await client.thumbnails.get_universe_thumbnails( + universes=[universe], + size=(768, 432) +) +if len(universes_thumbnails) > 0: + universe_thumbnails = universes_thumbnails[0] + for universe_thumbnail in universe_thumbnails.thumbnails: + print(universe_thumbnail.image_url) +``` + +## Badges +To generate badge icons, use the [`get_badge_icons()`][roblox.thumbnails.ThumbnailProvider.get_badge_icons] method. +```python +badge = await client.get_badge(2124867793) +badge_icons = await client.thumbnails.get_badge_icons( + badges=[badge], + size=(150, 150) +) +if len(badge_icons) > 0: + icon = badge_icons[0] + print(icon.image_url) +``` + +## Gamepasses +To generate gamepass icons, use the +[`get_gamepass_icons()`][roblox.thumbnails.ThumbnailProvider.get_gamepass_icons] method. +This example uses [`get_base_gamepass()`][roblox.client.Client.get_base_gamepass] because there is no `get_gamepass` method. +```python +gamepass = client.get_base_gamepass(25421830) +gamepass_icons = await client.thumbnails.get_gamepass_icons( + gamepasses=[gamepass], + size=(150, 150) +) +if len(gamepass_icons) > 0: + icon = gamepass_icons[0] + print(icon.image_url) +``` \ No newline at end of file diff --git a/docs/users.html b/docs/users.html deleted file mode 100644 index 210385ba..00000000 --- a/docs/users.html +++ /dev/null @@ -1,692 +0,0 @@ - - - - - - -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
-from ro_py.thumbnails import UserThumbnailGenerator
-from ro_py.utilities.pages import Pages
-from ro_py.assets import UserAsset
-import iso8601
-
-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 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):
-        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)
-
-    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.cso, friend_raw["id"])
-            )
-        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['id'], group['name'], group['memberCount']))
-        return groups
-
-    async def get_limiteds(self):
-        """
-        Gets all limiteds the user owns.
-
-        Returns
-        -------
-        list
-        """
-        return Pages(
-            requests=self.requests,
-            url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
-            handler=limited_handler
-        )
-
-
-class PartialUser(User):
-    """
-    Represents a user with less information then the normal User class.
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-
-
-
-
-
-
-

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 PartialUser -(*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.
-    """
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-

Ancestors

- -

Inherited members

- -
-
-class User -(cso, roblox_id, name=None) -
-
-

Represents a Roblox user and their profile. -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 - -
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):
-        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)
-
-    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.cso, friend_raw["id"])
-            )
-        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['id'], group['name'], group['memberCount']))
-        return groups
-
-    async def get_limiteds(self):
-        """
-        Gets all limiteds the user owns.
-
-        Returns
-        -------
-        list
-        """
-        return Pages(
-            requests=self.requests,
-            url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc",
-            handler=limited_handler
-        )
-
-

Subclasses

- -

Methods

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

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) -
-
-

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) -
-
-

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.cso, friend_raw["id"])
-        )
-    return friends_list
-
-
-
-async def get_friends_count(self) -
-
-

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) -
-
-
-
- -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['id'], group['name'], group['memberCount']))
-    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(
-        requests=self.requests,
-        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. -: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) -
-
-

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) -
-
-

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
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/utilities/asset_type.html b/docs/utilities/asset_type.html deleted file mode 100644 index 98754bc9..00000000 --- a/docs/utilities/asset_type.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - -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/utilities/cache.html b/docs/utilities/cache.html deleted file mode 100644 index fdf792de..00000000 --- a/docs/utilities/cache.html +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - -ro_py.utilities.cache API documentation - - - - - - - - - - - - -
-
-
-

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
-
-
-
-
-
-
-
-
-
-

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
-
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/utilities/caseconvert.html b/docs/utilities/caseconvert.html deleted file mode 100644 index a590c530..00000000 --- a/docs/utilities/caseconvert.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - -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/utilities/errors.html b/docs/utilities/errors.html deleted file mode 100644 index df08866c..00000000 --- a/docs/utilities/errors.html +++ /dev/null @@ -1,670 +0,0 @@ - - - - - - -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.
-
-"""
-
-
-# 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
-
-
-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 ChatError(Exception):
-    """Called in chat when a chat action fails."""
-
-
-class InvalidPageError(Exception):
-    """Called when an invalid page is requested."""
-
-
-class UserDoesNotExistError(Exception):
-    """Called when a user does not exist."""
-
-
-class GameJoinError(Exception):
-    """Called when an error occurs when joining a game."""
-
-
-class InvalidPlaceIDError(Exception):
-    """Called when place ID is invalid."""
-
-
-class IncorrectKeyError(Exception):
-    """Raised when the api key for 2captcha is incorrect."""
-    pass
-
-
-class InsufficientCreditError(Exception):
-    """Raised when there is insufficient credit in 2captcha."""
-    pass
-
-
-class NoAvailableWorkersError(Exception):
-    """Raised when there are no available workers."""
-    pass
-
-
-c_errors = {
-    "400": BadRequest,
-    "401": Unauthorized,
-    "403": Forbidden,
-    "404": NotFound,
-    "409": Conflict,
-    "429": TooManyRequests,
-    "500": InternalServerError,
-    "502": BadGateway
-}
-
-
-
-
-
-
-
-
-
-

Classes

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

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 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

-
    -
  • 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 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) -
-
-

Called when an error occurs when joining a game.

-
- -Expand source code - -
class GameJoinError(Exception):
-    """Called when an error occurs when joining a game."""
-
-

Ancestors

-
    -
  • builtins.Exception
  • -
  • builtins.BaseException
  • -
-
-
-class IncorrectKeyError -(*args, **kwargs) -
-
-

Raised when the api key for 2captcha is incorrect.

-
- -Expand source code - -
class IncorrectKeyError(Exception):
-    """Raised when the api key for 2captcha is incorrect."""
-    pass
-
-

Ancestors

-
    -
  • builtins.Exception
  • -
  • builtins.BaseException
  • -
-
-
-class InsufficientCreditError -(*args, **kwargs) -
-
-

Raised when there is insufficient credit in 2captcha.

-
- -Expand source code - -
class InsufficientCreditError(Exception):
-    """Raised when there is insufficient credit in 2captcha."""
-    pass
-
-

Ancestors

-
    -
  • builtins.Exception
  • -
  • 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) -
-
-

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 InvalidPlaceIDError -(*args, **kwargs) -
-
-

Called when place ID is invalid.

-
- -Expand source code - -
class InvalidPlaceIDError(Exception):
-    """Called when place ID is invalid."""
-
-

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 NoAvailableWorkersError -(*args, **kwargs) -
-
-

Raised when there are no available workers.

-
- -Expand source code - -
class NoAvailableWorkersError(Exception):
-    """Raised when there are no available workers."""
-    pass
-
-

Ancestors

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

404 HTTP error (also used for other things)

-
- -Expand source code - -
class NotFound(Exception):
-    """404 HTTP error (also used for other things)"""
-    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
  • -
-
-
-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) -
-
-

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
  • -
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/utilities/index.html b/docs/utilities/index.html deleted file mode 100644 index 565969c3..00000000 --- a/docs/utilities/index.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - -ro_py.utilities API documentation - - - - - - - - - - - - -
- - -
- - - \ No newline at end of file diff --git a/docs/utilities/pages.html b/docs/utilities/pages.html deleted file mode 100644 index 50a473bd..00000000 --- a/docs/utilities/pages.html +++ /dev/null @@ -1,474 +0,0 @@ - - - - - - -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, 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, cso, 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.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 = None
-
-    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
-        )
-        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:
-            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:
-            await self.get_page(self.data.next_page_cursor)
-        else:
-            raise InvalidPageError
-
-
-
-
-
-
-
-
-
-

Classes

-
-
-class Page -(requests, data, handler=None, handler_args=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, 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
-
-

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 -(cso, url, sort_order=SortOrder.Ascending, limit=10, extra_parameters=None, handler=None, handler_args=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, cso, 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.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 = None
-
-    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
-        )
-        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:
-            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:
-            await 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

-
-
-async def get_page(self, cursor=None) -
-
-

Gets a page at the specified cursor position.

-
- -Expand source code - -
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
-    )
-    self.data = Page(
-        requests=self.cso,
-        data=page_req.json(),
-        handler=self.handler,
-        handler_args=self.handler_args
-    ).data
-
-
-
-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:
-        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:
-        await 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/utilities/requests.html b/docs/utilities/requests.html deleted file mode 100644 index 89649ee3..00000000 --- a/docs/utilities/requests.html +++ /dev/null @@ -1,702 +0,0 @@ - - - - - - -ro_py.utilities.requests API documentation - - - - - - - - - - - - -
-
-
-

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
-
-
-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:
-    """
-    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
-    """
-    def __init__(self):
-        self.session = AsyncSession()
-        """Session to use for requests."""
-
-        """
-        Thank you @nsg for letting me know about this!
-        This allows us to access some extra content.
-        â–ŧâ–ŧâ–ŧ
-        """
-        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.
-        """
-
-        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)(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)
-
-        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 status_code_error(post_request.status_code)(f"[{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 status_code_error(patch_request.status_code)(f"[{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 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(
-            url="https://apis.roblox.com/captcha/v1/metadata"
-        )
-        captcha_meta_raw = captcha_meta_req.json()
-        return CaptchaMetadata(captcha_meta_raw)
-
-
-
-
-
-
-
-

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 -
-
-

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

-
- -Expand source code - -
class Requests:
-    """
-    This wrapper functions similarly to requests_async.Session, but made specifically for Roblox.
-    """
-    def __init__(self):
-        self.session = AsyncSession()
-        """Session to use for requests."""
-
-        """
-        Thank you @nsg for letting me know about this!
-        This allows us to access some extra content.
-        â–ŧâ–ŧâ–ŧ
-        """
-        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.
-        """
-
-        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)(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)
-
-        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 status_code_error(post_request.status_code)(f"[{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 status_code_error(patch_request.status_code)(f"[{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 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(
-            url="https://apis.roblox.com/captcha/v1/metadata"
-        )
-        captcha_meta_raw = captcha_meta_req.json()
-        return CaptchaMetadata(captcha_meta_raw)
-
-

Instance variables

-
-
var session
-
-

Session to use for requests.

-
-
-

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 status_code_error(delete_request.status_code)(f"[{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)
-
-    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)(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)
-
-
-
-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 status_code_error(patch_request.status_code)(f"[{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 status_code_error(post_request.status_code)(f"[{post_request.status_code}] {post_request_error[0]['message']}")
-
-
-
-
-
-
-
- -
- - - \ No newline at end of file diff --git a/docs/wall.html b/docs/wall.html deleted file mode 100644 index 35563f21..00000000 --- a/docs/wall.html +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - -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/examples/README.md b/examples/README.md new file mode 100644 index 00000000..fa4716e1 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,99 @@ +

+ ro.py +
+

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ boilerplate.py + Boilerplate for a standard ro.py application.
+ user_information.py + Grabs user information and prints it to the console.
+ multi_user_information.py + Grabs information about multiple users and prints it to the console.
+ user_information_by_username.py + Grabs user information by username and prints it to the console.
+ multi_user_information_by_usernames.py + Grabs information about multiple users by username and prints it to the console.
+ user_search.py + Searches for users and prints 10 to the console.
+ group_information.py + Grabs group information and prints it to the console.
+ place_information.py + Grabs place information and prints it to the console. Requires authentication.
+ multi_place_information.py + Grabs information about multiple places and prints it to the console.
+ asset_information.py + Grabs asset information and prints it to the console.
+ badge_information.py + Grabs badge information and prints it to the console.
+ plugin_information.py + Grabs plugin information and prints it to the console.
+ multi_plugin_information.py + Grabs information about multiple plugins and prints it to the console.
+ universe_information.py + Grabs universe information and prints it to the console.
+ multi_universe_information.py + Grabs information about multiple universes and prints it to the console.
+
diff --git a/examples/anti_captcha_login.py b/examples/anti_captcha_login.py deleted file mode 100644 index e07c6db7..00000000 --- a/examples/anti_captcha_login.py +++ /dev/null @@ -1,18 +0,0 @@ -from ro_py.client import Client -from ro_py.extensions.anticaptcha import AntiCaptcha -import asyncio - -client = Client() -captcha = AntiCaptcha("ANTI CAPTCHA API KEY") - - -async def main(): - unsolved_captcha = await client.user_login("username", "password") - solved = await captcha.solve(unsolved_captcha) - await client.user_login("username", "password", token=solved) - me = await client.get_self() - print(f"logged in as {me.name} with id {me.id}") - - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/asset_information.py b/examples/asset_information.py new file mode 100644 index 00000000..f00592f8 --- /dev/null +++ b/examples/asset_information.py @@ -0,0 +1,33 @@ +""" +Grabs asset information. +""" + +import asyncio +from roblox import Client + +client = Client() + + +async def main(): + asset = await client.get_asset(8100249026) + + print("ID:", asset.id) + print("Name:", asset.name) + print(f"Description: {asset.description!r}") + print("Type:", asset.type.name) + print("Creator:") + print("\tType:", asset.creator_type.name) + print("\tName:", asset.creator.name) + print("\tID:", asset.creator.id) + print("Price:", asset.price) + print("Sales:", asset.sales) + print("Is Model:", asset.is_public_domain) + print("Is For Sale:", asset.is_for_sale) + print("Is Limited:", asset.is_limited) + print("Is Limited U:", asset.is_limited_unique) + print("\tRemaining:", asset.remaining) + print("Created:", asset.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("Updated:", asset.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/badge_information.py b/examples/badge_information.py new file mode 100644 index 00000000..70e2bb37 --- /dev/null +++ b/examples/badge_information.py @@ -0,0 +1,25 @@ +""" +Grabs badge information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + badge = await client.get_badge(2124867793) + + print("ID:", badge.id) + print("Name:", badge.name) + print(f"Description: {badge.description!r}") + print("Enabled:", badge.enabled) + print("Awarded Count:", badge.statistics.awarded_count) + print("Awarded Universe:") + print("\tName:", badge.awarding_universe.name) + print("\tID:", badge.awarding_universe.id) + print("Created:", badge.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("Updated:", badge.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/boilerplate.py b/examples/boilerplate.py new file mode 100644 index 00000000..8390608c --- /dev/null +++ b/examples/boilerplate.py @@ -0,0 +1,17 @@ +""" +Boilerplate for a standalone ro.py application. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + """ + Place your code here. + """ + pass + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/group_information.py b/examples/group_information.py new file mode 100644 index 00000000..5c750e29 --- /dev/null +++ b/examples/group_information.py @@ -0,0 +1,25 @@ +""" +Grabs group information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + group = await client.get_group(9695397) + + print("ID:", group.id) + print("Name:", group.name) + print("Members:", group.member_count) + print("Owner:", group.owner.display_name) + if group.shout: + print("Shout:") + print("\tCreated:", group.shout.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("\tUpdated:", group.shout.updated.strftime("%m/%d/%Y, %H:%M:%S")) + print(f"\tBody: {group.shout.body!r}") + print(f"\tPoster:", group.shout.poster.display_name) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/guilogin.py b/examples/guilogin.py deleted file mode 100644 index ed8a4b14..00000000 --- a/examples/guilogin.py +++ /dev/null @@ -1,23 +0,0 @@ -""" - -ro.py -GUI Login Example - -This example uses the prompt extension to login with a GUI dialog. - -""" - - -import asyncio -from ro_py.client import Client -from ro_py.extensions.prompt import authenticate_prompt - -client = Client() - - -async def main(): - await authenticate_prompt(client) - - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/joingame.py b/examples/joingame.py deleted file mode 100644 index 17277ecb..00000000 --- a/examples/joingame.py +++ /dev/null @@ -1,28 +0,0 @@ -""" - -ro.py -GameJoin example - -This example logs in with a GUI and then joins Sword Fights on the Heights IV. - -""" - -import asyncio -from ro_py.client import Client -from ro_py.extensions.prompt import authenticate_prompt - -client = Client() - - -async def main(): - auth_prompt = await authenticate_prompt(client) - game = await client.get_game_by_place_id(47324) # âš”ī¸ Sword Fights on the Heights IV - print(f"Authenticated: {auth_prompt}") - if auth_prompt: - await game.root_place.join() - else: - print("Failed to authenticate.") - - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/multi_place_information.py b/examples/multi_place_information.py new file mode 100644 index 00000000..3b621b65 --- /dev/null +++ b/examples/multi_place_information.py @@ -0,0 +1,26 @@ +""" +Grabs multiple places' information. +A cookie is required to grab places' information. +""" + +import asyncio +from roblox import Client +client = Client("cookie_here") + + +async def main(): + places = await client.get_places([8100260845, 8100266389]) + + for place in places: + print("ID:", place.id) + print("\tName:", place.name) + print(f"\tDescription: {place.description!r}") + print("\tPlayable:", place.is_playable) + if not place.is_playable: + print("\tReason:", place.reason_prohibited) + if place.price > 0: + print("\tPrice:", place.price) + print("\tCreator:", place.builder) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/multi_plugin_information.py b/examples/multi_plugin_information.py new file mode 100644 index 00000000..2449bd6f --- /dev/null +++ b/examples/multi_plugin_information.py @@ -0,0 +1,22 @@ +""" +Grabs multiple plugins' information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + plugins = await client.get_plugins([8100268552, 8100269650]) + + for plugin in plugins: + print("ID:", plugin.id) + print("\tName:", plugin.name) + print(f"\tDescription: {plugin.description!r}") + print("\tComments Enabled:", plugin.comments_enabled) + print("\tCreated:", plugin.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("\tUpdated:", plugin.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/multi_universe_information.py b/examples/multi_universe_information.py new file mode 100644 index 00000000..239c5866 --- /dev/null +++ b/examples/multi_universe_information.py @@ -0,0 +1,32 @@ +""" +Grabs multiple universes' information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + universes = await client.get_universes([13058, 15642]) + + for universe in universes: + print("ID:", universe.id) + print("\tName:", universe.name) + print(f"\tDescription: {universe.description!r}") + print("\tCopying Allowed:", universe.copying_allowed) + print("\tCreator:") + print("\t\tName:", universe.creator.name) + print("\t\tType:", universe.creator.id) + print("\t\tType:", universe.creator_type.name) + print("\tPrice:", universe.price) + print("\tVisits:", universe.visits) + print("\tFavorites:", universe.favorited_count) + print("\tMax Players:", universe.max_players) + print("\tVIP Servers:", universe.create_vip_servers_allowed) + print("\tAvatar Type:", universe.universe_avatar_type.name) + print("\tCreated:", universe.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("\tUpdated:", universe.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/multi_user_information.py b/examples/multi_user_information.py new file mode 100644 index 00000000..3449f083 --- /dev/null +++ b/examples/multi_user_information.py @@ -0,0 +1,23 @@ +""" +Grabs multiple users' information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + users = await client.get_users([2067807455, 1], expand=True) + + for user in users: + status = await user.get_status() + print("ID:", user.id) + print("\tName:", user.name) + print("\tDisplay Name:", user.display_name) + print("\tCreated:", user.created.strftime("%m/%d/%Y, %H:%M:%S")) + print(f"\tStatus: {status!r}") + print(f"\tDescription: {user.description!r}") + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/multi_user_information_by_usernames.py b/examples/multi_user_information_by_usernames.py new file mode 100644 index 00000000..e2ab8073 --- /dev/null +++ b/examples/multi_user_information_by_usernames.py @@ -0,0 +1,23 @@ +""" +Grabs multiple users' information by usernames. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + users = await client.get_users_by_usernames(["ro_python", "Roblox"], expand=True) + + for user in users: + status = await user.get_status() + print("ID:", user.id) + print("\tName:", user.name) + print("\tDisplay Name:", user.display_name) + print("\tCreated:", user.created.strftime("%m/%d/%Y, %H:%M:%S")) + print(f"\tStatus: {status!r}") + print(f"\tDescription: {user.description!r}") + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/on_asset_change.py b/examples/on_asset_change.py deleted file mode 100644 index 6dfb431c..00000000 --- a/examples/on_asset_change.py +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 9ccdda96..00000000 --- a/examples/on_shout_event.py +++ /dev/null @@ -1,14 +0,0 @@ -from ro_py.client import Client -import asyncio -client = Client() - - -async def on_shout(old_shout, new_shout): - print(old_shout, new_shout) - - -async def main(): - g = await client.get_group(1) - await g.events.bind(on_shout, "on_shout_update") - -asyncio.run(main()) diff --git a/examples/place_information.py b/examples/place_information.py new file mode 100644 index 00000000..008fd7c1 --- /dev/null +++ b/examples/place_information.py @@ -0,0 +1,25 @@ +""" +Grabs place information. +A cookie is required to grab place information. +""" + +import asyncio +from roblox import Client +client = Client("cookie_here") + + +async def main(): + place = await client.get_place(8100260845) + + print("ID:", place.id) + print("Name:", place.name) + print(f"Description: {place.description!r}") + print("Playable:", place.is_playable) + if not place.is_playable: + print("Reason:", place.reason_prohibited) + if place.price > 0: + print("Price:", place.price) + print("Creator:", place.builder) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/plugin_information.py b/examples/plugin_information.py new file mode 100644 index 00000000..72188fa2 --- /dev/null +++ b/examples/plugin_information.py @@ -0,0 +1,21 @@ +""" +Grabs plugin information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + plugin = await client.get_plugin(8100268552) + + print("ID:", plugin.id) + print("Name:", plugin.name) + print(f"Description: {plugin.description!r}") + print("Comments Enabled:", plugin.comments_enabled) + print("Created:", plugin.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("Updated:", plugin.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/twocaptcha_login.py b/examples/twocaptcha_login.py deleted file mode 100644 index 0ce294b8..00000000 --- a/examples/twocaptcha_login.py +++ /dev/null @@ -1,18 +0,0 @@ -from ro_py.client import Client -from ro_py.extensions.twocaptcha import TwoCaptcha -import asyncio - -client = Client() -captcha = TwoCaptcha("ANTI CAPTCHA API KEY") - - -async def main(): - unsolved_captcha = await client.user_login("username", "password") - solved = await captcha.solve(unsolved_captcha) - await client.user_login("username", "password", token=solved) - me = await client.get_self() - print(f"logged in as {me.name} with id {me.id}") - - -if __name__ == '__main__': - asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/universe_information.py b/examples/universe_information.py new file mode 100644 index 00000000..3094aed2 --- /dev/null +++ b/examples/universe_information.py @@ -0,0 +1,31 @@ +""" +Grabs universe information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + universe = await client.get_universe(3118067569) + + print("ID:", universe.id) + print("Name:", universe.name) + print(f"Description: {universe.description!r}") + print("Copying Allowed:", universe.copying_allowed) + print("Creator:") + print("\tName:", universe.creator.name) + print("\tType:", universe.creator.id) + print("\tType:", universe.creator_type.name) + print("Price:", universe.price) + print("Visits:", universe.visits) + print("Favorites:", universe.favorited_count) + print("Max Players:", universe.max_players) + print("VIP Servers:", universe.create_vip_servers_allowed) + print("Avatar Type:", universe.universe_avatar_type.name) + print("Created:", universe.created.strftime("%m/%d/%Y, %H:%M:%S")) + print("Updated:", universe.updated.strftime("%m/%d/%Y, %H:%M:%S")) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/user.py b/examples/user.py deleted file mode 100644 index ee9720d7..00000000 --- a/examples/user.py +++ /dev/null @@ -1,36 +0,0 @@ -""" - -ro.py -User Example - -This example loads a user from their user ID. - -""" - - -from ro_py.client import Client -import asyncio - -client = Client() - -user_id = 576059883 - - -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() diff --git a/examples/user_information.py b/examples/user_information.py new file mode 100644 index 00000000..ba0327d0 --- /dev/null +++ b/examples/user_information.py @@ -0,0 +1,20 @@ +""" +Grabs user information. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + user = await client.get_user(2067807455) + + print("ID:", user.id) + print("Name:", user.name) + print("Display Name:", user.display_name) + print("Created:", user.created.strftime("%m/%d/%Y, %H:%M:%S")) + print(f"Description: {user.description!r}") + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/user_information_by_username.py b/examples/user_information_by_username.py new file mode 100644 index 00000000..70e35654 --- /dev/null +++ b/examples/user_information_by_username.py @@ -0,0 +1,22 @@ +""" +Grabs user information by username. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + user = await client.get_user_by_username("ro_python", expand=True) + status = await user.get_status() + + print("ID:", user.id) + print("Name:", user.name) + print("Display Name:", user.display_name) + print("Created:", user.created.strftime("%m/%d/%Y, %H:%M:%S")) + print(f"Status: {status!r}") + print(f"Description: {user.description!r}") + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/user_search.py b/examples/user_search.py new file mode 100644 index 00000000..02b57137 --- /dev/null +++ b/examples/user_search.py @@ -0,0 +1,19 @@ +""" +Searches for users who have a keyword in their username. +""" + +import asyncio +from roblox import Client +client = Client() + + +async def main(): + users = client.user_search("Roblox", max_items=10) + + async for user in users: + print("ID:", user.id) + print("\tName:", user.name) + print("\tDisplay Name:", user.display_name) + + +asyncio.get_event_loop().run_until_complete(main()) diff --git a/examples/username.py b/examples/username.py deleted file mode 100644 index 9427db38..00000000 --- a/examples/username.py +++ /dev/null @@ -1,36 +0,0 @@ -""" - -ro.py -Username Example - -This example loads a User from their username. - -""" - - -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/gh-assets/clearfloat.svg b/gh-assets/clearfloat.svg new file mode 100644 index 00000000..f9abc17d --- /dev/null +++ b/gh-assets/clearfloat.svg @@ -0,0 +1,2 @@ + + diff --git a/gh-assets/discord-button.svg b/gh-assets/discord-button.svg new file mode 100644 index 00000000..704869e4 --- /dev/null +++ b/gh-assets/discord-button.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/gh-assets/docs-button.svg b/gh-assets/docs-button.svg new file mode 100644 index 00000000..9cd7c1fc --- /dev/null +++ b/gh-assets/docs-button.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/gh-assets/logo-wordmark.svg b/gh-assets/logo-wordmark.svg new file mode 100644 index 00000000..380cfe4d --- /dev/null +++ b/gh-assets/logo-wordmark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/gh-assets/logo.svg b/gh-assets/logo.svg new file mode 100644 index 00000000..cccc1c66 --- /dev/null +++ b/gh-assets/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..3b6c13c9 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,92 @@ +# Source file: https://github.com/Elttob/Fusion/blob/main/mkdocs.yml + +site_name: ro.py +site_url: https://ro.py.jmk.gg/ +repo_name: ro-py/ro.py +repo_url: https://github.com/ro-py/ro.py +edit_uri: edit/main/docs/ + +theme: + name: material + custom_dir: docs/overrides + logo: assets/logo.svg + favicon: assets/logo.svg + font: + text: Inter + code: JetBrains Mono + icon: + repo: fontawesome/brands/github + + palette: + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light theme + + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: blue + toggle: + icon: material/brightness-7 + name: Switch to dark theme + + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: blue + toggle: + icon: material/brightness-3 + name: Switch to automatic theme + + features: + - content.code.annotate + - navigation.tabs + +extra_css: + - stylesheets/main.css + +watch: + - roblox + +plugins: + - search + - gen-files: + scripts: + - docs/scripts/gen_ref_pages.py + - literate-nav: + nav_file: SUMMARY.md + - mkdocstrings + - section-index + + +markdown_extensions: + - admonition + - attr_list + - def_list + - meta + - pymdownx.betterem + - pymdownx.details + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tabbed: + alternate_style: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink + - pymdownx.superfences + - pymdownx.highlight: + guess_lang: false + - toc: + permalink: true + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/ro-py/ro.py + name: ro.py on GitHub + - icon: fontawesome/brands/discord + link: https://discord.gg/N8yUdkSJwA + name: RoAPI Discord + version: + provider: mike diff --git a/resources/appicon.png b/resources/appicon.png deleted file mode 100644 index 399c5949..00000000 Binary files a/resources/appicon.png and /dev/null differ diff --git a/resources/appicon_large.png b/resources/appicon_large.png deleted file mode 100644 index 1f530eb1..00000000 Binary files a/resources/appicon_large.png and /dev/null differ diff --git a/resources/dropshadow.png b/resources/dropshadow.png deleted file mode 100644 index 456fdc2c..00000000 Binary files a/resources/dropshadow.png and /dev/null differ diff --git a/resources/header.pdn b/resources/header.pdn deleted file mode 100644 index 1349ea6c..00000000 Binary files a/resources/header.pdn and /dev/null differ diff --git a/resources/header.png b/resources/header.png deleted file mode 100644 index 659d2745..00000000 Binary files a/resources/header.png and /dev/null differ diff --git a/resources/login_captcha_prompt.png b/resources/login_captcha_prompt.png deleted file mode 100644 index 5162fb34..00000000 Binary files a/resources/login_captcha_prompt.png and /dev/null differ diff --git a/resources/login_prompt.png b/resources/login_prompt.png deleted file mode 100644 index 6fdb8dce..00000000 Binary files a/resources/login_prompt.png and /dev/null differ diff --git a/resources/logo.png b/resources/logo.png deleted file mode 100644 index def07499..00000000 Binary files a/resources/logo.png and /dev/null differ diff --git a/ro_py/__init__.py b/ro_py/__init__.py deleted file mode 100644 index 4c5eb415..00000000 --- a/ro_py/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" - -

- ro.py -
-

-

ro.py is a powerful Python 3 wrapper for the Roblox Web API.

-

- Information | - Requirements | - Disclaimer | - Documentation | - Examples | - Credits | - License -

- -""" - -from ro_py.client import Client diff --git a/ro_py/accountinformation.py b/ro_py/accountinformation.py deleted file mode 100644 index 6ba8a1e0..00000000 --- a/ro_py/accountinformation.py +++ /dev/null @@ -1,135 +0,0 @@ -""" - -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 - } - ) diff --git a/ro_py/accountsettings.py b/ro_py/accountsettings.py deleted file mode 100644 index 264bb3ad..00000000 --- a/ro_py/accountsettings.py +++ /dev/null @@ -1,83 +0,0 @@ -""" - -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] diff --git a/ro_py/assets.py b/ro_py/assets.py deleted file mode 100644 index ecc45ce0..00000000 --- a/ro_py/assets.py +++ /dev/null @@ -1,144 +0,0 @@ -""" - -This file houses functions and classes that pertain to Roblox assets. - -""" -from ro_py.utilities.errors import NotLimitedError -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/" - - -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, 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 - 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"]) - # 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"] - - 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.") - - -class UserAsset(Asset): - def __init__(self, requests, asset_id, user_asset_id): - super().__init__(requests, asset_id) - self.user_asset_id = user_asset_id - - -class Events: - def __init__(self, cso, asset): - self.cso = cso - self.asset = asset - - async 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)) - - 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/badges.py b/ro_py/badges.py deleted file mode 100644 index 56e6e9e8..00000000 --- a/ro_py/badges.py +++ /dev/null @@ -1,58 +0,0 @@ -""" - -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, cso, badge_id): - self.id = badge_id - self.cso = cso - self.requests = cso.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"] - ) diff --git a/ro_py/captcha.py b/ro_py/captcha.py deleted file mode 100644 index c46ffd11..00000000 --- a/ro_py/captcha.py +++ /dev/null @@ -1,31 +0,0 @@ -""" - -This file houses functions and classes that pertain to the Roblox captcha. - -""" - - -class UnsolvedLoginCaptcha: - def __init__(self, data, pkey): - self.pkey = 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.pkey = 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"] diff --git a/ro_py/catalog.py b/ro_py/catalog.py deleted file mode 100644 index 05a2c6ea..00000000 --- a/ro_py/catalog.py +++ /dev/null @@ -1,21 +0,0 @@ -""" - -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" diff --git a/ro_py/chat.py b/ro_py/chat.py deleted file mode 100644 index 147a7b92..00000000 --- a/ro_py/chat.py +++ /dev/null @@ -1,176 +0,0 @@ -""" - -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, cso, conversation_id): - self.cso = cso - self.requests = cso.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, cso, conversation_id=None, raw=False, raw_data=None): - self.cso = cso - self.requests = cso.requests - self.raw = raw - self.id = None - self.title = None - self.initiator = None - self.type = None - self.typing = ConversationTyping(self.cso, conversation_id) - - if self.raw: - data = raw_data - self.id = data["id"] - self.title = data["title"] - self.initiator = data["initiator"]["targetId"] - self.type = data["conversationType"] - self.typing = ConversationTyping(self.cso, 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 = await self.cso.client.get_user(data["initiator"]["targetId"]) - self.type = data["conversationType"] - - 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 - ---------- - 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, cso, message_id, conversation_id): - self.cso = cso - self.requests = cso.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.cso, 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, cso): - self.cso = cso - self.requests = cso.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( - cso=self.cso, - raw=True, - raw_data=conversation_raw - )) - return conversations diff --git a/ro_py/client.py b/ro_py/client.py deleted file mode 100644 index 46a8cbb2..00000000 --- a/ro_py/client.py +++ /dev/null @@ -1,278 +0,0 @@ -""" - -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.events import EventTypes -from ro_py.trades import TradesWrapper -from ro_py.utilities.requests import Requests -from ro_py.accountsettings import AccountSettings -from ro_py.utilities.cache import Cache, CacheType -from ro_py.accountinformation import AccountInformation -from ro_py.utilities.errors import UserDoesNotExistError, InvalidPlaceIDError -from ro_py.captcha import UnsolvedLoginCaptcha - - -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.""" - - -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.events = EventTypes - """Types of events used for binding events to a function.""" - - 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) - - 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_self(self): - self_req = await self.requests.get( - url="https://roblox.com/my/profile" - ) - data = self_req.json() - return User(self.cso, data['UserId'], data['Username']) - - 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.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() - 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.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() - 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.cso.cache.get(CacheType.Groups, group_id) - if not group: - group = Group(self.cso, group_id) - self.cso.cache.set(CacheType.Groups, group_id, group) - await group.update() - return group - - async def get_game_by_universe_id(self, universe_id): - """ - Gets a Roblox game. - - Parameters - ---------- - universe_id - ID of the game to generate the object from. - """ - game = self.cso.cache.get(CacheType.Games, universe_id) - if not game: - game = Game(self.cso, universe_id) - self.cso.cache.set(CacheType.Games, universe_id, game) - await game.update() - return game - - async def get_game_by_place_id(self, place_id): - """ - Gets a Roblox game by one of it's place's Plaece IDs. - - Parameters - ---------- - place_id - ID of the place to generate the object from. - """ - place_req = await self.requests.get( - url="https://games.roblox.com/v1/games/multiget-place-details", - params={ - "placeIds": place_id - } - ) - place_data = place_req.json() - - try: - place_details = place_data[0] - except IndexError: - raise InvalidPlaceIDError("Invalid place ID.") - - universe_id = place_details["universeId"] - - return await self.get_game_by_universe_id(universe_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.cso.cache.get(CacheType.Assets, asset_id) - if not asset: - asset = Asset(self.cso, asset_id) - self.cso.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.cso.cache.get(CacheType.Assets, badge_id) - if not badge: - badge = Badge(self.cso, badge_id) - self.cso.cache.set(CacheType.Assets, badge_id, badge) - await badge.update() - return badge diff --git a/ro_py/economy.py b/ro_py/economy.py deleted file mode 100644 index 35506e03..00000000 --- a/ro_py/economy.py +++ /dev/null @@ -1,27 +0,0 @@ -""" - -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"] diff --git a/ro_py/events.py b/ro_py/events.py deleted file mode 100644 index 1302fd58..00000000 --- a/ro_py/events.py +++ /dev/null @@ -1,8 +0,0 @@ -import enum - - -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/extensions/__init__.py b/ro_py/extensions/__init__.py deleted file mode 100644 index 5ad02ba6..00000000 --- a/ro_py/extensions/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - -This folder houses extensions that wrap other parts of ro.py but aren't used enough to implement. - -""" 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/bots.py b/ro_py/extensions/bots.py deleted file mode 100644 index e3018bc3..00000000 --- a/ro_py/extensions/bots.py +++ /dev/null @@ -1,37 +0,0 @@ -""" - -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 diff --git a/ro_py/extensions/prompt.py b/ro_py/extensions/prompt.py deleted file mode 100644 index 5c65f424..00000000 --- a/ro_py/extensions/prompt.py +++ /dev/null @@ -1,309 +0,0 @@ -""" - -This extension houses functions that allow human verification prompts for interactive applications. - -""" - - -import wx -import wxasync -from wx import html2 -import pytweening -from wx.lib.embeddedimage import PyEmbeddedImage - -icon_image = PyEmbeddedImage( - b'iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1B' - b'AACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAhaSURBVGhDxZl5bFRVFMbPfTNtQRTF' - b'uOAWVKKIylqptCBiNBQ1MURjQv8Qt8RoxCWKawTFGLdo4oIaY2LUGCUmBo3BAKJgtRTFQi3u' - b'KBA1LhEXqMK0nb7n79w7M05f36zt1C+9c++7b/re+c757rnLGCkBQbvMkyq5XHxZw+UGyldm' - b'guyzN/8nFEWgftGqC/Z1j1q55YrTa7jcKCNlgnRKkv/+juuPKOtob6T+FkJJ6iFDQQIYP5xq' - b'K+WG1kfmriQKZ0tcVhOFmHj0xikBpVsSPO1rWi3U66k3SbXsNONoVRDFEJhovNingd+7mcsZ' - b'kEhA4hUZJk2Y/B/0SUomRvEpPbKHvs9pfZgitJno/EI9qFAfFkK9icXFi9dMpX2l65IleHy3' - b'NTYNjUIPRUl1UwxCi0u91MgtvGUlpLYGHbKWsiTYKrMpB/OtAaOYCLyK8fMDPylB4P/MRy1R' - b'+Jko3C3D5Z6ih7CSTRcl6MtPvL2N1nrqD6m/IEJ/U5eEvAQwfj+qDuPFxyoBr2qY+D2JZRC4' - b'DgIHcm8TWekE6/lSoG9Njx9tdxE/IztofUxRQprhvoFQF3VeFCIwRYzZRDOG5/m24c9LMB5m' - b'QqINEvMZqK9ajw4EaoVGRgkpuniikW9ptVKvo/6Ieoc5VXrt/SwUInCtV1WzzO/5zxHISfxk' - b'15ogMOe2Lmg0+G4VOj+nsK9KQDYhl+H20vclrXRCaCM6P1AXHMRn2gdkAePFxKrmGBNcZCZZ' - b'j9xJ5u8qKh0UC32nziaaENQxRvaDUC3RvoF6BeOngyTQjALuyhkBvD+C6jNS6LFIxnWmoFkp' - b'6E1qzq9DSnt40NMM6GuGbE5WZ+no/Fva8vltPII/JvA1qfcFxuuA1inqetcj9+GtX23YhwJq' - b'kvPp6nwEZnjxakwKaSiFIMnINd5NROp4M5mUGMj9ZKShg8t8zfkIoP9o4xXMCQzoqlE0l7oe' - b'eY4obEGnlYdGOil/8NkeSQCvHkB1Wlj7YfhEgTEyn+/PJgo6Au4kvH7+3DYIcFLtIOq/5orA' - b'yeT7o6L03wdECAKa7B6Yvmh1NSRW4ZkVpNXKwhFoNlNyp9GZJl7NvdwSSkOjwFiZzoRwaapr' - b'MXm7c1DTahhO/x/oR67XzBI0XixcpMxipHQoUfgSET1ZsSg4/e/is10v+xHACF3j1BbSfzbc' - b'OqnmGJq3ux55lAG9k7mh8FRZKpx82nGUkoh8/Cno/8iC+g9B0yr/dzUOmMTD/0B9N5KrN1Pv' - b'tdEYRkkv3gYCR8DKRxFFQPXPawrrPxuaVk28SufHJXU3rRFIvCnfSx1vmEzIL2FcPI+0dD2T' - b'vQ0qDUreLRyb7SeIIkD+L837GTjOQVVqVWnmSi+Lrm2Ul81ENkNGyBvyYNnjQ63tZcbXFJpC' - b'HwKE/yCqKaXovw+cPNa2PDzHNsKw6/tAxpYtI+cY1b9OYhbhCEwwnje6VP1bsFcgpeoastV1' - b'9AeLPh3WDXalWQ6ctRn5KMIEzjCx0vWvMIbRFQQ7aX7jeiIxDu+P6b8tKQJO/2pYZgArwgRK' - b'yv/ZYEbWagPL63yL6gb0z1o8dVUK1NJee6qhRzwZZAigfz0lKF//apUx2xtufUcTZj8EW2x1' - b'TlnGK5z+t6D/v2wrhcxwgsBZePG98gkAY3T/tIOds57SreN6Y3fnru2fPNOUDDRv+PI688GF' - b'ZSVSHT375DYIPOw6HLIlhP4HvKCvYSycxHMuY9f2InNDe82Bh3+Mc+aRRhVL7f42LNxCUDdr' - b'/tI9cQj2URdf/JpWZes/A1anuqzQfbMu8sBwBge53zymEsV7HUThmZLnAbVSz5HEnvT1gSXw' - b'45iRh1JN0pcPKiDk9yR0nTSGq0WuUx5CQj+kNF0c3HfbcMBu28pCOpiT0P8hZeX/IpBaJy0k' - b'CuMx4jfEcG9qTVMcnJXv288Q0gRmDYL+c8Kuk2JVusu7Txbs0a6X0HRrUdtPp3/1bIu9DsGb' - b'fvNqrc+QCnk/Dbf9jM+rP2zDuURBB4huP/U3hvzQSPnyI59f2OsQPGOCw6knDrr++4HtJzqi' - b'cT9SGg6J9eyslhcc0E5qqv9O2wpBHzgZzxysYa40/N4ePZqcTPMq22HkbmLxZ97x4CIUqX+F' - b'EkD/paSEgcFG2pg7iMKReHU78ng051hQ47vtyilS/wol0KjrGBfdykNneqKgsl3seuQJZtiv' - b'Iw/FnP71EFc3QpFQq1eQq5uZgv70yETkbM0YsK8cIXtA7MUuJwrTUtq+y90JwQljE9/5x7Yi' - b'kMkBLMKOJt/V0pzNnD0TX42H0AhCY71m10h5TupKhRev1sz0bhCYxtYFjfhP3mZAN9rT6DR0' - b'WZiQhRB4ynX0R2QSa7h1Le4PjsfgOi7P4un11Cd4sWqrVtWxm/QGRkgjzsBuYgm+nM3OVCTT' - b'wiOH2azvLONFUgcBt5aNQCSBMIgOhgcn8rAGLomQJXYcXvQ0Ki5CpRNKHdNvozkNErsh8SSr' - b'p4X2kFLlk7S/Q0+EwF7qSBRFIAwIjcDYk7EXqVlCtSyhjzLIAib2+L3YtJz63e0eCCyFwGgs' - b'2kwkjrAEErIc45vcN6NRFoEwIDQKi3XBPItyJoTs2or5RZO/i1AOQqnst5v7GoVtkLgWES2z' - b'NxNyNQSete0cGBQCYUBoNBbrEUo6IZzqefGR4qG4iISgmc/v6VoOgSYI6NBtIQpTmQH0kCxz' - b'hBKFihDIxgUP/Sa7fm8fg8HTuFRCM7B+HAOYPZbLcCo7QtFLuxES70LifL77OGUCBPL+cFVx' - b'AmEQHXQVjGX8TOdSM5zWY+M1+8eTiU7dsNdvuHJugnR6Hsa/pf+TD0NOIAwIIZngJG0yu80h' - b'AbxAFN5wdwtB5F91LAlTEJXvrgAAAABJRU5ErkJggg==' -) - - -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(icon_image.GetIcon()) - - 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(icon_image.GetIcon()) - - 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 diff --git a/ro_py/extensions/twocaptcha.py b/ro_py/extensions/twocaptcha.py deleted file mode 100644 index b683e5db..00000000 --- a/ro_py/extensions/twocaptcha.py +++ /dev/null @@ -1,43 +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" - 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.") - 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?key={self.api_key}&id={task_id}&json=1&action=get") - captcha_data = captcha_req.json() - if captcha_data['request'] != "CAPCHA_NOT_READY": - solution = captcha_data['request'] - break - return solution diff --git a/ro_py/gamepersistence.py b/ro_py/gamepersistence.py deleted file mode 100644 index edec6098..00000000 --- a/ro_py/gamepersistence.py +++ /dev/null @@ -1,298 +0,0 @@ -""" - -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 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) - 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 resumeFunction, boost::function 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 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: - 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 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)` - 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 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.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 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.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 resumeFunction, boost::function 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 diff --git a/ro_py/games.py b/ro_py/games.py deleted file mode 100644 index 594be562..00000000 --- a/ro_py/games.py +++ /dev/null @@ -1,190 +0,0 @@ -""" - -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 -from ro_py.thumbnails import GameThumbnailGenerator -from ro_py.utilities.errors import GameJoinError -from ro_py.utilities.cache import CacheType -import subprocess -import json -import os - -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, cso, universe_id): - self.id = universe_id - self.cso = cso - self.requests = cso.requests - self.name = 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.max_players = None - self.studio_access_to_apis_allowed = None - self.create_vip_servers_allowed = None - self.thumbnails = GameThumbnailGenerator(self.requests, self.id) - - 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"] - 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: - 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": - self.creator = self.cso.cache.get(CacheType.Groups, game_info["creator"]["id"]) - if not self.creator: - self.creator = Group(self.cso, game_info["creator"]["id"]) - self.cso.cache.set(CacheType.Groups, game_info["creator"]["id"], self.creator) - await self.creator.update() - 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): - """ - Returns - ------- - ro_py.games.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.cso, badge["id"])) - return badges - - -class Place: - def __init__(self, requests, id): - self.requests = requests - self.id = id - pass - - 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. - """ - local_app_data = os.getenv('LocalAppData') - roblox_appdata_path = local_app_data + "\\Roblox" - roblox_launcher = None - - app_storage = roblox_appdata_path + "\\LocalStorage" - app_versions = roblox_appdata_path + "\\Versions" - - with open(app_storage + "\\appStorage.json") as app_storage_file: - app_storage_data = json.load(app_storage_file) - browser_tracker_id = app_storage_data["BrowserTrackerId"] - - for directory in os.listdir(app_versions): - dir_path = app_versions + "\\" + directory - if os.path.isdir(dir_path): - if os.path.isfile(dir_path + "\\" + "RobloxPlayerBeta.exe"): - roblox_launcher = dir_path + "\\" + "RobloxPlayerBeta.exe" - - if not roblox_launcher: - raise GameJoinError("Couldn't find RobloxPlayerBeta.exe.") - - ticket_req = self.requests.back_post(url="https://auth.roblox.com/v1/authentication-ticket/") - auth_ticket = ticket_req.headers["rbx-authentication-ticket"] - - launch_url = "https://assetgame.roblox.com/game/PlaceLauncher.ashx" \ - "?request=RequestGame" \ - f"&browserTrackerId={browser_tracker_id}" \ - f"&placeId={self.id}" \ - "&isPlayTogetherGame=false" - join_parameters = [ - roblox_launcher, - "--play", - "-a", - negotiate_url, - "-t", - auth_ticket, - "-j", - launch_url, - "-b", - browser_tracker_id, - "--launchtime=" + str(launchtime), - "--rloc", - rloc, - "--gloc", - gloc - ] - join_process = subprocess.run( - args=join_parameters, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - return join_process.stdout, join_process.stderr - diff --git a/ro_py/gender.py b/ro_py/gender.py deleted file mode 100644 index e1e2a70d..00000000 --- a/ro_py/gender.py +++ /dev/null @@ -1,17 +0,0 @@ -""" - -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 diff --git a/ro_py/groups.py b/ro_py/groups.py deleted file mode 100644 index 5bffce64..00000000 --- a/ro_py/groups.py +++ /dev/null @@ -1,396 +0,0 @@ -""" - -This file houses functions and classes that pertain to Roblox groups. - -""" -import copy -import iso8601 -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.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, cso, shout_data): - self.cso = cso - self.data = shout_data - self.body = shout_data["body"] - # TODO: Make this a PartialUser - self.poster = None - - -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.created = iso8601.parse_date(data['created']) - - async def accept(self): - accept_req = await self.requests.post( - url=endpoint + f"/v1/groups/{self.group.id}/join-requests/users/{self.requests.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}" - ) - return accept_req.status_code == 200 - - -def join_request_handler(cso, data, args): - join_requests = [] - for request in data: - join_requests.append(JoinRequest(cso, request, args)) - return join_requests - - -def member_handler(cso, data, args): - members = [] - for member in data: - members.append() - return members - - -class Group: - """ - Represents a group. - """ - def __init__(self, cso, group_id): - self.cso = cso - self.requests = cso.requests - self.id = group_id - self.wall = Wall(self.cso, self) - 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.events = Events(cso, self) - - 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 = 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 group_info.get('shout'): - self.shout = Shout(self.cso, 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.cso, self, role)) - return roles - - 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.cso, self, group_data['role']) - member = Member(self.cso, roblox_id, "", self, role) - return await member.update() - - async def get_join_requests(self, sort_order=SortOrder.Ascending, limit=100): - pages = Pages( - cso=self.cso, - url=endpoint + f"/v1/groups/{self.id}/join-requests", - sort_order=sort_order, - limit=limit, - handler=join_request_handler, - handler_args=self - ) - await pages.get_page() - return pages - - async def get_members(self, sort_order=SortOrder.Ascending, limit=100): - pages = Pages( - cso=self.cso, - url=endpoint + f"/v1/groups/{self.id}/users?limit=100&sortOrder=Desc", - sort_order=sort_order, - limit=limit, - handler=member_handler, - handler_args=self - ) - await pages.get_page() - return pages - - -class PartialGroup(Group): - """ - Represents a group with less information - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - -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, cso, roblox_id, name, group, role): - super().__init__(cso, roblox_id, name) - 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.id == self.role.id: - 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.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 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: - def __init__(self, cso, group): - self.cso = cso - self.group = group - - async def bind(self, func, event, delay=15): - """ - Binds a function to an event. - - Parameters - ---------- - func : function - Function that will be bound to the event. - event : str - 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): - current_group_reqs = await self.group.get_join_requests() - old_req = current_group_reqs.data.requester.id - while True: - await asyncio.sleep(delay) - 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: - await func(new_req) - - async def on_wall_post(self, func, delay): - current_wall_posts = await self.group.wall.get_posts() - newest_wall_poster = current_wall_posts.data[0].poster.id - while True: - await asyncio.sleep(delay) - current_wall_posts = await self.group.wall.get_posts() - current_wall_posts = current_wall_posts.data - if current_wall_posts[0].poster.id != newest_wall_poster: - 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 - for new_post in new_posts: - await func(new_post) - - 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: - await func(current_shout, self.group.shout) - current_shout = self.group.shout diff --git a/ro_py/notifications.py b/ro_py/notifications.py deleted file mode 100644 index db07b666..00000000 --- a/ro_py/notifications.py +++ /dev/null @@ -1,135 +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_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 - - 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 - - await self.connection.start() - - async def close(self): - """ - Closes the connection and stops receiving notifications. - """ - self.connection.stop() diff --git a/ro_py/robloxbadges.py b/ro_py/robloxbadges.py deleted file mode 100644 index 2f0bff86..00000000 --- a/ro_py/robloxbadges.py +++ /dev/null @@ -1,18 +0,0 @@ -""" - -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"] diff --git a/ro_py/robloxdocs.py b/ro_py/robloxdocs.py deleted file mode 100644 index 249a7815..00000000 --- a/ro_py/robloxdocs.py +++ /dev/null @@ -1,115 +0,0 @@ -""" - -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) diff --git a/ro_py/robloxstatus.py b/ro_py/robloxstatus.py deleted file mode 100644 index 97bd9fc8..00000000 --- a/ro_py/robloxstatus.py +++ /dev/null @@ -1,58 +0,0 @@ -""" - -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]) diff --git a/ro_py/roles.py b/ro_py/roles.py deleted file mode 100644 index c46017ed..00000000 --- a/ro_py/roles.py +++ /dev/null @@ -1,157 +0,0 @@ -""" - -This file contains classes and functions related to Roblox roles. - -""" - - -import enum - -endpoint = "https://groups.roblox.com" - - -class RolePermissions: - """ - 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, cso, group, role_data): - self.cso = cso - self.requests = cso.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 diff --git a/ro_py/thumbnails.py b/ro_py/thumbnails.py deleted file mode 100644 index 2b014440..00000000 --- a/ro_py/thumbnails.py +++ /dev/null @@ -1,212 +0,0 @@ -""" - -This file houses functions and classes that pertain to Roblox icons and thumbnails. - -""" - -from ro_py.utilities.errors import InvalidShotTypeError -import enum - -endpoint = "https://thumbnails.roblox.com/" - - -class ReturnPolicy(enum.Enum): - place_holder = "PlaceHolder" - auto_generated = "AutoGenerated" - force_auto_generated = "ForceAutoGenerated" - - -class ThumbnailType(enum.Enum): - avatar_full_body = 0 - avatar_bust = 1 - avatar_headshot = 2 - - -class ThumbnailSize(enum.Enum): - 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" - - -class ThumbnailFormat(enum.Enum): - format_png = "Png" - format_jpg = "Jpeg" - 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 - self.id = 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): - """ - Gets a full body, bust, or headshot image of the user. - - Parameters - ---------- - shot_type : ro_py.thumbnails.ThumbnailType - Type of shot. - size : ro_py.thumbnails.ThumbnailSize - The thumbnail size. - file_format : ro_py.thumbnails.ThumbnailFormat - The thumbnail format - is_circular : bool - The circle thumbnail output parameter. - - Returns - ------- - Image URL - """ - - shot_type = shot_type.value - file_format = file_format.value - size = size.value - - shot_endpoint = endpoint + "v1/users/" - if shot_type == 0: - shot_endpoint = shot_endpoint + "avatar" - elif shot_type == 1: - shot_endpoint = shot_endpoint + "avatar-bust" - elif shot_type == 2: - shot_endpoint = shot_endpoint + "avatar-headshot" - else: - raise InvalidShotTypeError("Invalid shot type.") - shot_req = await self.requests.get( - url=shot_endpoint, - params={ - "userIds": [self.id], - "size": size, - "format": file_format, - "isCircular": is_circular - } - ) - return shot_req.json()["data"][0]["imageUrl"] - - -""" -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 -""" diff --git a/ro_py/trades.py b/ro_py/trades.py deleted file mode 100644 index 8640d449..00000000 --- a/ro_py/trades.py +++ /dev/null @@ -1,262 +0,0 @@ -""" - -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, UserAsset -from ro_py.users import 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_date(created) - self.experation = 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, expiration, 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.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) - 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.cso, - 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 TradeRequest: - def __init__(self): - self.recieve_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 - """Robux that will be recieved when the trade is accepted.""" - - def request_item(self, asset: UserAsset): - """ - Appends asset to self.recieve_asset. - - Parameters - ---------- - asset : ro_py.assets.UserAsset - """ - self.recieve_asset.append(asset) - - def send_item(self, asset: UserAsset): - """ - Appends asset to self.send_asset. - - Parameters - ---------- - asset : ro_py.assets.UserAsset - """ - self.send_asset.append(asset) - - def request_robux(self, robux: int): - """ - Sets self.request_robux to robux - - Parameters - ---------- - robux : int - """ - self.recieve_robux = robux - - def send_robux(self, robux: int): - """ - Sets self.send_robux to robux - - Parameters - ---------- - robux : int - """ - self.send_robux = robux - - -class TradesWrapper: - """ - Represents the Roblox trades page. - """ - 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 = Pages( - cso=self.cso, - 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, roblox_id, trade): - """ - Sends a trade request. - - Parameters - ---------- - roblox_id : int - User who will recieve the trade. - trade : ro_py.trades.TradeRequest - Trade that will be sent to the user. - - Returns - ------- - int - """ - me = await self.get_self() - - data = { - "offers": [ - { - "userId": roblox_id, - "userAssetIds": [], - "robux": None - }, - { - "userId": me.id, - "userAssetIds": [], - "robux": None - } - ] - } - - for asset in trade.send_asset: - data['offers'][1]['userAssetIds'].append(asset.user_asset_id) - - for asset in trade.recieve_asset: - data['offers'][0]['userAssetIds'].append(asset.user_asset_id) - - data['offers'][0]['robux'] = trade.recieve_robux - data['offers'][1]['robux'] = trade.send_robux - - trade_req = await self.requests.post( - url=endpoint + "/v1/trades/send", - data=data - ) - - return trade_req.status == 200 diff --git a/ro_py/users.py b/ro_py/users.py deleted file mode 100644 index be8609b0..00000000 --- a/ro_py/users.py +++ /dev/null @@ -1,156 +0,0 @@ -""" - -This file houses functions and classes that pertain to Roblox users and profiles. - -""" - -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 - -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 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): - 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) - - 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( - await self.cso.client.get_user(friend_raw["id"]) - ) - 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['id'], group['name'], group['memberCount'])) - return groups - - async def get_limiteds(self): - """ - Gets all limiteds the user owns. - - Returns - ------- - list - """ - return Pages( - requests=self.requests, - url=f"https://inventory.roblox.com/v1/users/{self.id}/assets/collectibles?cursor=&limit=100&sortOrder=Desc", - handler=limited_handler - ) - - -class PartialUser(User): - """ - Represents a user with less information then the normal User class. - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) diff --git a/ro_py/utilities/__init__.py b/ro_py/utilities/__init__.py deleted file mode 100644 index 79c8c475..00000000 --- a/ro_py/utilities/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" - -This folder houses utilities that are used internally for ro.py. - -""" \ No newline at end of file diff --git a/ro_py/utilities/asset_type.py b/ro_py/utilities/asset_type.py deleted file mode 100644 index 64510b83..00000000 --- a/ro_py/utilities/asset_type.py +++ /dev/null @@ -1,56 +0,0 @@ -""" - -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/ro_py/utilities/cache.py b/ro_py/utilities/cache.py deleted file mode 100644 index 083d4ac0..00000000 --- a/ro_py/utilities/cache.py +++ /dev/null @@ -1,29 +0,0 @@ -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 diff --git a/ro_py/utilities/caseconvert.py b/ro_py/utilities/caseconvert.py deleted file mode 100644 index 08c4120d..00000000 --- a/ro_py/utilities/caseconvert.py +++ /dev/null @@ -1,7 +0,0 @@ -import re - -pattern = re.compile(r'(? errors.py - -This file houses custom exceptions unique to this module. - -""" - - -# 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 - - -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 ChatError(Exception): - """Called in chat when a chat action fails.""" - - -class InvalidPageError(Exception): - """Called when an invalid page is requested.""" - - -class UserDoesNotExistError(Exception): - """Called when a user does not exist.""" - - -class GameJoinError(Exception): - """Called when an error occurs when joining a game.""" - - -class InvalidPlaceIDError(Exception): - """Called when place ID is invalid.""" - - -class IncorrectKeyError(Exception): - """Raised when the api key for 2captcha is incorrect.""" - pass - - -class InsufficientCreditError(Exception): - """Raised when there is insufficient credit in 2captcha.""" - pass - - -class NoAvailableWorkersError(Exception): - """Raised when there are no available workers.""" - pass - - -c_errors = { - "400": BadRequest, - "401": Unauthorized, - "403": Forbidden, - "404": NotFound, - "409": Conflict, - "429": TooManyRequests, - "500": InternalServerError, - "502": BadGateway -} diff --git a/ro_py/utilities/pages.py b/ro_py/utilities/pages.py deleted file mode 100644 index 6342bc9d..00000000 --- a/ro_py/utilities/pages.py +++ /dev/null @@ -1,96 +0,0 @@ -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, cso, 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.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 = None - - 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 - ) - 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: - 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: - await self.get_page(self.data.next_page_cursor) - else: - raise InvalidPageError diff --git a/ro_py/utilities/requests.py b/ro_py/utilities/requests.py deleted file mode 100644 index 9cb3bfe6..00000000 --- a/ro_py/utilities/requests.py +++ /dev/null @@ -1,176 +0,0 @@ -from ro_py.utilities.errors import ApiError, c_errors -from ro_py.captcha import CaptchaMetadata -from json.decoder import JSONDecodeError -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: - """ - This wrapper functions similarly to requests_async.Session, but made specifically for Roblox. - """ - def __init__(self): - self.session = AsyncSession() - """Session to use for requests.""" - - """ - Thank you @nsg for letting me know about this! - This allows us to access some extra content. - â–ŧâ–ŧâ–ŧ - """ - 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. - """ - - 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)(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) - - 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 status_code_error(post_request.status_code)(f"[{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 status_code_error(patch_request.status_code)(f"[{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 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( - url="https://apis.roblox.com/captcha/v1/metadata" - ) - captcha_meta_raw = captcha_meta_req.json() - return CaptchaMetadata(captcha_meta_raw) diff --git a/ro_py/wall.py b/ro_py/wall.py deleted file mode 100644 index a37bc0f8..00000000 --- a/ro_py/wall.py +++ /dev/null @@ -1,75 +0,0 @@ -import iso8601 -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: - """ - 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(self.cso, 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( - 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): - 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 diff --git a/roblox/__init__.py b/roblox/__init__.py new file mode 100644 index 00000000..c93589bb --- /dev/null +++ b/roblox/__init__.py @@ -0,0 +1,26 @@ +""" + +ro.py +A modern, asynchronous wrapper for the Roblox web API. + +Copyright 2020-present jmkdev +License: MIT, see LICENSE + +""" + +__title__ = "roblox" +__author__ = "jmkdev" +__license__ = "MIT" +__copyright__ = "Copyright 2020-present jmkdev" +__version__ = "2.0.0" + +import logging + +from .client import Client +from .creatortype import CreatorType +from .thumbnails import ThumbnailState, ThumbnailFormat, ThumbnailReturnPolicy, AvatarThumbnailType +from .universes import UniverseGenre, UniverseAvatarType +from .utilities.exceptions import * +from .utilities.types import * + +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/roblox/account.py b/roblox/account.py new file mode 100644 index 00000000..4915882b --- /dev/null +++ b/roblox/account.py @@ -0,0 +1,99 @@ +""" + +Contains classes and functions related to the authenticated Roblox account. +Not to be confused with users.py or the Account system. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from datetime import date + +if TYPE_CHECKING: + from .client import Client + + +class AccountProvider: + """ + Provides methods that control the authenticated user's account. + """ + + def __init__(self, client: Client): + """ + Arguments: + client: The Client to be used when getting information on an account. + """ + self._client: Client = client + + async def get_birthday(self) -> date: + """ + Gets the authenticated user's birthday. + + Returns: + The authenticated user's birthday. + """ + birthday_response = await self._client.requests.get( + url=self._client.url_generator.get_url("accountinformation", "v1/birthdate") + ) + birthday_data = birthday_response.json() + return date( + month=birthday_data["birthMonth"], + day=birthday_data["birthDay"], + year=birthday_data["birthYear"] + ) + + async def set_birthday( + self, + birthday: date, + password: str = None + ): + """ + Changes the authenticated user's birthday. + This endpoint *may* require your password, and requires an unlocked PIN. + + Arguments: + birthday: A date object that represents the birthday to update the Client's account to. + password: The password to the Client's account, this is required when changing the birthday. + """ + await self._client.requests.post( + url=self._client.url_generator.get_url("accountinformation", "v1/birthdate"), + json={ + "birthMonth": birthday.month, + "birthDay": birthday.day, + "birthYear": birthday.year, + "password": password + } + ) + + async def get_description(self) -> string: + """ + Gets the authenticated user's description. + + Returns: + The authenticated user's description. + """ + description_response = await self._client.requests.get( + url=self._client.url_generator.get_url("accountinformation", "v1/description") + ) + description_data = description_response.json() + return description_data["description"] + + async def set_description( + self, + description: string, + ): + """ + Updates the authenticated user's description. + This endpoint *may* require your token, and requires an unlocked PIN. + + Arguments: + description: A string object that represents the description to update the Client's account to. + """ + await self._client.requests.post( + url=self._client.url_generator.get_url("accountinformation", "v1/description"), + json={ + "description": description + } + ) + diff --git a/roblox/assets.py b/roblox/assets.py new file mode 100644 index 00000000..27dd1c31 --- /dev/null +++ b/roblox/assets.py @@ -0,0 +1,186 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox asset information endpoints. + +""" + +from __future__ import annotations +from typing import Union, Optional, TYPE_CHECKING + +from datetime import datetime +from dateutil.parser import parse + +from .bases.baseasset import BaseAsset +from .creatortype import CreatorType +from .partials.partialgroup import AssetPartialGroup +from .partials.partialuser import PartialUser + +if TYPE_CHECKING: + from .client import Client + +asset_type_names = { + 1: "Image", + 2: "T-Shirt", + 3: "Audio", + 4: "Mesh", + 5: "Lua", + 6: "HTML", + 7: "Text", + 8: "Hat", + 9: "Place", + 10: "Model", + 11: "Shirt", + 12: "Pants", + 13: "Decal", + 16: "Avatar", + 17: "Head", + 18: "Face", + 19: "Gear", + 21: "Badge", + 22: "Group Emblem", + 24: "Animation", + 25: "Arms", + 26: "Legs", + 27: "Torso", + 28: "Right Arm", + 29: "Left Arm", + 30: "Left Leg", + 31: "Right Leg", + 32: "Package", + 33: "YouTubeVideo", + 34: "Pass", + 35: "App", + 37: "Code", + 38: "Plugin", + 39: "SolidModel", + 40: "MeshPart", + 41: "Hair Accessory", + 42: "Face Accessory", + 43: "Neck Accessory", + 44: "Shoulder Accessory", + 45: "Front Accessory", + 46: "Back Accessory", + 47: "Waist Accessory", + 48: "Climb Animation", + 49: "Death Animation", + 50: "Fall Animation", + 51: "Idle Animation", + 52: "Jump Animation", + 53: "Run Animation", + 54: "Swim Animation", + 55: "Walk Animation", + 56: "Pose Animation", + 59: "LocalizationTableManifest", + 60: "LocalizationTableTranslation", + 61: "Emote Animation", + 62: "Video", + 63: "TexturePack", + 64: "T-Shirt Accessory", + 65: "Shirt Accessory", + 66: "Pants Accessory", + 67: "Jacket Accessory", + 68: "Sweater Accessory", + 69: "Shorts Accessory", + 70: "Left Shoe Accessory", + 71: "Right Shoe Accessory", + 72: "Dress Skirt Accessory", + 73: "Font Family", + 74: "Font Face", + 75: "MeshHiddenSurfaceRemoval" +} + + +class AssetType: + """ + Represents a Roblox asset type. + + Attributes: + id: Id of the Asset + name: Name of the Asset + """ + + def __init__(self, type_id: int): + """ + Arguments: + type_id: The AssetTypeID to instantiate this AssetType object with. + This is used to determine the name of the AssetType. + """ + + self.id: int = type_id + self.name: Optional[str] = asset_type_names.get(type_id) + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.id} name={self.name!r}>" + + +class EconomyAsset(BaseAsset): + """ + Represents a Roblox asset. + It is intended to parse data from https://economy.roblox.com/v2/assets/ASSETID/details. + + Attributes: + id: Id of the Asset + product_id: Product id of the asset + name: Name of the Asset + description: Description of the Asset + type: Type of the Asset + creator_type: Type of creator can be user or group see enum + creator: creator can be a user or group object + icon_image: BaseAsset + created: When the asset was created + updated: When the asset was updated for the las time + price: price of the asset + sales: amount of sales of the asset + is_new: if the asset it new + is_for_sale: if the asset is for sale + is_public_domain: if the asset is public domain + is_limited: if the asset is a limited item + is_limited_unique: if the asset is a unique limited item + remaining: How many items there are remaining if it is limited + minimum_membership_level: Minimum membership level required to buy item + content_rating_type_id: Unknown + sale_availability_locations: Unknown + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client to be used when getting information on assets. + data: The data from the request. + """ + super().__init__(client=client, asset_id=data["AssetId"]) + + self.product_type: Optional[str] = data["ProductType"] + self.id: int = data["AssetId"] + self.product_id: int = data["ProductId"] # TODO: make this a BaseProduct + self.name: str = data["Name"] + self.description: str = data["Description"] + self.type: AssetType = AssetType(type_id=data["AssetTypeId"]) + + self.creator_type: CreatorType = CreatorType(data["Creator"]["CreatorType"]) + self.creator: Union[PartialUser, AssetPartialGroup] + + if self.creator_type == CreatorType.user: + self.creator: PartialUser = PartialUser(client=client, data=data["Creator"]) + elif self.creator_type == CreatorType.group: + self.creator: AssetPartialGroup = AssetPartialGroup(client=client, data=data["Creator"]) + + self.icon_image: BaseAsset = BaseAsset(client=client, asset_id=data["IconImageAssetId"]) + + self.created: datetime = parse(data["Created"]) + self.updated: datetime = parse(data["Updated"]) + + self.price: Optional[int] = data["PriceInRobux"] + self.sales: int = data["Sales"] + + self.is_new: bool = data["IsNew"] + self.is_for_sale: bool = data["IsForSale"] + self.is_public_domain: bool = data["IsPublicDomain"] + self.is_limited: bool = data["IsLimited"] + self.is_limited_unique: bool = data["IsLimitedUnique"] + + self.remaining: Optional[int] = data["Remaining"] + + self.minimum_membership_level: int = data["MinimumMembershipLevel"] + self.content_rating_type_id: int = data["ContentRatingTypeId"] + self.sale_availability_locations = data["SaleAvailabilityLocations"] diff --git a/roblox/badges.py b/roblox/badges.py new file mode 100644 index 00000000..be367fa5 --- /dev/null +++ b/roblox/badges.py @@ -0,0 +1,82 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox badge information endpoints. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from datetime import datetime +from dateutil.parser import parse + +from .bases.baseasset import BaseAsset +from .bases.basebadge import BaseBadge +from .partials.partialuniverse import PartialUniverse + + +if TYPE_CHECKING: + from .client import Client + + +class BadgeStatistics: + """ + Attributes: + past_day_awarded_count: How many instances of this badge were awarded in the last day. + awarded_count: How many instances of this badge have been awarded. + win_rate_percentage: Percentage of players who have joined the parent universe have been awarded this badge. + """ + + def __init__(self, data: dict): + """ + Arguments: + data: The raw input data. + """ + self.past_day_awarded_count: int = data["pastDayAwardedCount"] + self.awarded_count: int = data["awardedCount"] + self.win_rate_percentage: int = data["winRatePercentage"] + + def __repr__(self): + return f"<{self.__class__.__name__} past_day_awarded_count={self.past_day_awarded_count} awarded_count={self.awarded_count} win_rate_percentage={self.win_rate_percentage}>" + + +class Badge(BaseBadge): + """ + Represents a badge from the API. + + Attributes: + id: The badge Id. + name: The name of the badge. + description: The badge description. + display_name: The localized name of the badge. + display_description: The localized badge description. + enabled: Whether or not the badge is enabled. + icon: The badge icon. + display_icon: The localized badge icon. + created: When the badge was created. + updated: When the badge was updated. + statistics: Badge award statistics. + awarding_universe: The universe the badge is being awarded from. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client to be used when getting information on badges. + data: The data from the endpoint. + """ + self.id: int = data["id"] + + super().__init__(client=client, badge_id=self.id) + + self.name: str = data["name"] + self.description: str = data["description"] + self.display_name: str = data["displayName"] + self.display_description: str = data["displayDescription"] + self.enabled: bool = data["enabled"] + self.icon: BaseAsset = BaseAsset(client=client, asset_id=data["iconImageId"]) + self.display_icon: BaseAsset = BaseAsset(client=client, asset_id=data["displayIconImageId"]) + self.created: datetime = parse(data["created"]) + self.updated: datetime = parse(data["updated"]) + + self.statistics: BadgeStatistics = BadgeStatistics(data=data["statistics"]) + self.awarding_universe: PartialUniverse = PartialUniverse(client=client, data=data["awardingUniverse"]) diff --git a/roblox/bases/__init__.py b/roblox/bases/__init__.py new file mode 100644 index 00000000..42ed21d4 --- /dev/null +++ b/roblox/bases/__init__.py @@ -0,0 +1,7 @@ +""" + +Contains base objects representing IDs on Roblox. +As IDs represent objects on Roblox, you only need the ID of something to send requests for them. +These bases represent one of those IDs. + +""" diff --git a/roblox/bases/baseasset.py b/roblox/bases/baseasset.py new file mode 100644 index 00000000..b2c4f073 --- /dev/null +++ b/roblox/bases/baseasset.py @@ -0,0 +1,47 @@ +""" + +This file contains the BaseAsset object, which represents a Roblox asset ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem +from ..resale import AssetResaleData + +if TYPE_CHECKING: + from ..client import Client + + +class BaseAsset(BaseItem): + """ + Represents a Roblox asset ID. + + Attributes: + id: The asset ID. + """ + + def __init__(self, client: Client, asset_id: int): + """ + Arguments: + client: The Client this object belongs to. + asset_id: The asset ID. + """ + + self._client: Client = client + self.id: int = asset_id + + async def get_resale_data(self) -> AssetResaleData: + """ + Gets the asset's limited resale data. + The asset must be a limited item for this information to be present. + + Returns: + The asset's limited resale data. + """ + resale_response = await self._client.requests.get( + url=self._client.url_generator.get_url("economy", f"v1/assets/{self.id}/resale-data") + ) + resale_data = resale_response.json() + return AssetResaleData(data=resale_data) diff --git a/roblox/bases/basebadge.py b/roblox/bases/basebadge.py new file mode 100644 index 00000000..4544df78 --- /dev/null +++ b/roblox/bases/basebadge.py @@ -0,0 +1,32 @@ +""" + +This file contains the BaseBadge object, which represents a Roblox badge ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseBadge(BaseItem): + """ + Represents a Roblox badge ID. + + Attributes: + id: The badge ID. + """ + + def __init__(self, client: Client, badge_id: int): + """ + Arguments: + client: The Client this object belongs to. + badge_id: The badge ID. + """ + + self._client: Client = client + self.id: int = badge_id diff --git a/roblox/bases/baseconversation.py b/roblox/bases/baseconversation.py new file mode 100644 index 00000000..7d5c05f4 --- /dev/null +++ b/roblox/bases/baseconversation.py @@ -0,0 +1,32 @@ +""" + +This file contains the BaseConversation object, which represents a Roblox conversation ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseConversation(BaseItem): + """ + Represents a Roblox chat conversation ID. + + Attributes: + id: The conversation ID. + """ + + def __init__(self, client: Client, conversation_id: int): + """ + Arguments: + client: The Client this object belongs to. + conversation_id: The conversation ID. + """ + + self._client: Client = client + self.id: int = conversation_id diff --git a/roblox/bases/basegamepass.py b/roblox/bases/basegamepass.py new file mode 100644 index 00000000..11e88d19 --- /dev/null +++ b/roblox/bases/basegamepass.py @@ -0,0 +1,32 @@ +""" + +This file contains the BaseGamePass object, which represents a Roblox gamepass ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseGamePass(BaseItem): + """ + Represents a Roblox gamepass ID. + + Attributes: + id: The gamepass ID. + """ + + def __init__(self, client: Client, gamepass_id: int): + """ + Arguments: + client: The Client this object belongs to. + gamepass_id: The gamepass ID. + """ + + self._client: Client = client + self.id: int = gamepass_id diff --git a/roblox/bases/basegroup.py b/roblox/bases/basegroup.py new file mode 100644 index 00000000..c216add0 --- /dev/null +++ b/roblox/bases/basegroup.py @@ -0,0 +1,476 @@ +""" + +This file contains the BaseGroup object, which represents a Roblox group ID. +It also contains the GroupSettings object, which represents a group's settings. + +""" + +from __future__ import annotations +from typing import Optional, List, Union, TYPE_CHECKING + +from datetime import datetime +from dateutil.parser import parse + +from .baseitem import BaseItem +from ..members import Member, MemberRelationship +from ..partials.partialuser import PartialUser, RequestedUsernamePartialUser +from ..roles import Role +from ..shout import Shout +from ..sociallinks import SocialLink +from ..utilities.exceptions import InvalidRole +from ..utilities.iterators import PageIterator, SortOrder +from ..wall import WallPost, WallPostRelationship + +if TYPE_CHECKING: + from ..client import Client + from .baseuser import BaseUser + from ..utilities.types import UserOrUserId, RoleOrRoleId + + +class JoinRequest: + """ + Represents a group join request. + + Attributes: + created: When this join request was sent. + requester: The user that sent the join request. + group: The parent group that this join request is linked to. + """ + + def __init__(self, client: Client, data: dict, group: Union[BaseGroup, int]): + self._client: Client = client + self.created: datetime = parse(data["created"]) + self.requester: PartialUser = PartialUser(client=self._client, data=data["requester"]) + self.group: BaseGroup + if isinstance(group, int): + self.group = BaseGroup(client=self._client, group_id=group) + else: + self.group = group + + def __int__(self): + return self.requester.id + + async def accept(self): + """ + Accepts this join request. + """ + await self.group.accept_user(self) + + async def decline(self): + """ + Declines this join request. + """ + await self.group.decline_user(self) + + +class GroupSettings: + """ + Represents a group's settings. + + Attributes: + is_approval_required: Whether approval is required to join this group. + is_builders_club_required: Whether a membership is required to join this group. + are_enemies_allowed: Whether group enemies are allowed. + are_group_funds_visible: Whether group funds are visible. + are_group_games_visible: Whether group games are visible. + is_group_name_change_enabled: Whether group name changes are enabled. + can_change_group_name: Whether the name of this group can be changed. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client this object belongs to. + data: The group settings data. + """ + + self._client: Client = client + self.is_approval_required: bool = data["isApprovalRequired"] + self.is_builders_club_required: bool = data["isBuildersClubRequired"] + self.are_enemies_allowed: bool = data["areEnemiesAllowed"] + self.are_group_funds_visible: bool = data["areGroupFundsVisible"] + self.are_group_games_visible: bool = data["areGroupGamesVisible"] + self.is_group_name_change_enabled: bool = data["isGroupNameChangeEnabled"] + self.can_change_group_name: bool = data["canChangeGroupName"] + + +class GroupNameHistoryItem: + """ + Represents a group's previous name. + + Attributes: + name: The group's previous name. + created: A datetime object representing when this name was changed. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client this object belongs to. + data: The group's previous name data. + """ + + self._client: Client = client + self.name: str = data["name"] + self.created: datetime = parse(data["created"]) + + def __repr__(self): + return f"<{self.__class__.__name__} name={self.name!r} created={self.created}>" + + +class BaseGroup(BaseItem): + """ + Represents a Roblox group ID. + + Attributes: + id: The group's ID. + """ + + def __init__(self, client: Client, group_id: int): + """ + Parameters: + client: The Client this object belongs to. + group_id: The group's ID. + """ + self._client: Client = client + self.id: int = group_id + + async def get_settings(self) -> GroupSettings: + """ + Gets all the settings of the selected group + + Returns: + The group's settings. + """ + settings_response = await self._client.requests.get( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/settings"), + ) + settings_data = settings_response.json() + return GroupSettings( + client=self._client, + data=settings_data + ) + + async def update_settings( + self, + is_approval_required: Optional[bool] = None, + is_builders_club_required: Optional[bool] = None, + are_enemies_allowed: Optional[bool] = None, + are_group_funds_visible: Optional[bool] = None, + are_group_games_visible: Optional[bool] = None, + ) -> None: + """ + Updates this group's settings. Passing `None` will default this setting to the value already present in the + + Arguments: + is_approval_required: Whether approval is required via a join request before joining this group. + is_builders_club_required: Whether users are required to have a Premium subscription to join this group. + are_enemies_allowed: Whether this group can send and recieve enemy requests. + are_group_funds_visible: Whether the group fund balance is visible to external users. + are_group_games_visible: Whether group games are visible to external users. + """ + await self._client.requests.patch( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/settings"), + json={ + "isApprovalRequired": is_approval_required, + "isBuildersClubRequired": is_builders_club_required, + "areEnemiesAllowed": are_enemies_allowed, + "areGroupFundsVisible": are_group_funds_visible, + "areGroupGamesVisible": are_group_games_visible, + } + ) + + def get_members(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets all members of a group. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing the group's members. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/users"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: Member(client=client, data=data, group=self) + ) + + def get_member(self, user: Union[int, BaseUser]) -> MemberRelationship: + """ + Gets a member of a group. + + Arguments: + user: A BaseUser or a User ID. + + Returns: + A member. + """ + return MemberRelationship( + client=self._client, + user=user, + group=self + ) + + async def get_member_by_username(self, username: str, exclude_banned_users: bool = False) -> MemberRelationship: + """ + Gets a member of a group by username. + + Arguments: + username: A Roblox username. + exclude_banned_users: Whether to exclude banned users from the data. + + Returns: + A member. + """ + + user: RequestedUsernamePartialUser = await self._client.get_user_by_username( + username=username, + exclude_banned_users=exclude_banned_users, + expand=False + ) + + return MemberRelationship( + client=self._client, + user=user, + group=self + ) + + async def get_roles(self) -> List[Role]: + """ + Gets all roles of the group. + + Returns: + A list of the group's roles. + """ + roles_response = await self._client.requests.get( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/roles") + ) + roles_data = roles_response.json() + return [Role( + client=self._client, + data=role_data, + group=self + ) for role_data in roles_data["roles"]] + + async def set_role(self, user: UserOrUserId, role: RoleOrRoleId) -> None: + """ + Sets a users role. + + Arguments: + user: The user who's rank will be changed. + role: The new role. + """ + await self._client.requests.patch( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/users/{int(user)}"), + json={ + "roleId": int(role) + } + ) + + async def set_rank(self, user: UserOrUserId, rank: int) -> None: + """ + Changes a member's role using a rank number. + + Arguments: + user: The user who's rank will be changed. + rank: The rank number to change to. (1-255) + """ + roles = await self.get_roles() + + role = next((role for role in roles if role.rank == rank), None) + if not role: + raise InvalidRole(f"Role with rank number {rank} does not exist.") + + await self.set_role(int(user), role) + + async def kick_user(self, user: UserOrUserId): + """ + Kicks a user from a group. + + Arguments: + user: The user who will be kicked from the group. + """ + await self._client.requests.delete( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/users/{int(user)}") + ) + + async def delete_all_messages(self, user: UserOrUserId): + """ + Deletes all messages from a user in a group. + + Arguments: + user: The user who will have their messages deleted. + """ + await self._client.requests.delete( + url=self._client.url_generator.get_url("groups", f"/v1/groups/{self.id}/wall/users/{int(user)}/posts") + ) + + def get_wall_posts(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets wall posts of a group. + + Arguments: + page_size: How many posts should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: A PageIterator. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("groups", f"v2/groups/{self.id}/wall/posts"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: WallPost(client=client, data=data, group=self) + ) + + def get_wall_post(self, post_id: int) -> WallPostRelationship: + """ + Gets a wall post from an ID. + + Arguments: + post_id: A post ID. + + Returns: + A basic wall post relationship. + """ + return WallPostRelationship( + client=self._client, + post_id=post_id, + group=self + ) + + def get_join_requests(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets all of this group's join requests. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing group join requests. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/join-requests"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: JoinRequest(client=client, data=data, group=self) + ) + + async def get_join_request(self, user: Union[int, BaseUser]) -> Optional[JoinRequest]: + """ + Gets a specific user's join request to this group. + + Returns: + The user's join request, or None if they have no active join request. + """ + join_response = await self._client.requests.get( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/join-requests/users/{int(user)}") + ) + join_data = join_response.json() + return join_data and JoinRequest( + + client=self._client, + data=join_data, + group=self + ) or None + + async def accept_user(self, user: Union[int, BaseUser, JoinRequest]): + """ + Accepts a user's request to join this group. + + Arguments: + user: The user to accept into this group. + """ + await self._client.requests.post( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/join-requests/users/{int(user)}") + ) + + async def decline_user(self, user: Union[int, BaseUser, JoinRequest]): + """ + Declines a user's request to join this group. + + Arguments: + user: The user to decline from this group. + """ + await self._client.requests.delete( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/join-requests/users/{int(user)}") + ) + + async def update_shout(self, message: str) -> Optional[Shout]: + """ + Updates the shout. + + Arguments: + message: The new shout message. + """ + shout_response = await self._client.requests.patch( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/status"), + json={ + "message": message + } + ) + shout_data = shout_response.json() + + new_shout: Optional[Shout] = shout_data and Shout( + client=self._client, + data=shout_data + ) or None + + return new_shout + + async def get_social_links(self) -> List[SocialLink]: + """ + Gets the group's social links. + + Returns: + A list of the universe's social links. + """ + + links_response = await self._client.requests.get( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/social-links") + ) + links_data = links_response.json()["data"] + return [SocialLink(client=self._client, data=link_data) for link_data in links_data] + + def get_name_history( + self, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None + ) -> PageIterator: + """ + Grabs the groups's name history. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing the groups's name history. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url( + "groups", f"v1/groups/{self.id}/name-history" + ), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: GroupNameHistoryItem(client=client, data=data), + ) \ No newline at end of file diff --git a/roblox/bases/baseinstance.py b/roblox/bases/baseinstance.py new file mode 100644 index 00000000..32fb2b54 --- /dev/null +++ b/roblox/bases/baseinstance.py @@ -0,0 +1,33 @@ +""" + +This file contains the BaseInstance object, which represents a Roblox instance ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseInstance(BaseItem): + """ + Represents a Roblox instance ID. + Instance IDs represent the ownership of a single Roblox item. + + Attributes: + id: The instance ID. + """ + + def __init__(self, client: Client, instance_id: int): + """ + Arguments: + client: The Client this object belongs to. + instance_id: The asset instance ID. + """ + + self._client: Client = client + self.id: int = instance_id diff --git a/roblox/bases/baseitem.py b/roblox/bases/baseitem.py new file mode 100644 index 00000000..1438c3b8 --- /dev/null +++ b/roblox/bases/baseitem.py @@ -0,0 +1,28 @@ +""" + +This file contains the BaseItem class, which all bases inherit. + +""" + + +class BaseItem: + """ + This object represents a base Roblox item. All other bases inherit this object. + This object overrides equals and not-equals methods ensuring that two bases with the same ID are always equal. + """ + id = None + + def __repr__(self): + attributes_repr = "".join(f" {key}={value!r}" for key, value in self.__dict__.items() if not key.startswith("_")) + return f"<{self.__class__.__name__}{attributes_repr}>" + + def __int__(self): + return self.id + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.id == self.id + + def __ne__(self, other): + if isinstance(other, self.__class__): + return other.id != self.id + return True diff --git a/roblox/bases/basejob.py b/roblox/bases/basejob.py new file mode 100644 index 00000000..258dacab --- /dev/null +++ b/roblox/bases/basejob.py @@ -0,0 +1,35 @@ +""" + +This file contains the BaseJob object, which represents a Roblox job ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseJob(BaseItem): + """ + Represents Roblox job ID. + + Job IDs are UUIDs that represent a single game server instance. + Learn more on the Developer Hub [here](https://developer.roblox.com/en-us/api-reference/property/DataModel/JobId). + + Attributes: + id: The job ID. + """ + + def __init__(self, client: Client, job_id: str): + """ + Arguments: + client: The Client this object belongs to. + job_id: The job ID. + """ + + self._client: Client = client + self.id: str = job_id diff --git a/roblox/bases/baseplace.py b/roblox/bases/baseplace.py new file mode 100644 index 00000000..396c6d68 --- /dev/null +++ b/roblox/bases/baseplace.py @@ -0,0 +1,125 @@ +""" + +This file contains the BasePlace object, which represents a Roblox place ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from ..bases.baseasset import BaseAsset +from ..utilities.iterators import PageIterator, SortOrder + +if TYPE_CHECKING: + from ..client import Client + from ..jobs import ServerType + + +class BasePlace(BaseAsset): + """ + Represents a Roblox place ID. + Places are a form of Asset and as such this object derives from BaseAsset. + + Attributes: + id: The place ID. + """ + + def __init__(self, client: Client, place_id: int): + """ + Arguments: + client: The Client this object belongs to. + place_id: The place ID. + """ + + super().__init__(client, place_id) + + self._client: Client = client + self.id: int = place_id + + async def get_instances(self, start_index: int = 0): + """ + Returns a list of this place's current active servers, known in the API as "game instances". + This list always contains 10 items or fewer. + For more items, add 10 to the start index and repeat until no more items are available. + + !!! warning + This function has been deprecated. The Roblox endpoint used by this function has been removed. Please update any code using this method to use + + Arguments: + start_index: Where to start in the server index. + """ + from ..jobs import GameInstances + + instances_response = await self._client.requests.get( + url=self._client.url_generator.get_url("www", f"games/getgameinstancesjson"), + params={ + "placeId": self.id, + "startIndex": start_index + } + ) + instances_data = instances_response.json() + return GameInstances( + client=self._client, + data=instances_data + ) + + def get_servers( + self, + server_type: ServerType, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Descending, + exclude_full_games: bool = False, + max_items: int = None + ) -> PageIterator: + """ + Grabs the place's servers. + + Arguments: + server_type: The type of servers to return. + page_size: How many servers should be returned for each page. + sort_order: Order in which data should be grabbed. + exclude_full_games: Whether to exclude full servers. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing servers. + """ + from ..jobs import Server + + return PageIterator( + client=self._client, + url=self._client._url_generator.get_url("games", f"v1/games/{self.id}/servers/{server_type.value}"), + page_size=page_size, + max_items=max_items, + sort_order=sort_order, + extra_parameters={"excludeFullGames": exclude_full_games}, + handler=lambda client, data: Server(client=client, data=data), + ) + + def get_private_servers( + self, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Descending, + max_items: int = None + ) -> PageIterator: + """ + Grabs the private servers of a place the authenticated user can access. + + Arguments: + page_size: How many private servers should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing private servers. + """ + from ..jobs import PrivateServer + + return PageIterator( + client=self._client, + url=self._client._url_generator.get_url("games", f"v1/games/{self.id}/private-servers"), + page_size=page_size, + max_items=max_items, + sort_order=sort_order, + handler=lambda client, data: PrivateServer(client=client, data=data), + ) diff --git a/roblox/bases/baseplugin.py b/roblox/bases/baseplugin.py new file mode 100644 index 00000000..778dbe86 --- /dev/null +++ b/roblox/bases/baseplugin.py @@ -0,0 +1,55 @@ +""" + +This file contains the BasePlugin object, which represents a Roblox plugin ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseasset import BaseAsset + +if TYPE_CHECKING: + from ..client import Client + + +class BasePlugin(BaseAsset): + """ + Represents a Roblox plugin ID. + Plugins are a form of Asset and as such this object derives from BaseAsset. + + Attributes: + id: The plugin ID. + """ + + def __init__(self, client: Client, plugin_id: int): + """ + Arguments: + client: The Client this object belongs to. + plugin_id: The plugin ID. + """ + + super().__init__(client, plugin_id) + + self._client: Client = client + self.id: int = plugin_id + + async def update(self, name: str = None, description: str = None, comments_enabled: str = None): + """ + Updates the plugin's data. + + Arguments: + name: The new group name. + description: The new group description. + comments_enabled: Whether to enable comments. + """ + await self._client.requests.patch( + url=self._client.url_generator.get_url( + "develop", f"v1/plugins/{self.id}" + ), + json={ + "name": name, + "description": description, + "commentsEnabled": comments_enabled + } + ) diff --git a/roblox/bases/baserobloxbadge.py b/roblox/bases/baserobloxbadge.py new file mode 100644 index 00000000..f2efa8c0 --- /dev/null +++ b/roblox/bases/baserobloxbadge.py @@ -0,0 +1,34 @@ +""" + +This file contains the BaseRobloxBadge object, which represents a Roblox roblox badge ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseRobloxBadge(BaseItem): + """ + Represents a Roblox roblox badge ID. + !!! warning + This is not a badge! It is a **roblox badge**. + + Attributes: + id: The roblox badge ID. + """ + + def __init__(self, client: Client, roblox_badge_id: int): + """ + Arguments: + client: The Client this object belongs to. + roblox_badge_id: The roblox badge ID. + """ + + self._client: Client = client + self.id: int = roblox_badge_id diff --git a/roblox/bases/baserole.py b/roblox/bases/baserole.py new file mode 100644 index 00000000..ad16d507 --- /dev/null +++ b/roblox/bases/baserole.py @@ -0,0 +1,32 @@ +""" + +This file contains the BaseRole object, which represents a Roblox group role ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseRole(BaseItem): + """ + Represents a Roblox group role ID. + + Attributes: + id: The role ID. + """ + + def __init__(self, client: Client, role_id: int): + """ + Arguments: + client: The Client this object belongs to. + role_id: The role ID. + """ + + self._client: Client = client + self.id: int = role_id diff --git a/roblox/bases/basesociallink.py b/roblox/bases/basesociallink.py new file mode 100644 index 00000000..5e6f9164 --- /dev/null +++ b/roblox/bases/basesociallink.py @@ -0,0 +1,32 @@ +""" + +This file contains the BaseUniverseSocialLink object, which represents a Roblox social link ID. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING + +from .baseitem import BaseItem + +if TYPE_CHECKING: + from ..client import Client + + +class BaseUniverseSocialLink(BaseItem): + """ + Represents a Roblox universe social link ID. + + Attributes: + id: The universe social link ID. + """ + + def __init__(self, client: Client, social_link_id: int): + """ + Arguments: + client: The Client this object belongs to. + social_link_id: The universe social link ID. + """ + + self._client: Client = client + self.id: int = social_link_id diff --git a/roblox/bases/baseuniverse.py b/roblox/bases/baseuniverse.py new file mode 100644 index 00000000..3993d402 --- /dev/null +++ b/roblox/bases/baseuniverse.py @@ -0,0 +1,160 @@ +""" + +This file contains the BaseUniverse object, which represents a Roblox universe ID. +It also contains the UniverseLiveStats object, which represents a universe's live stats. + +""" + +from __future__ import annotations +from typing import TYPE_CHECKING, Dict, List + +from .baseitem import BaseItem +from ..gamepasses import GamePass +from ..sociallinks import SocialLink +from ..utilities.iterators import PageIterator, SortOrder + +if TYPE_CHECKING: + from ..client import Client + from ..badges import Badge + + +class UniverseLiveStats: + """ + Represents a universe's live stats. + + Attributes: + total_player_count: The amount of players present in this universe's subplaces. + game_count: The amount of active servers for this universe's subplaces. + player_counts_by_device_type: A dictionary where the keys are device types and the values are the amount of + this universe's subplace's active players which are on that device type. + """ + + def __init__(self, data: dict): + self.total_player_count: int = data["totalPlayerCount"] + self.game_count: int = data["gameCount"] + self.player_counts_by_device_type: Dict[str, int] = data["playerCountsByDeviceType"] + + +def _universe_badges_handler(client: Client, data: dict) -> Badge: + # inline imports are used here, sorry + from ..badges import Badge + return Badge(client=client, data=data) + + +class BaseUniverse(BaseItem): + """ + Represents a Roblox universe ID. + + Attributes: + id: The universe ID. + """ + + def __init__(self, client: Client, universe_id: int): + """ + Arguments: + client: The Client this object belongs to. + universe_id: The universe ID. + """ + + self._client: Client = client + self.id: int = universe_id + + async def get_favorite_count(self) -> int: + """ + Grabs the universe's favorite count. + + Returns: + The universe's favorite count. + """ + favorite_count_response = await self._client.requests.get( + url=self._client.url_generator.get_url("games", f"v1/games/{self.id}/favorites/count") + ) + favorite_count_data = favorite_count_response.json() + return favorite_count_data["favoritesCount"] + + async def is_favorited(self) -> bool: + """ + Grabs the authenticated user's favorite status for this game. + + Returns: + Whether the authenticated user has favorited this game. + """ + is_favorited_response = await self._client.requests.get( + url=self._client.url_generator.get_url("games", f"v1/games/{self.id}/favorites") + ) + is_favorited_data = is_favorited_response.json() + return is_favorited_data["isFavorited"] + + def get_badges(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets the universe's badges. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing this universe's badges. + """ + + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("badges", f"v1/universes/{self.id}/badges"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=_universe_badges_handler, + ) + + async def get_live_stats(self) -> UniverseLiveStats: + """ + Gets the universe's live stats. + This data does not update live. These are just the stats that are shown on the website's live stats display. + + Returns: + The universe's live stats. + """ + stats_response = await self._client.requests.get( + url=self._client.url_generator.get_url("develop", f"v1/universes/{self.id}/live-stats") + ) + stats_data = stats_response.json() + return UniverseLiveStats(data=stats_data) + + def get_gamepasses(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets the universe's gamepasses. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing the universe's gamepasses. + """ + + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("games", f"v1/games/{self.id}/game-passes"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: GamePass(client, data), + ) + + async def get_social_links(self) -> List[SocialLink]: + """ + Gets the universe's social links. + + Returns: + A list of the universe's social links. + """ + + links_response = await self._client.requests.get( + url=self._client.url_generator.get_url("games", f"v1/games/{self.id}/social-links/list") + ) + links_data = links_response.json()["data"] + return [SocialLink(client=self._client, data=link_data) for link_data in links_data] diff --git a/roblox/bases/baseuser.py b/roblox/bases/baseuser.py new file mode 100644 index 00000000..16080df0 --- /dev/null +++ b/roblox/bases/baseuser.py @@ -0,0 +1,332 @@ +""" + +This file contains the BaseUser object, which represents a Roblox user ID. + +""" + +from __future__ import annotations +from typing import Optional, List, TYPE_CHECKING + +from .baseitem import BaseItem +from ..bases.basebadge import BaseBadge +from ..instances import ItemInstance, InstanceType, AssetInstance, GamePassInstance, instance_classes +from ..partials.partialbadge import PartialBadge +from ..presence import Presence +from ..promotionchannels import UserPromotionChannels +from ..robloxbadges import RobloxBadge +from ..utilities.iterators import PageIterator, SortOrder + +if TYPE_CHECKING: + from ..client import Client + from ..friends import Friend + from ..roles import Role + from ..utilities.types import AssetOrAssetId, GamePassOrGamePassId, GroupOrGroupId + + +class BaseUser(BaseItem): + """ + Represents a Roblox user ID. + + Attributes: + id: The user ID. + """ + + def __init__(self, client: Client, user_id: int): + """ + Arguments: + client: The Client this object belongs to. + user_id: The user ID. + """ + + self._client: Client = client + self.id: int = user_id + + def username_history( + self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, max_items: int = None + ) -> PageIterator: + """ + Grabs the user's username history. + + Arguments: + page_size: How many members should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing the user's username history. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url( + "users", f"v1/users/{self.id}/username-history" + ), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: data["name"], + ) + + async def get_presence(self) -> Optional[Presence]: + """ + Grabs the user's presence. + + Returns: + The user's presence, if they have an active presence. + """ + presences = await self._client.presence.get_user_presences([self.id]) + try: + return presences[0] + except IndexError: + return None + + async def get_friends(self) -> List[Friend]: + """ + Grabs the user's friends. + + Returns: + A list of the user's friends. + """ + + from ..friends import Friend + friends_response = await self._client.requests.get( + url=self._client.url_generator.get_url("friends", f"v1/users/{self.id}/friends") + ) + friends_data = friends_response.json()["data"] + return [Friend(client=self._client, data=friend_data) for friend_data in friends_data] + + async def get_currency(self) -> int: + """ + Grabs the user's current Robux amount. Only works on the authenticated user. + + Returns: + The user's Robux amount. + """ + currency_response = await self._client.requests.get( + url=self._client.url_generator.get_url("economy", f"v1/users/{self.id}/currency") + ) + currency_data = currency_response.json() + return currency_data["robux"] + + async def has_premium(self) -> bool: + """ + Checks if the user has a Roblox Premium membership. + + Returns: + Whether the user has Premium or not. + """ + premium_response = await self._client.requests.get( + url=self._client.url_generator.get_url("premiumfeatures", f"v1/users/{self.id}/validate-membership") + ) + premium_data = premium_response.text + return premium_data == "true" + + async def get_item_instance(self, item_type: InstanceType, item_id: int) -> Optional[ItemInstance]: + """ + Gets an item instance for a specific user. + + Arguments: + item_type: The type of item to get an instance for. + item_id: The item's ID. + + Returns: An ItemInstance, if it exists. + """ + + item_type: str = item_type.value.lower() + + # this is so we can have special classes for other types + item_class = instance_classes.get(item_type) or ItemInstance + + instance_response = await self._client.requests.get( + url=self._client.url_generator.get_url("inventory", f"v1/users/{self.id}/items/{item_type}/{item_id}") + ) + instance_data = instance_response.json()["data"] + if len(instance_data) > 0: + return item_class( + client=self._client, + data=instance_data[0] + ) + else: + return None + + async def get_asset_instance(self, asset: AssetOrAssetId) -> Optional[AssetInstance]: + """ + Checks if a user owns the asset, and returns details about the asset if they do. + + Returns: + An asset instance, if the user owns this asset. + """ + return await self.get_item_instance( + item_type=InstanceType.asset, + item_id=int(asset) + ) + + async def get_gamepass_instance(self, gamepass: GamePassOrGamePassId) -> Optional[GamePassInstance]: + """ + Checks if a user owns the gamepass, and returns details about the asset if they do. + + Returns: + An gamepass instance, if the user owns this gamepass. + """ + return await self.get_item_instance( + item_type=InstanceType.gamepass, + item_id=int(gamepass) + ) + + async def get_badge_awarded_dates(self, badges: list[BaseBadge]) -> List[PartialBadge]: + """ + Gets the dates that each badge in a list of badges were awarded to this user. + + Returns: + A list of partial badges containing badge awarded dates. + """ + awarded_response = await self._client.requests.get( + url=self._client.url_generator.get_url("badges", f"v1/users/{self.id}/badges/awarded-dates"), + params={ + "badgeIds": [badge.id for badge in badges] + } + ) + awarded_data: list = awarded_response.json()["data"] + return [ + PartialBadge( + client=self._client, + data=partial_data + ) for partial_data in awarded_data + ] + + async def get_group_roles(self) -> List[Role]: + """ + Gets a list of roles for all groups this user is in. + + Returns: + A list of roles. + """ + from ..roles import Role + from ..groups import Group + roles_response = await self._client.requests.get( + url=self._client.url_generator.get_url("groups", f"v1/users/{self.id}/groups/roles") + ) + roles_data = roles_response.json()["data"] + return [ + Role( + client=self._client, + data=role_data["role"], + group=Group( + client=self._client, + data=role_data["group"] + ) + ) for role_data in roles_data + ] + + async def get_roblox_badges(self) -> List[RobloxBadge]: + """ + Gets the user's Roblox badges. + + Returns: + A list of Roblox badges. + """ + + badges_response = await self._client.requests.get( + url=self._client.url_generator.get_url("accountinformation", f"v1/users/{self.id}/roblox-badges") + ) + badges_data = badges_response.json() + return [RobloxBadge(client=self._client, data=badge_data) for badge_data in badges_data] + + async def get_promotion_channels(self) -> UserPromotionChannels: + """ + Gets the user's promotion channels. + + Returns: + The user's promotion channels. + """ + channels_response = await self._client.requests.get( + url=self._client.url_generator.get_url("accountinformation", f"v1/users/{self.id}/promotion-channels") + ) + channels_data = channels_response.json() + return UserPromotionChannels( + data=channels_data + ) + + async def _get_friend_channel_count(self, channel: str) -> int: + count_response = await self._client.requests.get( + url=self._client.url_generator.get_url("friends", f"v1/users/{self.id}/{channel}/count") + ) + return count_response.json()["count"] + + def _get_friend_channel_iterator( + self, + channel: str, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Ascending, max_items: int = None + ) -> PageIterator: + from ..friends import Friend + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("friends", f"v1/users/{self.id}/{channel}"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: Friend(client=client, data=data) + ) + + async def get_friend_count(self) -> int: + """ + Gets the user's friend count. + + Returns: + The user's friend count. + """ + return await self._get_friend_channel_count("friends") + + async def get_follower_count(self) -> int: + """ + Gets the user's follower count. + + Returns: + The user's follower count. + """ + return await self._get_friend_channel_count("followers") + + async def get_following_count(self) -> int: + """ + Gets the user's following count. + + Returns: + The user's following count. + """ + return await self._get_friend_channel_count("followings") + + def get_followers( + self, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Ascending, max_items: int = None + ) -> PageIterator: + """ + Gets the user's followers. + + Returns: + A PageIterator containing everyone who follows this user. + """ + return self._get_friend_channel_iterator( + channel="followers", + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + ) + + def get_followings( + self, + page_size: int = 10, + sort_order: SortOrder = SortOrder.Ascending, max_items: int = None + ) -> PageIterator: + """ + Gets the user's followings. + + Returns: + A PageIterator containing everyone that this user is following. + """ + return self._get_friend_channel_iterator( + channel="followings", + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + ) diff --git a/roblox/chat.py b/roblox/chat.py new file mode 100644 index 00000000..f63f3bf8 --- /dev/null +++ b/roblox/chat.py @@ -0,0 +1,92 @@ +""" + +Contains classes relating to the Roblox chat. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from .conversations import Conversation +from .utilities.iterators import PageNumberIterator + +if TYPE_CHECKING: + from .client import Client + + +class ChatSettings: + """ + Represents the authenticated user's Roblox chat settings. + + Attributes: + chat_enabled: Whether chat is enabled for the user. + is_active_chat_user: Whether the user is an active chat user. New accounts are active by default and become + inactive if they do not send any messages over a period of time. + is_connect_tab_enabled: Whether the Connect tab is enabled for this user. + """ + + def __init__(self, data: dict): + """ + Arguments: + data: The raw input data. + """ + self.chat_enabled: bool = data["chatEnabled"] + self.is_active_chat_user: bool = data["isActiveChatUser"] + self.is_connect_tab_enabled: bool = data["isConnectTabEnabled"] + + def __repr__(self): + return f"<{self.__class__.__name__} chat_enabled={self.chat_enabled} is_active_chat_user={self.is_active_chat_user} is_connect_tab_enabled={self.is_connect_tab_enabled}>" + + +class ChatProvider: + """ + Provides information and data related to the Roblox chat system. + """ + + def __init__(self, client: Client): + """ + Arguments: + client: The Client for getting information about chat. + """ + self._client: Client = client + + def __repr__(self): + return f"<{self.__class__.__name__}>" + + async def get_unread_conversation_count(self) -> int: + """ + Gets the authenticated user's unread conversation count. + + Returns: + The user's unread conversation count. + """ + unread_response = await self._client.requests.get( + url=self._client.url_generator.get_url("chat", "v2/get-unread-conversation-count") + ) + unread_data = unread_response.json() + return unread_data["count"] + + async def get_settings(self) -> ChatSettings: + """ + Gets the authenticated user's chat settings. + + Returns: + The user's chat settings. + """ + settings_response = await self._client.requests.get( + url=self._client.url_generator.get_url("chat", "v2/chat-settings") + ) + settings_data = settings_response.json() + return ChatSettings(data=settings_data) + + def get_user_conversations(self) -> PageNumberIterator: + """ + Gets the user's conversations. + + Returns: + The user's conversations as a PageNumberIterator. + """ + return PageNumberIterator( + client=self._client, + url=self._client.url_generator.get_url("chat", "v2/get-user-conversations"), + handler=lambda client, data: Conversation(client=client, data=data) + ) diff --git a/roblox/client.py b/roblox/client.py new file mode 100644 index 00000000..5090408d --- /dev/null +++ b/roblox/client.py @@ -0,0 +1,552 @@ +""" + +Contains the Client, which is the core object at the center of all ro.py applications. + +""" + +from typing import Union, List, Optional + +from .account import AccountProvider +from .assets import EconomyAsset +from .badges import Badge +from .bases.baseasset import BaseAsset +from .bases.basebadge import BaseBadge +from .bases.basegamepass import BaseGamePass +from .bases.basegroup import BaseGroup +from .bases.baseplace import BasePlace +from .bases.baseplugin import BasePlugin +from .bases.baseuniverse import BaseUniverse +from .bases.baseuser import BaseUser +from .chat import ChatProvider +from .delivery import DeliveryProvider +from .groups import Group +from .partials.partialuser import PartialUser, RequestedUsernamePartialUser, PreviousUsernamesPartialUser +from .places import Place +from .plugins import Plugin +from .presence import PresenceProvider +from .thumbnails import ThumbnailProvider +from .universes import Universe +from .users import User +from .utilities.exceptions import BadRequest, NotFound, AssetNotFound, BadgeNotFound, GroupNotFound, PlaceNotFound, \ + PluginNotFound, UniverseNotFound, UserNotFound +from .utilities.iterators import PageIterator +from .utilities.requests import Requests +from .utilities.url import URLGenerator + + +class Client: + """ + Represents a Roblox client. + + Attributes: + requests: The requests object, which is used to send requests to Roblox endpoints. + url_generator: The URL generator object, which is used to generate URLs to send requests to endpoints. + presence: The presence provider object. + thumbnails: The thumbnail provider object. + delivery: The delivery provider object. + chat: The chat provider object. + account: The account provider object. + """ + + def __init__(self, token: str = None, base_url: str = "roblox.com"): + """ + Arguments: + token: A .ROBLOSECURITY token to authenticate the client with. + base_url: The base URL to use when sending requests. + """ + self._url_generator: URLGenerator = URLGenerator(base_url=base_url) + self._requests: Requests = Requests() + + self.url_generator: URLGenerator = self._url_generator + self.requests: Requests = self._requests + + self.presence: PresenceProvider = PresenceProvider(client=self) + self.thumbnails: ThumbnailProvider = ThumbnailProvider(client=self) + self.delivery: DeliveryProvider = DeliveryProvider(client=self) + self.chat: ChatProvider = ChatProvider(client=self) + self.account: AccountProvider = AccountProvider(client=self) + + if token: + self.set_token(token) + + def __repr__(self): + return f"<{self.__class__.__name__}>" + + # Authentication + def set_token(self, token: Optional[str] = None) -> None: + """ + Authenticates the client with the passed .ROBLOSECURITY token. + This method does not send any requests and will not throw if the token is invalid. + + Arguments: + token: A .ROBLOSECURITY token to authenticate the client with. + + """ + self._requests.session.cookies[".ROBLOSECURITY"] = token + + # Users + async def get_user(self, user_id: int) -> User: + """ + Gets a user with the specified user ID. + + Arguments: + user_id: A Roblox user ID. + + Returns: + A user object. + """ + try: + user_response = await self._requests.get( + url=self.url_generator.get_url("users", f"v1/users/{user_id}") + ) + except NotFound as exception: + raise UserNotFound( + message="Invalid user.", + response=exception.response + ) from None + user_data = user_response.json() + return User(client=self, data=user_data) + + async def get_authenticated_user( + self, expand: bool = True + ) -> Union[User, PartialUser]: + """ + Grabs the authenticated user. + + Arguments: + expand: Whether to return a User (2 requests) rather than a PartialUser (1 request) + + Returns: + The authenticated user. + """ + authenticated_user_response = await self._requests.get( + url=self._url_generator.get_url("users", f"v1/users/authenticated") + ) + authenticated_user_data = authenticated_user_response.json() + + if expand: + return await self.get_user(authenticated_user_data["id"]) + else: + return PartialUser(client=self, data=authenticated_user_data) + + async def get_users( + self, + user_ids: List[int], + exclude_banned_users: bool = False, + expand: bool = False, + ) -> Union[List[PartialUser], List[User]]: + """ + Grabs a list of users corresponding to each user ID in the list. + + Arguments: + user_ids: A list of Roblox user IDs. + exclude_banned_users: Whether to exclude banned users from the data. + expand: Whether to return a list of Users (2 requests) rather than PartialUsers (1 request) + + Returns: + A List of Users or partial users. + """ + users_response = await self._requests.post( + url=self._url_generator.get_url("users", f"v1/users"), + json={"userIds": user_ids, "excludeBannedUsers": exclude_banned_users}, + ) + users_data = users_response.json()["data"] + + if expand: + return [await self.get_user(user_data["id"]) for user_data in users_data] + else: + return [ + PartialUser(client=self, data=user_data) + for user_data in users_data + ] + + async def get_users_by_usernames( + self, + usernames: List[str], + exclude_banned_users: bool = False, + expand: bool = False, + ) -> Union[List[RequestedUsernamePartialUser], List[User]]: + """ + Grabs a list of users corresponding to each username in the list. + + Arguments: + usernames: A list of Roblox usernames. + exclude_banned_users: Whether to exclude banned users from the data. + expand: Whether to return a list of Users (2 requests) rather than RequestedUsernamePartialUsers (1 request) + + Returns: + A list of User or RequestedUsernamePartialUser, depending on the expand argument. + """ + users_response = await self._requests.post( + url=self._url_generator.get_url("users", f"v1/usernames/users"), + json={"usernames": usernames, "excludeBannedUsers": exclude_banned_users}, + ) + users_data = users_response.json()["data"] + + if expand: + return [await self.get_user(user_data["id"]) for user_data in users_data] + else: + return [ + RequestedUsernamePartialUser(client=self, data=user_data) + for user_data in users_data + ] + + async def get_user_by_username( + self, username: str, exclude_banned_users: bool = False, expand: bool = True + ) -> Union[RequestedUsernamePartialUser, User]: + """ + Grabs a user corresponding to the passed username. + + Arguments: + username: A Roblox username. + exclude_banned_users: Whether to exclude banned users from the data. + expand: Whether to return a User (2 requests) rather than a RequestedUsernamePartialUser (1 request) + + Returns: + A User or RequestedUsernamePartialUser depending on the expand argument. + """ + users = await self.get_users_by_usernames( + usernames=[username], + exclude_banned_users=exclude_banned_users, + expand=expand, + ) + try: + return users[0] + except IndexError: + raise UserNotFound("Invalid username.") from None + + def get_base_user(self, user_id: int) -> BaseUser: + """ + Gets a base user. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + user_id: A Roblox user ID. + + Returns: + A BaseUser. + """ + return BaseUser(client=self, user_id=user_id) + + def user_search(self, keyword: str, page_size: int = 10, + max_items: int = None) -> PageIterator: + """ + Search for users with a keyword. + + Arguments: + keyword: A keyword to search for. + page_size: How many members should be returned for each page. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing RequestedUsernamePartialUser. + """ + return PageIterator( + client=self, + url=self._url_generator.get_url("users", f"v1/users/search"), + page_size=page_size, + max_items=max_items, + extra_parameters={"keyword": keyword}, + handler=lambda client, data: PreviousUsernamesPartialUser(client=client, data=data), + ) + + # Groups + async def get_group(self, group_id: int) -> Group: + """ + Gets a group by its ID. + + Arguments: + group_id: A Roblox group ID. + + Returns: + A Group. + """ + try: + group_response = await self._requests.get( + url=self._url_generator.get_url("groups", f"v1/groups/{group_id}") + ) + except BadRequest as exception: + raise GroupNotFound( + message="Invalid group.", + response=exception.response + ) from None + group_data = group_response.json() + return Group(client=self, data=group_data) + + def get_base_group(self, group_id: int) -> BaseGroup: + """ + Gets a base group. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + group_id: A Roblox group ID. + + Returns: + A BaseGroup. + """ + return BaseGroup(client=self, group_id=group_id) + + # Universes + async def get_universes(self, universe_ids: List[int]) -> List[Universe]: + """ + Grabs a list of universes corresponding to each ID in the list. + + Arguments: + universe_ids: A list of Roblox universe IDs. + + Returns: + A list of Universes. + """ + universes_response = await self._requests.get( + url=self._url_generator.get_url("games", "v1/games"), + params={"universeIds": universe_ids}, + ) + universes_data = universes_response.json()["data"] + return [ + Universe(client=self, data=universe_data) + for universe_data in universes_data + ] + + async def get_universe(self, universe_id: int) -> Universe: + """ + Gets a universe with the passed ID. + + Arguments: + universe_id: A Roblox universe ID. + + Returns: + A Universe. + """ + universes = await self.get_universes(universe_ids=[universe_id]) + try: + return universes[0] + except IndexError: + raise UniverseNotFound("Invalid universe.") from None + + def get_base_universe(self, universe_id: int) -> BaseUniverse: + """ + Gets a base universe. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + universe_id: A Roblox universe ID. + + Returns: + A BaseUniverse. + """ + return BaseUniverse(client=self, universe_id=universe_id) + + # Places + async def get_places(self, place_ids: List[int]) -> List[Place]: + """ + Grabs a list of places corresponding to each ID in the list. + + Arguments: + place_ids: A list of Roblox place IDs. + + Returns: + A list of Places. + """ + places_response = await self._requests.get( + url=self._url_generator.get_url( + "games", f"v1/games/multiget-place-details" + ), + params={"placeIds": place_ids}, + ) + places_data = places_response.json() + return [ + Place(client=self, data=place_data) for place_data in places_data + ] + + async def get_place(self, place_id: int) -> Place: + """ + Gets a place with the passed ID. + + Arguments: + place_id: A Roblox place ID. + + Returns: + A Place. + """ + places = await self.get_places(place_ids=[place_id]) + try: + return places[0] + except IndexError: + raise PlaceNotFound("Invalid place.") from None + + def get_base_place(self, place_id: int) -> BasePlace: + """ + Gets a base place. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + place_id: A Roblox place ID. + + Returns: + A BasePlace. + """ + return BasePlace(client=self, place_id=place_id) + + # Assets + async def get_asset(self, asset_id: int) -> EconomyAsset: + """ + Gets an asset with the passed ID. + + Arguments: + asset_id: A Roblox asset ID. + + Returns: + An Asset. + """ + try: + asset_response = await self._requests.get( + url=self._url_generator.get_url( + "economy", f"v2/assets/{asset_id}/details" + ) + ) + except BadRequest as exception: + raise AssetNotFound( + message="Invalid asset.", + response=exception.response + ) from None + asset_data = asset_response.json() + return EconomyAsset(client=self, data=asset_data) + + def get_base_asset(self, asset_id: int) -> BaseAsset: + """ + Gets a base asset. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + asset_id: A Roblox asset ID. + + Returns: + A BaseAsset. + """ + return BaseAsset(client=self, asset_id=asset_id) + + # Plugins + async def get_plugins(self, plugin_ids: List[int]) -> List[Plugin]: + """ + Grabs a list of plugins corresponding to each ID in the list. + + Arguments: + plugin_ids: A list of Roblox plugin IDs. + + Returns: + A list of Plugins. + """ + plugins_response = await self._requests.get( + url=self._url_generator.get_url( + "develop", "v1/plugins" + ), + params={ + "pluginIds": plugin_ids + } + ) + plugins_data = plugins_response.json()["data"] + return [Plugin(client=self, data=plugin_data) for plugin_data in plugins_data] + + async def get_plugin(self, plugin_id: int) -> Plugin: + """ + Grabs a plugin with the passed ID. + + Arguments: + plugin_id: A Roblox plugin ID. + + Returns: + A Plugin. + """ + plugins = await self.get_plugins([plugin_id]) + try: + return plugins[0] + except IndexError: + raise PluginNotFound("Invalid plugin.") from None + + def get_base_plugin(self, plugin_id: int) -> BasePlugin: + """ + Gets a base plugin. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + plugin_id: A Roblox plugin ID. + + Returns: + A BasePlugin. + """ + return BasePlugin(client=self, plugin_id=plugin_id) + + # Badges + async def get_badge(self, badge_id: int) -> Badge: + """ + Gets a badge with the passed ID. + + Arguments: + badge_id: A Roblox badge ID. + + Returns: + A Badge. + """ + try: + badge_response = await self._requests.get( + url=self._url_generator.get_url( + "badges", f"v1/badges/{badge_id}" + ) + ) + except NotFound as exception: + raise BadgeNotFound( + message="Invalid badge.", + response=exception.response + ) from None + badge_data = badge_response.json() + return Badge(client=self, data=badge_data) + + def get_base_badge(self, badge_id: int) -> BaseBadge: + """ + Gets a base badge. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + badge_id: A Roblox badge ID. + + Returns: + A BaseBadge. + """ + return BaseBadge(client=self, badge_id=badge_id) + + # Gamepasses + def get_base_gamepass(self, gamepass_id: int) -> BaseGamePass: + """ + Gets a base gamepass. + + !!! note + This method does not send any requests - it just generates an object. + For more information on bases, please see [Bases](../tutorials/bases.md). + + Arguments: + gamepass_id: A Roblox gamepass ID. + + Returns: A BaseGamePass. + """ + return BaseGamePass(client=self, gamepass_id=gamepass_id) diff --git a/roblox/conversations.py b/roblox/conversations.py new file mode 100644 index 00000000..209479f3 --- /dev/null +++ b/roblox/conversations.py @@ -0,0 +1,107 @@ +""" + + +Contains objects related to Roblox chat conversations. + + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from datetime import datetime +from dateutil.parser import parse + +from enum import Enum +from typing import List, Optional + +from .bases.baseconversation import BaseConversation +from .partials.partialuniverse import ChatPartialUniverse +from .partials.partialuser import PartialUser + +if TYPE_CHECKING: + from .client import Client + + +class ConversationType(Enum): + """ + A chat conversation's type. + """ + + multi_user_conversation = "MultiUserConversation" + """Represents a chat with multiples users on the website.""" + one_to_one_conversation = "OneToOneConversation" + """Represents a one-to-one conversation with person A and B.""" + cloud_edit_conversation = "CloudEditConversation" + """Represents a chat in a team-create session.""" + + +class ConversationTitle: + """ + A chat conversation's title. + + Attributes: + title_for_viewer: Specifies the title for the conversation specific to the viewer. + is_default_title: Specifies if the title displayed for the user is generated as a default title or was edited by + the user. + """ + + def __init__(self, data: dict): + """ + Arguments: + data: The raw input data. + """ + self.title_for_viewer: str = data["titleForViewer"] + self.is_default_title: bool = data["isDefaultTitle"] + + def __repr__(self): + return f"<{self.__class__.__name__} title_for_viewer={self.title_for_viewer!r} is_default_title={self.is_default_title}>" + + +class Conversation(BaseConversation): + """ + Represents a Roblox chat conversation. + + Attributes: + id: Chat conversation ID. + title: Chat conversation title. + initiator: Conversation initiator entity. + has_unread_messages: Whether the conversation have any unread messages. + participants: Participants involved in the conversation. + conversation_type: Type of the conversation. + conversation_title: Specifies if the conversation title is generated by default. + last_updated: Specifies the datetime when the conversation was last updated. + conversation_universe: Specifies the universe associated with the conversation. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client object. + data: The conversation data. + """ + super().__init__(client=client, conversation_id=self.id) + self.id: int = data["id"] + self.title: str = data["title"] + + # Technically the initiator could be a group, but in practice that doesn't happen + # so this is a partialuser + # Nikita Petko: Well uhhh, the initiator is of the ChatParticipant model, + # where it can either be from User or System. + self.initiator: PartialUser = PartialUser(client, data["initiator"]) + + self.has_unread_messages: bool = data["hasUnreadMessages"] + self.participants: List[PartialUser] = [PartialUser( + client=client, + data=participant_data + ) for participant_data in data["participants"]] + + self.conversation_type: ConversationType = ConversationType(data["conversationType"]) + self.conversation_title: ConversationTitle = ConversationTitle( + data=data["conversationTitle"] + ) + self.last_updated: datetime = parse(data["lastUpdated"]) + self.conversation_universe: Optional[ChatPartialUniverse] = data[ + "conversationUniverse"] and ChatPartialUniverse( + client=client, + data=data["conversationUniverse"] + ) diff --git a/roblox/creatortype.py b/roblox/creatortype.py new file mode 100644 index 00000000..e4413af3 --- /dev/null +++ b/roblox/creatortype.py @@ -0,0 +1,19 @@ +""" + +Contains client enums. +fixme: this should be deprecated! + +""" + +from enum import Enum + + +class CreatorType(Enum): + """ + Represents the type of creator for objects that can be owned by either a group or a user, like Assets. + """ + + group = "Group" + """The creator is a group.""" + user = "User" + """The creator is a user.""" diff --git a/roblox/delivery.py b/roblox/delivery.py new file mode 100644 index 00000000..3dfc4ed2 --- /dev/null +++ b/roblox/delivery.py @@ -0,0 +1,187 @@ +""" + +Contains classes and functions related to Roblox asset delivery. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from .utilities.url import cdn_site + +if TYPE_CHECKING: + from .client import Client + + +def get_cdn_number(cdn_hash: str) -> int: + """ + Gets the number in the CDN where number represents X in tX.rbxcdn.com + + Arguments: + cdn_hash: The CDN cdn_hash to generate a CDN number for. + + Returns: + The CDN number for the supplied cdn_hash. + """ + i = 31 + for char in cdn_hash[:32]: + i ^= ord(char) # i ^= int(char, 16) also works + return i % 8 + + +class BaseCDNHash: + """ + Represents a cdn_hash on a Roblox content delivery network. + + Attributes: + cdn_hash: The CDN hash as a string. + """ + + def __init__(self, client: Client, cdn_hash: str): + """ + Arguments: + client: The Client object. + cdn_hash: The CDN hash as a string. + """ + + self._client: Client = client + self.cdn_hash: str = cdn_hash + + def __repr__(self): + return f"<{self.__class__.__name__} cdn_hash={self.cdn_hash}>" + + def get_cdn_number(self) -> int: + """ + Returns the CDN number of this CDN hash. + + Returns: + The computed number of the given cdn_hash + """ + + return get_cdn_number(self.cdn_hash) + + def _get_url(self, prefix: str, site: str = cdn_site) -> str: + cdn_number: int = self.get_cdn_number() + return self._client.url_generator.get_url(f"{prefix}{cdn_number}", self.cdn_hash, site) + + def get_url(self, site: str = cdn_site) -> str: + """ + Gets the cdn_hash's URL. This should be implemented by subclasses. + + Arguments: + site: Represents the URL for what site it should target, be it rbxcdn.com, or roblox.com etc. + + Returns: + The computed URL from the given cdn_hash attribute. + """ + + raise NotImplementedError + + +class ThumbnailCDNHash(BaseCDNHash): + """ + Represents a CDN hash on tX.rbxcdn.com. + """ + + def __init__(self, client: Client, cdn_hash: str): + super().__init__(client=client, cdn_hash=cdn_hash) + + def get_url(self, site: str = cdn_site) -> str: + """ + Returns this CDN hash's URL. + """ + + return self._get_url("t", cdn_site) + + +class ContentCDNHash(BaseCDNHash): + """ + Represents a CDN hash on cX.rbxcdn.com. + """ + + def __init__(self, client: Client, cdn_hash: str): + super().__init__(client=client, cdn_hash=cdn_hash) + + def get_url(self, site: str = cdn_site) -> str: + """ + Returns: + This hash's URL. + """ + + return self._get_url("c", cdn_site) + + +class DeliveryProvider: + """ + Provides CDN hashes and other delivery-related objects. + """ + + def __init__(self, client: Client): + """ + Arguments: + client: The client object, which is passed to all objects this client generates. + """ + self._client: Client = client + + def get_cdn_hash(self, cdn_hash: str) -> BaseCDNHash: + """ + Gets a Roblox CDN cdn_hash. + + Arguments: + cdn_hash: The cdn_hash. + + Returns: + A BaseCDNHash. + """ + + return BaseCDNHash( + client=self._client, + cdn_hash=cdn_hash + ) + + def get_cdn_hash_from_url(self, url: str, site: str = cdn_site) -> BaseCDNHash: + """ + todo: turn this into something that actually splits into path. + + Arguments: + url: A CDN url. + site: The site this cdn_hash is located at. + + Returns: + The CDN cdn_hash for the supplied CDN URL. + """ + + return self.get_cdn_hash( + cdn_hash=url.split(f".{site}/")[1] + ) + + def get_thumbnail_cdn_hash(self, cdn_hash: str) -> ThumbnailCDNHash: + """ + Gets a Roblox CDN cdn_hash. + + Arguments: + cdn_hash: The cdn_hash. + + Returns: + A ThumbnailCDNHash. + """ + + return ThumbnailCDNHash( + client=self._client, + cdn_hash=cdn_hash + ) + + def get_content_cdn_hash(self, cdn_hash: str) -> ContentCDNHash: + """ + Gets a Roblox CDN cdn_hash. + + Arguments: + cdn_hash: The cdn_hash. + + Returns: + A ContentCDNHash. + """ + + return ContentCDNHash( + client=self._client, + cdn_hash=cdn_hash + ) diff --git a/roblox/friends.py b/roblox/friends.py new file mode 100644 index 00000000..924c3c67 --- /dev/null +++ b/roblox/friends.py @@ -0,0 +1,37 @@ +""" + +Contains classes related to Roblox friend data and parsing. + +""" +from __future__ import annotations +from typing import Optional, TYPE_CHECKING + +from .users import User + +if TYPE_CHECKING: + from .client import Client + + +class Friend(User): + """ + Represents a friend. + + Attributes: + is_online: Whether the user is currently online. + presence_type: Their presence type. Don't use this. + is_deleted: Whether the account is deleted. + friend_frequent_rank: Unknown + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + data: The data we get back from the endpoint. + client: The Client object, which is passed to all objects this Client generates. + """ + super().__init__(client=client, data=data) + + self.is_online: Optional[bool] = data.get("isOnline") + self.presence_type: Optional[int] = data.get("presenceType") + self.is_deleted: bool = data["isDeleted"] + self.friend_frequent_rank: int = data["friendFrequentRank"] diff --git a/roblox/gamepasses.py b/roblox/gamepasses.py new file mode 100644 index 00000000..86177b47 --- /dev/null +++ b/roblox/gamepasses.py @@ -0,0 +1,35 @@ +""" + +Contains classes related to Roblox gamepass data and parsing. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from typing import Optional + +from .bases.basegamepass import BaseGamePass + + +class GamePass(BaseGamePass): + """ + Represents a Roblox gamepass. + + Attributes: + id: The gamepass ID. + name: The gamepass name. + display_name: The gamepass display name. + price: The gamepass price. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + self.id: int = data["id"] + super().__init__(client=self._client, gamepass_id=self.id) + self.name: str = data["name"] + self.display_name: str = data["displayName"] + # TODO: add product here + self.price: Optional[int] = data["price"] diff --git a/roblox/groups.py b/roblox/groups.py new file mode 100644 index 00000000..28f2885a --- /dev/null +++ b/roblox/groups.py @@ -0,0 +1,90 @@ +""" + +Contains classes related to Roblox group data and parsing. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from typing import Optional, Tuple + +from .bases.basegroup import BaseGroup +from .partials.partialuser import PartialUser +from .shout import Shout + + +class Group(BaseGroup): + """ + Represents a group. + + Attributes: + id: the id of the group. + name: name of the group. + description: description of the group. + owner: player who owns the group. + shout: the current group shout. + member_count: amount of members in the group. + is_builders_club_only: can only people with builder club join. + public_entry_allowed: can you join without your join request having to be accepted. + is_locked: Is the group locked? + has_verified_badge: If the group has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + data: The data we get back from the endpoint. + client: The Client object, which is passed to all objects this Client generates. + """ + super().__init__(client, data["id"]) + + self._client: Client = client + + self.id: int = data["id"] + self.name: str = data["name"] + self.description: str = data["description"] + self.owner: Optional[PartialUser] = PartialUser(client=client, data=data["owner"]) if data.get("owner") else \ + None + self.shout: Optional[Shout] = Shout( + client=self._client, + data=data["shout"] + ) if data.get("shout") else None + + self.member_count: int = data["memberCount"] + self.is_builders_club_only: bool = data["isBuildersClubOnly"] + self.public_entry_allowed: bool = data["publicEntryAllowed"] + self.is_locked: bool = data.get("isLocked") or False + self.has_verified_badge: bool = data["hasVerifiedBadge"] + + async def update_shout(self, message: str, update_self: bool = True) -> Tuple[Optional[Shout], Optional[Shout]]: + """ + Updates the shout. + + Arguments: + message: The new shout message. + update_self: Whether to update self.shout automatically. + Returns: + The old and new shout. + """ + shout_response = await self._client.requests.patch( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.id}/status"), + json={ + "message": message + } + ) + + shout_data = shout_response.json() + + old_shout: Optional[Shout] = self.shout + new_shout: Optional[Shout] = shout_data and Shout( + client=self._client, + data=shout_data + ) or None + + if update_self: + self.shout = new_shout + + return old_shout, new_shout diff --git a/roblox/instances.py b/roblox/instances.py new file mode 100644 index 00000000..5e6f1b81 --- /dev/null +++ b/roblox/instances.py @@ -0,0 +1,91 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox item instance information endpoints. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from enum import Enum + +from .bases.baseasset import BaseAsset +from .bases.basebadge import BaseBadge +from .bases.basegamepass import BaseGamePass +from .bases.baseinstance import BaseInstance + + +class InstanceType(Enum): + """ + Represents an asset instance type. + """ + asset = "Asset" + gamepass = "GamePass" + badge = "Badge" + + +class ItemInstance(BaseInstance): + """ + Represents an instance of a Roblox item of some kind. + + Attributes: + _client: The Client object, which is passed to all objects this Client generates. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The data from the endpoint. + """ + self._client: Client = client + + self.name: str = data["name"] + self.type: str = data["type"] # fixme + + super().__init__(client=self._client, instance_id=data["instanceId"]) + + +class AssetInstance(ItemInstance): + """ + Represents an instance of a Roblox asset. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + super().__init__(client=self._client, data=data) + + self.asset: BaseAsset = BaseAsset(client=self._client, asset_id=data["id"]) + + +class BadgeInstance(ItemInstance): + """ + Represents an instance of a Roblox badge. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + super().__init__(client=self._client, data=data) + + self.badge: BaseBadge = BaseBadge(client=self._client, badge_id=data["id"]) + + +class GamePassInstance(ItemInstance): + """ + Represents an instance of a Roblox gamepass. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + super().__init__(client=self._client, data=data) + + self.gamepass: BaseGamePass = BaseGamePass(client=self._client, gamepass_id=data["id"]) + + +instance_classes = { + "asset": AssetInstance, + "badge": BadgeInstance, + "gamepass": GamePassInstance +} diff --git a/roblox/jobs.py b/roblox/jobs.py new file mode 100644 index 00000000..13927154 --- /dev/null +++ b/roblox/jobs.py @@ -0,0 +1,246 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox server instance (or "job") endpoints. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .client import Client + +from typing import List +from enum import Enum + +from .bases.basejob import BaseJob +from .bases.baseplace import BasePlace +from .bases.baseuser import BaseUser +from .bases.baseitem import BaseItem +from .partials.partialuser import PartialUser + + +class GameInstancePlayerThumbnail: + """ + Represent a player in a game instance's thumbnail. + As the asset part of these thumbnails is no longer in use, this endpoint does not attempt to implement asset + information. + + Attributes: + url: The thumbnail's URL. + final: Whether the thumbnail is finalized or not. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + + self.url: str = data["Url"] + self.final: bool = data["IsFinal"] + + def __repr__(self): + return f"<{self.__class__.__name__} url={self.url!r} final={self.final}" + + +class GameInstancePlayer(BaseUser): + """ + Represents a single player in a game instance. + Data, like user ID and username, may be filled with placeholder data. + Do not rely on this object containing proper data. If the id attribute is 0, this object should not be used. + + Attributes: + id: The player's user ID. + name: The player's username. + thumbnail: The player's thumbnail. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + self.id: int = data["Id"] + super().__init__(client=self._client, user_id=self.id) + + self.name: str = data["Username"] + self.thumbnail: GameInstancePlayerThumbnail = GameInstancePlayerThumbnail( + client=self._client, + data=data["Thumbnail"] + ) + + +class GameInstance(BaseJob): + """ + Represents a game (or place) instance, or "job". + + Attributes: + id: The instance's job ID. + capacity: The server's capacity. + ping: The server's ping. + fps: The server's FPS. + show_slow_game_message: Whether to show the "slow game" message. + place: The server's place. + current_players: A list of the players in this server. + can_join: Whether the authenticated user can join this server. + show_shutdown_button: Whether to show the shutdown button on this server. + friends_description: What text should be shown if this server is a "friends are in" server. + friends_mouseover: What text should be shown on mouseover if this server is a "friends are in" server. + capacity_message: The server's capacity as a parsed message. + join_script: JavaScript code that, when evaluated on a /games page on the Roblox website, launches this game. + app_join_script: JavaScript code that, when evaluated on a /games page on the Roblox website, launches this game + through the Roblox mobile app. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + self.id: str = data["Guid"] + + super().__init__(client=self._client, job_id=self.id) + + self.capacity: int = data["Capacity"] + self.ping: int = data["Ping"] + self.fps: float = data["Fps"] + self.show_slow_game_message: bool = data["ShowSlowGameMessage"] + self.place: BasePlace = BasePlace(client=self._client, place_id=data["PlaceId"]) + + self.current_players: List[GameInstancePlayer] = [ + GameInstancePlayer( + client=self._client, + data=player_data + ) for player_data in data["CurrentPlayers"] + ] + + self.can_join: bool = data["UserCanJoin"] + self.show_shutdown_button: bool = data["ShowShutdownButton"] + self.friends_description: str = data["FriendsDescription"] + self.friends_mouseover = data["FriendsMouseover"] + self.capacity_message: str = data["PlayersCapacity"] # TODO: reconsider + + self.join_script: str = data["JoinScript"] + self.app_join_script: str = data["RobloxAppJoinScript"] + + +class GameInstances: + """ + Represents a game/place's active server instances. + + Attributes: + place: The place. + show_shutdown_all_button: Whether to show the "Shutdown All" button on the server list. + is_game_instance_list_unavailable: Whether the list is unavailable. + collection: A list of the game instances. + total_collection_size: How many active servers there are. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + + self.place: BasePlace = BasePlace(client=self._client, place_id=data["PlaceId"]) + self.show_shutdown_all_button: bool = data["ShowShutdownAllButton"] + self.is_game_instance_list_unavailable: bool = data["IsGameInstanceListUnavailable"] + self.collection: List[GameInstance] = [ + GameInstance( + client=self._client, + data=instance_data + ) for instance_data in data["Collection"] + ] + self.total_collection_size: int = data["TotalCollectionSize"] + + +class ServerType(Enum): + """ + Represents the type of server. + """ + + public = "Public" + friend = "Friend" + + +class ServerPlayer(BaseUser): + """ + Represents a player in a server. + + Attributes: + id: The player's user id. + name: The player's username. + display_name: The player's display name. + player_token: The player's token. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client this object belongs to. + data: A GameServerPlayerResponse object. + """ + + super().__init__(client=client, user_id=data["id"]) + + self.player_token: str = data["playerToken"] + self.name: str = data["name"] + self.display_name: str = data["displayName"] + + +class Server(BaseItem): + """ + Represents a public server. + + Attributes: + id: The server's job id. + max_players: The maximum number of players that can be in the server at once. + playing: The amount of players in the server. + player_tokens: A list of thumbnail tokens for all the players in the server. + players: A list of ServerPlayer objects representing the players in the server. Only friends of the authenticated user will show up here. + fps: The server's fps. + ping: The server's ping. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client this object belongs to. + data: A GameServerResponse object. + """ + + self._client: Client = client + + self.id: Optional[str] = data.get("id") + self.max_players: int = data["maxPlayers"] + self.playing: int = data.get("playing", 0) + self.player_tokens: List[str] = data["playerTokens"] + self.players: List[ServerPlayer] = [ + ServerPlayer(client=self._client, data=player_data) + for player_data in data["players"] + ] + + self.fps: float = data.get("fps") + self.ping: Optional[int] = data.get("ping") + + +class PrivateServer(Server): + """ + Represents a private server. + + Attributes: + id: The private server's job id. + vip_server_id: The private server's vipServerId. + max_players: The maximum number of players that can be in the server at once. + playing: The amount of players in the server. + player_tokens: A list of thumbnail tokens for all the players in the server. + players: A list of ServerPlayer objects representing the players in the server. Only friends of the authenticated user will show up here. + fps: The server's fps. + ping: The server's ping. + name: The private server's name. + access_code: The private server's access code. + owner: A PartialUser object representing the owner of the private server. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client this object belongs to. + data: A PrivateServerResponse object. + """ + + super().__init__(client=client, data=data) + + self.name: str = data["name"] + self.vip_server_id: int = data["vipServerId"] + self.access_code: str = data["accessCode"] + self.owner: PartialUser = PartialUser(client=self._client, data=data["owner"]) \ No newline at end of file diff --git a/roblox/members.py b/roblox/members.py new file mode 100644 index 00000000..aff4274f --- /dev/null +++ b/roblox/members.py @@ -0,0 +1,94 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox group member endpoints. + +""" + +from __future__ import annotations + +from typing import Union, TYPE_CHECKING + +from .bases.baseuser import BaseUser +from .partials.partialrole import PartialRole + +if TYPE_CHECKING: + from .client import Client + from .bases.basegroup import BaseGroup + from .utilities.types import RoleOrRoleId + + +class MemberRelationship(BaseUser): + """ + Represents a relationship between a user and a group. + + Attributes: + group: The corresponding group. + """ + + def __init__(self, client: Client, user: Union[BaseUser, int], group: Union[BaseGroup, int]): + self._client: Client = client + super().__init__(client=self._client, user_id=int(user)) + + self.group: BaseGroup + + if isinstance(group, int): + self.group = BaseGroup(client=self._client, group_id=group) + else: + self.group = group + + async def set_role(self, role: RoleOrRoleId): + """ + Sets this member's role. + + Arguments: + role: The new role this member should be assigned. + """ + await self.group.set_role(self, role) + + async def set_rank(self, rank: int): + """ + Sets this member's rank. + + Arguments: + rank: The new rank this member should be assigned. Should be in the range of 0-255. + """ + await self.group.set_rank(self, rank) + + async def kick(self): + """ + Kicks this member from the group. + """ + await self.group.kick_user(self) + + async def delete_all_messages(self): + """ + Deletes all wall posts created by this member in the group. + """ + await self.group.delete_all_messages(self) + + +class Member(MemberRelationship): + """ + Represents a group member. + + Attributes: + id: The member's ID. + name: The member's name. + display_name: The member's display name. + role: The member's role. + group: The member's group. + has_verified_badge: If the member has a verified badge. + """ + + def __init__(self, client: Client, data: dict, group: BaseGroup): + self._client: Client = client + + self.id: int = data["user"]["userId"] + self.name: str = data["user"]["username"] + self.display_name: str = data["user"]["displayName"] + self.has_verified_badge: bool = data["user"]["hasVerifiedBadge"] + + super().__init__(client=self._client, user=self.id, group=group) + + self.role: PartialRole = PartialRole(client=self._client, data=data["role"]) + self.group: BaseGroup = group diff --git a/roblox/partials/__init__.py b/roblox/partials/__init__.py new file mode 100644 index 00000000..6e435eb0 --- /dev/null +++ b/roblox/partials/__init__.py @@ -0,0 +1,6 @@ +""" + +Contains partial objects representing objects on Roblox. +Some endpoints return some, but not all, data for an object, and these partial objects represent that data. + +""" diff --git a/roblox/partials/partialbadge.py b/roblox/partials/partialbadge.py new file mode 100644 index 00000000..14122e47 --- /dev/null +++ b/roblox/partials/partialbadge.py @@ -0,0 +1,41 @@ +""" + +This file contains partial objects related to Roblox badges. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from datetime import datetime +from dateutil.parser import parse + +from ..bases.basebadge import BaseBadge + +if TYPE_CHECKING: + from ..client import Client + + +class PartialBadge(BaseBadge): + """ + Represents partial badge data. + + Attributes: + _data: The data we get back from the endpoint. + _client: The cCient object, which is passed to all objects this Client generates. + id: The universe ID. + awarded: The date when the badge was awarded. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The raw data. + """ + self._client: Client = client + + self.id: int = data["badgeId"] + + super().__init__(client=client, badge_id=self.id) + + self.awarded: datetime = parse(data["awardedDate"]) diff --git a/roblox/partials/partialgroup.py b/roblox/partials/partialgroup.py new file mode 100644 index 00000000..19360de0 --- /dev/null +++ b/roblox/partials/partialgroup.py @@ -0,0 +1,68 @@ +""" + +This file contains partial objects related to Roblox groups. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from ..bases.basegroup import BaseGroup +from ..bases.baseuser import BaseUser + +if TYPE_CHECKING: + from ..client import Client + + +class AssetPartialGroup(BaseGroup): + """ + Represents a partial group in the context of a Roblox asset. + Intended to parse the `data[0]["creator"]` data from https://games.roblox.com/v1/games. + + Attributes: + _client: The Client object, which is passed to all objects this Client generates. + id: The group's name. + creator: The group's owner. + name: The group's name. + has_verified_badge: If the group has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The data from the endpoint. + """ + self._client: Client = client + + self.creator: BaseUser = BaseUser(client=client, user_id=data["Id"]) + self.id: int = data["CreatorTargetId"] + self.name: str = data["Name"] + self.has_verified_badge: bool = data["HasVerifiedBadge"] + + super().__init__(client, self.id) + + +class UniversePartialGroup(BaseGroup): + """ + Represents a partial group in the context of a Roblox universe. + + Attributes: + _data: The data we get back from the endpoint. + _client: The client object, which is passed to all objects this client generates. + id: Id of the group + name: Name of the group + has_verified_badge: If the group has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The ClientSharedObject. + data: The data from the endpoint. + """ + self._client: Client = client + self.id = data["id"] + self.name: str = data["name"] + self.has_verified_badge: bool = data["hasVerifiedBadge"] + + super().__init__(client, self.id) diff --git a/roblox/partials/partialrole.py b/roblox/partials/partialrole.py new file mode 100644 index 00000000..c3a0ddc8 --- /dev/null +++ b/roblox/partials/partialrole.py @@ -0,0 +1,32 @@ +""" + +This file contains partial objects related to Roblox group roles. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from ..bases.baserole import BaseRole + +if TYPE_CHECKING: + from ..client import Client + + +class PartialRole(BaseRole): + """ + Represents partial group role information. + + Attributes: + _client: The Client object. + id: The role's ID. + name: The role's name. + rank: The role's rank ID. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + + self.id: int = data["id"] + super().__init__(client=self._client, role_id=self.id) + self.name: str = data["name"] + self.rank: int = data["rank"] diff --git a/roblox/partials/partialuniverse.py b/roblox/partials/partialuniverse.py new file mode 100644 index 00000000..71f012f7 --- /dev/null +++ b/roblox/partials/partialuniverse.py @@ -0,0 +1,66 @@ +""" + +This file contains partial objects related to Roblox universes. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING + +from ..bases.baseplace import BasePlace +from ..bases.baseuniverse import BaseUniverse + +if TYPE_CHECKING: + from ..client import Client + + +class PartialUniverse(BaseUniverse): + """ + Represents partial universe information. + + Attributes:. + _client: The Client object, which is passed to all objects this Client generates. + id: The universe ID. + name: The name of the universe. + root_place: The universe's root place. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The raw data. + """ + self._client: Client = client + + self.id: int = data["id"] + + super().__init__(client=client, universe_id=self.id) + + self.name: str = data["name"] + self.root_place: BasePlace = BasePlace(client=client, place_id=data["rootPlaceId"]) + + +class ChatPartialUniverse(BaseUniverse): + """ + Represents a partial universe in the context of a chat conversation. + + Attributes: + _data: The data we get back from the endpoint. + _client: The client object, which is passed to all objects this client generates. + id: The universe ID. + root_place: The universe's root place. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The ClientSharedObject. + data: The raw data. + """ + self._client: Client = client + + self.id: int = data["universeId"] + + super().__init__(client=client, universe_id=self.id) + + self.root_place: BasePlace = BasePlace(client=client, place_id=data["rootPlaceId"]) diff --git a/roblox/partials/partialuser.py b/roblox/partials/partialuser.py new file mode 100644 index 00000000..320fa71a --- /dev/null +++ b/roblox/partials/partialuser.py @@ -0,0 +1,78 @@ +""" + +This file contains partial objects related to Roblox users. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING, Optional, List + +from ..bases.baseuser import BaseUser + +if TYPE_CHECKING: + from ..client import Client + + +class PartialUser(BaseUser): + """ + Represents partial user information. + + Attributes: + _client: The Client object, which is passed to all objects this Client generates. + id: The user's ID. + name: The user's name. + display_name: The user's display name. + has_verified_badge: If the user has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The data from the endpoint. + """ + self._client: Client = client + + self.id: int = data.get("id") or data.get("userId") or data.get("Id") + + super().__init__(client=client, user_id=self.id) + + self.name: str = data.get("name") or data.get("Name") or data.get("username") or data.get("Username") + self.display_name: str = data.get("displayName") + self.has_verified_badge: bool = data.get("hasVerifiedBadge", False) or data.get("HasVerifiedBadge", False) + + +class RequestedUsernamePartialUser(PartialUser): + """ + Represents a partial user in the context of a search where the requested username is present. + + Attributes: + requested_username: The requested username. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The data from the endpoint. + """ + super().__init__(client=client, data=data) + + self.requested_username: Optional[str] = data.get("requestedUsername") + + +class PreviousUsernamesPartialUser(PartialUser): + """ + Represents a partial user in the context of a search where the user's previous usernames are present. + Attributes: + previous_usernames: A list of the user's previous usernames. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The data from the endpoint. + """ + super().__init__(client=client, data=data) + + self.previous_usernames: List[str] = data["previousUsernames"] diff --git a/roblox/places.py b/roblox/places.py new file mode 100644 index 00000000..3c49397b --- /dev/null +++ b/roblox/places.py @@ -0,0 +1,59 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox place information endpoints. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .client import Client +from .bases.baseplace import BasePlace +from .bases.baseuniverse import BaseUniverse + + +class Place(BasePlace): + """ + Represents a Roblox place. + + Attributes: + id: id of the place. + name: Name of the place. + description: Description of the place. + url: URL for the place. + builder: The name of the user or group who owns the place. + builder_id: The ID of the player or group who owns the place. + is_playable: Whether the authenticated user can play this game. + reason_prohibited: If the place is not playable, contains the reason why the user cannot play the game. + universe: The BaseUniverse that contains this place. + universe_root_place: The root place that the universe contains. + price: How much it costs to play the game. + image_token: Can be used to generate thumbnails for this place. + has_verified_badge: If the place has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client object, which is passed to all objects this Client generates. + data: data to make the magic happen. + """ + super().__init__(client=client, place_id=data["placeId"]) + + self._client: Client = client + + self.id: int = data["placeId"] + self.name: str = data["name"] + self.description: str = data["description"] + self.url: str = data["url"] + + self.builder: str = data["builder"] + self.builder_id: int = data["builderId"] + + self.is_playable: bool = data["isPlayable"] + self.reason_prohibited: str = data["reasonProhibited"] + self.universe: BaseUniverse = BaseUniverse(client=self._client, universe_id=data["universeId"]) + self.universe_root_place: BasePlace = BasePlace(client=self._client, place_id=data["universeRootPlaceId"]) + + self.price: int = data["price"] + self.image_token: str = data["imageToken"] + self.has_verified_badge: bool = data["hasVerifiedBadge"] diff --git a/roblox/plugins.py b/roblox/plugins.py new file mode 100644 index 00000000..8b3470d0 --- /dev/null +++ b/roblox/plugins.py @@ -0,0 +1,46 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox plugin information endpoints. + +""" +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .client import Client +from datetime import datetime + +from dateutil.parser import parse + +from .bases.baseplugin import BasePlugin + + +class Plugin(BasePlugin): + """ + Represents a Roblox plugin. + It is intended to parse data from https://develop.roblox.com/v1/plugins. + + Attributes: + id: The ID of the plugin. + name: The name of the plugin. + description: The plugin's description. + comments_enabled: Whether comments are enabled or disabled. + version_id: The plugin's current version ID. + created: When the plugin was created. + updated: When the plugin was updated. + """ + + def __init__(self, client: Client, data: dict): + """ + Attributes: + client: The Client object, which is passed to all objects this Client generates. + data: data to make the magic happen. + """ + super().__init__(client=client, plugin_id=data["id"]) + + self.id: int = data["id"] + self.name: str = data["name"] + self.description: str = data["description"] + self.comments_enabled: bool = data["commentsEnabled"] + self.version_id: int = data["versionId"] + self.created: datetime = parse(data["created"]) + self.updated: datetime = parse(data["updated"]) diff --git a/roblox/presence.py b/roblox/presence.py new file mode 100644 index 00000000..de34b8d4 --- /dev/null +++ b/roblox/presence.py @@ -0,0 +1,113 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox presence endpoints. + +""" + +from __future__ import annotations + +from datetime import datetime +from enum import IntEnum +from typing import Optional, List +from typing import TYPE_CHECKING + +from dateutil.parser import parse + +from .bases.basejob import BaseJob +from .bases.baseplace import BasePlace +from .bases.baseuniverse import BaseUniverse + +if TYPE_CHECKING: + from .client import Client + from .bases.baseuser import BaseUser + from .utilities.types import UserOrUserId + + +class PresenceType(IntEnum): + """ + Represents a user's presence type. + """ + offline = 0 + online = 1 + in_game = 2 + in_studio = 3 + + +class Presence: + """ + Represents a user's presence. + + Attributes: + user_presence_type: The type of the presence. + last_location: A string representing the user's last location. + place: The place the user is playing or editing. + root_place: The root place of the parent universe of the last place the user is playing or editing. + job: The job of the root place that the user is playing or editing. + universe: The universe the user is playing or editing. + last_online: When the user was last online. + user: The user this presence belongs to. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: Client object. + data: The data from the request. + """ + self._client: Client = client + + self.user_presence_type: PresenceType = PresenceType(data["userPresenceType"]) + self.last_location: str = data["lastLocation"] + + self.place: Optional[BasePlace] = BasePlace( + client=client, + place_id=data["placeId"] + ) if data.get("placeId") else None + + self.root_place: Optional[BasePlace] = BasePlace( + client=client, + place_id=data["rootPlaceId"] + ) if data.get("rootPlaceId") else None + + self.job: Optional[BaseJob] = BaseJob(self._client, data["gameId"]) if data.get("gameId") else None + + self.universe: Optional[BaseUniverse] = BaseUniverse( + client=client, + universe_id=data["universeId"] + ) if data.get("universeId") else None + + self.user: BaseUser = client.get_base_user(data["userId"]) + self.last_online: datetime = parse(data["lastOnline"]) + + def __repr__(self): + return f"<{self.__class__.__name__} user_presence_type={self.user_presence_type}>" + + +class PresenceProvider: + """ + The PresenceProvider is an object that represents https://presence.roblox.com/ and provides multiple functions + for fetching user presence information. + """ + + def __init__(self, client: Client): + self._client: Client = client + + async def get_user_presences(self, users: List[UserOrUserId]) -> List[Presence]: + """ + Grabs a list of Presence objects corresponding to each user in the list. + + Arguments: + users: The list of users you want to get Presences from. + + Returns: + A list of Presences. + """ + + presences_response = await self._client.requests.post( + url=self._client.url_generator.get_url("presence", "v1/presence/users"), + json={ + "userIds": list(map(int, users)) + } + ) + presences_data = presences_response.json()["userPresences"] + return [Presence(client=self._client, data=presence_data) for presence_data in presences_data] diff --git a/roblox/promotionchannels.py b/roblox/promotionchannels.py new file mode 100644 index 00000000..80bc2884 --- /dev/null +++ b/roblox/promotionchannels.py @@ -0,0 +1,29 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox promotion channel endpoints. + +""" + +from typing import Optional + + +class UserPromotionChannels: + """ + Represents a user's promotion channels. + + Attributes: + facebook: A link to the user's Facebook profile. + twitter: A Twitter handle. + youtube: A link to the user's YouTube channel. + twitch: A link to the user's Twitch channel. + """ + + def __init__(self, data: dict): + self.facebook: Optional[str] = data["facebook"] + self.twitter: Optional[str] = data["twitter"] + self.youtube: Optional[str] = data["youtube"] + self.twitch: Optional[str] = data["twitch"] + self.guilded: Optional[str] = data["guilded"] + + def __repr__(self): + return f"<{self.__class__.__name__}>" diff --git a/roblox/py.typed b/roblox/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/roblox/resale.py b/roblox/resale.py new file mode 100644 index 00000000..49f7b99b --- /dev/null +++ b/roblox/resale.py @@ -0,0 +1,29 @@ +""" + +Contains classes related to Roblox resale. + +""" + +from typing import List + + +class AssetResaleData: + """ + Represents an asset's resale data. + + Attributes: + asset_stock: The asset's stock. + sales: The asset's sales. + number_remaining: On a Limited U item that hasn't ran out, this is the amount remaining. + recent_average_price: The item's recent average price. + original_price: What price this item was originally sold at. + price_data_points: A list of tuples containing a limited item's price points over time. + """ + + def __init__(self, data: dict): + self.asset_stock: int = data["assetStock"] + self.sales: int = data["sales"] + self.number_remaining: int = data["numberRemaining"] + self.recent_average_price: int = data["recentAveragePrice"] + self.original_price: int = data["originalPrice"] + self.price_data_points: List[dict] = data["priceDataPoints"] diff --git a/roblox/robloxbadges.py b/roblox/robloxbadges.py new file mode 100644 index 00000000..c1cdb027 --- /dev/null +++ b/roblox/robloxbadges.py @@ -0,0 +1,33 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox badge endpoints. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from .bases.baserobloxbadge import BaseRobloxBadge + + +class RobloxBadge(BaseRobloxBadge): + """ + Represents a Roblox roblox badge. + + Attributes: + id: The badge's ID. + name: The badge's name. + description: The badge's description. + image_url: A link to the badge's image. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + self.id: int = data["id"] + super().__init__(client=self._client, roblox_badge_id=self.id) + + self.name: str = data["name"] + self.description: str = data["description"] + self.image_url: str = data["imageUrl"] diff --git a/roblox/roles.py b/roblox/roles.py new file mode 100644 index 00000000..2d601439 --- /dev/null +++ b/roblox/roles.py @@ -0,0 +1,71 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox group role endpoints. + +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from .bases.baserole import BaseRole +from .partials.partialuser import PartialUser +from .utilities.iterators import PageIterator, SortOrder + +if TYPE_CHECKING: + from .client import Client + from .bases.basegroup import BaseGroup + + +class Role(BaseRole): + """ + Represents a Roblox group's role. + + Attributes: + id: The role's ID. + group: The group that this role is a part of. + name: The role's name. + description: The role's description. + rank: The rank, from 0-255, of this role. + member_count: How many members exist with this role. + """ + + def __init__(self, client: Client, data: dict, group: BaseGroup = None): + """ + Arguments: + client: The Client object. + data: The raw role data. + group: The parent group. + """ + self._client: Client = client + + self.id: int = data["id"] + super().__init__(client=self._client, role_id=self.id) + + self.group: Optional[BaseGroup] = group + self.name: str = data["name"] + self.description: Optional[str] = data.get("description") + self.rank: int = data["rank"] + self.member_count: Optional[int] = data.get("memberCount") + + def get_members(self, page_size: int = 10, sort_order: SortOrder = SortOrder.Ascending, + max_items: int = None) -> PageIterator: + """ + Gets all members with this role. + + Arguments: + page_size: How many users should be returned for each page. + sort_order: Order in which data should be grabbed. + max_items: The maximum items to return when looping through this object. + + Returns: + A PageIterator containing all members with this role. + """ + return PageIterator( + client=self._client, + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.group.id}/roles/{self.id}/users"), + page_size=page_size, + sort_order=sort_order, + max_items=max_items, + handler=lambda client, data: PartialUser(client=client, data=data) + ) diff --git a/roblox/shout.py b/roblox/shout.py new file mode 100644 index 00000000..dac9d73b --- /dev/null +++ b/roblox/shout.py @@ -0,0 +1,52 @@ +""" + +Contains the Shout object, which represents a group's shout. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from datetime import datetime + +from dateutil.parser import parse + +from .partials.partialuser import PartialUser + + +class Shout: + """ + Represents a Group Shout. + + Attributes: + body: The text of the shout. + created: When the shout was created. + updated: When the shout was updated. + poster: Who posted the shout. + """ + + def __init__( + self, + client: Client, + data: dict + ): + """ + Arguments: + client: Client object. + data: The data from the request. + """ + self._client: Client = client + + self.body: str = data["body"] + self.created: datetime = parse(data["created"]) + self.updated: datetime = parse(data["updated"]) + self.poster: PartialUser = PartialUser( + client=self._client, + data=data["poster"] + ) + + def __repr__(self): + return f"<{self.__class__.__name__} created={self.created} updated={self.updated} body={self.body!r} " \ + f"poster={self.poster!r}>" diff --git a/roblox/sociallinks.py b/roblox/sociallinks.py new file mode 100644 index 00000000..5620c405 --- /dev/null +++ b/roblox/sociallinks.py @@ -0,0 +1,47 @@ +""" + +Contains objects related to Roblox social links. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from enum import Enum + +from .bases.basesociallink import BaseUniverseSocialLink + + +class SocialLinkType(Enum): + """ + Represents a type of social link. + """ + + facebook = "Facebook" + twitter = "Twitter" + youtube = "YouTube" + twitch = "Twitch" + discord = "Discord" + roblox_group = "RobloxGroup" + + +class SocialLink(BaseUniverseSocialLink): + """ + Represents a universe or group's social links. + + Attributes: + id: The social link's ID. + title: The social link's title. + url: The social link's URL. + type: The social link's type. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + self.id: int = data["id"] + super().__init__(client=self._client, social_link_id=self.id) + self.title: str = data["title"] + self.url: str = data["url"] + self.type: SocialLinkType = SocialLinkType(data["type"]) diff --git a/roblox/threedthumbnails.py b/roblox/threedthumbnails.py new file mode 100644 index 00000000..b5b4f7b2 --- /dev/null +++ b/roblox/threedthumbnails.py @@ -0,0 +1,87 @@ +""" +Contains classes related to 3D thumbnails. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from typing import List + +from .delivery import ThumbnailCDNHash + + +class ThreeDThumbnailVector3: + """ + Represents a Vector3 used in a 3D thumbnail. + + Attributes: + x: The X component of the vector. + y: The Y component of the vector. + z: The Z component of the vector. + """ + + def __init__(self, data: dict): + self.x: float = data["x"] + self.y: float = data["y"] + self.z: float = data["z"] + + +class ThreeDThumbnailCamera: + """ + Represents a camera in a 3D thumbnail. + + Attributes: + fov: The camera's field of view. + position: The camera's position. + direction: The camera's direction. + """ + + def __init__(self, data: dict): + self.fov: float = data["fov"] + self.position: ThreeDThumbnailVector3 = ThreeDThumbnailVector3(data["position"]) + self.direction: ThreeDThumbnailVector3 = ThreeDThumbnailVector3(data["direction"]) + + +class ThreeDThumbnailAABB: + """ + Represents AABB data in a 3D thumbnail. + Roblox uses this data to calculate the maximum render distance used when rendering 3D thumbnails. + ```js + THREE.Vector3(json.aabb.max.x, json.aabb.max.y, json.aabb.max.z).length() * 4; + ``` + + Attributes: + min: The minimum render position. + max: The maximum render position. + """ + + def __init__(self, data: dict): + self.min: ThreeDThumbnailVector3 = ThreeDThumbnailVector3(data["min"]) + self.max: ThreeDThumbnailVector3 = ThreeDThumbnailVector3(data["max"]) + + +class ThreeDThumbnail: + """ + Represents a user's 3D Thumbnail data. + For more info, see https://robloxapi.wiki/wiki/3D_Thumbnails. + + Attributes: + mtl: A CDN hash pointing to the MTL data. + obj: A CDN hash pointing to the OBJ data. + textures: A list of CDN hashes pointing to PNG texture data. + camera: The camera object. + aabb: The AABB object. + """ + + def __init__(self, client: Client, data: dict): + self._client: Client = client + + self.mtl: ThumbnailCDNHash = self._client.delivery.get_thumbnail_cdn_hash(data["mtl"]) + self.obj: ThumbnailCDNHash = self._client.delivery.get_thumbnail_cdn_hash(data["obj"]) + self.textures: List[ThumbnailCDNHash] = [ + self._client.delivery.get_thumbnail_cdn_hash(cdn_hash) for cdn_hash in data["textures"] + ] + self.camera: ThreeDThumbnailCamera = ThreeDThumbnailCamera(data["camera"]) + self.aabb: ThreeDThumbnailAABB = ThreeDThumbnailAABB(data["aabb"]) diff --git a/roblox/thumbnails.py b/roblox/thumbnails.py new file mode 100644 index 00000000..d4ddd485 --- /dev/null +++ b/roblox/thumbnails.py @@ -0,0 +1,564 @@ +""" + +Contains objects related to Roblox thumbnails. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from enum import Enum +from typing import Optional, List, Union, Tuple + +from .threedthumbnails import ThreeDThumbnail +from .utilities.types import AssetOrAssetId, BadgeOrBadgeId, GamePassOrGamePassId, GroupOrGroupId, PlaceOrPlaceId, \ + UniverseOrUniverseId, UserOrUserId + + +class ThumbnailState(Enum): + """ + The current state of the thumbnail. + """ + + completed = "Completed" + in_review = "InReview" + pending = "Pending" + error = "Error" + moderated = "Moderated" + blocked = "Blocked" + + +class ThumbnailReturnPolicy(Enum): + """ + The return policy for place/universe thumbnails. + """ + + place_holder = "PlaceHolder" + auto_generated = "AutoGenerated" + force_auto_generated = "ForceAutoGenerated" + + +class ThumbnailFormat(Enum): + """ + Format returned by the endpoint. + """ + + png = "Png" + jpeg = "Jpeg" + + +class AvatarThumbnailType(Enum): + """ + Type of avatar thumbnail. + """ + + full_body = "full_body" + headshot = "headshot" + bust = "bust" + + +SizeTupleOrString = Union[Tuple[int, int], str] + + +def _to_size_string(size_item: SizeTupleOrString): + if isinstance(size_item, tuple): + return f"{int(size_item[0])}x{int(size_item[1])}" + else: + return size_item + + +class Thumbnail: + """ + Represents a Roblox thumbnail as returned by almost all endpoints on https://thumbnails.roblox.com/. + + Attributes: + target_id: The id of the target of the image. + state: The current state of the image. + image_url: Url of the image. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: Client object. + data: The data from the request. + """ + self._client: Client = client + + self.target_id: int = data["targetId"] + self.state: ThumbnailState = ThumbnailState(data["state"]) + self.image_url: Optional[str] = data["imageUrl"] + + def __repr__(self): + return f"<{self.__class__.__name__} target_id={self.target_id} name={self.state!r} " \ + f"image_url={self.image_url!r}>" + + async def get_3d_data(self) -> ThreeDThumbnail: + """ + Generates 3D thumbnail data for this endpoint. + + Returns: + A ThreeDThumbnail. + """ + threed_response = await self._client.requests.get( + url=self.image_url + ) + threed_data = threed_response.json() + return ThreeDThumbnail( + client=self._client, + data=threed_data + ) + + +class UniverseThumbnails: + """ + Represents a universe's thumbnails as returned by https://thumbnails.roblox.com/v1/games/multiget/thumbnails. + + Attributes: + universe_id: The id of the target of the image. + error: The errors you got. + thumbnails: List of thumbnails. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: Shared object. + data: The data from the request. + """ + self._client: Client = client + # todo add base universe maby + self.universe_id: int = data["universeId"] + self.error: Optional[str] = data["error"] + self.thumbnails: List[Thumbnail] = [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in data["thumbnails"] + ] + + +class ThumbnailProvider: + """ + The ThumbnailProvider that provides multiple functions for generating user thumbnails. + """ + + def __init__(self, client: Client): + """ + Arguments: + client: Client object. + """ + self._client: Client = client + + async def get_asset_thumbnails( + self, + assets: List[AssetOrAssetId], + return_policy: ThumbnailReturnPolicy = ThumbnailReturnPolicy.place_holder, + size: SizeTupleOrString = (30, 30), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns asset thumbnails for the asset ID passed. + Supported sizes: + - 30x30 + - 42x42 + - 50x50 + - 60x62 + - 75x75 + - 110x110 + - 140x140 + - 150x150 + - 160x100 + - 160x600 + - 250x250 + - 256x144 + - 300x250 + - 304x166 + - 384x216 + - 396x216 + - 420x420 + - 480x270 + - 512x512 + - 576x324 + - 700x700 + - 728x90 + - 768x432 + + Arguments: + assets: Assets you want the thumbnails of. + return_policy: How you want it returns look at enum. + size: size of the image. + image_format: Format of the image. + is_circular: if the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/assets"), + params={ + "assetIds": list(map(int, assets)), + "returnPolicy": return_policy.value, + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_asset_thumbnail_3d(self, asset: AssetOrAssetId) -> Thumbnail: + """ + Returns a 3D asset thumbnail for the user ID passed. + + Arguments: + asset: Asset you want the thumbnails of. + + Returns: + A Thumbnail. + """ + thumbnail_response = await self._client.requests.get( + url=self._client.url_generator.get_url( + "thumbnails", "v1/assets-thumbnail-3d" + ), + params={"assetId": int(asset)}, + ) + thumbnail_data = thumbnail_response.json() + return Thumbnail(client=self._client, data=thumbnail_data) + + async def get_badge_icons( + self, + badges: List[BadgeOrBadgeId], + size: SizeTupleOrString = (150, 150), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns badge icons for each badge ID passed. + Supported sizes: + - 150x150 + + Arguments: + badges: Badges you want the thumbnails of. + size: size of the image. + image_format: Format of the image. + is_circular: if the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/badges/icons"), + params={ + "badgeIds": list(map(int, badges)), + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_gamepass_icons( + self, + gamepasses: List[GamePassOrGamePassId], + # TODO Make size enum + size: SizeTupleOrString = (150, 150), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns gamepass icons for each gamepass ID passed. + Supported sizes: + - 150x150 + + Arguments: + gamepasses: Gamepasses you want the thumbnails of. + size: size of the image. + image_format: Format of the image. + is_circular: If the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/game-passes"), + params={ + "gamePassIds": list(map(int, gamepasses)), + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_universe_icons( + self, + universes: List[UniverseOrUniverseId], + return_policy: ThumbnailReturnPolicy = ThumbnailReturnPolicy.place_holder, + size: SizeTupleOrString = (50, 50), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns universe icons for each universe ID passed. + Supported sizes: + - 50x50 + - 128x128 + - 150x150 + - 256x256 + - 512x512 + - 768x432 + + Arguments: + universes: Universes you want the thumbnails of. + return_policy: How you want it returns look at enum. + size: size of the image. + image_format: Format of the image. + is_circular: If the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/games/icons"), + params={ + "universeIds": list(map(int, universes)), + "returnPolicy": return_policy.value, + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_universe_thumbnails( + self, + universes: List[UniverseOrUniverseId], + size: SizeTupleOrString = (768, 432), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + count_per_universe: int = None, + defaults: bool = None, + ) -> List[UniverseThumbnails]: + """ + Returns universe thumbnails for each universe ID passed. + Supported sizes: + - 768x432 + - 576x324 + - 480x270 + - 384x216 + - 256x144 + + Arguments: + universes: Universes you want the thumbnails of. + size: size of the image. + image_format: Format of the image. + count_per_universe: Unknown. + is_circular: If the image is a circle yes or no. + defaults: Whether to return default thumbnails. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url( + "thumbnails", "v1/games/multiget/thumbnails" + ), + params={ + "universeIds": list(map(int, universes)), + "countPerUniverse": count_per_universe, + "defaults": defaults, + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + UniverseThumbnails(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_group_icons( + self, + groups: List[GroupOrGroupId], + size: SizeTupleOrString = (150, 150), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns icons for each group ID passed. + Supported sizes: + - 150x150 + - 420x420 + + Arguments: + groups: Groups you want the thumbnails of. + size: size of the image. + image_format: Format of the image. + is_circular: If the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/groups/icons"), + params={ + "groupIds": list(map(int, groups)), + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_place_icons( + self, + places: List[PlaceOrPlaceId], + return_policy: ThumbnailReturnPolicy = ThumbnailReturnPolicy.place_holder, + size: SizeTupleOrString = (50, 50), + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns icons for each place ID passed. + Supported sizes: + - 50x50 + - 128x128 + - 150x150 + - 256x256 + - 512x512 + - 768x432 + + Arguments: + places: Places you want the thumbnails of. + return_policy: How you want it returns look at enum. + size: size of the image. + image_format: Format of the image. + is_circular: if the image is a circle yes or no. + Returns: + A List of Thumbnails. + """ + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/places/gameicons"), + params={ + "placeIds": list(map(int, places)), + "returnPolicy": return_policy.value, + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_user_avatar_thumbnails( + self, + users: List[UserOrUserId], + type: AvatarThumbnailType = AvatarThumbnailType.full_body, + size: SizeTupleOrString = None, + image_format: ThumbnailFormat = ThumbnailFormat.png, + is_circular: bool = False, + ) -> List[Thumbnail]: + """ + Returns avatar thumbnails for each user ID passed. + The valid sizes depend on the `type` parameter. + + | Size | full_body | headshot | bust | + |---|---|---|---| + | 30x30 | âœ”ī¸ | ❌ | ❌ | + | 48x48 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 50x50 | ❌ | âœ”ī¸ | âœ”ī¸ | + | 60x60 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 75x75 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 100x100 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 110x110 | âœ”ī¸ | âœ”ī¸ | ❌ | + | 140x140 | âœ”ī¸ | ❌ | ❌ | + | 150x150 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 150x200 | âœ”ī¸ | ❌ | ❌ | + | 180x180 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 250x250 | âœ”ī¸ | ❌ | ❌ | + | 352x352 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 420x420 | âœ”ī¸ | âœ”ī¸ | âœ”ī¸ | + | 720x720 | âœ”ī¸ | ❌ | ❌ | + + Arguments: + users: Id of the users you want the thumbnails of. + type: Type of avatar thumbnail you want look at enum. + size: size of the image. + image_format: Format of the image. + is_circular: If the image is a circle yes or no. + + Returns: + A list of Thumbnails. + """ + uri: str + if type == AvatarThumbnailType.full_body: + uri = "avatar" + size = size or (30, 30) + elif type == AvatarThumbnailType.bust: + uri = "avatar-bust" + size = size or (48, 48) + elif type == AvatarThumbnailType.headshot: + uri = "avatar-headshot" + size = size or (48, 48) + else: + raise ValueError("Avatar type is invalid.") + + thumbnails_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", f"v1/users/{uri}"), + params={ + "userIds": list(map(int, users)), + "size": _to_size_string(size), + "format": image_format.value, + "isCircular": is_circular, + }, + ) + + thumbnails_data = thumbnails_response.json()["data"] + return [ + Thumbnail(client=self._client, data=thumbnail_data) + for thumbnail_data in thumbnails_data + ] + + async def get_user_avatar_thumbnail_3d(self, user: UserOrUserId) -> Thumbnail: + """ + Returns the user's thumbnail in 3d. + + Arguments: + user: User you want the 3d thumbnail of. + + Returns: + A Thumbnail. + """ + thumbnail_response = await self._client.requests.get( + url=self._client.url_generator.get_url("thumbnails", "v1/users/avatar-3d"), + params={ + "userId": int(user) + }, + ) + thumbnail_data = thumbnail_response.json() + return Thumbnail(client=self._client, data=thumbnail_data) diff --git a/roblox/universes.py b/roblox/universes.py new file mode 100644 index 00000000..437ef353 --- /dev/null +++ b/roblox/universes.py @@ -0,0 +1,125 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox universe information endpoints. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client +from datetime import datetime +from enum import Enum +from typing import Optional, List, Union + +from dateutil.parser import parse + +from .bases.baseplace import BasePlace +from .bases.baseuniverse import BaseUniverse +from .creatortype import CreatorType +from .partials.partialgroup import UniversePartialGroup +from .partials.partialuser import PartialUser + + +class UniverseAvatarType(Enum): + """ + The current avatar type of the universe. + """ + + R6 = "MorphToR6" + R15 = "MorphToR15" + player_choice = "PlayerChoice" + + +class UniverseGenre(Enum): + """ + The universe's genre. + """ + + all = "All" + building = "Building" + horror = "Horror" + town_and_city = "Town and City" + military = "Military" + comedy = "Comedy" + medieval = "Medieval" + adventure = "Adventure" + sci_fi = "Sci-Fi" + naval = "Naval" + fps = "FPS" + rpg = "RPG" + sports = "Sports" + fighting = "Fighting" + western = "Western" + + +class Universe(BaseUniverse): + """ + Represents the response data of https://games.roblox.com/v1/games. + + Attributes: + id: The ID of this specific universe + root_place: The thumbnail provider object. + name: The delivery provider object. + description: The description of the game. + creator_type: Is the creator a group or a user. + creator: creator information. + price: how much you need to pay to play the game. + allowed_gear_genres: Unknown + allowed_gear_categories: Unknown + is_genre_enforced: Unknown + copying_allowed: are you allowed to copy the game. + playing: amount of people currently playing the game. + visits: amount of visits to the game. + max_players: the maximum amount of players ber server. + created: when the game was created. + updated: when the game as been updated for the last time. + studio_access_to_apis_allowed: does studio have access to the apis. + create_vip_servers_allowed: can you create a vip server? + universe_avatar_type: type of avatars in the game. + genre: what genre the game is. + is_all_genre: if it is all genres? + is_favorited_by_user: if the authenticated user has it favorited. + favorited_count: the total amount of people who favorited the game. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: The Client. + data: The universe data. + """ + + self._client: Client = client + + self.id: int = data["id"] + super().__init__(client=client, universe_id=self.id) + self.root_place: BasePlace = BasePlace(client=client, place_id=data["rootPlaceId"]) + self.name: str = data["name"] + self.description: str = data["description"] + self.creator_type: Enum = CreatorType(data["creator"]["type"]) + # isRNVAccount is not part of PartialUser, UniversePartialGroup + self.creator: Union[PartialUser, UniversePartialGroup] + if self.creator_type == CreatorType.group: + self.creator = UniversePartialGroup(client, data["creator"]) + elif self.creator_type == CreatorType.user: + self.creator = PartialUser(client, data["creator"]) + self.price: Optional[int] = data["price"] + self.allowed_gear_genres: List[str] = data["allowedGearGenres"] + self.allowed_gear_categories: List[str] = data["allowedGearCategories"] + self.is_genre_enforced: bool = data["isGenreEnforced"] + self.copying_allowed: bool = data["copyingAllowed"] + self.playing: int = data["playing"] + self.visits: int = data["visits"] + self.max_players: int = data["maxPlayers"] + self.created: datetime = parse(data["created"]) + self.updated: datetime = parse(data["updated"]) + self.studio_access_to_apis_allowed: bool = data["studioAccessToApisAllowed"] + self.create_vip_servers_allowed: bool = data["createVipServersAllowed"] + self.universe_avatar_type: UniverseAvatarType = UniverseAvatarType(data["universeAvatarType"]) + self.genre: UniverseGenre = UniverseGenre(data["genre"]) + self.is_all_genre: bool = data["isAllGenre"] + # gameRating seems to be null across all games, so I omitted it from this class. + self.is_favorited_by_user: bool = data["isFavoritedByUser"] + self.favorited_count: int = data["favoritedCount"] diff --git a/roblox/users.py b/roblox/users.py new file mode 100644 index 00000000..4b14f84f --- /dev/null +++ b/roblox/users.py @@ -0,0 +1,51 @@ +""" + +This module contains classes intended to parse and deal with data from Roblox user information endpoints. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .client import Client +from datetime import datetime + +from dateutil.parser import parse + +from .bases.baseuser import BaseUser + + +class User(BaseUser): + """ + Represents a single conversation. + + Attributes: + id: The id of the current user. + name: The name of the current user. + display_name: The display name of the current user. + external_app_display_name: The external app display name of the current user. + is_banned: If the user is banned. + description: The description the current user wrote for themself. + created: When the user created their account. + has_verified_badge: If the user has a verified badge. + """ + + def __init__(self, client: Client, data: dict): + """ + Arguments: + client: Client object. + data: The data from the request. + """ + super().__init__(client=client, user_id=data["id"]) + + self._client: Client = client + + self.name: str = data["name"] + self.display_name: str = data["displayName"] + self.external_app_display_name: Optional[str] = data["externalAppDisplayName"] + self.id: int = data["id"] + self.is_banned: bool = data["isBanned"] + self.description: str = data["description"] + self.created: datetime = parse(data["created"]) + self.has_verified_badge: bool = data["hasVerifiedBadge"] diff --git a/roblox/utilities/__init__.py b/roblox/utilities/__init__.py new file mode 100644 index 00000000..c80d202d --- /dev/null +++ b/roblox/utilities/__init__.py @@ -0,0 +1,5 @@ +""" + +Contains utilities used internally for ro.py. + +""" diff --git a/roblox/utilities/exceptions.py b/roblox/utilities/exceptions.py new file mode 100644 index 00000000..2ee2f0d5 --- /dev/null +++ b/roblox/utilities/exceptions.py @@ -0,0 +1,233 @@ +""" + +Contains exceptions used by ro.py. + +""" + +from typing import Optional, List, Dict, Type +from httpx import Response + + +# Generic exceptions + +class RobloxException(Exception): + """ + Base exception for all of ro.py. + """ + pass + + +# Other objects + +class ResponseError: + """ + Represents an error returned by a Roblox game server. + + Attributes: + code: The error code. + message: The error message. + user_facing_message: A more simple error message intended for frontend use. + field: The field causing this error. + retryable: Whether retrying this exception could supress this issue. + """ + + def __init__(self, data: dict): + self.code: int = data["code"] + self.message: Optional[str] = data.get("message") + self.user_facing_message: Optional[str] = data.get("userFacingMessage") + self.field: Optional[str] = data.get("field") + self.retryable: Optional[str] = data.get("retryable") + + +# HTTP exceptions +# Exceptions that Roblox endpoints do not respond with are not included here. + +class HTTPException(RobloxException): + """ + Exception that's raised when an HTTP request fails. + + Attributes: + response: The HTTP response object. + status: The HTTP response status code. + errors: A list of Roblox response errors. + """ + + def __init__(self, response: Response, errors: Optional[list] = None): + """ + Arguments: + response: The raw response object. + errors: A list of errors. + """ + self.response: Response = response + self.status: int = response.status_code + self.errors: List[ResponseError] + + if errors: + self.errors = [ + ResponseError(data=error_data) for error_data in errors + ] + else: + self.errors = [] + + if self.errors: + error_string = self._generate_string() + super().__init__( + f"{response.status_code} {response.reason_phrase}: {response.url}.\n\nErrors:\n{error_string}") + else: + super().__init__(f"{response.status_code} {response.reason_phrase}: {response.url}") + + def _generate_string(self) -> str: + parsed_errors = [] + for error in self.errors: + # Make each error into a parsed string + parsed_error = f"\t{error.code}: {error.message}" + error_messages = [] + + error.user_facing_message and error_messages.append(f"User-facing message: {error.user_facing_message}") + error.field and error_messages.append(f"Field: {error.field}") + error.retryable and error_messages.append(f"Retryable: {error.retryable}") + + if error_messages: + error_message_string = "\n\t\t".join(error_messages) + parsed_error += f"\n\t\t{error_message_string}" + + parsed_errors.append(parsed_error) + + # Turn the parsed errors into a joined string + return "\n".join(parsed_errors) + + +class BadRequest(HTTPException): + """HTTP exception raised for status code 400.""" + pass + + +class Unauthorized(HTTPException): + """HTTP exception raised for status code 401. This usually means you aren't properly authenticated.""" + + +class Forbidden(HTTPException): + """HTTP exception raised for status code 403. This usually means the X-CSRF-Token was not properly provided.""" + pass + + +class NotFound(HTTPException): + """ + HTTP exception raised for status code 404. + This usually means we have an internal URL issue - please make a GitHub issue about this! + """ + pass + + +class TooManyRequests(HTTPException): + """ + HTTP exception raised for status code 429. + This means that Roblox has [ratelimited](https://en.wikipedia.org/wiki/Rate_limiting) you. + """ + pass + + +class InternalServerError(HTTPException): + """ + HTTP exception raised for status code 500. + This usually means that there was an issue on Roblox's end, but due to faulty coding on Roblox's part this can + sometimes mean that an endpoint used internally was disabled or that invalid parameters were passed. + """ + pass + + +_codes_exceptions: Dict[int, Type[HTTPException]] = { + 400: BadRequest, + 401: Unauthorized, + 403: Forbidden, + 404: NotFound, + 429: TooManyRequests, + 500: InternalServerError +} + + +def get_exception_from_status_code(code: int) -> Type[HTTPException]: + """ + Gets an exception that should be raised instead of the generic HTTPException for this status code. + """ + return _codes_exceptions.get(code) or HTTPException + + +# Misc exceptions +class InvalidRole(RobloxException): + """ + Raised when a role doesn't exist. + """ + pass + + +class NoMoreItems(RobloxException): + """ + Raised when there are no more items left to iterate through. + """ + pass + + +# Exceptions raised for certain Client methods +class ItemNotFound(RobloxException): + """ + Raised for invalid items. + """ + + def __init__(self, message: str, response: Optional[Response] = None): + """ + Arguments: + response: The raw response object. + """ + self.response: Optional[Response] = response + self.status: Optional[int] = response.status_code if response else None + super().__init__(message) + + +class AssetNotFound(ItemNotFound): + """ + Raised for invalid asset IDs. + """ + pass + + +class BadgeNotFound(ItemNotFound): + """ + Raised for invalid badge IDs. + """ + pass + + +class GroupNotFound(ItemNotFound): + """ + Raised for invalid group IDs. + """ + pass + + +class PlaceNotFound(ItemNotFound): + """ + Raised for invalid place IDs. + """ + pass + + +class PluginNotFound(ItemNotFound): + """ + Raised for invalid plugin IDs. + """ + pass + + +class UniverseNotFound(ItemNotFound): + """ + Raised for invalid universe IDs. + """ + pass + + +class UserNotFound(ItemNotFound): + """ + Raised for invalid user IDs or usernames. + """ + pass diff --git a/roblox/utilities/iterators.py b/roblox/utilities/iterators.py new file mode 100644 index 00000000..11ec82f3 --- /dev/null +++ b/roblox/utilities/iterators.py @@ -0,0 +1,325 @@ +""" + +This module contains iterators used internally by ro.py to provide paginated information. + +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..client import Client + +from enum import Enum +from typing import Callable, Optional, AsyncIterator + +from .exceptions import NoMoreItems + + +class SortOrder(Enum): + """ + Order in which page data should load in. + """ + + Ascending = "Asc" + Descending = "Desc" + + +class IteratorItems(AsyncIterator): + """ + Represents the items inside of an iterator. + """ + + def __init__(self, iterator: RobloxIterator, max_items: Optional[int] = None): + self._iterator = iterator + self._position: int = 0 + self._global_position: int = 0 + self._items: list = [] + self._max_items = max_items + + def __aiter__(self): + self._position = 0 + self._items = [] + return self + + async def __anext__(self): + if self._position == len(self._items): + # we are at the end of our current page of items. start again with a new page + self._position = 0 + try: + # get new items + self._items = await self._iterator.next() + except NoMoreItems: + # if there aren't any more items, reset and break the loop + self._position = 0 + self._global_position = 0 + self._items = [] + raise StopAsyncIteration + + if self._max_items is not None and self._global_position >= self._max_items: + raise StopAsyncIteration + + # if we got here we know there are more items + try: + item = self._items[self._position] + except IndexError: + # edge case for group roles + raise StopAsyncIteration + # we advance the iterator by one for the next iteration + self._position += 1 + self._global_position += 1 + return item + + +class IteratorPages(AsyncIterator): + """ + Represents the pages inside of an iterator. + """ + + def __init__(self, iterator: RobloxIterator): + self._iterator = iterator + + def __aiter__(self): + return self + + async def __anext__(self): + try: + page = await self._iterator.next() + return page + except NoMoreItems: + raise StopAsyncIteration + + +class RobloxIterator: + """ + Represents a basic iterator which all iterators should implement. + """ + + def __init__(self, max_items: int = None): + self.max_items: Optional[int] = max_items + + async def next(self): + """ + Moves to the next page and returns that page's data. + """ + + raise NotImplementedError + + async def flatten(self, max_items: int = None) -> list: + """ + Flattens the data into a list. + """ + if max_items is None: + max_items = self.max_items + + items: list = [] + + while True: + try: + new_items = await self.next() + items += new_items + except NoMoreItems: + break + + if max_items is not None and len(items) >= max_items: + break + + return items[:max_items] + + def __aiter__(self): + return IteratorItems( + iterator=self, + max_items=self.max_items + ) + + def items(self, max_items: int = None) -> IteratorItems: + """ + Returns an AsyncIterable containing each iterator item. + """ + if max_items is None: + max_items = self.max_items + return IteratorItems( + iterator=self, + max_items=max_items + ) + + def pages(self) -> IteratorPages: + """ + Returns an AsyncIterable containing each iterator page. Each page is a list of items. + """ + return IteratorPages(self) + + +class PageIterator(RobloxIterator): + """ + Represents a cursor-based, paginated Roblox object. Learn more about iterators in the pagination tutorial: + [Pagination](../../tutorials/pagination.md) + For more information about how cursor-based pagination works, see https://robloxapi.wiki/wiki/Pagination. + + Attributes: + _client: The Client. + url: The endpoint to hit for new page data. + sort_order: The sort order to use for returned data. + page_size: How much data should be returned per-page. + extra_parameters: Extra parameters to pass to the endpoint. + handler: A callable object to use to convert raw endpoint data to parsed objects. + handler_kwargs: Extra keyword arguments to pass to the handler. + next_cursor: Cursor to use to advance to the next page. + previous_cursor: Cursor to use to advance to the previous page. + iterator_position: What position in the iterator_items the iterator is currently at. + iterator_items: List of current items the iterator is working on. + """ + + def __init__( + self, + client: Client, + url: str, + sort_order: SortOrder = SortOrder.Ascending, + page_size: int = 10, + max_items: int = None, + extra_parameters: Optional[dict] = None, + handler: Optional[Callable] = None, + handler_kwargs: Optional[dict] = None + ): + """ + Parameters: + client: The Client. + url: The endpoint to hit for new page data. + sort_order: The sort order to use for returned data. + page_size: How much data should be returned per-page. + max_items: The maximum amount of items to return when this iterator is looped through. + extra_parameters: Extra parameters to pass to the endpoint. + handler: A callable object to use to convert raw endpoint data to parsed objects. + handler_kwargs: Extra keyword arguments to pass to the handler. + """ + super().__init__(max_items=max_items) + + self._client: Client = client + + # store some basic arguments in the object + self.url: str = url + self.sort_order: SortOrder = sort_order + self.page_size: int = page_size + + self.extra_parameters: dict = extra_parameters or {} + self.handler: Callable = handler + self.handler_kwargs: dict = handler_kwargs or {} + + # cursors to use for next, previous + self.next_cursor: str = "" + self.previous_cursor: str = "" + + # iter values + self.iterator_position: int = 0 + self.iterator_items: list = [] + self.next_started: bool = False + + async def next(self): + """ + Advances the iterator to the next page. + """ + if self.next_started and not self.next_cursor: + """ + If we just started and there is no cursor, this is the last page, because we can go back but not forward. + We should raise an exception here. + """ + raise NoMoreItems("No more items.") + + if not self.next_started: + self.next_started = True + + page_response = await self._client.requests.get( + url=self.url, + params={ + "cursor": self.next_cursor, + "limit": self.page_size, + "sortOrder": self.sort_order.value, + **self.extra_parameters + } + ) + page_data = page_response.json() + + # fill in cursors + self.next_cursor = page_data["nextPageCursor"] + self.previous_cursor = page_data["previousPageCursor"] + + data = page_data["data"] + + if self.handler: + data = [ + self.handler( + client=self._client, + data=item_data, + **self.handler_kwargs + ) for item_data in data + ] + + return data + + +class PageNumberIterator(RobloxIterator): + """ + Represents an iterator that is advanced with page numbers and sizes, like those seen on chat.roblox.com. + + Attributes: + url: The endpoint to hit for new page data. + page_number: The current page number. + page_size: The size of each page. + extra_parameters: Extra parameters to pass to the endpoint. + handler: A callable object to use to convert raw endpoint data to parsed objects. + handler_kwargs: Extra keyword arguments to pass to the handler. + """ + + def __init__( + self, + client: Client, + url: str, + page_size: int = 10, + extra_parameters: Optional[dict] = None, + handler: Optional[Callable] = None, + handler_kwargs: Optional[dict] = None + ): + super().__init__() + + self._client: Client = client + + self.url: str = url + self.page_number: int = 1 + self.page_size: int = page_size + + self.extra_parameters: dict = extra_parameters or {} + self.handler: Callable = handler + self.handler_kwargs: dict = handler_kwargs or {} + + self.iterator_position = 0 + self.iterator_items = [] + + async def next(self): + """ + Advances the iterator to the next page. + """ + page_response = await self._client.requests.get( + url=self.url, + params={ + "pageNumber": self.page_number, + "pageSize": self.page_size, + **self.extra_parameters + } + ) + data = page_response.json() + + if len(data) == 0: + raise NoMoreItems("No more items.") + + self.page_number += 1 + + if self.handler: + data = [ + self.handler( + client=self._client, + data=item_data, + **self.handler_kwargs + ) for item_data in data + ] + + return data diff --git a/roblox/utilities/requests.py b/roblox/utilities/requests.py new file mode 100644 index 00000000..bd1937d0 --- /dev/null +++ b/roblox/utilities/requests.py @@ -0,0 +1,167 @@ +""" + +This module contains classes used internally by ro.py for sending requests to Roblox endpoints. + +""" + +from __future__ import annotations + +import asyncio +from json import JSONDecodeError +from typing import Dict + +from httpx import AsyncClient, Response + +from .exceptions import get_exception_from_status_code + +_xcsrf_allowed_methods: Dict[str, bool] = { + "post": True, + "put": True, + "patch": True, + "delete": True +} + + +class CleanAsyncClient(AsyncClient): + """ + This is a clean-on-delete version of httpx.AsyncClient. + """ + + def __init__(self): + super().__init__() + + def __del__(self): + try: + asyncio.get_event_loop().create_task(self.aclose()) + except RuntimeError: + pass + + +class Requests: + """ + A special request object that implements special functionality required to connect to some Roblox endpoints. + + Attributes: + session: Base session object to use when sending requests. + xcsrf_token_name: The header that will contain the Cross-Site Request Forgery token. + """ + + def __init__( + self, + session: CleanAsyncClient = None, + xcsrf_token_name: str = "X-CSRF-Token" + ): + """ + Arguments: + session: A custom session object to use for sending requests, compatible with httpx.AsyncClient. + xcsrf_token_name: The header to place X-CSRF-Token data into. + """ + self.session: CleanAsyncClient + + if session is None: + self.session = CleanAsyncClient() + else: + self.session = session + + self.xcsrf_token_name: str = xcsrf_token_name + + self.session.headers["User-Agent"] = "Roblox/WinInet" + self.session.headers["Referer"] = "www.roblox.com" + + async def request(self, method: str, *args, **kwargs) -> Response: + """ + Arguments: + method: The request method. + + Returns: + An HTTP response. + """ + + handle_xcsrf_token = kwargs.pop("handle_xcsrf_token", True) + skip_roblox = kwargs.pop("skip_roblox", False) + + response = await self.session.request(method, *args, **kwargs) + + if skip_roblox: + return response + + method = method.lower() + + if handle_xcsrf_token and self.xcsrf_token_name in response.headers and _xcsrf_allowed_methods.get(method): + self.session.headers[self.xcsrf_token_name] = response.headers[self.xcsrf_token_name] + if response.status_code == 403: # Request failed, send it again + response = await self.session.request(method, *args, **kwargs) + + if kwargs.get("stream"): + # Streamed responses should not be decoded, so we immediately return the response. + return response + + if response.is_error: + # Something went wrong, parse an error + content_type = response.headers.get("Content-Type") + errors = None + if content_type and content_type.startswith("application/json"): + data = None + try: + data = response.json() + except JSONDecodeError: + pass + errors = data and data.get("errors") + + exception = get_exception_from_status_code(response.status_code)( + response=response, + errors=errors + ) + raise exception + else: + return response + + async def get(self, *args, **kwargs) -> Response: + """ + Sends a GET request. + + Returns: + An HTTP response. + """ + + return await self.request("GET", *args, **kwargs) + + async def post(self, *args, **kwargs) -> Response: + """ + Sends a POST request. + + Returns: + An HTTP response. + """ + + return await self.request("POST", *args, **kwargs) + + async def put(self, *args, **kwargs) -> Response: + """ + Sends a PATCH request. + + Returns: + An HTTP response. + """ + + return await self.request("PUT", *args, **kwargs) + + async def patch(self, *args, **kwargs) -> Response: + """ + Sends a PATCH request. + + Returns: + An HTTP response. + """ + + return await self.request("PATCH", *args, **kwargs) + + async def delete(self, *args, **kwargs) -> Response: + """ + Sends a DELETE request. + + Returns: + An HTTP response. + """ + + return await self.request("DELETE", *args, **kwargs) diff --git a/roblox/utilities/types.py b/roblox/utilities/types.py new file mode 100644 index 00000000..7d66439b --- /dev/null +++ b/roblox/utilities/types.py @@ -0,0 +1,25 @@ +""" + +Contains types used internally by ro.py. + +""" + +from typing import Union + +from ..bases.baseasset import BaseAsset +from ..bases.basebadge import BaseBadge +from ..bases.basegamepass import BaseGamePass +from ..bases.basegroup import BaseGroup +from ..bases.baseplace import BasePlace +from ..bases.baserole import BaseRole +from ..bases.baseuniverse import BaseUniverse +from ..bases.baseuser import BaseUser + +AssetOrAssetId = Union[BaseAsset, int] +BadgeOrBadgeId = Union[BaseBadge, int] +GamePassOrGamePassId = Union[BaseGamePass, int] +GroupOrGroupId = Union[BaseGroup, int] +PlaceOrPlaceId = Union[BasePlace, int] +UniverseOrUniverseId = Union[BaseUniverse, int] +UserOrUserId = Union[BaseUser, int] +RoleOrRoleId = Union[BaseRole, int] diff --git a/roblox/utilities/url.py b/roblox/utilities/url.py new file mode 100644 index 00000000..f8bb7348 --- /dev/null +++ b/roblox/utilities/url.py @@ -0,0 +1,50 @@ +""" + +This module contains functions and objects used internally by ro.py to generate URLs. + +""" + +root_site = "roblox.com" +cdn_site = "rbxcdn.com" + + +class URLGenerator: + """ + Generates URLs based on a chosen base URL. + + Attributes: + base_url: The base URL. + """ + + def __init__(self, base_url: str): + self.base_url = base_url + + def get_subdomain(self, subdomain: str, protocol: str = "https") -> str: + """ + Returns the full URL of a subdomain, given the base subdomain name. + + Arguments: + subdomain: The URL subdomain. + protocol: The URL protocol. + """ + return f"{protocol}://{subdomain}.{self.base_url}" + + def get_url( + self, + subdomain: str, + path: str = "", + base_url: str = None, + protocol: str = "https", + ) -> str: + """ + Returns a full URL, given a subdomain name, protocol, and path. + + Arguments: + subdomain: The URL subdomain. + protocol: The URL protocol. + path: The URL path. + base_url: The base URL. + """ + if base_url is None: + base_url = self.base_url + return f"{protocol}://{subdomain}.{base_url}/{path}" diff --git a/roblox/wall.py b/roblox/wall.py new file mode 100644 index 00000000..f3f8b52f --- /dev/null +++ b/roblox/wall.py @@ -0,0 +1,90 @@ +""" +Contains objects related to Roblox group walls. +""" + +from __future__ import annotations + +from datetime import datetime +from typing import Optional, Union, TYPE_CHECKING + +from dateutil.parser import parse + +from .members import Member + +if TYPE_CHECKING: + from .client import Client + from .bases.basegroup import BaseGroup + + +class WallPostRelationship: + """ + Represents a Roblox wall post ID. + + Attributes: + id: The post ID. + group: The group whose wall this post exists on. + """ + + def __init__(self, client: Client, post_id: int, group: Union[BaseGroup, int]): + """ + Arguments: + client: The Client. + post_id: The post ID. + """ + + self._client: Client = client + self.id: int = post_id + + self.group: BaseGroup + + if isinstance(group, int): + self.group = BaseGroup(client=self._client, group_id=group) + else: + self.group = group + + async def delete(self): + """ + Deletes this wall post. + """ + await self._client.requests.delete( + url=self._client.url_generator.get_url("groups", f"v1/groups/{self.group.id}/wall/posts/{self.id}") + ) + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.id} group={self.group}>" + + +class WallPost(WallPostRelationship): + """ + Represents a post on a Roblox group wall. + + Attributes: + id: The post ID. + poster: The member who made the post. + body: Body of the post. + created: Creation date of the post. + updated: Last updated date of the post. + """ + + def __init__(self, client: Client, data: dict, group: BaseGroup): + self._client: Client = client + + self.id: int = data["id"] + + super().__init__( + client=self._client, + post_id=self.id, + group=group + ) + + self.poster: Optional[Member] = data["poster"] and Member( + client=self._client, + data=data["poster"], + group=self.group + ) or None + self.body: str = data["body"] + self.created: datetime = parse(data["created"]) + self.updated: datetime = parse(data["updated"]) + + def __repr__(self): + return f"<{self.__class__.__name__} id={self.id} body={self.body!r} group={self.group}>" diff --git a/setup.py b/setup.py index 3d326d14..737095d8 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,39 @@ import setuptools -import setup_info -with open("README.md", "r") as fh: +with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read() -setuptools.setup(**setup_info.setup_info) +setup_info = { + "name": "roblox", + "version": "2.0.0", + "author": "jmkdev", + "author_email": "jmk@jmksite.dev", + "description": "ro.py is a modern object-oriented asynchronous Python wrapper for the Roblox web API.", + "long_description": long_description, + "long_description_content_type": "text/markdown", + "url": "https://github.com/ro-py/ro.py", + "packages": setuptools.find_packages(), + "classifiers": [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Framework :: AsyncIO", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", + "Topic :: Software Development :: Libraries" + ], + "project_urls": { + "Discord": "https://discord.gg/tjRfCbDMSk", + "Issue Tracker": "https://github.com/ro-py/ro.py/issues", + "GitHub": "https://github.com/ro-py/ro.py/", + "Examples": "https://github.com/ro-py/ro.py/tree/main/examples", + "Twitter": "https://twitter.com/jmkdev" + }, + "python_requires": '>=3.7', + "install_requires": [ + "httpx>=0.21.0", + "python-dateutil>=2.8.0" + ] +} + + +setuptools.setup(**setup_info) diff --git a/setup_info.py b/setup_info.py deleted file mode 100644 index 151c3347..00000000 --- a/setup_info.py +++ /dev/null @@ -1,30 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup_info = { - "name": "ro-py", - "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.", - "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" - ] -}