diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7791ee9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [main] + paths: + - "discord/**" + pull_request: + branches: [main] + paths: + - "discord/**" + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + defaults: + run: + working-directory: discord + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: pnpm typecheck + + - name: Lint + run: pnpm lint + + - name: Test + run: pnpm test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0df1655..e41918c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,24 +1,12 @@ -# 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" +name: CodeQL on: push: - branches: [ main ] + branches: [main] pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] + branches: [main] schedule: - - cron: '28 0 * * 0' + - cron: "28 0 * * 0" jobs: analyze: @@ -32,39 +20,19 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support + language: ['javascript-typescript'] steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v7 - # 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 + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} - # 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 + - name: Autobuild + uses: github/codeql-action/autobuild@v4 - # ℹ️ 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 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/container-images.yml b/.github/workflows/container-images.yml index 0d8827c..aff6be3 100644 --- a/.github/workflows/container-images.yml +++ b/.github/workflows/container-images.yml @@ -7,7 +7,6 @@ on: paths: - ".github/workflows/container-images.yml" - "discord/**" - - "tex/**" - "README.md" - "coderunbot.env.example" pull_request: @@ -16,7 +15,6 @@ on: paths: - ".github/workflows/container-images.yml" - "discord/**" - - "tex/**" - "README.md" - "coderunbot.env.example" workflow_dispatch: @@ -39,23 +37,19 @@ jobs: - image_name: coderunbot-discord context: ./discord file: ./discord/Containerfile - - image_name: coderunbot-tex - context: ./tex - file: ./tex/Dockerfile - steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: gaato @@ -63,7 +57,7 @@ jobs: - name: Extract image metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/gaato/${{ matrix.image_name }} tags: | @@ -73,7 +67,7 @@ jobs: type=sha,prefix=sha- - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.file }} diff --git a/LICENSE b/LICENSE index 261eeb9..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,661 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 656024c..bdff352 100644 --- a/README.md +++ b/README.md @@ -12,54 +12,68 @@ This is a bot for internal use that adds other functions to CodeRunBot. Also, th ## Links -- [Privacy Policy](bots/config/privacy-policy.md) +- [Privacy Policy](discord/config/privacy-policy.md) - [Invite Link](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot) - [Discord Server](https://discord.gg/qRpYRTgvXM) ## Requirements -- Python >= 3.14 -- [tex.gaato.net](https://github.com/gaato/tex.gaato.net) +- Node.js >= 24 +- [pnpm](https://pnpm.io/) ## How to run (CodeRunBot) Set `CODERUNBOT_TOKEN` in the environment file. -### with uv (recommended) +### with pnpm ```bash -$ cd discord -$ uv sync --locked -$ uv run python -m bots +cd discord +pnpm install +pnpm build +pnpm start ``` -### without uv +### with Podman (or Docker) ```bash -$ python -m venv .venv -$ . .venv/bin/activate -$ pip install aiohttp google-api-python-client google-auth-oauthlib iso639-lang openai "py-cord[voice]>=2.6" python-dotenv -$ python -m bots +podman build -t coderunbot discord +podman run --rm --env-file coderunbot.env coderunbot ``` +Add `-v ./discord/data:/app/data` to persist the opt-out list across runs +when using the default local state backend. + ## How to run (gaato bot) -Basically the same as CodeRunBot, but set the environment file to include `GAATO_BOT_TOKEN`, `GOOGLE_API_KEY` and `WOLFRAM_APPID` and the execution command is as follows. You also need ffmpeg. +Basically the same as CodeRunBot, but set the environment file to include `GAATO_BOT_TOKEN` and `WOLFRAM_APPID`, then run: ```bash -$ GAATO_BOT=1 uv run python -m bots +cd discord +GAATO_BOT=1 pnpm start ``` +With Podman, add `-e GAATO_BOT=1` to the `podman run` command instead. + ## For developer Pull requests are welcome. -Please use [uv](https://docs.astral.sh/uv/) to manage dependencies. -Do not rewrite `uv.lock` by hand. +Please use [pnpm](https://pnpm.io/) to manage dependencies. +Do not rewrite `pnpm-lock.yaml` by hand. +TypeScript is pinned to the 6.x line because dependency-cruiser 18 does not +support TypeScript 7 yet. ## Environment For containerized or service-manager based deployment, keep secrets outside the repo and point the process at an environment file. - Example file: [`coderunbot.env.example`](coderunbot.env.example) -- `TEX_BASE_URL` defaults to `http://127.0.0.1:8080` -- The TeX renderer can be started with `HOST` and `PORT` overrides + +## License + +Copyright (C) Gakuto Furuya + +This program is free software, licensed under the +[GNU Affero General Public License v3.0 or later](LICENSE) (AGPL-3.0-or-later). +If you run a modified version of this bot as a network service, the AGPL +requires you to offer its source code to the users interacting with it. diff --git a/coderunbot.env.example b/coderunbot.env.example index c21b9f3..4c33527 100644 --- a/coderunbot.env.example +++ b/coderunbot.env.example @@ -9,8 +9,3 @@ GAATO_BOT_TOKEN= # ---- Optional features ---- OPENAI_API_KEY= WOLFRAM_APPID= -# GOOGLE_API_KEY= # mentioned in README; may be unused depending on enabled cogs - -# ---- TeX renderer endpoint ---- -# TEX_BASE_URL defaults to http://127.0.0.1:8080 when unset -TEX_BASE_URL=http://127.0.0.1:8080 diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 10b44f6..0000000 --- a/compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.8' - -services: - coderunbot: - build: discord - env_file: .env - restart: unless-stopped - gaato-bot: - build: discord - env_file: .env - environment: - - GAATO_BOT=1 - restart: unless-stopped - tex: - build: tex - restart: unless-stopped diff --git a/discord/.dependency-cruiser.cjs b/discord/.dependency-cruiser.cjs new file mode 100644 index 0000000..e38a407 --- /dev/null +++ b/discord/.dependency-cruiser.cjs @@ -0,0 +1,32 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: "no-cross-feature-imports", + comment: "Features are isolated and may only depend on platform/shared code.", + severity: "error", + scope: "folder", + from: { path: "^src/features/([^/]+)$" }, + to: { + path: "^src/features/[^/]+$", + pathNot: "^src/features/$1$", + }, + }, + { + name: "platform-shared-do-not-know-features", + severity: "error", + from: { path: "^src/(platform|shared)/" }, + to: { path: "^src/features/" }, + }, + { + name: "config-does-not-know-features", + severity: "error", + from: { path: "^src/config\\.ts$" }, + to: { path: "^src/features/" }, + }, + ], + options: { + doNotFollow: { path: "node_modules" }, + tsConfig: { fileName: "tsconfig.json" }, + }, +}; diff --git a/discord/.dockerignore b/discord/.dockerignore index da0ab4e..ddc3c2a 100644 --- a/discord/.dockerignore +++ b/discord/.dockerignore @@ -1,6 +1,3 @@ .env -.venv -__pycache__/ -*.pyc -*.pyo -*.pyd +node_modules/ +dist/ diff --git a/discord/.gitignore b/discord/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/discord/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/discord/Containerfile b/discord/Containerfile index 76ef6ac..0b3de2a 100644 --- a/discord/Containerfile +++ b/discord/Containerfile @@ -1,23 +1,36 @@ -FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim +FROM node:24-slim AS build WORKDIR /app -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PYTHONPATH=/app \ - UV_LINK_MODE=copy \ - UV_NO_DEV=1 \ - UV_PROJECT_ENVIRONMENT=/app/.venv +RUN corepack enable -RUN groupadd --gid 10001 app \ - && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --frozen-lockfile -COPY pyproject.toml uv.lock /app/ -RUN uv sync --locked --no-install-project \ - && chown -R app:app /app +COPY tsconfig.json ./ +COPY src ./src +RUN pnpm build \ + && pnpm prune --prod -COPY --chown=app:app bots /app/bots +FROM node:24-slim AS runtime + +WORKDIR /app + +ENV NODE_ENV=production + +RUN apt-get update \ + && apt-get install -y --no-install-recommends fonts-noto-cjk fonts-noto-cjk-extra \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid 10001 app \ + && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app \ + && mkdir -p /app/data \ + && chown app:app /app/data + +COPY --from=build --chown=app:app /app/node_modules ./node_modules +COPY --from=build --chown=app:app /app/dist ./dist +COPY --chown=app:app package.json ./package.json +COPY --chown=app:app config ./config USER app -CMD ["/app/.venv/bin/python", "-m", "bots"] +CMD ["node", "dist/index.js"] diff --git a/discord/Dockerfile b/discord/Dockerfile deleted file mode 100644 index 76ef6ac..0000000 --- a/discord/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim - -WORKDIR /app - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PYTHONPATH=/app \ - UV_LINK_MODE=copy \ - UV_NO_DEV=1 \ - UV_PROJECT_ENVIRONMENT=/app/.venv - -RUN groupadd --gid 10001 app \ - && useradd --uid 10001 --gid app --home-dir /app --shell /usr/sbin/nologin --no-create-home app - -COPY pyproject.toml uv.lock /app/ -RUN uv sync --locked --no-install-project \ - && chown -R app:app /app - -COPY --chown=app:app bots /app/bots - -USER app - -CMD ["/app/.venv/bin/python", "-m", "bots"] diff --git a/discord/biome.json b/discord/biome.json new file mode 100644 index 0000000..e9ec6b8 --- /dev/null +++ b/discord/biome.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.4/schema.json", + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noProcessEnv": "error" + } + } + }, + "overrides": [ + { + "includes": ["src/env.ts"], + "linter": { + "rules": { + "style": { + "noProcessEnv": "off" + } + } + } + } + ] +} diff --git a/discord/bots/__init__.py b/discord/bots/__init__.py deleted file mode 100644 index 1d33c33..0000000 --- a/discord/bots/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -from collections import OrderedDict -from typing import Any - -import discord - -SUPPORT_SERVER_LINK = 'discord.gg/qRpYRTgvXM' -LOG_CHANNEL_ID = 1118867011448078417 -DEVELOPER_ID = 572432137035317249 - - -class DeleteButton(discord.ui.Button): - def __init__( - self, - user: discord.abc.User, - label='Delete', - style=discord.ButtonStyle.danger, - *args: Any, - **kwargs: Any, - ): - self.user_id = user.id - super().__init__(label=label, style=style, *args, **kwargs) - - async def callback(self, interaction: discord.Interaction) -> None: - if interaction.user is None or interaction.message is None: - return - if self.user_id == interaction.user.id: - await interaction.message.delete() - - -class LimitedSizeDict(OrderedDict): - - def __init__(self, size_limit=None, *args: Any, **kwds: Any): - self.size_limit = size_limit - super().__init__(*args, **kwds) - self._check_size_limit() - - def __setitem__(self, key: Any, value: Any) -> None: - super().__setitem__(key, value) - self._check_size_limit() - - def _check_size_limit(self) -> None: - if self.size_limit is not None: - while len(self) > self.size_limit: - self.popitem(last=False) diff --git a/discord/bots/__main__.py b/discord/bots/__main__.py deleted file mode 100644 index 342be40..0000000 --- a/discord/bots/__main__.py +++ /dev/null @@ -1,31 +0,0 @@ -import os - -from bots.core.bot import Bot -from dotenv import load_dotenv - -CODERUNBOT_COGS = ["bots.cogs.TeX", "bots.cogs.Code", "bots.cogs.Privacy"] -GAATO_BOT_COGS = [ - "bots.cogs.TeX", - "bots.cogs.Code", - "bots.cogs.Privacy", - "bots.cogs.Wolfram", - "bots.cogs.Misc", - "bots.cogs.Translate", -] - - -def main() -> None: - load_dotenv(verbose=True) - - coderunbot_token = os.environ.get("CODERUNBOT_TOKEN") - gaato_bot_token = os.environ.get("GAATO_BOT_TOKEN") - - if os.environ.get("GAATO_BOT"): - Bot(gaato_bot_token, GAATO_BOT_COGS, ")").run() - return - - Bot(coderunbot_token, CODERUNBOT_COGS, "]").run() - - -if __name__ == "__main__": - main() diff --git a/discord/bots/cogs/Code.py b/discord/bots/cogs/Code.py deleted file mode 100644 index 210761e..0000000 --- a/discord/bots/cogs/Code.py +++ /dev/null @@ -1,333 +0,0 @@ -import io -import pathlib -import re -from typing import cast - -import aiohttp -import discord -import requests -from discord.ext import commands -from discord.interactions import Interaction - -from .. import DeleteButton, LimitedSizeDict - -URL = "https://wandbox.org/api/" -BASE_DIR = pathlib.Path(__file__).parent.parent - - -# dbname = BASE_DIR.parent / "db.sqlite3" -# conn = sqlite3.connect(dbname, check_same_thread=False) -# c = conn.cursor() -# c.execute( -# "CREATE TABLE IF NOT EXISTS code (message_id INTEGER, author_id INTEGER, language TEXT, code TEXT, stdin TEXT)" -# ) - - -def get_autocomplete_languages() -> list[str]: - with requests.get(URL + "list.json") as r: - r.raise_for_status() - result = r.json() - language_names = set( - map(lambda data: data["language"].lower().replace(" ", ""), result) - ) - print(list(language_names)) - return list(language_names) - - -autocomplete_languages = get_autocomplete_languages() - - -def auto_complete_language(ctx: discord.AutocompleteContext) -> list[str]: - return list( - filter( - lambda language_code: language_code.startswith(ctx.value.lower()), - autocomplete_languages, - ) - ) - - -async def get_languages() -> dict[str, str]: - async with aiohttp.ClientSession() as session: - async with session.get(URL + "list.json") as r: - if r.status == 200: - result = await r.json() - language_names = set(map(lambda data: data["language"], result)) - languages_dict = {} - for language_name in language_names: - # Find all compilers for this language - compilers = [ - item["name"] - for item in result - if item["language"] == language_name - ] - # Prefer stable versions: filter out HEAD, devel, or git versions - stable_compilers = [ - c for c in compilers - if "head" not in c.lower() and "devel" not in c.lower() and "git" not in c.lower() - ] - # Use stable version if available, otherwise use the first available - selected = stable_compilers[0] if stable_compilers else compilers[0] - languages_dict[language_name.lower().replace(" ", "")] = selected - return languages_dict - - -async def run_core( - author: discord.abc.User, - language: str, - code: str, - stdin: str = "", -) -> tuple[discord.Embed, list[discord.File]]: - language_dict = await get_languages() - if language not in language_dict.keys(): - embed = discord.Embed( - title="The following languages are supported", - description=", ".join(language_dict.keys()), - color=0xFF0000, - ) - embed.set_author(name=author.name) - return embed, [] - if language == "nim": - compiler_option = ( - "--hint[Processing]:off\n" - "--hint[Conf]:off\n" - "--hint[Link]:off\n" - "--hint[SuccessX]:off" - ) - else: - compiler_option = "" - url = URL + "compile.json" - params = { - "compiler": language_dict[language], - "code": code, - "stdin": stdin, - "compiler-option-raw": compiler_option, - } - async with aiohttp.ClientSession() as session: - try: - async with session.post(url, json=params) as r: - if r.status == 200: - try: - # Some responses from Wandbox may omit the Content-Type header. - # Allow parsing JSON even when content-type is missing by setting content_type=None. - result = await r.json(content_type=None) - except ValueError: - # Not JSON — read text for debugging and return error embed. - text = await r.text() - embed = discord.Embed( - title="Wandbox Error", - description=( - "Wandbox returned a non-JSON response. " - "Check the service status or your request." - ), - color=0xFF0000, - ) - max_preview = 750 - preview = text[:max_preview] + ("..." if len(text) > max_preview else "") - embed.add_field(name="Response Preview", value=f"``\n{preview}\n```") - embed.set_author(name=author.name) - return embed, [] - else: - embed = discord.Embed( - title="Connection Error", description=f"{r.status}", color=0xFF0000 - ) - embed.set_author(name=author.name) - return embed, [] - except aiohttp.ClientError as e: - embed = discord.Embed( - title="Connection Error", - description=str(e), - color=0xFF0000, - ) - embed.set_author(name=author.name) - return embed, [] - embed = discord.Embed(title=f"Result ({language_dict[language]}):") - embed_color = 0xFF0000 - files = [] - for k, v in result.items(): - if k in ("program_message", "compiler_message"): - continue - if v == "": - continue - if k == "status" and v == "0": - embed_color = 0x007000 - if language == "nim" and k == "compiler_error": - v = re.sub(r"CC: \S+\n", "", v) - if v == "": - continue - if len(v) > 1000 or len(v.split("\n")) > 100: - files.append(discord.File(io.BytesIO(v.encode()), k + ".txt")) - else: - embed.add_field( - name=k, - value="```\n" + v + "\n```", - ) - embed.color = embed_color - embed.set_author(name=author.name) - return embed, files - - -# class EditButton(discord.ui.Button): -# def __init__(self, label="Edit", style=discord.ButtonStyle.primary, **kwargs): -# super().__init__(label=label, style=style, **kwargs) - -# async def callback(self, interaction: discord.Interaction): -# c = conn.cursor() -# c.execute("SELECT * FROM code WHERE message_id = ?", (interaction.message.id,)) -# result = c.fetchone() -# if result is None: -# embed = discord.Embed( -# title="Error", description="Not found.", color=0xFF0000 -# ) -# await interaction.response.send_message(embed=embed, ephemeral=True) -# return -# await interaction.response.send_modal( -# RunModal(result[2], code=result[3], stdin=result[4]) -# ) - - -# class ViewCodeButton(discord.ui.Button): -# def __init__( -# self, label="View Code", style=discord.ButtonStyle.secondary, **kwargs -# ): -# super().__init__(label=label, style=style, **kwargs) - -# async def callback(self, interaction: discord.Interaction): -# c = conn.cursor() -# c.execute("SELECT * FROM code WHERE message_id = ?", (interaction.message.id,)) -# result = c.fetchone() -# if result is None: -# embed = discord.Embed( -# title="Error", description="Not found.", color=0xFF0000 -# ) -# await interaction.response.send_message(embed=embed, ephemeral=True) -# return -# embed = discord.Embed( -# title=result[2], -# description=f"```{result[2]}\n{result[3]}```", -# color=0x007000, -# ) -# if result[4] != "": -# embed.add_field( -# name="Standard Input", -# value=f"```\n{result[4]}\n```", -# ) -# embed.set_author( -# name=interaction.guild.get_member(result[1]).name, -# icon_url=interaction.guild.get_member(result[1]).display_avatar.url, -# ) -# embed.set_footer( -# text=f"Requested by {interaction.user.name}", -# icon_url=interaction.user.display_avatar.url, -# ) -# view = discord.ui.View(DeleteButton(interaction.user), timeout=None) -# await interaction.response.send_message(embed=embed, view=view) - - -class RunModal(discord.ui.Modal): - def __init__( - self, language: str, title="Run code", code="", stdin="", *args, **kwargs - ): - super().__init__(title=title, *args, **kwargs) - self.language = ( - language.lower() - .replace("pp", "++") - .replace("sharp", "#") - .replace("clisp", "lisp") - ) - self.add_item( - discord.ui.InputText( - label="Code", - placeholder="Write code here", - style=discord.InputTextStyle.long, - value=code, - ) - ) - self.add_item( - discord.ui.InputText( - label="Standard Input", - required=False, - style=discord.InputTextStyle.long, - value=stdin, - ) - ) - - async def callback(self, interaction: Interaction) -> None: - author = interaction.user - if author is None: - return - await interaction.response.defer(invisible=False) - code_value = cast(str, self.children[0].value or "") - stdin_value = cast(str, self.children[1].value or "") - embed, files = await run_core( - author, - self.language, - code_value, - stdin_value, - ) - embed.add_field( - name="Code", - value=f"```{self.language}\n{code_value}\n```", - ) - view = discord.ui.View(DeleteButton(author), timeout=None) - await interaction.followup.send( - embed=embed, files=files or None, view=view, wait=True - ) - # c.execute( - # "INSERT INTO code VALUES (?, ?, ?, ?, ?)", - # ( - # m.id, - # interaction.user.id, - # self.language, - # self.children[0].value, - # self.children[1].value, - # ), - # ) - - -class Code(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100) - - @commands.Cog.listener() - async def on_message_edit(self, before: discord.Message, after: discord.Message): - if before.content != after.content: - if before.id in self.user_message_id_to_bot_message: - await self.user_message_id_to_bot_message[before.id].delete() - - @commands.command() - async def run(self, ctx: commands.Context, language: str, *, code: str): - """Run code""" - code = re.sub(r"^```.*$", "", code, flags=re.MULTILINE) - view = discord.ui.View(DeleteButton(ctx.author), timeout=None) - embed, files = await run_core(ctx.author, language, code) - m = await ctx.reply(embed=embed, files=files or None, view=view) - self.user_message_id_to_bot_message[ctx.message.id] = m - - @discord.message_command() - async def escape(self, ctx: discord.ApplicationContext, message: discord.Message): - await ctx.respond( - discord.utils.escape_markdown( - discord.utils.escape_mentions(message.content) - ), - ephemeral=True, - ) - - @discord.slash_command( - name="run", - description="Run code", - options=[ - discord.Option( - name="language", - description="Language", - required=True, - autocomplete=auto_complete_language, - ) - ], - ) - async def run_slash(self, ctx: discord.ApplicationContext, language: str): - await ctx.send_modal(RunModal(language, title="Run code")) - - -def setup(bot): - return bot.add_cog(Code(bot)) diff --git a/discord/bots/cogs/Misc.py b/discord/bots/cogs/Misc.py deleted file mode 100644 index 1e7e60c..0000000 --- a/discord/bots/cogs/Misc.py +++ /dev/null @@ -1,138 +0,0 @@ -import os -import pathlib -from collections import defaultdict -from datetime import datetime, timedelta -from typing import DefaultDict, cast - -import discord -from discord.ext import commands -from openai import AsyncOpenAI -from openai.types.chat import ChatCompletionMessageParam - - -# from sudachipy import tokenizer, dictionary - - - -BASE_DIR = pathlib.Path(__file__).parent.parent - -client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) - - -class Misc(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.mention_times: DefaultDict[int, list[datetime]] = defaultdict(list) - # self.tokenizer_obj = dictionary.Dictionary().create() - - async def fetch_message_history( - self, - channel: discord.TextChannel | discord.Thread | discord.DMChannel, - limit: int = 10, - ) -> list[ChatCompletionMessageParam]: - history = await channel.history(limit=limit).flatten() - messages = [] - bot_user = self.bot.user - for message in history: - messages.append( - cast(ChatCompletionMessageParam, { - "role": "user" if message.author != bot_user else "assistant", - "content": message.content, - }) - ) - messages.reverse() - return messages - - def is_mention_limit_exceeded( - self, user_id: int, time_limit: int = 60, max_mentions: int = 3 - ) -> bool: - now = datetime.now() - mention_times = self.mention_times[user_id] - # Remove mentions older than time_limit seconds - self.mention_times[user_id] = [time for time in mention_times if now - time < timedelta(seconds=time_limit)] - # Check if mention limit is exceeded - print(self.mention_times) - return len(self.mention_times[user_id]) >= max_mentions - - @commands.Cog.listener("on_message") - async def on_mentioned(self, message: discord.Message) -> None: - if message.author.bot: - return - bot_user = self.bot.user - if bot_user is None: - return - if str(bot_user.id) in message.content: - if self.is_mention_limit_exceeded(message.author.id): - return - self.mention_times[message.author.id].append(datetime.now()) - if not isinstance( - message.channel, (discord.TextChannel, discord.Thread, discord.DMChannel) - ): - return - async with message.channel.typing(): - history = await self.fetch_message_history(message.channel, limit=10) - history.append( - cast(ChatCompletionMessageParam, { - "role": "assistant" if message.author.id == bot_user.id else "user", - "content": message.content, - }) - ) - gaato_messages: list[ChatCompletionMessageParam] = [ - cast( - ChatCompletionMessageParam, - { - "role": "system", - "content": "これはDiscordでのチャットです。" - "以下の様々なユーザーによる直近のメッセージ履歴を参考に、" - "あなたがメンションされている最後のメッセージに返信してください。", - }, - ), - *history, - ] - short_reply_messages: list[ChatCompletionMessageParam] = [ - cast( - ChatCompletionMessageParam, - { - "role": "system", - "content": "これはDiscordのチャットです。" - "以下は直近のメッセージ履歴です。" - "一言で返信してください。", - }, - ), - *history, - ] - if message.author.id == 572432137035317249: # gaato. - response = await client.chat.completions.create( - model= "gpt-4-turbo", - messages=gaato_messages, - ) - else: - response = await client.chat.completions.create( - model= "gpt-3.5-turbo", - messages=short_reply_messages, - ) - allowed_mentions = discord.AllowedMentions.none() - allowed_mentions.replied_user = True - await message.reply( - response.choices[0].message.content or "", - allowed_mentions=allowed_mentions, - ) - await self.bot.process_commands(message) - - # @discord.slash_command( - # name='sudachi', - # description='形態素解析', - # ) - # async def sudachi(self, ctx: discord.ApplicationContext, text: str): - # """形態素解析""" - # mode = tokenizer.Tokenizer.SplitMode.C - # tokens = self.tokenizer_obj.tokenize(text, mode) - # outputs = [f'{text}'] - # for t in tokens: - # outputs.append(f'{t.surface()}\t{",".join(t.part_of_speech())}\t{t.reading_form()}\t{t.normalized_form()}') - # view = discord.ui.View(DeleteButton(ctx.author), timeout=None) - # await ctx.respond(f'```\n' + '\n'.join(outputs) + '\n```', view=view) - - -def setup(bot: commands.Bot): - return bot.add_cog(Misc(bot)) diff --git a/discord/bots/cogs/Privacy.py b/discord/bots/cogs/Privacy.py deleted file mode 100644 index ff24090..0000000 --- a/discord/bots/cogs/Privacy.py +++ /dev/null @@ -1,43 +0,0 @@ -import pathlib - -import discord -from discord.commands import slash_command -from discord.ext import commands - -from bots.core.bot import OPT_OUT_USERS - -BASE_DIR = pathlib.Path(__file__).parent.parent - - -class Privacy(commands.Cog): - - def __init__(self, bot: commands.Bot): - self.bot = bot - - @slash_command(name='privacy-policy') - async def privacy_policy(self, ctx: discord.ApplicationContext): - """Show the privacy policy""" - with open(BASE_DIR / 'config' / 'privacy-policy.md', 'r') as f: - await ctx.respond(f.read()) - - @slash_command(name='opt-out') - async def opt_out(self, ctx: discord.ApplicationContext): - """Opt out of your message content data to be tracked""" - if ctx.author.id in OPT_OUT_USERS: - await ctx.respond('Your message content is already off-track. To use other commands, please use the /opt-in command.') - else: - OPT_OUT_USERS.add(ctx.author.id) - await ctx.respond('This bot will not track your message content from now on. Most commands will no longer respond.') - - @slash_command(name='opt-in') - async def opt_in(self, ctx: discord.ApplicationContext): - """Opt out of your message content data to be tracked""" - if ctx.author.id in OPT_OUT_USERS: - OPT_OUT_USERS.remove(ctx.author.id) - await ctx.respond('This bot will now track the content of your messages. It will only be used to provide commands. Use the /privacy-policy command to view the privacy policy.') - else: - await ctx.respond('This bot is already tracking your message content.') - - -def setup(bot): - return bot.add_cog(Privacy(bot)) diff --git a/discord/bots/cogs/TeX.py b/discord/bots/cogs/TeX.py deleted file mode 100644 index 0da5346..0000000 --- a/discord/bots/cogs/TeX.py +++ /dev/null @@ -1,161 +0,0 @@ -import io -import os -import pathlib -from typing import Optional, cast - -import aiohttp -import discord -from discord.ext import commands - -from .. import DeleteButton, LimitedSizeDict - -BASE_DIR = pathlib.Path(__file__).parent.parent - - -async def respond_core( - author: discord.abc.User, code: str, spoiler: bool -) -> tuple[str, discord.Embed, Optional[discord.File]]: - base_url = os.environ.get("TEX_BASE_URL", "http://127.0.0.1:8080").rstrip("/") - url = f"{base_url}/render/png" - params = {"latex": code} - headers = {"Content-Type": "application/json"} - async with aiohttp.ClientSession() as session: - async with session.post(url, json=params, headers=headers) as r: - if r.status != 200: - error_message = await r.text() - embed = discord.Embed( - title="Rendering Error", - description=f"```\n{error_message}\n```", - color=0xFF0000, - ) - embed.set_author( - name=author.name, - ) - return "", embed, None - - result = await r.read() - file = discord.File(io.BytesIO(result), filename="tex.png", spoiler=spoiler) - embed = discord.Embed(color=0x008000) - embed.set_author(name=author.name) - if not spoiler: - embed.set_image(url="attachment://tex.png") - if "\\\\" in code and "\\begin" not in code and "\\end" not in code: - embed.add_field( - name="Hint", value="You can use gather or align environment." - ) - return "", embed, file - - -class TeXModal(discord.ui.Modal): - def __init__( - self, spoiler, env=None, value="", title="LaTeX to Image", *arg, **kwargs - ): - self.spoiler = spoiler - if env: - value = f"\\begin{{{env}}}\n{value}\n\\end{{{env}}}" - super().__init__(title=title, *arg, **kwargs) - self.add_item( - discord.ui.InputText( - label="Code", - placeholder="Input TeX code here", - style=discord.InputTextStyle.long, - value=value, - ) - ) - - async def callback(self, interaction: discord.Interaction) -> None: - author = interaction.user - if author is None: - return - await interaction.response.defer(invisible=False) - code_value = cast(str, self.children[0].value or "") - content, embed, file = await respond_core( - author, - code_value, - self.spoiler, - ) - embed.add_field( - name="Code", - value=f"```tex\n{code_value}\n```", - ) - view = discord.ui.View(DeleteButton(author), timeout=None) - if file is None: - await interaction.followup.send( - content=content, embed=embed, view=view, wait=True - ) - else: - await interaction.followup.send( - content=content, embed=embed, file=file, view=view, wait=True - ) - - -class TeX(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100) - - @commands.Cog.listener() - async def on_message_edit(self, before: discord.Message, after: discord.Message): - if before.content != after.content: - if before.id in self.user_message_id_to_bot_message: - await self.user_message_id_to_bot_message[before.id].delete() - - async def respond(self, ctx: commands.Context, code: str, spoiler: bool): - async with ctx.channel.typing(): - view = discord.ui.View(DeleteButton(ctx.author), timeout=None) - code = code.replace("```tex", "").replace("```", "").strip() - content, embed, file = await respond_core(ctx.author, code, spoiler) - if file is None: - m = await ctx.reply(content=content, embed=embed, view=view) - else: - m = await ctx.reply(content=content, embed=embed, file=file, view=view) - return m - - @commands.command() - async def tex(self, ctx: commands.Context, *, code: str): - """LaTeX to image (in math mode)""" - m = await self.respond(ctx, code, False) - self.user_message_id_to_bot_message[ctx.message.id] = m - - @commands.command() - async def stex(self, ctx: commands.Context, *, code: str): - """LaTeX to spoiler image (in math mode)""" - m = await self.respond(ctx, code, True) - self.user_message_id_to_bot_message[ctx.message.id] = m - - @discord.slash_command( - name="tex", - description="TeX to image", - options=[ - discord.Option( - type=str, - name="env", - description="The environment to use", - required=False, - choices=[ - discord.OptionChoice(name="align", value="align"), - discord.OptionChoice(name="gather", value="gather"), - ], - ), - discord.Option( - type=bool, - name="spoiler", - description="Whether to mark the image as a spoiler", - required=False, - default=False, - ), - ], - ) - async def tex_slash( - self, - ctx: discord.ApplicationContext, - env: Optional[str] = None, - spoiler: bool = False, - ): - # modal = TeXModal(plain, spoiler) - modal = TeXModal(spoiler, env) - await ctx.send_modal(modal) - - -def setup(bot): - return bot.add_cog(TeX(bot)) diff --git a/discord/bots/cogs/Translate.py b/discord/bots/cogs/Translate.py deleted file mode 100644 index 1d88c2d..0000000 --- a/discord/bots/cogs/Translate.py +++ /dev/null @@ -1,121 +0,0 @@ -import os - -import discord -import dotenv -import iso639 -from discord.ext import commands -from iso639.exceptions import InvalidLanguageValue -from openai import AsyncOpenAI - -from .. import DeleteButton - -dotenv.load_dotenv(verbose=True) - - -client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) - -two_letter_codes: dict[str, list[str]] = {} -three_letter_codes: dict[str, list[str]] = {} -language_names: list[str] = [] -for lang in iso639.iter_langs(): - if not lang.pt1: - continue - if lang.pt1: - if lang.pt1 in two_letter_codes: - two_letter_codes[lang.pt1].append(lang.name) - else: - two_letter_codes[lang.pt1] = [lang.name] - language_names.append(f"{lang.name}") - - -def autocomplete_language(ctx: discord.AutocompleteContext) -> list[str]: - input_value = ctx.value.lower() - if len(input_value) <= 1: - return [] - elif len(input_value) == 2: - return two_letter_codes.get(input_value, []) - elif len(input_value) == 3: - return three_letter_codes.get(input_value, []) - else: - matching_languages = [ - name for name in language_names if name.lower().startswith(input_value) - ] - matching_languages.sort(key=lambda name: len(name)) - return matching_languages[:30] - - -class Translate(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @discord.slash_command( - name="translate", - description="Translate text", - description_localizaitons={ - "ja_JP": "テキストを翻訳します", - }, - options=[ - discord.Option( - name="text", - description="Text to translate", - description_localizaitons={ - "ja_JP": "翻訳するテキスト", - }, - required=True, - ), - discord.Option( - name="to", - description="Language to translate to", - description_localizaitons={ - "ja_JP": "翻訳先の言語", - }, - autocomplete=autocomplete_language, - required=True, - ), - ], - ) - async def translate(self, ctx: discord.ApplicationContext, text: str, to: str): - """Translate text""" - await ctx.defer() - try: - lang = iso639.Lang(to) - except InvalidLanguageValue: - return await ctx.followup.send("Invalid language", ephemeral=True) - if not lang.pt1: - return await ctx.followup.send("Invalid language", ephemeral=True) - response = await client.chat.completions.create( - model="gpt-4", - messages=[ - { - "role": "system", - "content": "This is a direct translation task. " - f"Translate the following text to {lang.name}. " - "Do not add any additional comments or language indicators.", - }, - { - "role": "user", - "content": text, - }, - ], - max_tokens=2000, - ) - embed = discord.Embed( - title="Translate", - color=discord.Color.blurple(), - ) - embed.add_field( - name="Original text", - value=text, - inline=False, - ) - embed.add_field( - name=f"Translated to {to}", - value=response.choices[0].message.content or "", - inline=False, - ) - view = discord.ui.View(DeleteButton(ctx.user)) - await ctx.followup.send(embed=embed, view=view) - - -def setup(bot): - return bot.add_cog(Translate(bot)) diff --git a/discord/bots/cogs/Wolfram.py b/discord/bots/cogs/Wolfram.py deleted file mode 100644 index 3a18082..0000000 --- a/discord/bots/cogs/Wolfram.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -import pathlib -from collections import OrderedDict - -import aiohttp -import discord -import dotenv -from discord.ext import commands, pages - -from .. import SUPPORT_SERVER_LINK, DeleteButton - -dotenv.load_dotenv(verbose=True) -URL = 'http://api.wolframalpha.com/v2/query' -BASE_DIR = pathlib.Path(__file__).parent.parent - - -class LimitedSizeDict(OrderedDict): - - def __init__(self, size_limit=None, *args, **kwds): - self.size_limit = size_limit - super().__init__(*args, **kwds) - self._check_size_limit() - - def __setitem__(self, key, value): - super().__setitem__(key, value) - self._check_size_limit() - - def _check_size_limit(self): - if self.size_limit is not None: - while len(self) > self.size_limit: - self.popitem(last=False) - - -class Wolfram(commands.Cog): - - def __init__(self, bot: commands.Bot): - self.bot = bot - self.user_message_id_to_bot_message = LimitedSizeDict(size_limit=100) - - @commands.Cog.listener() - async def on_message_edit(self, before: discord.Message, after: discord.Message): - if before.content != after.content: - if before.id in self.user_message_id_to_bot_message: - await self.user_message_id_to_bot_message[before.id].delete() - - @commands.Cog.listener() - async def on_message_delete(self, message: discord.Message): - if message.id in self.user_message_id_to_bot_message: - await self.user_message_id_to_bot_message[message.id].delete() - - - @commands.command(aliases=['wolfram']) - async def wolf(self, ctx: commands.Context, *, query: str): - - async with ctx.channel.typing(): - view = discord.ui.View(DeleteButton(ctx.author), timeout=None) - - async with aiohttp.ClientSession() as session: - async with session.get(URL, params={'input': query, 'format': 'image,plaintext', 'output': 'JSON', 'appid': os.environ.get('WOLFRAM_APPID')}) as resp: - if resp.status == 200: - data = await resp.json() - else: - embed = discord.Embed( - title='Connection Error', - description=f'{resp.status}', - color=0xff0000 - ) - embed.set_author( - name=ctx.author.name, - icon_url=ctx.author.display_avatar.url - ) - self.user_message_id_to_bot_message[ctx.message.id] = await ctx.reply(content=f'Please Report us!\n{SUPPORT_SERVER_LINK}', embed=embed, view=view) - return - - page_list = [] - if data['queryresult']['success']: - for pod in data['queryresult']['pods']: - for subpod in pod['subpods']: - embed = discord.Embed( - title=pod['title'], - description=subpod['plaintext'], - color=0x00ff00, - ) - if 'img' in subpod: - embed.set_image(url=subpod['img']['src']) - embed.set_author(name=ctx.author.name, icon_url=ctx.author.display_avatar.url) - page_list.append(embed) - paginator = pages.Paginator(pages=page_list) - # paginator.add_button(DeleteButton(self.bot)) - m = await paginator.send(ctx) - else: - embed = discord.Embed( - title='Error', - description='Wolfram|Alpha は、その入力を理解できませんでした。', - color=0xff0000, - ) - embed.set_author( - name=ctx.author.name, - icon_url=ctx.author.display_avatar.url - ) - m = await ctx.reply(embed=embed, view=view) - self.user_message_id_to_bot_message[ctx.message.id] = m - -def setup(bot): - return bot.add_cog(Wolfram(bot)) diff --git a/discord/bots/cogs/__init__.py b/discord/bots/cogs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discord/bots/config/privacy-policy.md b/discord/bots/config/privacy-policy.md deleted file mode 100644 index 1724d17..0000000 --- a/discord/bots/config/privacy-policy.md +++ /dev/null @@ -1,35 +0,0 @@ -# Privacy Policy - -## Owner - -Gakuto Furuya (gaato.) - -## Bot - -- **CodeRunBot**#4348 - -## What data we collect - -- Message content of the channel where the bot can be seen -- Internal error logs (not related to the user) - -## How we use it - -### Message content - -- The bot uses to determine if a command is being called. -- If so, the bot use it to create a response. - -It is not used for any other purpose and is discarded immediately. - -### Internal error logs - -- For debugging purposes. - -## How to opt-out of providing us that data - -The /opt-out command will prevent the bot from tracking the content of your messages. Note that this command will disable most of the features of the bot. - -## An accessible way for users to request to and deletion of their data - -Originally, users data is not saved because it is immediately discarded. diff --git a/discord/bots/core/__init__.py b/discord/bots/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/discord/bots/core/bot.py b/discord/bots/core/bot.py deleted file mode 100644 index bda8709..0000000 --- a/discord/bots/core/bot.py +++ /dev/null @@ -1,167 +0,0 @@ -import io -import asyncio -import pathlib -import traceback -from typing import TypeAlias, TypeGuard - -import discord -from discord.ext import commands - -from .. import DEVELOPER_ID, LOG_CHANNEL_ID, SUPPORT_SERVER_LINK, DeleteButton -from .state import OptOutUsers, TextState - -BASE_DIR = pathlib.Path(__file__).parent.parent -OPT_OUT_USERS = OptOutUsers( - TextState(BASE_DIR / "data" / "opt-out-users.txt", "opt-out-users.txt") -) -SendableChannel: TypeAlias = ( - discord.TextChannel | discord.Thread | discord.DMChannel | discord.GroupChannel -) - - -def is_sendable_channel(channel: object) -> TypeGuard[SendableChannel]: - return isinstance( - channel, - (discord.TextChannel, discord.Thread, discord.DMChannel, discord.GroupChannel), - ) - - -def ensure_event_loop() -> asyncio.AbstractEventLoop: - try: - return asyncio.get_event_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop - - -class Bot(commands.Bot): - def __init__(self, token: str | None, cogs: list[str], prefix: str): - self.token = token - self.logging_channel: SendableChannel | None = None - self.developer: discord.User | None = None - intents = discord.Intents.default() - intents.message_content = True - ensure_event_loop() - super().__init__(command_prefix=prefix, intents=intents) - self.load_cogs(cogs) - - def load_cogs(self, cogs: list[str]) -> None: - for cog in cogs: - self.load_extension(cog) - print('Loaded ' + cog) - - async def on_ready(self) -> None: - user = self.user - if user is None: - return - print(f'Logged in as {user} (ID: {user.id})') - print(f'Pycord Version: {discord.__version__}') - channel = self.get_channel(LOG_CHANNEL_ID) - self.logging_channel = channel if is_sendable_channel(channel) else None - self.developer = self.get_user(DEVELOPER_ID) - - - async def on_message(self, message: discord.Message) -> None: - if message.author.id in OPT_OUT_USERS: - return - await super().on_message(message) - - async def on_message_edit( - self, before: discord.Message, after: discord.Message - ) -> None: - if before.content == after.content: - return - await self.on_message(after) - - async def on_command_error( - self, context: commands.Context, exception: commands.CommandError - ) -> None: - if isinstance(exception, commands.CommandNotFound): - return - if isinstance(exception, commands.UserInputError): - view = discord.ui.View(DeleteButton(context.author), timeout=None) - embed = discord.Embed( - title='Invalid Input', - description=f'```\n{exception}\n```', - color=0xff0000, - ) - embed.set_author( - name=context.author.name, - icon_url=context.author.display_avatar.url, - ) - await context.reply(embed=embed, view=view) - return - view = discord.ui.View(DeleteButton(context.author), timeout=None) - embed = discord.Embed( - title='Unhandled Error', - color=0xff0000, - ) - embed.set_author( - name=context.author.name, - icon_url=context.author.display_avatar.url, - ) - await context.reply( - content=f'Please Report us!\n{SUPPORT_SERVER_LINK}', - embed=embed, - view=view, - ) - await self.log_error(context, exception) - await super().on_command_error(context, exception) - - async def on_slash_command_error( - self, ctx: discord.ApplicationContext, exception: Exception - ) -> None: - await self.log_error(ctx, exception) - - async def log_error( - self, ctx: commands.Context | discord.ApplicationContext, exception: Exception - ) -> None: - if isinstance(ctx, commands.Context): - content = ctx.message.content - else: - command_name = ctx.command.qualified_name if ctx.command else "unknown" - option_values = " ".join(str(arg) for arg in (ctx.options or {}).values()) - content = f'/{command_name} {option_values}'.strip() - exception_text = ''.join(traceback.format_exception(type(exception), exception, exception.__traceback__)) - content_formatted = "```\n" + content + "\n```" - # attempt to resolve the channel if not cached - if self.logging_channel is None: - try: - channel = await self.fetch_channel(LOG_CHANNEL_ID) - except Exception: - print("Unable to fetch log channel", LOG_CHANNEL_ID) - print(exception_text) - return - self.logging_channel = channel if is_sendable_channel(channel) else None - if self.logging_channel is None: - print("Log channel is not sendable", LOG_CHANNEL_ID) - print(exception_text) - return - error_file = discord.File( - io.BytesIO(exception_text.encode()), - filename='error.txt', - ) - # Try to send to Discord log channel, but don't raise if it fails; fallback to stdout - try: - await self.logging_channel.send(content=content_formatted, file=error_file) - except discord.Forbidden: - print('Bot missing access to log channel:', LOG_CHANNEL_ID) - print(exception_text) - except Exception: - print('Failed to send log to channel:', LOG_CHANNEL_ID) - traceback.print_exc() - - def run(self) -> None: - if self.token is None: - print('Missing Discord token') - return - try: - self.loop.run_until_complete(self.start(self.token)) - except discord.LoginFailure: - print('Invalid Discord Token') - except KeyboardInterrupt: - print('Shutdown') - self.loop.run_until_complete(self.close()) - except Exception: - traceback.print_exc() diff --git a/discord/bots/core/state.py b/discord/bots/core/state.py deleted file mode 100644 index 10b29e1..0000000 --- a/discord/bots/core/state.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -import pathlib - -import oci -from oci.exceptions import ServiceError - - -class TextState: - def __init__(self, local_path: pathlib.Path, object_name: str) -> None: - self.local_path = local_path - self.object_name = object_name - self.backend = os.environ.get("BOT_STATE_BACKEND", "local") - self.bucket = os.environ.get("BOT_STATE_BUCKET") - self.namespace = os.environ.get("BOT_STATE_NAMESPACE") - self.prefix = os.environ.get("BOT_STATE_PREFIX", "").strip("/") - self._client = None - - @property - def key(self) -> str: - if self.prefix: - return f"{self.prefix}/{self.object_name}" - return self.object_name - - @property - def client(self): - if self._client is None: - self._client = oci.object_storage.ObjectStorageClient(self._oci_config()) - return self._client - - def _oci_config(self) -> dict[str, str]: - key_file = os.environ.get("OCI_PRIVATE_KEY_FILE") - if key_file: - return { - "user": os.environ["OCI_USER_OCID"], - "fingerprint": os.environ["OCI_FINGERPRINT"], - "tenancy": os.environ["OCI_TENANCY_OCID"], - "region": os.environ["OCI_REGION"], - "key_file": key_file, - **( - {"pass_phrase": os.environ["OCI_PRIVATE_KEY_PASSPHRASE"]} - if os.environ.get("OCI_PRIVATE_KEY_PASSPHRASE") - else {} - ), - } - - key_content = os.environ.get("OCI_PRIVATE_KEY") - if key_content: - return { - "user": os.environ["OCI_USER_OCID"], - "fingerprint": os.environ["OCI_FINGERPRINT"], - "tenancy": os.environ["OCI_TENANCY_OCID"], - "region": os.environ["OCI_REGION"], - "key_content": key_content, - **( - {"pass_phrase": os.environ["OCI_PRIVATE_KEY_PASSPHRASE"]} - if os.environ.get("OCI_PRIVATE_KEY_PASSPHRASE") - else {} - ), - } - return oci.config.from_file() - - def read(self) -> str: - if self.backend != "oci": - return self._read_local() - if not self.bucket or not self.namespace: - raise RuntimeError( - "BOT_STATE_BUCKET and BOT_STATE_NAMESPACE are required when BOT_STATE_BACKEND=oci" - ) - try: - response = self.client.get_object(self.namespace, self.bucket, self.key) - except ServiceError as error: - if error.status != 404: - raise - self.write("") - return "" - return response.data.content.decode("utf-8") - - def write(self, value: str) -> None: - if self.backend != "oci": - self._write_local(value) - return - if not self.bucket or not self.namespace: - raise RuntimeError( - "BOT_STATE_BUCKET and BOT_STATE_NAMESPACE are required when BOT_STATE_BACKEND=oci" - ) - self.client.put_object( - self.namespace, - self.bucket, - self.key, - value.encode("utf-8"), - content_type="text/plain; charset=utf-8", - ) - - def _read_local(self) -> str: - if not self.local_path.exists(): - return "" - return self.local_path.read_text() - - def _write_local(self, value: str) -> None: - self.local_path.parent.mkdir(parents=True, exist_ok=True) - self.local_path.write_text(value) - - -class OptOutUsers: - def __init__(self, state: TextState) -> None: - self.state = state - self.users = self._parse(state.read()) - - def __contains__(self, user_id: int) -> bool: - return user_id in self.users - - def add(self, user_id: int) -> None: - self.users.add(user_id) - self.save() - - def remove(self, user_id: int) -> None: - self.users.remove(user_id) - self.save() - - def save(self) -> None: - value = "".join(f"{user_id}\n" for user_id in sorted(self.users)) - self.state.write(value) - - @staticmethod - def _parse(value: str) -> set[int]: - return {int(line) for line in value.splitlines() if line.strip()} diff --git a/discord/config/privacy-policy.md b/discord/config/privacy-policy.md new file mode 100644 index 0000000..483c073 --- /dev/null +++ b/discord/config/privacy-policy.md @@ -0,0 +1,65 @@ +# Privacy Policy + +## Owner + +Gakuto Furuya (gaato.) + +## Bots + +- **CodeRunBot**#4348 +- **gaato bot** (private) + +## What data we process + +- **Message content** in channels the bot can read, used to detect and execute + commands (e.g. `]run`, `]tex`) and, for gaato bot, to reply when mentioned. + Message content is processed transiently and is not stored by us, except as + described under "Error logs" below. +- **Opt-out list**: if you use `/opt-out`, your Discord user ID is stored + persistently in cloud object storage so that the setting survives restarts. + `/opt-in` removes it. +- **Error logs**: when a command fails unexpectedly, the invoking command text + and the internal error trace are posted to a private developer-only Discord + channel for debugging. +- **Usage statistics**: anonymous aggregate counters — which command was used, + whether it succeeded, the language chosen for `/run`, and the number of servers + the bot is in — are stored in cloud object storage. These counters contain no + user IDs, no server IDs, and no message content. + +## Third-party services + +Some commands send your input to external services to do their work: + +- `/run`, `]run`: your code and standard input are sent to + [Wandbox](https://wandbox.org) for execution. +- `]wolf`, `/wolf` (gaato bot): your query is sent to Wolfram|Alpha. +- `/translate` and mention replies (gaato bot): the text to translate, or the + mentioning message together with up to 10 recent messages in the channel, is + sent to OpenAI. Messages authored by opted-out users are excluded from that + context. +- `/tex`, `]tex`: LaTeX is rendered inside the bot itself; nothing is sent to + a third party. + +These services receive only what is needed to fulfil the command and are +governed by their own privacy policies. + +## Opting out + +`/opt-out` stops all passive processing of your message content: prefix +commands, mention replies, edit tracking, and inclusion of your messages in +OpenAI context. Explicitly invoked slash commands and context menu commands +keep working, since using them is itself consent to process that input. + +## Source code + +The bots are free software (AGPL-3.0-or-later). The full source code is +available at , also shown by the +`/source` command. + +## Data deletion + +We do not build up a store of user data: message content is discarded after a +command completes. To remove your stored user ID from the opt-out list, use +`/opt-in`. For anything else (e.g. an error log that captured your command), +ask on the [support server](https://discord.gg/qRpYRTgvXM) and it will be +deleted. diff --git a/discord/bots/data/.gitignore b/discord/data/.gitignore similarity index 100% rename from discord/bots/data/.gitignore rename to discord/data/.gitignore diff --git a/discord/package.json b/discord/package.json new file mode 100644 index 0000000..cc808d5 --- /dev/null +++ b/discord/package.json @@ -0,0 +1,41 @@ +{ + "name": "coderunbot-discord", + "version": "0.1.0", + "private": true, + "license": "AGPL-3.0-or-later", + "type": "module", + "packageManager": "pnpm@11.9.0", + "engines": { + "node": ">=24" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit", + "lint": "biome check src && dependency-cruiser --config .dependency-cruiser.cjs --include-only '^src' src && madge --circular --extensions ts src", + "format": "biome format --write src", + "test": "vitest run" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.1089.0", + "@mathjax/mathjax-newcm-font": "^4.1.3", + "@mathjax/src": "^4.1.3", + "discord.js": "^14.27.0", + "dotenv": "^17.4.2", + "i18next": "^26.3.6", + "iso-639-1": "^3.1.6", + "openai": "^6.48.0", + "pino": "^10.3.1", + "sharp": "^0.35.3" + }, + "devDependencies": { + "@biomejs/biome": "^2.5.4", + "@types/node": "^24.13.3", + "dependency-cruiser": "^18.1.0", + "madge": "^8.0.0", + "tsx": "^4.23.1", + "typescript": "^6.0.3", + "vitest": "^4.1.10" + } +} diff --git a/discord/pnpm-lock.yaml b/discord/pnpm-lock.yaml new file mode 100644 index 0000000..c0944a9 --- /dev/null +++ b/discord/pnpm-lock.yaml @@ -0,0 +1,3461 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1089.0 + version: 3.1089.0 + '@mathjax/mathjax-newcm-font': + specifier: ^4.1.3 + version: 4.1.3 + '@mathjax/src': + specifier: ^4.1.3 + version: 4.1.3 + discord.js: + specifier: ^14.27.0 + version: 14.27.0 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + i18next: + specifier: ^26.3.6 + version: 26.3.6(typescript@6.0.3) + iso-639-1: + specifier: ^3.1.6 + version: 3.1.6 + openai: + specifier: ^6.48.0 + version: 6.48.0(@aws-sdk/credential-provider-node@3.972.70)(@smithy/signature-v4@5.6.6)(ws@8.21.1) + pino: + specifier: ^10.3.1 + version: 10.3.1 + sharp: + specifier: ^0.35.3 + version: 0.35.3(@types/node@24.13.3) + devDependencies: + '@biomejs/biome': + specifier: ^2.5.4 + version: 2.5.4 + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 + dependency-cruiser: + specifier: ^18.1.0 + version: 18.1.0 + madge: + specifier: ^8.0.0 + version: 8.0.0(typescript@6.0.3) + tsx: + specifier: ^4.23.1 + version: 4.23.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)) + +packages: + + '@aws-sdk/checksums@3.1000.18': + resolution: {integrity: sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1089.0': + resolution: {integrity: sha512-Sc+JOtNeP+v+XdP9Rb4Hh+uhiNO2hh/CZQbKYooNakhOIgK6/K0cjoDCEGeFmkG0vf2DopTmSctzKlcKwy1BPw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.975.3': + resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.59': + resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.61': + resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.973.4': + resolution: {integrity: sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.66': + resolution: {integrity: sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.70': + resolution: {integrity: sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.59': + resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.973.3': + resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.65': + resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.64': + resolution: {integrity: sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.33': + resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.41': + resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1088.0': + resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.36': + resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.5.4': + resolution: {integrity: sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.4': + resolution: {integrity: sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.4': + resolution: {integrity: sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.4': + resolution: {integrity: sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.4': + resolution: {integrity: sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.4': + resolution: {integrity: sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.4': + resolution: {integrity: sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.4': + resolution: {integrity: sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.4': + resolution: {integrity: sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@dependents/detective-less@5.0.3': + resolution: {integrity: sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA==} + engines: {node: '>=18'} + + '@discordjs/builders@1.14.1': + resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.2': + resolution: {integrity: sha512-c5HI3hJuRWWrnpyCZAUYfIUTAEv9weX4tE2UfDqtM3ztizdKE9RFEf3G5IWhPyO+kPldUDpGv02XJAQzdUTC9Q==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + + '@discoveryjs/json-ext@1.1.0': + resolution: {integrity: sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==} + engines: {node: '>=14.17.0'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@mathjax/mathjax-newcm-font@4.1.3': + resolution: {integrity: sha512-gzAB3dFHilHX1l5x2xUqRL+1jDQt3Fyza1DkEMVXWC4E8SvsGdlgEza47HYi2WhVcgfkvf4zgUGzuhbq3Pjlew==} + + '@mathjax/src@4.1.3': + resolution: {integrity: sha512-rIrWquuBSoJuoMBdC/1qD+AUHTorlccPicoVy6P2xbUgnuDBpCcpbHtOAsB8L3hdCHtNBg92lF8e3Fz+pkcQbw==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@smithy/core@3.29.5': + resolution: {integrity: sha512-i0dk2t5B+CwV/dcJdUHILYkOQF5lof8f44dFCfDWToGCxjT9YQ+CgHqTAvJxzc3+zqQwm2QtVoJ5IqiNar/CnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.10': + resolution: {integrity: sha512-MJenAe4OKRZUo1LdYYFDCsSHxaHvInIU/z52GsheO9vl1/VSySVCr0zkyKD6TFiGkSUaWGxvKZ/70OvgUZR5HQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.6.7': + resolution: {integrity: sha512-3zpg8yqqyXzoK2TsRDdkqVOj2RDBFfLXwCczOZ5c7TWB4eiaebfSCsbMjDPYB3PJ9ihV62QaeadZ+wLadZtNGA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.9.7': + resolution: {integrity: sha512-wCU8HCLjAtAVqxxe0j2xff9LcEPw3yjBbg5IdQDIYFnxnPxbxcSLc7rgex7kqm9L/WYOnJEgaWQlfDkZleozMA==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.6.6': + resolution: {integrity: sha512-efP6DN3UTFrzIsGO42/xcabv8jU7+9nwEdphFUH7yL0k010ERyAWaO41KFQIDLcFZLZ8xzIQr4wplFxNzslSGQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@ts-graphviz/adapter@2.0.6': + resolution: {integrity: sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==} + engines: {node: '>=18'} + + '@ts-graphviz/ast@2.0.7': + resolution: {integrity: sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==} + engines: {node: '>=18'} + + '@ts-graphviz/common@2.1.5': + resolution: {integrity: sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==} + engines: {node: '>=18'} + + '@ts-graphviz/core@2.0.7': + resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} + engines: {node: '>=18'} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@vue/compiler-core@3.5.40': + resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} + + '@vue/compiler-dom@3.5.40': + resolution: {integrity: sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==} + + '@vue/compiler-sfc@3.5.40': + resolution: {integrity: sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==} + + '@vue/compiler-ssr@3.5.40': + resolution: {integrity: sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==} + + '@vue/shared@3.5.40': + resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} + + acorn-jsx-walk@2.0.0: + resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-loose@8.5.2: + resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + app-module-path@2.2.0: + resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-module-types@6.0.2: + resolution: {integrity: sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==} + engines: {node: '>=18'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + dependency-cruiser@18.1.0: + resolution: {integrity: sha512-pbsH0gQ15BxXi97SCuMeVgsh8VzYpziGIVaMdnALbVu+TYjSZrW9/nOvsPU+NjHLmJSF9R1UijjoACq6Ne/ybQ==} + engines: {node: ^22||^24||>=26} + hasBin: true + + dependency-tree@11.5.0: + resolution: {integrity: sha512-K9zBwKDZrot3RkxizugpVSdImxULAg4Ycp3+ydy2r561k96oiiw6nfsOR15fwNDQ5BF2UXe+2JFM/H5Xz4MGQg==} + engines: {node: '>=18'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detective-amd@6.1.0: + resolution: {integrity: sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg==} + engines: {node: '>=18'} + hasBin: true + + detective-cjs@6.1.1: + resolution: {integrity: sha512-pSh7mkCKEtLlmANqLu3KDFS3NV8Hx41jy/JF1/gAWOgU+Uo5QTkeI1tWNP4dWGo4L0E9j18Ez9EPsTleautKqA==} + engines: {node: '>=18'} + + detective-es6@5.0.2: + resolution: {integrity: sha512-+qHHGYhjupiVs4rnIpI9nZ5B130A4AmE35ZX1w33hb46vcZ7T3jfDbvmPw0FhWtMHn5BS5HHu7ZtnZ53bMcXZA==} + engines: {node: '>=18'} + + detective-postcss@8.0.4: + resolution: {integrity: sha512-DZ7M/hWPZyr17ZUdoQ+TVXaPj70mYr4XXrAE+GeJbca44haCvZgb191L/jLJmFYewhxRJuBd4lUtNSu986TXag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4.47 + + detective-sass@6.0.2: + resolution: {integrity: sha512-i3xpXHDKS0qI2aFW4asQ7fqlPK00ndOVZELvQapFJCaF0VxYmsNWtd0AmvXbTLMk7bfO5VdIeorhY9KfmHVoVA==} + engines: {node: '>=18'} + + detective-scss@5.0.2: + resolution: {integrity: sha512-9JOEMZ8pDh3ShXmftq7hoQqqJsClaGgxo1hghfCeFlmKf5TC/Twtwb0PAaK8dXwpg9Z0uCmEYSrCxO+kel2eEg==} + engines: {node: '>=18'} + + detective-stylus@5.0.1: + resolution: {integrity: sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==} + engines: {node: '>=18'} + + detective-typescript@14.1.2: + resolution: {integrity: sha512-bIeEn0eVi/JRsE1YizBR2ilnMlWRAIBJJ6kXCKNFxEEWhUcEY3R6I3KYIAy48ieURbD1hcb3Ebvl8AqeoPMSzg==} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + + detective-vue2@2.3.0: + resolution: {integrity: sha512-3gwbZPqVTm9sL9XdZsgEJ7x4x99O853VVZHapQAiEkGuMJMpFPjHDrecSgfqnS5JW3FJfYXesLZGvUOibjn49g==} + engines: {node: '>=18'} + peerDependencies: + typescript: ^5.4.4 || ^6.0.2 + + discord-api-types@0.38.50: + resolution: {integrity: sha512-J2n/bpIETX3DQ6AJ7/0xbsTLmYiJQtO/LKcXKC1YDbB56OUwtDbdXOFE8Q4g8jVGHBR2VAy1+D4ngaIgkMNV9w==} + + discord.js@14.27.0: + resolution: {integrity: sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==} + engines: {node: '>=18'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + engines: {node: '>=10.13.0'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + filing-cabinet@5.5.1: + resolution: {integrity: sha512-PzLBTChlVPn6LnNxF0KWs+XqPziVh3Sfmz/3TXOymHxu6a9yhrDcQn7YwgpcRM6mqhR2WHVGPR8RU4fmcF1IVA==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-amd-module-type@6.0.2: + resolution: {integrity: sha512-7zShVYAYtMnj9S65CfN+hvpBCByfuB1OY8xID01nZEzXTZbx4YyysAfi+nMl95JSR6odt4q8TCj2W63KAoyVLQ==} + engines: {node: '>=18'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + gonzales-pe@4.3.0: + resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} + engines: {node: '>=0.6.0'} + hasBin: true + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + i18next@26.3.6: + resolution: {integrity: sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==} + peerDependencies: + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + typescript: + optional: true + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-url-superb@4.0.0: + resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} + engines: {node: '>=10'} + + iso-639-1@3.1.6: + resolution: {integrity: sha512-ZFar/L4ngX7wZh2QX+Fiftmuf0igWJsrJtfizrovWifF1gAWkfmRa5Z1m0LQZbm0hKCHRDYhLRSLFrSqNe4EJA==} + engines: {node: '>=6.0'} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + madge@8.0.0: + resolution: {integrity: sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: ^5.4.4 + peerDependenciesMeta: + typescript: + optional: true + + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mj-context-menu@1.0.0: + resolution: {integrity: sha512-OSgBFQRCVhrZwRa9lhqnKcJU92dd/YXgBjup0uyeuj9bOaVsf4myOyrjU4PfhYkdDk6AqVUTov7v5uFMvIbduA==} + + module-definition@6.0.2: + resolution: {integrity: sha512-SvAU3lB0+Yjbq55yHY3wkRZBOh+fhU1SnIF3IFbTewv6mtAh7yUT8ACHAJ2mGIJ7tCes2QuCL/cl6m0JSZ/ArA==} + engines: {node: '>=18'} + hasBin: true + + module-lookup-amd@9.1.3: + resolution: {integrity: sha512-Jc3XmOaR9FdfMJSK8+vyLgsCkzm8z2L0NS6vrlRWi12DjS7MY7TMNE7E1yj8yXx837xtMDbKSSgcdXnFlJ2YLg==} + engines: {node: '>=18'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-source-walk@7.0.2: + resolution: {integrity: sha512-71kFFjYaSshDTA8/a2HiTYPLdASWjLJxUyJxGE+ffxU+KhxSBtM9kiLUX+R2yooFdSFKMFpi4n3PFtDy6qXv8A==} + engines: {node: '>=18'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + openai@6.48.0: + resolution: {integrity: sha512-KhVp+FyV50QrXNextvL9hIU5l6ox5HYuKQjGVk7lIqprgJol90+dQXWONV6S1lRWsKA1bXjrow8RsUT14M1hNA==} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postcss-values-parser@6.0.2: + resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.2.9 + + postcss@8.5.19: + resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + engines: {node: ^10 || ^12 || >=14} + + precinct@12.3.2: + resolution: {integrity: sha512-JbJevI1K80z8e/WIyDt/4vUN/4qcfBSKKqOjJA4mosPPPb7zODKRJQV7YN7apVWN3k58nZYm/vEsLgEGYmnxwg==} + engines: {node: '>=18'} + hasBin: true + + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quote-unquote@1.0.0: + resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + + requirejs-config-file@4.0.0: + resolution: {integrity: sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==} + engines: {node: '>=10.13.0'} + + requirejs@2.3.8: + resolution: {integrity: sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==} + engines: {node: '>=0.4.0'} + hasBin: true + + resolve-dependency-path@4.0.1: + resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==} + engines: {node: '>=18'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sass-lookup@6.1.2: + resolution: {integrity: sha512-GjmndmKQBtlPil79RK72L7yc5kDXZPCQeH97bP8R8DcxtXQJO6vECExb3WP/m6+cxaV9h4ZxrSRvCkPG2v/VSw==} + engines: {node: '>=18'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + speech-rule-engine@5.0.0-rc.4: + resolution: {integrity: sha512-3dFcH2QtQNhtvUUc09TxoaEK4WG03B9Z57krFG9jocrKykKGu35BhIkWr0vgEAxZZomEWkeluff69y/EbYzP4Q==} + hasBin: true + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stream-to-array@2.3.0: + resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + stylus-lookup@6.1.2: + resolution: {integrity: sha512-O+Q/SJ8s1X2aMLh4213fQ9X/bND9M3dhSsyTRe+O1OXPcewGLiYmAtKCrnP7FDvDBaXB2ZHPkCt3zi4cJXBlCQ==} + engines: {node: '>=18'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-graphviz@2.1.6: + resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==} + engines: {node: '>=18'} + + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@6.27.0: + resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} + engines: {node: '>=18.17'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + walkdir@0.4.1: + resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} + engines: {node: '>=6.0.0'} + + watskeburt@6.0.0: + resolution: {integrity: sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==} + engines: {node: ^22.13||^24||>=26} + hasBin: true + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@aws-sdk/checksums@3.1000.18': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1089.0': + dependencies: + '@aws-sdk/checksums': 3.1000.18 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.70 + '@aws-sdk/middleware-sdk-s3': 3.972.64 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/core@3.975.3': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.36 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.29.5 + '@smithy/signature-v4': 5.6.6 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.59': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.61': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.973.4': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-login': 3.972.66 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/credential-provider-imds': 4.4.10 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.66': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.70': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-ini': 3.973.4 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/credential-provider-imds': 4.4.10 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.59': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.973.3': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/token-providers': 3.1088.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.65': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.64': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.33': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/fetch-http-handler': 5.6.7 + '@smithy/node-http-handler': 4.9.7 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.41': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.6 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1088.0': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.36': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.3.0': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@biomejs/biome@2.5.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.4 + '@biomejs/cli-darwin-x64': 2.5.4 + '@biomejs/cli-linux-arm64': 2.5.4 + '@biomejs/cli-linux-arm64-musl': 2.5.4 + '@biomejs/cli-linux-x64': 2.5.4 + '@biomejs/cli-linux-x64-musl': 2.5.4 + '@biomejs/cli-win32-arm64': 2.5.4 + '@biomejs/cli-win32-x64': 2.5.4 + + '@biomejs/cli-darwin-arm64@2.5.4': + optional: true + + '@biomejs/cli-darwin-x64@2.5.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.4': + optional: true + + '@biomejs/cli-linux-arm64@2.5.4': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.4': + optional: true + + '@biomejs/cli-linux-x64@2.5.4': + optional: true + + '@biomejs/cli-win32-arm64@2.5.4': + optional: true + + '@biomejs/cli-win32-x64@2.5.4': + optional: true + + '@dependents/detective-less@5.0.3': + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + '@discordjs/builders@1.14.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.50 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.50 + + '@discordjs/rest@2.6.2': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.5 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.50 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.27.0 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.50 + + '@discordjs/ws@1.2.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.50 + tslib: 2.8.1 + ws: 8.21.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@discoveryjs/json-ext@1.1.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.2 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': + optional: true + + '@img/sharp-win32-ia32@0.35.3': + optional: true + + '@img/sharp-win32-x64@0.35.3': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@mathjax/mathjax-newcm-font@4.1.3': {} + + '@mathjax/src@4.1.3': + dependencies: + '@mathjax/mathjax-newcm-font': 4.1.3 + mhchemparser: 4.2.1 + mj-context-menu: 1.0.0 + speech-rule-engine: 5.0.0-rc.4 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@pinojs/redact@0.4.0': {} + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.18.1 + + '@sapphire/snowflake@3.5.5': {} + + '@smithy/core@3.29.5': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.10': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.6.7': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.9.7': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.6.6': + dependencies: + '@smithy/core': 3.29.5 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@ts-graphviz/adapter@2.0.6': + dependencies: + '@ts-graphviz/common': 2.1.5 + + '@ts-graphviz/ast@2.0.7': + dependencies: + '@ts-graphviz/common': 2.1.5 + + '@ts-graphviz/common@2.1.5': {} + + '@ts-graphviz/core@2.0.7': + dependencies: + '@ts-graphviz/ast': 2.0.7 + '@ts-graphviz/common': 2.1.5 + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 24.13.3 + + '@typescript-eslint/project-service@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/tsconfig-utils@8.64.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/types@8.64.0': {} + + '@typescript-eslint/typescript-estree@8.64.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.64.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.64.0(typescript@5.9.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.64.0': + dependencies: + '@typescript-eslint/types': 8.64.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@vladfrangu/async_event_emitter@2.4.7': {} + + '@vue/compiler-core@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.40 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.40': + dependencies: + '@vue/compiler-core': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/compiler-sfc@3.5.40': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.40 + '@vue/compiler-dom': 3.5.40 + '@vue/compiler-ssr': 3.5.40 + '@vue/shared': 3.5.40 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.19 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.40': + dependencies: + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + + '@vue/shared@3.5.40': {} + + '@xmldom/xmldom@0.9.10': {} + + acorn-jsx-walk@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn-loose@8.5.2: + dependencies: + acorn: 8.17.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + app-module-path@2.2.0: {} + + assertion-error@2.0.1: {} + + ast-module-types@6.0.2: {} + + atomic-sleep@1.0.0: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bowser@2.14.1: {} + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@12.1.0: {} + + commander@14.0.3: {} + + commander@15.0.0: {} + + commander@7.2.0: {} + + commondir@1.0.1: {} + + convert-source-map@2.0.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-extend@0.6.0: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + dependency-cruiser@18.1.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + acorn-jsx-walk: 2.0.0 + acorn-loose: 8.5.2 + acorn-walk: 8.3.5 + commander: 15.0.0 + enhanced-resolve: 5.24.2 + ignore: 7.0.6 + interpret: 3.1.1 + is-installed-globally: 1.0.0 + json5: 2.2.3 + picomatch: 4.0.5 + prompts: 2.4.2 + rechoir: 0.8.0 + safe-regex: 2.1.1 + semver: 7.8.5 + tsconfig-paths-webpack-plugin: 4.2.0 + watskeburt: 6.0.0 + + dependency-tree@11.5.0: + dependencies: + '@discoveryjs/json-ext': 1.1.0 + commander: 12.1.0 + filing-cabinet: 5.5.1 + precinct: 12.3.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + detect-libc@2.1.2: {} + + detective-amd@6.1.0: + dependencies: + ast-module-types: 6.0.2 + escodegen: 2.1.0 + get-amd-module-type: 6.0.2 + node-source-walk: 7.0.2 + + detective-cjs@6.1.1: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + detective-es6@5.0.2: + dependencies: + node-source-walk: 7.0.2 + + detective-postcss@8.0.4(postcss@8.5.19): + dependencies: + is-url-superb: 4.0.0 + postcss: 8.5.19 + postcss-values-parser: 6.0.2(postcss@8.5.19) + + detective-sass@6.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-scss@5.0.2: + dependencies: + gonzales-pe: 4.3.0 + node-source-walk: 7.0.2 + + detective-stylus@5.0.1: {} + + detective-typescript@14.1.2(typescript@5.9.3): + dependencies: + '@typescript-eslint/typescript-estree': 8.64.0(typescript@5.9.3) + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + detective-vue2@2.3.0(typescript@5.9.3): + dependencies: + '@dependents/detective-less': 5.0.3 + '@vue/compiler-sfc': 3.5.40 + detective-es6: 5.0.2 + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + discord-api-types@0.38.50: {} + + discord.js@14.27.0: + dependencies: + '@discordjs/builders': 1.14.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.2 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.5 + discord-api-types: 0.38.50 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.27.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + dotenv@17.4.2: {} + + enhanced-resolve@5.24.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@7.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.1: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-visitor-keys@5.0.1: {} + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + filing-cabinet@5.5.1: + dependencies: + app-module-path: 2.2.0 + commander: 12.1.0 + enhanced-resolve: 5.24.2 + module-definition: 6.0.2 + module-lookup-amd: 9.1.3 + resolve: 1.22.12 + resolve-dependency-path: 4.0.1 + sass-lookup: 6.1.2 + stylus-lookup: 6.1.2 + tsconfig-paths: 4.2.0 + typescript: 5.9.3 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-amd-module-type@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + get-own-enumerable-property-symbols@3.0.2: {} + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + gonzales-pe@4.3.0: + dependencies: + minimist: 1.2.8 + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + i18next@26.3.6(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + + ieee754@1.2.1: {} + + ignore@7.0.6: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + interpret@3.1.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-interactive@1.0.0: {} + + is-obj@1.0.1: {} + + is-path-inside@4.0.0: {} + + is-regexp@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-url-superb@4.0.0: {} + + iso-639-1@3.1.6: {} + + json5@2.2.3: {} + + kleur@3.0.3: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash.snakecase@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@11.5.2: {} + + madge@8.0.0(typescript@6.0.3): + dependencies: + chalk: 4.1.2 + commander: 7.2.0 + commondir: 1.0.1 + debug: 4.4.3 + dependency-tree: 11.5.0 + ora: 5.4.1 + pluralize: 8.0.0 + pretty-ms: 7.0.1 + rc: 1.2.8 + stream-to-array: 2.3.0 + ts-graphviz: 2.1.6 + walkdir: 0.4.1 + optionalDependencies: + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + magic-bytes.js@1.13.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mhchemparser@4.2.1: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mj-context-menu@1.0.0: + dependencies: + rimraf: 6.1.3 + + module-definition@6.0.2: + dependencies: + ast-module-types: 6.0.2 + node-source-walk: 7.0.2 + + module-lookup-amd@9.1.3: + dependencies: + commander: 12.1.0 + requirejs: 2.3.8 + requirejs-config-file: 4.0.0 + + ms@2.1.3: {} + + nanoid@3.3.16: {} + + node-source-walk@7.0.2: + dependencies: + '@babel/parser': 7.29.7 + + obug@2.1.4: {} + + on-exit-leak-free@2.1.2: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + openai@6.48.0(@aws-sdk/credential-provider-node@3.972.70)(@smithy/signature-v4@5.6.6)(ws@8.21.1): + optionalDependencies: + '@aws-sdk/credential-provider-node': 3.972.70 + '@smithy/signature-v4': 5.6.6 + ws: 8.21.1 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + package-json-from-dist@1.0.1: {} + + parse-ms@2.1.0: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + pluralize@8.0.0: {} + + postcss-values-parser@6.0.2(postcss@8.5.19): + dependencies: + color-name: 1.1.4 + is-url-superb: 4.0.0 + postcss: 8.5.19 + quote-unquote: 1.0.0 + + postcss@8.5.19: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + precinct@12.3.2: + dependencies: + '@dependents/detective-less': 5.0.3 + commander: 12.1.0 + detective-amd: 6.1.0 + detective-cjs: 6.1.1 + detective-es6: 5.0.2 + detective-postcss: 8.0.4(postcss@8.5.19) + detective-sass: 6.0.2 + detective-scss: 5.0.2 + detective-stylus: 5.0.1 + detective-typescript: 14.1.2(typescript@5.9.3) + detective-vue2: 2.3.0(typescript@5.9.3) + module-definition: 6.0.2 + node-source-walk: 7.0.2 + postcss: 8.5.19 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + + process-warning@5.0.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + quick-format-unescaped@4.0.4: {} + + quote-unquote@1.0.0: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + rechoir@0.8.0: + dependencies: + resolve: 1.22.12 + + regexp-tree@0.1.27: {} + + requirejs-config-file@4.0.0: + dependencies: + esprima: 4.0.1 + stringify-object: 3.3.0 + + requirejs@2.3.8: {} + + resolve-dependency-path@4.0.1: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + safe-buffer@5.2.1: {} + + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + + safe-stable-stringify@2.5.0: {} + + sass-lookup@6.1.2: + dependencies: + commander: 12.1.0 + enhanced-resolve: 5.24.2 + + semver@7.8.5: {} + + sharp@0.35.3(@types/node@24.13.3): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + '@types/node': 24.13.3 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + speech-rule-engine@5.0.0-rc.4: + dependencies: + '@xmldom/xmldom': 0.9.10 + commander: 14.0.3 + wicked-good-xpath: 1.3.0 + + split2@4.2.0: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stream-to-array@2.3.0: + dependencies: + any-promise: 1.3.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-json-comments@2.0.1: {} + + stylus-lookup@6.1.2: + dependencies: + commander: 12.1.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tapable@2.3.3: {} + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-graphviz@2.1.6: + dependencies: + '@ts-graphviz/adapter': 2.0.6 + '@ts-graphviz/ast': 2.0.7 + '@ts-graphviz/common': 2.1.5 + '@ts-graphviz/core': 2.0.7 + + ts-mixer@6.0.4: {} + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.24.2 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + undici-types@7.18.2: {} + + undici@6.27.0: {} + + util-deprecate@1.0.2: {} + + vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + esbuild: 0.28.1 + fsevents: 2.3.3 + tsx: 4.23.1 + + vitest@4.1.10(@types/node@24.13.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - msw + + walkdir@0.4.1: {} + + watskeburt@6.0.0: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wicked-good-xpath@1.3.0: {} + + ws@8.21.1: {} diff --git a/discord/pnpm-workspace.yaml b/discord/pnpm-workspace.yaml new file mode 100644 index 0000000..dbb26c8 --- /dev/null +++ b/discord/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: true + sharp: true diff --git a/discord/pyproject.toml b/discord/pyproject.toml deleted file mode 100644 index 477d7c9..0000000 --- a/discord/pyproject.toml +++ /dev/null @@ -1,18 +0,0 @@ -[project] -name = "coderunbot-discord" -version = "0.1.0" -description = "Discord bots for coderunbot and gaato-bot" -requires-python = ">=3.14" -dependencies = [ - "aiohttp", - "google-api-python-client", - "google-auth-oauthlib", - "iso639-lang", - "oci", - "openai", - "py-cord[voice]>=2.6", - "python-dotenv", -] - -[tool.uv] -package = false diff --git a/discord/src/config.ts b/discord/src/config.ts new file mode 100644 index 0000000..5f44aae --- /dev/null +++ b/discord/src/config.ts @@ -0,0 +1,31 @@ +/** + * Defines bot profiles used by the composition root to select features and defaults. + */ +import type { BotName } from "./env.js"; +import type { FeatureId } from "./types.js"; + +export interface BotProfile { + readonly name: BotName; + readonly prefix: string; + readonly defaultLocale: "en" | "ja"; + readonly features: readonly FeatureId[]; +} + +export const BOT_PROFILES = { + coderunbot: { + name: "coderunbot", + prefix: "]", + defaultLocale: "en", + features: ["tex", "code", "privacy"], + }, + "gaato-bot": { + name: "gaato-bot", + prefix: ")", + defaultLocale: "ja", + features: ["tex", "code", "privacy", "wolfram", "misc", "translate"], + }, +} as const satisfies Record; + +export function getBotProfile(name: BotName): BotProfile { + return BOT_PROFILES[name]; +} diff --git a/discord/src/env.test.ts b/discord/src/env.test.ts new file mode 100644 index 0000000..8e61927 --- /dev/null +++ b/discord/src/env.test.ts @@ -0,0 +1,89 @@ +/** + * Covers environment parsing at the composition boundary, including local and S3 state modes. + */ +import { describe, expect, it } from "vitest"; +import { loadEnv } from "./env.js"; + +const baseEnv = { CODERUNBOT_TOKEN: "token" }; + +describe("loadEnv state configuration", () => { + it("uses current default OpenAI models", () => { + expect(loadEnv(baseEnv)).toMatchObject({ + openAIChatModel: "gpt-5.2", + openAIChatModelLite: "gpt-5-mini", + openAITranslateModel: "gpt-5-mini", + }); + }); + + it("loads optional gaato feature credentials", () => { + expect( + loadEnv({ + GAATO_BOT: "1", + GAATO_BOT_TOKEN: "token", + WOLFRAM_APPID: "wolfram", + OPENAI_API_KEY: "openai", + }), + ).toMatchObject({ + botName: "gaato-bot", + wolframAppId: "wolfram", + openAIApiKey: "openai", + }); + }); + + it("uses the local state file by default", () => { + expect(loadEnv(baseEnv).state).toEqual({ + backend: "local", + filePath: "data/opt-out-users.txt", + usageStatsFilePath: "data/usage-stats-coderunbot.json", + }); + }); + + it("builds a normalized S3 object key", () => { + expect( + loadEnv({ + ...baseEnv, + BOT_STATE_BACKEND: "s3", + BOT_STATE_PREFIX: "/bots/coderunbot/", + S3_ENDPOINT: "https://example.invalid", + S3_REGION: "test-region-1", + S3_BUCKET: "state", + S3_ACCESS_KEY_ID: "access-key", + S3_SECRET_ACCESS_KEY: "secret-key", + }).state, + ).toEqual({ + backend: "s3", + endpoint: "https://example.invalid", + region: "test-region-1", + bucket: "state", + key: "bots/coderunbot/opt-out-users.txt", + usageStatsKey: "bots/coderunbot/usage-stats-coderunbot.json", + accessKeyId: "access-key", + secretAccessKey: "secret-key", + }); + }); + + it("uses a separate usage statistics object for gaato bot", () => { + expect( + loadEnv({ + GAATO_BOT: "1", + GAATO_BOT_TOKEN: "token", + }).state, + ).toEqual({ + backend: "local", + filePath: "data/opt-out-users.txt", + usageStatsFilePath: "data/usage-stats-gaato-bot.json", + }); + }); + + it("fails fast when an S3 setting is missing", () => { + expect(() => + loadEnv({ + ...baseEnv, + BOT_STATE_BACKEND: "s3", + S3_ENDPOINT: "https://example.invalid", + }), + ).toThrow( + "S3_REGION, S3_BUCKET, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY must be set when BOT_STATE_BACKEND=s3", + ); + }); +}); diff --git a/discord/src/env.ts b/discord/src/env.ts new file mode 100644 index 0000000..b760f68 --- /dev/null +++ b/discord/src/env.ts @@ -0,0 +1,132 @@ +/** + * Loads and validates process configuration at the application's environment boundary. + * No other source file reads process.env, as enforced by Biome's noProcessEnv rule. + */ +import "dotenv/config"; + +export type BotName = "coderunbot" | "gaato-bot"; + +export interface LocalStateEnv { + readonly backend: "local"; + readonly filePath: string; + readonly usageStatsFilePath: string; +} + +export interface S3StateEnv { + readonly backend: "s3"; + readonly endpoint: string; + readonly region: string; + readonly bucket: string; + readonly key: string; + readonly usageStatsKey: string; + readonly accessKeyId: string; + readonly secretAccessKey: string; +} + +export type StateEnv = LocalStateEnv | S3StateEnv; + +export interface Env { + readonly botName: BotName; + readonly token: string; + readonly tokenEnvName: "CODERUNBOT_TOKEN" | "GAATO_BOT_TOKEN"; + readonly logChannelId: string; + readonly developerId: string; + readonly supportServerLink: string; + readonly wolframAppId?: string; + readonly openAIApiKey?: string; + readonly openAIChatModel: string; + readonly openAIChatModelLite: string; + readonly openAITranslateModel: string; + readonly state: StateEnv; +} + +const DEFAULT_LOG_CHANNEL_ID = "1118867011448078417"; +const DEFAULT_DEVELOPER_ID = "572432137035317249"; +const DEFAULT_SUPPORT_SERVER_LINK = "discord.gg/qRpYRTgvXM"; +const DEFAULT_OPENAI_CHAT_MODEL = "gpt-5.2"; +const DEFAULT_OPENAI_CHAT_MODEL_LITE = "gpt-5-mini"; +const DEFAULT_OPENAI_TRANSLATE_MODEL = "gpt-5-mini"; + +function nonEmpty(value: string | undefined): string | undefined { + // Empty strings count as unset, matching the Python bot's truthiness check for GAATO_BOT. + return value === undefined || value.trim().length === 0 ? undefined : value; +} + +function loadStateEnv(source: NodeJS.ProcessEnv, botName: BotName): StateEnv { + const backend = nonEmpty(source.BOT_STATE_BACKEND) ?? "local"; + if (backend === "local") { + return { + backend, + filePath: "data/opt-out-users.txt", + usageStatsFilePath: `data/usage-stats-${botName}.json`, + }; + } + if (backend !== "s3") { + throw new Error('BOT_STATE_BACKEND must be either "local" or "s3"'); + } + + const required = { + S3_ENDPOINT: nonEmpty(source.S3_ENDPOINT), + S3_REGION: nonEmpty(source.S3_REGION), + S3_BUCKET: nonEmpty(source.S3_BUCKET), + S3_ACCESS_KEY_ID: nonEmpty(source.S3_ACCESS_KEY_ID), + S3_SECRET_ACCESS_KEY: nonEmpty(source.S3_SECRET_ACCESS_KEY), + }; + const missing = Object.entries(required) + .filter(([, value]) => value === undefined) + .map(([name]) => name); + if (missing.length > 0) { + throw new Error( + `${missing.join(", ")} must be set when BOT_STATE_BACKEND=s3`, + ); + } + + const prefix = (nonEmpty(source.BOT_STATE_PREFIX) ?? "").replace( + /^\/+|\/+$/gu, + "", + ); + return { + backend, + endpoint: required.S3_ENDPOINT as string, + region: required.S3_REGION as string, + bucket: required.S3_BUCKET as string, + key: + prefix.length > 0 ? `${prefix}/opt-out-users.txt` : "opt-out-users.txt", + usageStatsKey: + prefix.length > 0 + ? `${prefix}/usage-stats-${botName}.json` + : `usage-stats-${botName}.json`, + accessKeyId: required.S3_ACCESS_KEY_ID as string, + secretAccessKey: required.S3_SECRET_ACCESS_KEY as string, + }; +} + +export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env { + const gaatoBot = nonEmpty(source.GAATO_BOT) !== undefined; + const botName = gaatoBot ? "gaato-bot" : "coderunbot"; + const tokenEnvName = gaatoBot ? "GAATO_BOT_TOKEN" : "CODERUNBOT_TOKEN"; + const token = nonEmpty(source[tokenEnvName]); + + if (token === undefined) { + throw new Error(`${tokenEnvName} must be set to a non-empty value`); + } + + return { + botName, + token, + tokenEnvName, + logChannelId: nonEmpty(source.LOG_CHANNEL_ID) ?? DEFAULT_LOG_CHANNEL_ID, + developerId: nonEmpty(source.DEVELOPER_ID) ?? DEFAULT_DEVELOPER_ID, + supportServerLink: + nonEmpty(source.SUPPORT_SERVER_LINK) ?? DEFAULT_SUPPORT_SERVER_LINK, + wolframAppId: nonEmpty(source.WOLFRAM_APPID), + openAIApiKey: nonEmpty(source.OPENAI_API_KEY), + openAIChatModel: + nonEmpty(source.OPENAI_CHAT_MODEL) ?? DEFAULT_OPENAI_CHAT_MODEL, + openAIChatModelLite: + nonEmpty(source.OPENAI_CHAT_MODEL_LITE) ?? DEFAULT_OPENAI_CHAT_MODEL_LITE, + openAITranslateModel: + nonEmpty(source.OPENAI_TRANSLATE_MODEL) ?? DEFAULT_OPENAI_TRANSLATE_MODEL, + state: loadStateEnv(source, botName), + }; +} diff --git a/discord/src/features/code/escape.test.ts b/discord/src/features/code/escape.test.ts new file mode 100644 index 0000000..f2900cf --- /dev/null +++ b/discord/src/features/code/escape.test.ts @@ -0,0 +1,38 @@ +/** + * Covers the escape context-menu reply for empty, normal, and oversized targets. + */ +import { describe, expect, it } from "vitest"; +import { escapeMentions, escapeReply } from "./index.js"; + +describe("escapeMentions", () => { + it("neutralizes mentions with a zero-width space", () => { + expect(escapeMentions("@everyone")).toBe("@​everyone"); + }); +}); + +describe("escapeReply", () => { + it("explains instead of failing when the target has no text", () => { + const reply = escapeReply("", "en"); + expect(reply.kind).toBe("plain"); + if (reply.kind === "plain") { + expect(reply.content).toBe("That message has no text to escape."); + } + expect(reply.ephemeral).toBe(true); + }); + + it("returns escaped markdown as plain ephemeral content", () => { + const reply = escapeReply("*hi* @everyone", "en"); + expect(reply.kind).toBe("plain"); + if (reply.kind === "plain") { + expect(reply.content).toBe("\\*hi\\* @​everyone"); + } + }); + + it("falls back to a file when escaping outgrows the content limit", () => { + // 1,500 asterisks escape to 3,000 characters, past the 2,000 limit. + const reply = escapeReply("*".repeat(1_500), "en"); + expect(reply.kind).toBe("components-v2"); + expect(reply.files).toHaveLength(1); + expect(reply.ephemeral).toBe(true); + }); +}); diff --git a/discord/src/features/code/index.ts b/discord/src/features/code/index.ts new file mode 100644 index 0000000..feec1b2 --- /dev/null +++ b/discord/src/features/code/index.ts @@ -0,0 +1,214 @@ +/** + * Defines the code-execution feature and its Discord command entry points. + */ +import { + ActionRowBuilder, + ApplicationCommandType, + AttachmentBuilder, + ContextMenuCommandBuilder, + escapeMarkdown, + FileBuilder, + ModalBuilder, + SlashCommandBuilder, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { UsageStats } from "../../shared/usageStats.js"; +import type { + Feature, + FeatureDependencies, + OutgoingReply, +} from "../../types.js"; +import { + parsePrefixRunInput, + runResultReply, + supportedLanguagesReply, + wandboxErrorReply, +} from "./presentation.js"; +import { + normalizeLanguageKey, + WandboxClient, + WandboxError, +} from "./wandbox.js"; + +export function escapeMentions(content: string): string { + return content.replaceAll("@", "@\u200b"); +} + +const PLAIN_CONTENT_LIMIT = 2_000; + +export function escapeReply(content: string, locale: string): OutgoingReply { + const t = getFixedT(locale); + // Components V2 messages and attachment-only messages have no text content. + if (content.length === 0) { + return { + kind: "plain", + content: t("code.escape.noText"), + ephemeral: true, + }; + } + + const escaped = escapeMentions(escapeMarkdown(content)); + // Escaping adds backslashes, so a long message can outgrow the content limit. + if (escaped.length > PLAIN_CONTENT_LIMIT) { + return { + kind: "components-v2", + components: [new FileBuilder().setURL("attachment://escaped.txt")], + files: [ + new AttachmentBuilder(Buffer.from(escaped), { name: "escaped.txt" }), + ], + ephemeral: true, + }; + } + + return { kind: "plain", content: escaped, ephemeral: true }; +} + +export function createCodeFeature(dependencies: FeatureDependencies): Feature { + const wandbox = new WandboxClient({ logger: dependencies.logger }); + const runCommand = new SlashCommandBuilder() + .setName("run") + .setDescription("Run code on Wandbox") + .setDescriptionLocalizations({ ja: "Wandbox でコードを実行します" }); + runCommand.addStringOption((option) => + option + .setName("language") + .setNameLocalizations({ ja: "言語" }) + .setDescription("Programming language") + .setDescriptionLocalizations({ ja: "プログラミング言語" }) + .setRequired(true) + .setAutocomplete(true), + ); + const escapeCommand = new ContextMenuCommandBuilder() + .setName("escape") + .setNameLocalizations({ ja: "エスケープ" }) + .setType(ApplicationCommandType.Message); + + return { + id: "code", + init: () => wandbox.init(), + prefixCommands: [ + { + name: "run", + async execute(_message, args, context) { + const input = parsePrefixRunInput(args); + return runCode( + wandbox, + dependencies.usageStats, + input.languageKey, + input.code, + "", + context.locale, + ); + }, + }, + ], + slashCommands: [ + { + data: runCommand, + async execute(interaction, context) { + if (!interaction.isChatInputCommand()) { + return undefined; + } + const languageKey = normalizeLanguageKey( + interaction.options.getString("language", true), + ); + const t = getFixedT(context.locale); + const code = new TextInputBuilder() + .setCustomId("code") + .setLabel(t("code.modal.codeLabel")) + .setPlaceholder(t("code.modal.codePlaceholder")) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true); + const stdin = new TextInputBuilder() + .setCustomId("stdin") + .setLabel(t("code.modal.stdinLabel")) + .setStyle(TextInputStyle.Paragraph) + .setRequired(false); + const modal = new ModalBuilder() + .setCustomId(`runModal:${languageKey}`) + .setTitle(t("code.modal.title")) + .addComponents( + new ActionRowBuilder().addComponents(code), + new ActionRowBuilder().addComponents(stdin), + ); + // Discord accepts a modal only as the interaction's first response. + await interaction.showModal(modal); + return undefined; + }, + async autocomplete(interaction) { + const focused = normalizeLanguageKey( + String(interaction.options.getFocused()), + ); + await interaction.respond( + wandbox + .getLanguageChoices() + .filter(({ value }) => value.startsWith(focused)) + // Discord caps autocomplete responses at 25 choices. + .slice(0, 25), + ); + }, + }, + { + data: escapeCommand, + async execute(interaction, context) { + if (!interaction.isMessageContextMenuCommand()) { + return undefined; + } + return escapeReply(interaction.targetMessage.content, context.locale); + }, + }, + ], + modalHandlers: { + runModal: async (interaction, context) => { + const languageKey = normalizeLanguageKey( + interaction.customId.slice("runModal:".length), + ); + const code = interaction.fields.getTextInputValue("code"); + const stdin = interaction.fields.getTextInputValue("stdin"); + // Acknowledge within Discord's three-second interaction window before remote work. + await interaction.deferReply(); + return runCode( + wandbox, + dependencies.usageStats, + languageKey, + code, + stdin, + context.locale, + ); + }, + }, + }; +} + +async function runCode( + wandbox: WandboxClient, + usageStats: Pick, + languageKey: string, + code: string, + stdin: string, + locale: string, +): Promise { + const t = getFixedT(locale); + const resolution = await wandbox.resolveCompiler(languageKey); + if (resolution === undefined) { + return supportedLanguagesReply(wandbox.getLanguageChoices(), t); + } + usageStats.recordRunLanguage(languageKey); + + try { + const result = await wandbox.compile(resolution.compiler, code, stdin); + return runResultReply(resolution.compiler, languageKey, code, result, t); + } catch (error) { + if ( + error instanceof WandboxError && + (error.code === "connection" || + error.code === "http" || + error.code === "non_json") + ) { + return wandboxErrorReply(error, t); + } + throw error; + } +} diff --git a/discord/src/features/code/presentation.test.ts b/discord/src/features/code/presentation.test.ts new file mode 100644 index 0000000..a8383fd --- /dev/null +++ b/discord/src/features/code/presentation.test.ts @@ -0,0 +1,93 @@ +/** + * Verifies code command parsing and Discord presentation payloads for the code feature. + */ +import { ContainerBuilder } from "discord.js"; +import { describe, expect, it } from "vitest"; +import { getFixedT } from "../../shared/i18n.js"; +import type { OutgoingReply } from "../../types.js"; +import { + parsePrefixRunInput, + runResultReply, + supportedLanguagesReply, +} from "./presentation.js"; + +function containerJson(reply: OutgoingReply) { + if (reply.kind !== "components-v2") { + throw new Error("expected a Components V2 reply"); + } + const container = reply.components[0]; + if (!(container instanceof ContainerBuilder)) { + throw new Error("expected a ContainerBuilder"); + } + return container.toJSON(); +} + +describe("parsePrefixRunInput", () => { + it("extracts the language key and removes complete fence lines", () => { + expect( + parsePrefixRunInput(" Python ```python\nprint('hello')\n```"), + ).toEqual({ languageKey: "python", code: "print('hello')" }); + }); + + it("does not rewrite language aliases", () => { + expect(parsePrefixRunInput("cpp code").languageKey).toBe("cpp"); + }); +}); + +describe("runResultReply", () => { + it("uses the status color and omits message fields", () => { + const reply = runResultReply( + "compiler", + "python", + "print(1)", + { + status: "0", + program_output: "1\n", + program_message: "duplicate output", + compiler_message: "ignored", + }, + getFixedT("en"), + ); + const json = containerJson(reply); + + expect(json.accent_color).toBe(0x2ecc71); + expect(JSON.stringify(json)).not.toContain("duplicate output"); + expect(JSON.stringify(json)).not.toContain("ignored"); + expect(JSON.stringify(json)).toContain("print(1)"); + }); + + it("uses a File component for output over 1000 characters", () => { + const reply = runResultReply( + "compiler", + "python", + "print(1)", + { status: "1", compiler_error: "x".repeat(1_001) }, + getFixedT("en"), + ); + + expect(reply.files).toHaveLength(1); + expect(containerJson(reply)).toMatchObject({ + accent_color: 0xe74c3c, + components: expect.arrayContaining([ + { file: { url: "attachment://compiler_error.txt" }, type: 13 }, + ]), + }); + }); +}); + +describe("supportedLanguagesReply", () => { + it("moves a long language list into a text attachment", () => { + const reply = supportedLanguagesReply( + Array.from({ length: 200 }, (_, index) => ({ + name: `Language ${index}`, + value: `language-${index}`, + })), + getFixedT("en"), + ); + + expect(reply.files).toHaveLength(1); + expect(JSON.stringify(containerJson(reply))).toContain( + "supported-languages.txt", + ); + }); +}); diff --git a/discord/src/features/code/presentation.ts b/discord/src/features/code/presentation.ts new file mode 100644 index 0000000..445ab25 --- /dev/null +++ b/discord/src/features/code/presentation.ts @@ -0,0 +1,175 @@ +/** + * Translates code feature results into Discord reply values at the feature boundary. + */ +import { + type AttachmentBuilder, + ContainerBuilder, + FileBuilder, + TextDisplayBuilder, +} from "discord.js"; +import type { TFunction } from "i18next"; +import { + FAILURE_ACCENT, + SUCCESS_ACCENT, + textComponent, +} from "../../platform/discord/components.js"; +import type { OutgoingReply } from "../../types.js"; +import type { LanguageChoice, WandboxCompileResult } from "./wandbox.js"; +import { normalizeLanguageKey, type WandboxError } from "./wandbox.js"; + +export interface PrefixRunInput { + readonly languageKey: string; + readonly code: string; +} + +export function parsePrefixRunInput(args: string): PrefixRunInput { + const input = args.trimStart(); + const separator = input.search(/\s/u); + if (separator === -1) { + return { languageKey: normalizeLanguageKey(input), code: "" }; + } + return { + languageKey: normalizeLanguageKey(input.slice(0, separator)), + code: stripRunFences(input.slice(separator).trimStart()), + }; +} + +export function stripRunFences(code: string): string { + return code.replace(/^```.*$/gmu, "").trim(); +} + +function safeCodeBlock(text: string): string { + return text.replaceAll("```", "`\u200b``"); +} + +function safeFilename(field: string): string { + const safe = field.replace(/[^a-z0-9_-]/giu, "-"); + return `${safe.length === 0 ? "output" : safe}.txt`; +} + +function addTextSection( + container: ContainerBuilder, + files: AttachmentBuilder[], + heading: string, + text: string, + filename: string, + language = "", +): void { + const rendered = textComponent(text, filename); + if (rendered.component instanceof FileBuilder) { + container + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(`### ${heading}`), + ) + .addFileComponents(rendered.component); + } else { + rendered.component.setContent( + `### ${heading}\n\`\`\`${language}\n${safeCodeBlock(text)}\n\`\`\``, + ); + container.addTextDisplayComponents(rendered.component); + } + files.push(...rendered.files); +} + +export function supportedLanguagesReply( + choices: readonly LanguageChoice[], + t: TFunction, +): OutgoingReply { + const container = new ContainerBuilder().setAccentColor(FAILURE_ACCENT); + const languages = choices.map(({ value }) => value).join(", "); + const rendered = textComponent(languages, "supported-languages.txt"); + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("code.supportedLanguagesHeading")}`, + ), + ); + if (rendered.component instanceof FileBuilder) { + container.addFileComponents(rendered.component); + } else if (languages.length > 0) { + container.addTextDisplayComponents(rendered.component); + } else { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(t("code.noLanguagesAvailable")), + ); + } + return { + kind: "components-v2", + components: [container], + files: rendered.files, + }; +} + +export function runResultReply( + compiler: string, + languageKey: string, + code: string, + result: WandboxCompileResult, + t: TFunction, +): OutgoingReply { + const succeeded = result.status === "0"; + const container = new ContainerBuilder() + .setAccentColor(succeeded ? SUCCESS_ACCENT : FAILURE_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `## ${t("code.resultHeading", { compiler })}`, + ), + ); + const files: AttachmentBuilder[] = []; + + for (const [field, value] of Object.entries(result)) { + if ( + value === undefined || + value === "" || + field === "program_message" || + field === "compiler_message" + ) { + continue; + } + addTextSection(container, files, field, value, safeFilename(field)); + } + addTextSection( + container, + files, + t("code.codeHeading"), + code, + "code.txt", + languageKey, + ); + + return { + kind: "components-v2", + components: [container], + files, + }; +} + +export function wandboxErrorReply( + error: WandboxError, + t: TFunction, +): OutgoingReply { + const description = + error.code === "non_json" + ? t("code.errors.nonJson") + : error.code === "http" && "status" in error + ? t("code.errors.http", { status: error.status }) + : t("code.errors.connection"); + const container = new ContainerBuilder() + .setAccentColor(FAILURE_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("code.errors.heading")}\n${description}`, + ), + ); + + if (error.code === "non_json" && "responseText" in error) { + const preview = String(error.responseText).slice(0, 750); + if (preview.length > 0) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("code.errors.responsePreview")}\n\`\`\`\n${safeCodeBlock(preview)}\n\`\`\``, + ), + ); + } + } + return { kind: "components-v2", components: [container] }; +} diff --git a/discord/src/features/code/wandbox.test.ts b/discord/src/features/code/wandbox.test.ts new file mode 100644 index 0000000..1d4a0c3 --- /dev/null +++ b/discord/src/features/code/wandbox.test.ts @@ -0,0 +1,197 @@ +/** + * Tests the discord.js-free Wandbox adapter's schema guards, selection, and cache behavior. + */ +import { describe, expect, it, vi } from "vitest"; +import { + languageChoices, + parseCompilerList, + resolveCompilerFromList, + WandboxClient, + WandboxNonJsonError, + WandboxSchemaError, +} from "./wandbox.js"; + +const silentLogger = { + error: vi.fn(), + warn: vi.fn(), +}; + +describe("resolveCompilerFromList", () => { + it("uses a pinned compiler when it is present", () => { + expect( + resolveCompilerFromList("python", [ + { name: "cpython-head", language: "Python" }, + { name: "cpython-3.14.0", language: "Python" }, + ]), + ).toEqual({ compiler: "cpython-3.14.0", source: "pinned" }); + }); + + it("uses the first stable compiler for an unpinned language", () => { + expect( + resolveCompilerFromList( + "brainfuck", + [ + { name: "brainfuck-git", language: "Brainfuck" }, + { name: "bfc-stable", language: "Brainfuck" }, + ], + {}, + ), + ).toEqual({ compiler: "bfc-stable", source: "heuristic" }); + }); + + it("falls back to the first compiler if no stable compiler exists", () => { + expect( + resolveCompilerFromList( + "future", + [ + { name: "future-head", language: "Future" }, + { name: "future-git", language: "Future" }, + ], + {}, + ), + ).toEqual({ compiler: "future-head", source: "heuristic" }); + }); + + it("falls back heuristically when a pinned compiler disappears", () => { + expect( + resolveCompilerFromList("python", [ + { name: "cpython-head", language: "Python" }, + { name: "cpython-3.15.0", language: "Python" }, + ]), + ).toEqual({ + compiler: "cpython-3.15.0", + source: "heuristic", + missingPinnedCompiler: "cpython-3.14.0", + }); + }); +}); + +describe("parseCompilerList", () => { + it("accepts entries containing the required fields", () => { + expect( + parseCompilerList([ + { name: "compiler", language: "Language", extra: true }, + ]), + ).toEqual([{ name: "compiler", language: "Language" }]); + }); + + it.each([ + {}, + [null], + [{ name: "compiler" }], + [{ name: 1, language: "Language" }], + ])("rejects a malformed response", (value) => { + expect(() => parseCompilerList(value)).toThrow(WandboxSchemaError); + }); +}); + +describe("languageChoices", () => { + it("preserves display names and uses normalized keys without alias hacks", () => { + expect( + languageChoices([ + { name: "gcc", language: "C++" }, + { name: "clang", language: "C++" }, + { name: "bfc", language: "Brain Fuck" }, + ]), + ).toEqual([ + { name: "C++", value: "c++" }, + { name: "Brain Fuck", value: "brainfuck" }, + ]); + }); +}); + +describe("WandboxClient.compile", () => { + it("filters Nim compiler CC lines and sends the hint-suppression flags", async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + status: "0", + compiler_error: "CC: stdlib_system.nim\nwarning\nCC: main.nim\n", + }), + { status: 200 }, + ), + ); + const client = new WandboxClient({ + logger: silentLogger, + fetch: fetchMock, + }); + + await expect(client.compile("nim-2.2.10", "echo 1", "")).resolves.toEqual({ + status: "0", + compiler_error: "warning\n", + }); + const body = JSON.parse( + String(fetchMock.mock.calls[0]?.[1]?.body), + ) as Record; + expect(body["compiler-option-raw"]?.split("\n")).toHaveLength(4); + }); + + it("retains the response text when JSON parsing fails", async () => { + const client = new WandboxClient({ + logger: silentLogger, + fetch: vi + .fn() + .mockResolvedValue( + new Response("service unavailable", { status: 200 }), + ), + }); + + const error = await client + .compile("compiler", "code", "") + .catch((value: unknown) => value); + expect(error).toBeInstanceOf(WandboxNonJsonError); + expect((error as WandboxNonJsonError).responseText).toBe( + "service unavailable", + ); + }); +}); + +describe("WandboxClient compiler-list cache", () => { + it("keeps stale compilers when a TTL refresh fails", async () => { + let now = 0; + const logger = { error: vi.fn(), warn: vi.fn() }; + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + new Response( + JSON.stringify([{ name: "cpython-3.14.0", language: "Python" }]), + ), + ) + .mockRejectedValueOnce(new TypeError("offline")); + const client = new WandboxClient({ + logger, + fetch: fetchMock, + now: () => now, + cacheTtlMs: 100, + }); + + await client.init(); + now = 101; + + await expect(client.resolveCompiler("python")).resolves.toMatchObject({ + compiler: "cpython-3.14.0", + }); + expect(fetchMock).toHaveBeenCalledTimes(2); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ staleCompilerCount: 1 }), + expect.stringContaining("retaining stale cache"), + ); + }); + + it("logs a schema error and starts with an empty list", async () => { + const logger = { error: vi.fn(), warn: vi.fn() }; + const client = new WandboxClient({ + logger, + fetch: vi + .fn() + .mockResolvedValue(new Response(JSON.stringify({ changed: true }))), + }); + + await expect(client.init()).resolves.toBeUndefined(); + expect(client.getLanguageChoices()).toEqual([]); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ error: expect.any(WandboxSchemaError) }), + expect.stringContaining("expected schema"), + ); + }); +}); diff --git a/discord/src/features/code/wandbox.ts b/discord/src/features/code/wandbox.ts new file mode 100644 index 0000000..755f7ca --- /dev/null +++ b/discord/src/features/code/wandbox.ts @@ -0,0 +1,368 @@ +/** + * Adapts Wandbox compiler discovery and execution for the code feature. + * The adapter is discord.js-free and unit-tested directly. + */ +import type { AppLogger } from "../../shared/logger.js"; + +const LIST_URL = "https://wandbox.org/api/list.json"; +const COMPILE_URL = "https://wandbox.org/api/compile.json"; +export const WANDBOX_CACHE_TTL_MS = 60 * 60 * 1_000; + +const NIM_COMPILER_OPTIONS = [ + "--hint[Processing]:off", + "--hint[Conf]:off", + "--hint[Link]:off", + "--hint[SuccessX]:off", +].join("\n"); + +// Dynamic selection broke twice: an API format changed, then a HEAD compiler won the heuristic. +// Prefer known compiler pins and fall back to a non-development candidate when a pin disappears. +export const PINNED_COMPILERS: Readonly> = { + python: "cpython-3.14.0", + "c++": "gcc-13.2.0", + c: "gcc-13.2.0-c", + "c#": "dotnetcore-8.0.402", + nim: "nim-2.2.10", + haskell: "ghc-9.10.1", + rust: "rust-1.82.0", + go: "go-1.23.2", + typescript: "typescript-5.6.2", + javascript: "nodejs-20.17.0", + java: "openjdk-jdk-21+35", + ruby: "ruby-3.4.9", + php: "php-8.3.12", + swift: "swift-6.0.1", + d: "dmd-2.109.1", + lua: "lua-5.4.7", + perl: "perl-5.40.0", + r: "r-4.4.1", + ocaml: "ocaml-5.2.0", +}; + +export interface WandboxCompiler { + readonly name: string; + readonly language: string; +} + +export interface LanguageChoice { + readonly name: string; + readonly value: string; +} + +export interface CompilerResolution { + readonly compiler: string; + readonly source: "pinned" | "heuristic"; + readonly missingPinnedCompiler?: string; +} + +export interface WandboxCompileResult { + readonly status?: string; + readonly signal?: string; + readonly compiler_output?: string; + readonly compiler_error?: string; + readonly compiler_message?: string; + readonly program_output?: string; + readonly program_error?: string; + readonly program_message?: string; + readonly permlink?: string; + readonly url?: string; + readonly [field: string]: string | undefined; +} + +export type WandboxErrorCode = "connection" | "http" | "non_json" | "schema"; + +export class WandboxError extends Error { + constructor( + readonly code: WandboxErrorCode, + message: string, + options?: ErrorOptions, + ) { + super(message, options); + this.name = "WandboxError"; + } +} + +export class WandboxHttpError extends WandboxError { + constructor( + readonly status: number, + readonly responseText: string, + ) { + super("http", `Wandbox returned HTTP ${status}`); + this.name = "WandboxHttpError"; + } +} + +export class WandboxNonJsonError extends WandboxError { + constructor( + readonly status: number, + readonly responseText: string, + options?: ErrorOptions, + ) { + super("non_json", "Wandbox returned a non-JSON response", options); + this.name = "WandboxNonJsonError"; + } +} + +export class WandboxSchemaError extends WandboxError { + constructor(message: string) { + super("schema", message); + this.name = "WandboxSchemaError"; + } +} + +export function normalizeLanguageKey(language: string): string { + return language.toLowerCase().replaceAll(" ", ""); +} + +export function parseCompilerList(value: unknown): WandboxCompiler[] { + // Guard the remote schema so an upstream format change produces a typed, loud failure. + if (!Array.isArray(value)) { + throw new WandboxSchemaError("Wandbox compiler list is not an array"); + } + + return value.map((entry, index) => { + if ( + typeof entry !== "object" || + entry === null || + !("name" in entry) || + !("language" in entry) || + typeof entry.name !== "string" || + typeof entry.language !== "string" || + entry.name.length === 0 || + entry.language.length === 0 + ) { + throw new WandboxSchemaError( + `Wandbox compiler list entry ${index} is missing a valid name or language`, + ); + } + return { name: entry.name, language: entry.language }; + }); +} + +export function languageChoices( + compilers: readonly WandboxCompiler[], +): LanguageChoice[] { + const choices = new Map(); + for (const compiler of compilers) { + const value = normalizeLanguageKey(compiler.language); + if (!choices.has(value)) { + choices.set(value, { name: compiler.language, value }); + } + } + return [...choices.values()]; +} + +export function resolveCompilerFromList( + languageKey: string, + compilers: readonly WandboxCompiler[], + pinnedCompilers: Readonly> = PINNED_COMPILERS, +): CompilerResolution | undefined { + const key = normalizeLanguageKey(languageKey); + const candidates = compilers.filter( + (compiler) => normalizeLanguageKey(compiler.language) === key, + ); + if (candidates.length === 0) { + return undefined; + } + + const pinned = pinnedCompilers[key]; + if (pinned !== undefined && candidates.some(({ name }) => name === pinned)) { + return { compiler: pinned, source: "pinned" }; + } + + const stable = candidates.find( + ({ name }) => !/(?:head|devel|git)/iu.test(name), + ); + return { + compiler: (stable ?? candidates[0])?.name ?? "", + source: "heuristic", + ...(pinned === undefined ? {} : { missingPinnedCompiler: pinned }), + }; +} + +function parseCompileResult(value: unknown): WandboxCompileResult { + // Apply the same schema guard to compile output so response changes fail loudly. + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new WandboxSchemaError("Wandbox compile result is not an object"); + } + + const result: Record = {}; + for (const [field, fieldValue] of Object.entries(value)) { + if (typeof fieldValue !== "string") { + throw new WandboxSchemaError( + `Wandbox compile result field ${field} is not a string`, + ); + } + result[field] = fieldValue; + } + return result; +} + +export interface WandboxClientOptions { + readonly logger: Pick; + readonly fetch?: typeof fetch; + readonly now?: () => number; + readonly cacheTtlMs?: number; +} + +export class WandboxClient { + readonly #logger: Pick; + readonly #fetch: typeof fetch; + readonly #now: () => number; + readonly #cacheTtlMs: number; + #compilers: WandboxCompiler[] = []; + // The compiler-list cache refreshes on a TTL rather than on every command. + #lastRefreshAttempt = Number.NEGATIVE_INFINITY; + #refreshing?: Promise; + + constructor(options: WandboxClientOptions) { + this.#logger = options.logger; + this.#fetch = options.fetch ?? globalThis.fetch; + this.#now = options.now ?? Date.now; + this.#cacheTtlMs = options.cacheTtlMs ?? WANDBOX_CACHE_TTL_MS; + } + + async init(): Promise { + await this.#refresh(); + } + + getLanguageChoices(): readonly LanguageChoice[] { + return languageChoices(this.#compilers); + } + + async resolveCompiler( + languageKey: string, + ): Promise { + if (this.#now() - this.#lastRefreshAttempt >= this.#cacheTtlMs) { + await this.#refresh(); + } + const resolution = resolveCompilerFromList(languageKey, this.#compilers); + return resolution; + } + + async compile( + compiler: string, + code: string, + stdin: string, + ): Promise { + let response: Response; + try { + response = await this.#fetch(COMPILE_URL, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + compiler, + code, + stdin, + "compiler-option-raw": compiler.startsWith("nim-") + ? NIM_COMPILER_OPTIONS + : "", + }), + }); + } catch (error) { + throw new WandboxError("connection", "Could not connect to Wandbox", { + cause: error, + }); + } + + if (!response.ok) { + throw new WandboxHttpError(response.status, await response.text()); + } + + const fallbackResponse = response.clone(); + let value: unknown; + try { + value = await response.json(); + } catch (error) { + throw new WandboxNonJsonError( + response.status, + await fallbackResponse.text(), + { cause: error }, + ); + } + + const result = parseCompileResult(value); + if (compiler.startsWith("nim-") && result.compiler_error !== undefined) { + return { + ...result, + compiler_error: result.compiler_error.replace(/^CC: \S+\r?\n/gmu, ""), + }; + } + return result; + } + + async #refresh(): Promise { + if (this.#refreshing !== undefined) { + return this.#refreshing; + } + + this.#refreshing = this.#refreshCompilerList(); + try { + await this.#refreshing; + } finally { + this.#refreshing = undefined; + } + } + + async #refreshCompilerList(): Promise { + this.#lastRefreshAttempt = this.#now(); + try { + let response: Response; + try { + response = await this.#fetch(LIST_URL); + } catch (error) { + throw new WandboxError( + "connection", + "Could not fetch the Wandbox compiler list", + { cause: error }, + ); + } + if (!response.ok) { + throw new WandboxHttpError(response.status, await response.text()); + } + + const fallbackResponse = response.clone(); + let value: unknown; + try { + value = await response.json(); + } catch (error) { + throw new WandboxNonJsonError( + response.status, + await fallbackResponse.text(), + { cause: error }, + ); + } + this.#compilers = parseCompilerList(value); + for (const [languageKey, pinnedCompiler] of Object.entries( + PINNED_COMPILERS, + )) { + const resolution = resolveCompilerFromList( + languageKey, + this.#compilers, + ); + if (resolution?.source !== "pinned") { + this.#logger.warn( + { + languageKey, + pinnedCompiler, + fallbackCompiler: resolution?.compiler, + }, + "Pinned Wandbox compiler is unavailable; using heuristic fallback when possible", + ); + } + } + } catch (error) { + // A failed refresh leaves #compilers untouched so commands can use stale valid data. + if (error instanceof WandboxSchemaError) { + this.#logger.error( + { error }, + "Wandbox compiler list response did not match the expected schema", + ); + } + this.#logger.warn( + { error, staleCompilerCount: this.#compilers.length }, + "Failed to refresh Wandbox compiler list; retaining stale cache", + ); + } + } +} diff --git a/discord/src/features/misc/index.test.ts b/discord/src/features/misc/index.test.ts new file mode 100644 index 0000000..2d02381 --- /dev/null +++ b/discord/src/features/misc/index.test.ts @@ -0,0 +1,78 @@ +/** + * Covers mention throttling, parsed-mention gating, and private-history filtering. + */ +import type { Message } from "discord.js"; +import { describe, expect, it, vi } from "vitest"; +import type { FeatureDependencies } from "../../types.js"; +import { + type ChatCompletionClient, + createMiscFeature, + MentionRateLimiter, + SHORT_REPLY_SYSTEM_PROMPT, +} from "./index.js"; + +describe("MentionRateLimiter", () => { + it("allows three messages per user in sixty seconds", () => { + const limiter = new MentionRateLimiter(); + expect(limiter.allow("user", 0)).toBe(true); + expect(limiter.allow("user", 1)).toBe(true); + expect(limiter.allow("user", 2)).toBe(true); + expect(limiter.allow("user", 3)).toBe(false); + expect(limiter.allow("user", 60_000)).toBe(true); + }); +}); + +describe("misc mention response", () => { + it("filters opted-out history and uses the mocked completion", async () => { + const fetched = new Map([ + ["3", { author: { id: "caller" }, content: "<@bot> hello" }], + ["2", { author: { id: "opted-out" }, content: "private" }], + ["1", { author: { id: "bot" }, content: "earlier reply" }], + ]); + const fetch = vi.fn().mockResolvedValue(fetched); + const sendTyping = vi.fn().mockResolvedValue(undefined); + const reply = vi.fn().mockResolvedValue(undefined); + const hasMention = vi.fn().mockReturnValue(true); + const message = { + author: { id: "caller", bot: false }, + mentions: { has: hasMention }, + channel: { messages: { fetch }, sendTyping }, + reply, + } as unknown as Message; + const create = vi.fn().mockResolvedValue({ + choices: [{ message: { content: "short answer" } }], + }); + const completion = { create } as ChatCompletionClient; + const recordCommand = vi.fn(); + const dependencies = { + client: { user: { id: "bot" } }, + optOutUsers: { has: (id: string) => id === "opted-out" }, + usageStats: { recordCommand }, + env: { + openAIApiKey: "key", + developerId: "developer", + openAIChatModel: "full-model", + openAIChatModelLite: "lite-model", + }, + } as unknown as FeatureDependencies; + + const feature = createMiscFeature(dependencies, completion); + await feature?.onMessage?.(message, {} as never); + + expect(hasMention).toHaveBeenCalledWith(dependencies.client.user); + expect(fetch).toHaveBeenCalledWith({ limit: 10 }); + expect(create).toHaveBeenCalledWith({ + model: "lite-model", + messages: [ + { role: "system", content: SHORT_REPLY_SYSTEM_PROMPT }, + { role: "assistant", content: "earlier reply" }, + { role: "user", content: "<@bot> hello" }, + ], + }); + expect(reply).toHaveBeenCalledWith({ + content: "short answer", + allowedMentions: { parse: [], repliedUser: true }, + }); + expect(recordCommand).toHaveBeenCalledWith("mention", "misc", "ok"); + }); +}); diff --git a/discord/src/features/misc/index.ts b/discord/src/features/misc/index.ts new file mode 100644 index 0000000..4a57a02 --- /dev/null +++ b/discord/src/features/misc/index.ts @@ -0,0 +1,128 @@ +/** + * Implements the miscellaneous mention-response feature over Discord and OpenAI. + */ +import type { Message } from "discord.js"; +import OpenAI from "openai"; +import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; +import type { OptOutUsers } from "../../shared/state.js"; +import type { Feature, FeatureDependencies } from "../../types.js"; + +export const DEVELOPER_SYSTEM_PROMPT = + "これはDiscordでのチャットです。" + + "以下の様々なユーザーによる直近のメッセージ履歴を参考に、" + + "あなたがメンションされている最後のメッセージに返信してください。"; + +export const SHORT_REPLY_SYSTEM_PROMPT = + "これはDiscordのチャットです。" + + "以下は直近のメッセージ履歴です。" + + "一言で返信してください。"; + +const RATE_LIMIT_WINDOW_MS = 60_000; +const RATE_LIMIT_COUNT = 3; + +export class MentionRateLimiter { + readonly #mentions = new Map(); + + allow(userId: string, now = Date.now()): boolean { + const recent = (this.#mentions.get(userId) ?? []).filter( + (timestamp) => now - timestamp < RATE_LIMIT_WINDOW_MS, + ); + if (recent.length >= RATE_LIMIT_COUNT) { + this.#mentions.set(userId, recent); + return false; + } + recent.push(now); + this.#mentions.set(userId, recent); + return true; + } +} + +export interface ChatCompletionClient { + create(options: { + readonly model: string; + readonly messages: readonly ChatCompletionMessageParam[]; + }): Promise<{ + readonly choices: readonly { + readonly message: { readonly content: string | null }; + }[]; + }>; +} + +export async function fetchRecentHistory( + message: Message, + botUserId: string, + optOutUsers: Pick, + limit = 10, +): Promise { + const fetched = await message.channel.messages.fetch({ limit }); + // Opted-out users' messages must never enter the OpenAI conversation context. + return [...fetched.values()] + .reverse() + .filter((entry) => !optOutUsers.has(entry.author.id)) + .map((entry) => ({ + role: entry.author.id === botUserId ? "assistant" : "user", + content: entry.content, + })); +} + +export function createMiscFeature( + dependencies: FeatureDependencies, + completionClient?: ChatCompletionClient, +): Feature | undefined { + if (dependencies.env.openAIApiKey === undefined) { + return undefined; + } + const completion = + completionClient ?? + new OpenAI({ apiKey: dependencies.env.openAIApiKey }).chat.completions; + const limiter = new MentionRateLimiter(); + + return { + id: "misc", + async onMessage(message) { + const botUser = dependencies.client.user; + if ( + message.author.bot || + botUser === null || + // Parsed mentions avoid the old bot's false positives from substring-matching its ID. + !message.mentions.has(botUser) || + !limiter.allow(message.author.id) + ) { + return undefined; + } + + if ("sendTyping" in message.channel) { + // Messages use typing for feedback; interactions must ack within three seconds instead. + await message.channel.sendTyping(); + } + const history = await fetchRecentHistory( + message, + botUser.id, + dependencies.optOutUsers, + ); + const isDeveloper = message.author.id === dependencies.env.developerId; + const response = await completion.create({ + model: isDeveloper + ? dependencies.env.openAIChatModel + : dependencies.env.openAIChatModelLite, + messages: [ + { + role: "system", + content: isDeveloper + ? DEVELOPER_SYSTEM_PROMPT + : SHORT_REPLY_SYSTEM_PROMPT, + }, + ...history, + ], + }); + // Replies directly instead of returning an OutgoingReply, matching the old bot: + // chat responses get no Delete button and no edit-follow tracking. + await message.reply({ + content: response.choices[0]?.message.content ?? "", + allowedMentions: { parse: [], repliedUser: true }, + }); + dependencies.usageStats.recordCommand("mention", "misc", "ok"); + return undefined; + }, + }; +} diff --git a/discord/src/features/ping/index.ts b/discord/src/features/ping/index.ts new file mode 100644 index 0000000..39ec1f8 --- /dev/null +++ b/discord/src/features/ping/index.ts @@ -0,0 +1,28 @@ +/** + * Supplies a small feature-layer smoke test through a Discord slash command. + */ +import { SlashCommandBuilder } from "discord.js"; +import { successContainer } from "../../platform/discord/components.js"; +import type { Feature, FeatureDependencies } from "../../types.js"; + +// Temporary smoke-test feature for the TypeScript foundation. Remove it once +// the real feature ports provide end-to-end command coverage. +export function createPingFeature(_dependencies: FeatureDependencies): Feature { + return { + id: "ping", + slashCommands: [ + { + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Check whether the bot is responding") + .setDescriptionLocalizations({ ja: "ボットの応答を確認します" }), + async execute() { + return { + kind: "components-v2", + components: [successContainer("pong")], + }; + }, + }, + ], + }; +} diff --git a/discord/src/features/privacy/index.test.ts b/discord/src/features/privacy/index.test.ts new file mode 100644 index 0000000..3f3da27 --- /dev/null +++ b/discord/src/features/privacy/index.test.ts @@ -0,0 +1,41 @@ +/** + * Covers the privacy-policy reply shape and its Components V2 size budget. + */ +import { readFile } from "node:fs/promises"; +import type { ChatInputCommandInteraction } from "discord.js"; +import { describe, expect, it } from "vitest"; +import type { RequestContext } from "../../platform/discord/context.js"; +import type { FeatureDependencies } from "../../types.js"; +import { createPrivacyFeature } from "./index.js"; + +const COMPONENTS_V2_TEXT_LIMIT = 4_000; + +function findCommand(name: string) { + const feature = createPrivacyFeature({} as FeatureDependencies); + const command = feature.slashCommands?.find( + (candidate) => candidate.data.name === name, + ); + if (command === undefined) { + throw new Error(`missing /${name}`); + } + return command; +} + +describe("privacy-policy command", () => { + it("fits the policy document within the Components V2 text budget", async () => { + const policy = await readFile( + new URL("../../../config/privacy-policy.md", import.meta.url), + "utf8", + ); + expect(policy.length).toBeLessThanOrEqual(COMPONENTS_V2_TEXT_LIMIT); + }); + + it("replies with a Components V2 text display, not plain content", async () => { + const command = findCommand("privacy-policy"); + const outgoing = await command.execute( + {} as ChatInputCommandInteraction, + { locale: "en", userId: "user" } as RequestContext, + ); + expect(outgoing?.kind).toBe("components-v2"); + }); +}); diff --git a/discord/src/features/privacy/index.ts b/discord/src/features/privacy/index.ts new file mode 100644 index 0000000..d7eefb3 --- /dev/null +++ b/discord/src/features/privacy/index.ts @@ -0,0 +1,95 @@ +/** + * Implements privacy-policy and opt-out commands in the feature layer. + */ +import { readFile } from "node:fs/promises"; +import { SlashCommandBuilder, TextDisplayBuilder } from "discord.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { Feature, FeatureDependencies } from "../../types.js"; + +const privacyPolicyUrl = new URL( + "../../../config/privacy-policy.md", + import.meta.url, +); + +// AGPL-3.0 §13: users interacting with the bot over the network must be offered +// its Corresponding Source. If you run a modified version, point this at your fork. +const SOURCE_URL = "https://github.com/gaato/coderunbot"; + +export function createPrivacyFeature({ + optOutUsers, +}: FeatureDependencies): Feature { + return { + id: "privacy", + slashCommands: [ + { + data: new SlashCommandBuilder() + .setName("privacy-policy") + .setDescription("Show the privacy policy") + .setDescriptionLocalizations({ + ja: "プライバシーポリシーを表示します", + }), + async execute() { + // The policy exceeds the 2,000-character plain-content limit; a Components V2 + // text display raises the ceiling to 4,000 characters for the whole message. + return { + kind: "components-v2", + components: [ + new TextDisplayBuilder().setContent( + await readFile(privacyPolicyUrl, "utf8"), + ), + ], + }; + }, + }, + { + data: new SlashCommandBuilder() + .setName("source") + .setDescription("Show where to get this bot's source code") + .setDescriptionLocalizations({ + ja: "このボットのソースコードの入手先を表示します", + }), + async execute(_interaction, context) { + const t = getFixedT(context.locale); + return { + kind: "plain", + content: t("privacy.source", { url: SOURCE_URL }), + }; + }, + }, + { + data: new SlashCommandBuilder() + .setName("opt-out") + .setDescription("Stop the bot from processing your message content") + .setDescriptionLocalizations({ + ja: "ボットによるメッセージ内容の処理を停止します", + }), + async execute(_interaction, context) { + const t = getFixedT(context.locale); + if (optOutUsers.has(context.userId)) { + return { kind: "plain", content: t("privacy.optOut.already") }; + } + + await optOutUsers.add(context.userId); + return { kind: "plain", content: t("privacy.optOut.success") }; + }, + }, + { + data: new SlashCommandBuilder() + .setName("opt-in") + .setDescription("Allow the bot to process your message content again") + .setDescriptionLocalizations({ + ja: "ボットによるメッセージ内容の処理を再開します", + }), + async execute(_interaction, context) { + const t = getFixedT(context.locale); + if (!optOutUsers.has(context.userId)) { + return { kind: "plain", content: t("privacy.optIn.already") }; + } + + await optOutUsers.remove(context.userId); + return { kind: "plain", content: t("privacy.optIn.success") }; + }, + }, + ], + }; +} diff --git a/discord/src/features/tex/index.ts b/discord/src/features/tex/index.ts new file mode 100644 index 0000000..73d15b1 --- /dev/null +++ b/discord/src/features/tex/index.ts @@ -0,0 +1,136 @@ +/** + * Defines TeX feature commands and delegates rendering to the Discord-free adapter. + */ +import { + ActionRowBuilder, + ModalBuilder, + SlashCommandBuilder, + TextInputBuilder, + TextInputStyle, +} from "discord.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { + Feature, + FeatureDependencies, + OutgoingReply, +} from "../../types.js"; +import { + stripTexFences, + texFailureReply, + texSuccessReply, +} from "./presentation.js"; +import { renderTexToPng, TexRenderError } from "./renderer.js"; + +type TexEnvironment = "align" | "gather"; + +export function createTexFeature(_dependencies: FeatureDependencies): Feature { + const slashCommand = new SlashCommandBuilder() + .setName("tex") + .setDescription("Render TeX as an image") + .setDescriptionLocalizations({ + ja: "TeX を画像としてレンダリングします", + }); + slashCommand.addStringOption((option) => + option + .setName("env") + .setDescription("The environment to use") + .setDescriptionLocalizations({ ja: "使用する環境" }) + .addChoices( + { name: "align", value: "align" }, + { name: "gather", value: "gather" }, + ), + ); + slashCommand.addBooleanOption((option) => + option + .setName("spoiler") + .setDescription("Mark the rendered image as a spoiler") + .setDescriptionLocalizations({ + ja: "レンダリング画像をスポイラーとして隠します", + }), + ); + + return { + id: "tex", + prefixCommands: [ + { + name: "tex", + async execute(_message, args, context) { + return renderReply(stripTexFences(args), false, context.locale); + }, + }, + { + name: "stex", + async execute(_message, args, context) { + return renderReply(stripTexFences(args), true, context.locale); + }, + }, + ], + slashCommands: [ + { + data: slashCommand, + async execute(interaction, context) { + if (!interaction.isChatInputCommand()) { + return undefined; + } + const environment = parseEnvironment( + interaction.options.getString("env"), + ); + const spoiler = interaction.options.getBoolean("spoiler") ?? false; + const t = getFixedT(context.locale); + const input = new TextInputBuilder() + .setCustomId("latex") + .setLabel(t("tex.modal.codeLabel")) + .setPlaceholder(t("tex.modal.codePlaceholder")) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true); + if (environment !== undefined) { + input.setValue(`\\begin{${environment}}\n\n\\end{${environment}}`); + } + + const modal = new ModalBuilder() + .setCustomId( + `texModal:${spoiler}:${environment === undefined ? "" : environment}`, + ) + .setTitle(t("tex.modal.title")) + .addComponents( + new ActionRowBuilder().addComponents(input), + ); + // Discord accepts a modal only as the interaction's first response. + await interaction.showModal(modal); + return undefined; + }, + }, + ], + modalHandlers: { + texModal: async (interaction, context) => { + const [, spoilerValue] = interaction.customId.split(":"); + const spoiler = spoilerValue === "true"; + const latex = interaction.fields.getTextInputValue("latex"); + // Acknowledge within Discord's three-second interaction window before rendering. + await interaction.deferReply(); + return renderReply(latex, spoiler, context.locale); + }, + }, + }; +} + +async function renderReply( + latex: string, + spoiler: boolean, + locale: string, +): Promise { + const t = getFixedT(locale); + try { + const png = await renderTexToPng(latex); + return texSuccessReply(latex, png, spoiler, t); + } catch (error) { + if (error instanceof TexRenderError) { + return texFailureReply(error.message, t); + } + throw error; + } +} + +function parseEnvironment(value: string | null): TexEnvironment | undefined { + return value === "align" || value === "gather" ? value : undefined; +} diff --git a/discord/src/features/tex/presentation.test.ts b/discord/src/features/tex/presentation.test.ts new file mode 100644 index 0000000..4651009 --- /dev/null +++ b/discord/src/features/tex/presentation.test.ts @@ -0,0 +1,90 @@ +/** + * Verifies TeX input cleanup, hints, and Discord presentation payloads. + */ +import { ContainerBuilder } from "discord.js"; +import { describe, expect, it } from "vitest"; +import { getFixedT } from "../../shared/i18n.js"; +import type { OutgoingReply } from "../../types.js"; +import { + shouldShowMultilineHint, + stripTexFences, + texSuccessReply, +} from "./presentation.js"; + +function containerJson(reply: OutgoingReply) { + if (reply.kind !== "components-v2") { + throw new Error("expected a Components V2 reply"); + } + const container = reply.components[0]; + if (!(container instanceof ContainerBuilder)) { + throw new Error("expected a ContainerBuilder"); + } + return container.toJSON(); +} + +describe("stripTexFences", () => { + it.each([ + ["```tex\nx^2\n```", "x^2"], + ["```\nx^2\n```", "x^2"], + [" x^2 ", "x^2"], + ])("removes an optional TeX or plain code fence", (input, expected) => { + expect(stripTexFences(input)).toBe(expected); + }); +}); + +describe("shouldShowMultilineHint", () => { + it("suggests an environment for bare line breaks", () => { + expect(shouldShowMultilineHint("x \\\\ y")).toBe(true); + }); + + it.each([ + "x + y", + "\\begin{align}x \\\\ y\\end{align}", + "\\begin{gather}x \\\\ y", + "x \\\\ y\\end{gather}", + ])("does not suggest one when inappropriate", (latex) => { + expect(shouldShowMultilineHint(latex)).toBe(false); + }); +}); + +describe("texSuccessReply", () => { + it("marks the media-gallery item itself as a spoiler", () => { + const reply = texSuccessReply( + "x^2", + Buffer.from("png"), + true, + getFixedT("en"), + ); + + expect(containerJson(reply)).toMatchObject({ + type: 17, + components: expect.arrayContaining([ + { + type: 12, + items: [ + { + media: { url: "attachment://tex.png" }, + spoiler: true, + }, + ], + }, + ]), + }); + }); + + it("uses a text attachment when the source is over the display limit", () => { + const reply = texSuccessReply( + "x".repeat(1_001), + Buffer.from("png"), + false, + getFixedT("en"), + ); + + expect(reply.files).toHaveLength(2); + expect(containerJson(reply)).toMatchObject({ + components: expect.arrayContaining([ + { file: { url: "attachment://tex-source.txt" }, type: 13 }, + ]), + }); + }); +}); diff --git a/discord/src/features/tex/presentation.ts b/discord/src/features/tex/presentation.ts new file mode 100644 index 0000000..b343505 --- /dev/null +++ b/discord/src/features/tex/presentation.ts @@ -0,0 +1,88 @@ +/** + * Builds Discord reply values from TeX feature results at the presentation boundary. + */ +import { + AttachmentBuilder, + ContainerBuilder, + FileBuilder, + MediaGalleryBuilder, + MediaGalleryItemBuilder, + TextDisplayBuilder, +} from "discord.js"; +import type { TFunction } from "i18next"; +import { + FAILURE_ACCENT, + SUCCESS_ACCENT, + textComponent, +} from "../../platform/discord/components.js"; +import type { OutgoingReply } from "../../types.js"; + +export function stripTexFences(input: string): string { + return input + .replace(/```tex/giu, "") + .replace(/```/gu, "") + .trim(); +} + +export function shouldShowMultilineHint(latex: string): boolean { + return ( + latex.includes("\\\\") && + !latex.includes("\\begin") && + !latex.includes("\\end") + ); +} + +export function texSuccessReply( + latex: string, + png: Buffer, + spoiler: boolean, + t: TFunction, +): OutgoingReply { + const container = new ContainerBuilder() + .setAccentColor(SUCCESS_ACCENT) + .addMediaGalleryComponents( + new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder() + .setURL("attachment://tex.png") + .setSpoiler(spoiler), + ), + ); + + if (shouldShowMultilineHint(latex)) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("tex.hintHeading")}\n${t("tex.multilineHint")}`, + ), + ); + } + + const source = textComponent(latex, "tex-source.txt"); + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(`### ${t("tex.codeHeading")}`), + ); + if (source.component instanceof FileBuilder) { + container.addFileComponents(source.component); + } else { + const safeLatex = latex.replaceAll("```", "`\u200b``"); + source.component.setContent(`\`\`\`tex\n${safeLatex}\n\`\`\``); + container.addTextDisplayComponents(source.component); + } + + return { + kind: "components-v2", + components: [container], + files: [new AttachmentBuilder(png, { name: "tex.png" }), ...source.files], + }; +} + +export function texFailureReply(message: string, t: TFunction): OutgoingReply { + const safeMessage = message.replaceAll("```", "`\u200b``"); + const container = new ContainerBuilder() + .setAccentColor(FAILURE_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("tex.errorHeading")}\n\`\`\`\n${safeMessage}\n\`\`\``, + ), + ); + return { kind: "components-v2", components: [container] }; +} diff --git a/discord/src/features/tex/renderer.test.ts b/discord/src/features/tex/renderer.test.ts new file mode 100644 index 0000000..365f7d2 --- /dev/null +++ b/discord/src/features/tex/renderer.test.ts @@ -0,0 +1,41 @@ +/** + * Exercises the discord.js-free TeX rendering adapter against SVG, PNG, and error cases. + */ +import { describe, expect, it } from "vitest"; +import { renderTexToPng, renderTexToSvg, TexRenderError } from "./renderer.js"; + +describe("TeX renderer", () => { + it("renders TeX to a PNG buffer", async () => { + const png = await renderTexToPng("x^2"); + + expect(png.subarray(0, 8)).toEqual( + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + ); + }); + + it("throws a typed error for invalid TeX", async () => { + await expect(renderTexToSvg("\\frac{1}{")).rejects.toThrow(TexRenderError); + }); + + it("throws a typed error for undefined macros", async () => { + await expect(renderTexToSvg("\\notarealmacro")).rejects.toThrow( + TexRenderError, + ); + }); + + it("reports the original TeX message for invalid input", async () => { + await expect(renderTexToSvg("{")).rejects.toThrow(/missing close brace/iu); + }); + + it("sets the Japanese serif fallback on SVG text nodes", async () => { + const svg = await renderTexToSvg("\\text{日本語}"); + + const textTags = svg.match(/]*>/gu) ?? []; + expect(textTags.length).toBeGreaterThan(0); + expect( + textTags.every((tag) => + tag.includes('font-family="Noto Serif CJK JP, serif"'), + ), + ).toBe(true); + }); +}); diff --git a/discord/src/features/tex/renderer.ts b/discord/src/features/tex/renderer.ts new file mode 100644 index 0000000..e553559 --- /dev/null +++ b/discord/src/features/tex/renderer.ts @@ -0,0 +1,188 @@ +/** + * In-process LaTeX → SVG → PNG renderer built on MathJax v4. + * + * This is the discord.js-free adapter of the tex feature: it knows nothing + * about Discord and exposes plain functions returning SVG text / PNG buffers, + * so it can be unit-tested directly. It replaces the standalone tex web + * service the Python bot used to call over HTTP. + */ +import { MathJaxNewcmFont } from "@mathjax/mathjax-newcm-font/js/svg.js"; +import { liteAdaptor } from "@mathjax/src/js/adaptors/liteAdaptor.js"; +import { RegisterHTMLHandler } from "@mathjax/src/js/handlers/html.js"; +import TexError from "@mathjax/src/js/input/tex/TexError.js"; +import { TeX } from "@mathjax/src/js/input/tex.js"; +import { mathjax } from "@mathjax/src/js/mathjax.js"; +import { SVG } from "@mathjax/src/js/output/svg.js"; +// The newcm font data is split into dynamically loaded pieces; this import +// teaches MathJax to load them with ESM import() when a glyph range is first +// needed (which is also why conversion must go through convertPromise()). +import "@mathjax/src/js/util/asyncLoad/esm.js"; +import sharp from "sharp"; + +// MathJax v4 dropped the v3 AllPackages bundle, so every TeX extension has to +// be imported for its registration side effect and listed in `packages` +// below. This is the full set shipped in @mathjax/src, minus: +// - noundefined: would render unknown macros as red text instead of letting +// them raise the TexError we turn into a typed render failure +// - noerrors: would swallow TeX errors and typeset the raw source +// - autoload / require: dynamic extension loading is pointless (and +// undesirable) when everything is preloaded +// - setoptions: lets TeX input override parser options at runtime +// - tagformat: only useful with a custom tag-format configuration +// - colorv2: superseded by (and conflicting with) the color package +// - fontsizev3: compatibility shim for v3 font-size behavior +// - texhtml: embeds raw HTML in the output, which we never want +import "@mathjax/src/js/input/tex/action/ActionConfiguration.js"; +import "@mathjax/src/js/input/tex/ams/AmsConfiguration.js"; +import "@mathjax/src/js/input/tex/amscd/AmsCdConfiguration.js"; +import "@mathjax/src/js/input/tex/bbm/BbmConfiguration.js"; +import "@mathjax/src/js/input/tex/bboldx/BboldxConfiguration.js"; +import "@mathjax/src/js/input/tex/bbox/BboxConfiguration.js"; +import "@mathjax/src/js/input/tex/begingroup/BegingroupConfiguration.js"; +import "@mathjax/src/js/input/tex/boldsymbol/BoldsymbolConfiguration.js"; +import "@mathjax/src/js/input/tex/braket/BraketConfiguration.js"; +import "@mathjax/src/js/input/tex/bussproofs/BussproofsConfiguration.js"; +import "@mathjax/src/js/input/tex/cancel/CancelConfiguration.js"; +import "@mathjax/src/js/input/tex/cases/CasesConfiguration.js"; +import "@mathjax/src/js/input/tex/centernot/CenternotConfiguration.js"; +import "@mathjax/src/js/input/tex/color/ColorConfiguration.js"; +import "@mathjax/src/js/input/tex/colortbl/ColortblConfiguration.js"; +import "@mathjax/src/js/input/tex/configmacros/ConfigMacrosConfiguration.js"; +import "@mathjax/src/js/input/tex/dsfont/DsfontConfiguration.js"; +import "@mathjax/src/js/input/tex/empheq/EmpheqConfiguration.js"; +import "@mathjax/src/js/input/tex/enclose/EncloseConfiguration.js"; +import "@mathjax/src/js/input/tex/extpfeil/ExtpfeilConfiguration.js"; +import "@mathjax/src/js/input/tex/gensymb/GensymbConfiguration.js"; +import "@mathjax/src/js/input/tex/html/HtmlConfiguration.js"; +import "@mathjax/src/js/input/tex/mathtools/MathtoolsConfiguration.js"; +import "@mathjax/src/js/input/tex/mhchem/MhchemConfiguration.js"; +import "@mathjax/src/js/input/tex/newcommand/NewcommandConfiguration.js"; +import "@mathjax/src/js/input/tex/physics/PhysicsConfiguration.js"; +import "@mathjax/src/js/input/tex/textcomp/TextcompConfiguration.js"; +import "@mathjax/src/js/input/tex/textmacros/TextMacrosConfiguration.js"; +import "@mathjax/src/js/input/tex/unicode/UnicodeConfiguration.js"; +import "@mathjax/src/js/input/tex/units/UnitsConfiguration.js"; +import "@mathjax/src/js/input/tex/upgreek/UpgreekConfiguration.js"; +import "@mathjax/src/js/input/tex/verb/VerbConfiguration.js"; + +const RENDER_PACKAGES = [ + "base", + "action", + "ams", + "amscd", + "bbm", + "bboldx", + "bbox", + "begingroup", + "boldsymbol", + "braket", + "bussproofs", + "cancel", + "cases", + "centernot", + "color", + "colortbl", + "configmacros", + "dsfont", + "empheq", + "enclose", + "extpfeil", + "gensymb", + "html", + "mathtools", + "mhchem", + "newcommand", + "physics", + "textcomp", + "textmacros", + "unicode", + "units", + "upgreek", + "verb", +]; + +// The newcm font has no CJK glyphs, so e.g. \text{日本語} comes out as plain +// SVG elements. Forcing this family makes librsvg (inside sharp) pick +// the Noto Serif CJK JP installed in the container instead of a fallback that +// lacks the glyphs. +const TEXT_FONT_FAMILY = "Noto Serif CJK JP, serif"; +const PADDING = 20; + +const adaptor = liteAdaptor(); +RegisterHTMLHandler(adaptor); + +export class TexRenderError extends Error { + constructor(message: string) { + super(message); + this.name = "TexRenderError"; + } +} + +export async function renderTexToSvg(latex: string): Promise { + // A fresh TeX input jax and document per call keeps parser state (e.g. + // \newcommand definitions, \begingroup scopes) from leaking across renders. + const tex = new TeX({ + packages: RENDER_PACKAGES, + // By default MathJax typesets compile errors as red text; throw + // instead so callers get a typed failure with the original TeX message. + formatError(_jax: unknown, error: unknown) { + throw error; + }, + }); + const svg = new SVG({ fontData: MathJaxNewcmFont }); + const document = mathjax.document("", { + InputJax: tex, + OutputJax: svg, + }); + + // convertPromise (not convert) so dynamically loaded font pieces can be + // awaited mid-conversion; see the asyncLoad/esm import above. + const converted = await document + .convertPromise(latex, { + display: true, + em: 16, + ex: 8, + containerWidth: 80, + }) + .catch((error: unknown) => { + if (error instanceof TexError) { + throw new TexRenderError(error.message); + } + throw error; + }); + + const svgElement = adaptor.tags(converted, "svg")[0]; + if (svgElement === undefined) { + throw new Error("MathJax did not produce an SVG element"); + } + + // Defensive fallback: formatError should surface every compile error as an + // exception, but fail loudly if an merror node slips through anyway. + const rendered = adaptor.outerHTML(svgElement); + if (rendered.includes("data-mjx-error")) { + const title = rendered.match(/\btitle=(['"])(.*?)\1/u)?.[2]; + throw new TexRenderError(title ?? "Unknown LaTeX rendering error"); + } + + for (const textElement of adaptor.tags(svgElement, "text")) { + adaptor.setAttribute(textElement, "font-family", TEXT_FONT_FAMILY); + } + return adaptor.outerHTML(svgElement); +} + +export async function renderTexToPng(latex: string): Promise { + const svg = await renderTexToSvg(latex); + const white = { r: 255, g: 255, b: 255 }; + return sharp(Buffer.from(svg)) + .resize({ height: 500 }) + .flatten({ background: white }) + .extend({ + top: PADDING, + bottom: PADDING, + left: PADDING, + right: PADDING, + background: { ...white, alpha: 1 }, + }) + .png() + .toBuffer(); +} diff --git a/discord/src/features/translate/index.test.ts b/discord/src/features/translate/index.test.ts new file mode 100644 index 0000000..dad32e5 --- /dev/null +++ b/discord/src/features/translate/index.test.ts @@ -0,0 +1,35 @@ +/** + * Tests translation-language autocomplete and resolution without Discord transport calls. + */ +import { describe, expect, it } from "vitest"; +import { autocompleteLanguages, resolveLanguage } from "./index.js"; + +describe("autocompleteLanguages", () => { + it("returns no choices for one character", () => { + expect(autocompleteLanguages("j")).toEqual([]); + }); + + it("resolves a two-letter code to its language name", () => { + expect(autocompleteLanguages("ja")).toEqual([ + { name: "Japanese", value: "ja" }, + ]); + }); + + it("never exceeds Discord's 25-choice limit", () => { + const names = Array.from( + { length: 30 }, + (_, index) => `Test language ${index}`, + ); + expect(autocompleteLanguages("test", names)).toHaveLength(25); + }); +}); + +describe("resolveLanguage", () => { + it("accepts both codes and language names", () => { + expect(resolveLanguage("ja")).toEqual({ code: "ja", name: "Japanese" }); + expect(resolveLanguage("Japanese")).toEqual({ + code: "ja", + name: "Japanese", + }); + }); +}); diff --git a/discord/src/features/translate/index.ts b/discord/src/features/translate/index.ts new file mode 100644 index 0000000..6d648cb --- /dev/null +++ b/discord/src/features/translate/index.ts @@ -0,0 +1,208 @@ +/** + * Implements the translation feature and its Discord presentation over OpenAI. + */ +import { + type AttachmentBuilder, + ContainerBuilder, + FileBuilder, + SlashCommandBuilder, + TextDisplayBuilder, +} from "discord.js"; +import ISO6391 from "iso-639-1"; +import OpenAI from "openai"; +import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; +import { + SUCCESS_ACCENT, + textComponent, +} from "../../platform/discord/components.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { + Feature, + FeatureDependencies, + OutgoingReply, +} from "../../types.js"; + +export interface LanguageChoice { + readonly name: string; + readonly value: string; +} + +export interface ResolvedLanguage { + readonly code: string; + readonly name: string; +} + +export function autocompleteLanguages( + input: string, + languageNames: readonly string[] = ISO6391.getAllNames(), +): LanguageChoice[] { + const normalized = input.trim().toLowerCase(); + if (normalized.length <= 1) { + return []; + } + if (normalized.length === 2) { + const name = ISO6391.getName(normalized); + return name.length === 0 ? [] : [{ name, value: normalized }]; + } + + // Discord caps autocomplete responses at 25 choices. + return languageNames + .filter((name) => name.toLowerCase().startsWith(normalized)) + .sort( + (left, right) => left.length - right.length || left.localeCompare(right), + ) + .slice(0, 25) + .map((name) => ({ name, value: ISO6391.getCode(name) })); +} + +export function resolveLanguage(input: string): ResolvedLanguage | undefined { + const normalized = input.trim().toLowerCase(); + const code = ISO6391.validate(normalized) + ? normalized + : ISO6391.getCode(input.trim()); + if (code.length === 0) { + return undefined; + } + return { code, name: ISO6391.getName(code) }; +} + +interface TranslationCompletionClient { + create(options: { + readonly model: string; + readonly messages: readonly ChatCompletionMessageParam[]; + }): Promise<{ + readonly choices: readonly { + readonly message: { readonly content: string | null }; + }[]; + }>; +} + +export function createTranslateFeature( + dependencies: FeatureDependencies, + completionClient?: TranslationCompletionClient, +): Feature | undefined { + if (dependencies.env.openAIApiKey === undefined) { + return undefined; + } + const completion = + completionClient ?? + new OpenAI({ apiKey: dependencies.env.openAIApiKey }).chat.completions; + const command = new SlashCommandBuilder() + .setName("translate") + .setDescription("Translate text") + .setDescriptionLocalizations({ ja: "テキストを翻訳します" }); + command.addStringOption((option) => + option + .setName("text") + .setDescription("Text to translate") + .setDescriptionLocalizations({ ja: "翻訳するテキスト" }) + .setRequired(true), + ); + command.addStringOption((option) => + option + .setName("to") + .setDescription("Language to translate to") + .setDescriptionLocalizations({ ja: "翻訳先の言語" }) + .setRequired(true) + .setAutocomplete(true), + ); + + return { + id: "translate", + slashCommands: [ + { + data: command, + async autocomplete(interaction) { + await interaction.respond( + autocompleteLanguages(String(interaction.options.getFocused())), + ); + }, + async execute(interaction, context) { + if (!interaction.isChatInputCommand()) { + return undefined; + } + const t = getFixedT(context.locale); + const language = resolveLanguage( + interaction.options.getString("to", true), + ); + if (language === undefined) { + return { + kind: "plain", + content: t("translate.invalidLanguage"), + ephemeral: true, + }; + } + + const original = interaction.options.getString("text", true); + // Acknowledge within Discord's three-second interaction window before the API call. + await interaction.deferReply(); + const response = await completion.create({ + model: dependencies.env.openAITranslateModel, + messages: [ + { + role: "system", + content: + "This is a direct translation task. " + + `Translate the following text to ${language.name}. ` + + "Do not add any additional comments or language indicators.", + }, + { role: "user", content: original }, + ], + }); + return translationReply( + original, + response.choices[0]?.message.content ?? "", + language.name, + context.locale, + ); + }, + }, + ], + }; +} + +function addSection( + container: ContainerBuilder, + files: AttachmentBuilder[], + heading: string, + content: string, + filename: string, +): void { + const rendered = textComponent(content, filename); + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(`### ${heading}`), + ); + if (rendered.component instanceof FileBuilder) { + container.addFileComponents(rendered.component); + } else { + rendered.component.setContent(content.length === 0 ? "\u200b" : content); + container.addTextDisplayComponents(rendered.component); + } + files.push(...rendered.files); +} + +function translationReply( + original: string, + translated: string, + languageName: string, + locale: string, +): OutgoingReply { + const t = getFixedT(locale); + const container = new ContainerBuilder().setAccentColor(SUCCESS_ACCENT); + const files: AttachmentBuilder[] = []; + addSection( + container, + files, + t("translate.originalHeading"), + original, + "original.txt", + ); + addSection( + container, + files, + t("translate.translatedHeading", { language: languageName }), + translated, + "translated.txt", + ); + return { kind: "components-v2", components: [container], files }; +} diff --git a/discord/src/features/wolfram/index.ts b/discord/src/features/wolfram/index.ts new file mode 100644 index 0000000..1d71c1f --- /dev/null +++ b/discord/src/features/wolfram/index.ts @@ -0,0 +1,147 @@ +/** + * Implements the Wolfram feature and paginates adapter results for Discord. + */ +import { + ContainerBuilder, + MediaGalleryBuilder, + MediaGalleryItemBuilder, + SlashCommandBuilder, + TextDisplayBuilder, +} from "discord.js"; +import { + FAILURE_ACCENT, + SUCCESS_ACCENT, +} from "../../platform/discord/components.js"; +import { paginatedReply } from "../../platform/discord/paginator.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { + Feature, + FeatureDependencies, + OutgoingReply, +} from "../../types.js"; +import { WolframClient, type WolframQueryResult } from "./wolframClient.js"; + +export function createWolframFeature( + dependencies: FeatureDependencies, +): Feature | undefined { + if (dependencies.env.wolframAppId === undefined) { + return undefined; + } + const client = new WolframClient(dependencies.env.wolframAppId); + const slashCommand = new SlashCommandBuilder() + .setName("wolf") + .setDescription("Query Wolfram|Alpha") + .setDescriptionLocalizations({ ja: "Wolfram|Alpha に問い合わせます" }); + slashCommand.addStringOption((option) => + option + .setName("query") + .setDescription("Query to send to Wolfram|Alpha") + .setDescriptionLocalizations({ + ja: "Wolfram|Alpha に送信するクエリ", + }) + .setRequired(true), + ); + + const execute = async ( + query: string, + userId: string, + locale: string, + ): Promise => { + const result = await client.query(query); + return wolframReply(result, userId, locale, dependencies); + }; + + return { + id: "wolfram", + prefixCommands: [ + { + name: "wolf", + aliases: ["wolfram"], + execute(_message, args, context) { + return execute(args, context.userId, context.locale); + }, + }, + ], + slashCommands: [ + { + data: slashCommand, + async execute(interaction, context) { + if (!interaction.isChatInputCommand()) { + return undefined; + } + // Acknowledge within Discord's three-second interaction window before the API call. + await interaction.deferReply(); + return execute( + interaction.options.getString("query", true), + context.userId, + context.locale, + ); + }, + }, + ], + }; +} + +function wolframReply( + result: WolframQueryResult, + userId: string, + locale: string, + dependencies: Pick, +): OutgoingReply { + const t = getFixedT(locale); + if (!result.success) { + return { + kind: "components-v2", + components: [ + new ContainerBuilder() + .setAccentColor(FAILURE_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent( + `### ${t("wolfram.errorHeading")}\n${t("wolfram.notUnderstood")}`, + ), + ), + ], + }; + } + + const pages = result.pods.flatMap((pod) => + pod.subpods.map((subpod) => { + const container = new ContainerBuilder() + .setAccentColor(SUCCESS_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(`## ${pod.title}`), + ); + if (subpod.plaintext.length > 0) { + container.addTextDisplayComponents( + new TextDisplayBuilder().setContent(subpod.plaintext), + ); + } + if (subpod.img !== undefined) { + container.addMediaGalleryComponents( + new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL(subpod.img.src), + ), + ); + } + return container; + }), + ); + + if (pages.length === 0) { + return { + kind: "components-v2", + components: [ + new ContainerBuilder() + .setAccentColor(FAILURE_ACCENT) + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(t("wolfram.noResults")), + ), + ], + }; + } + return paginatedReply({ + pages, + userId, + logger: dependencies.logger, + }); +} diff --git a/discord/src/features/wolfram/wolframClient.test.ts b/discord/src/features/wolfram/wolframClient.test.ts new file mode 100644 index 0000000..15d3cd3 --- /dev/null +++ b/discord/src/features/wolfram/wolframClient.test.ts @@ -0,0 +1,59 @@ +/** + * Tests the discord.js-free Wolfram adapter's response parsing and schema failures. + */ +import { describe, expect, it } from "vitest"; +import { parseWolframResponse, WolframResponseError } from "./wolframClient.js"; + +describe("parseWolframResponse", () => { + it("returns typed pods and subpods", () => { + expect( + parseWolframResponse({ + queryresult: { + success: true, + pods: [ + { + title: "Result", + subpods: [ + { + plaintext: "42", + img: { src: "https://example.invalid/result.png" }, + }, + ], + }, + ], + }, + }), + ).toEqual({ + success: true, + pods: [ + { + title: "Result", + subpods: [ + { + plaintext: "42", + img: { src: "https://example.invalid/result.png" }, + }, + ], + }, + ], + }); + }); + + it("accepts an unsuccessful query without pods", () => { + expect(parseWolframResponse({ queryresult: { success: false } })).toEqual({ + success: false, + pods: [], + }); + }); + + it("rejects malformed nested data", () => { + expect(() => + parseWolframResponse({ + queryresult: { + success: true, + pods: [{ title: "Result", subpods: [{ plaintext: 42 }] }], + }, + }), + ).toThrow(WolframResponseError); + }); +}); diff --git a/discord/src/features/wolfram/wolframClient.ts b/discord/src/features/wolfram/wolframClient.ts new file mode 100644 index 0000000..447cce6 --- /dev/null +++ b/discord/src/features/wolfram/wolframClient.ts @@ -0,0 +1,107 @@ +/** + * Adapts Wolfram|Alpha HTTP responses into feature-domain query results. + * The adapter is discord.js-free and unit-tested directly. + */ +const WOLFRAM_QUERY_URL = "http://api.wolframalpha.com/v2/query"; + +export interface WolframImage { + readonly src: string; +} + +export interface WolframSubpod { + readonly plaintext: string; + readonly img?: WolframImage; +} + +export interface WolframPod { + readonly title: string; + readonly subpods: readonly WolframSubpod[]; +} + +export interface WolframQueryResult { + readonly success: boolean; + readonly pods: readonly WolframPod[]; +} + +export class WolframResponseError extends Error { + constructor(message: string) { + super(message); + this.name = "WolframResponseError"; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function parseSubpod(value: unknown): WolframSubpod { + if (!isRecord(value) || typeof value.plaintext !== "string") { + throw new WolframResponseError("invalid Wolfram|Alpha subpod"); + } + if (value.img === undefined) { + return { plaintext: value.plaintext }; + } + if (!isRecord(value.img) || typeof value.img.src !== "string") { + throw new WolframResponseError("invalid Wolfram|Alpha subpod image"); + } + return { plaintext: value.plaintext, img: { src: value.img.src } }; +} + +function parsePod(value: unknown): WolframPod { + if ( + !isRecord(value) || + typeof value.title !== "string" || + !Array.isArray(value.subpods) + ) { + throw new WolframResponseError("invalid Wolfram|Alpha pod"); + } + return { + title: value.title, + subpods: value.subpods.map(parseSubpod), + }; +} + +export function parseWolframResponse(value: unknown): WolframQueryResult { + if (!isRecord(value) || !isRecord(value.queryresult)) { + throw new WolframResponseError("missing Wolfram|Alpha queryresult"); + } + const queryResult = value.queryresult; + if (typeof queryResult.success !== "boolean") { + throw new WolframResponseError("invalid Wolfram|Alpha success flag"); + } + if (queryResult.pods === undefined && !queryResult.success) { + return { success: false, pods: [] }; + } + if (!Array.isArray(queryResult.pods)) { + throw new WolframResponseError("invalid Wolfram|Alpha pods"); + } + return { + success: queryResult.success, + pods: queryResult.pods.map(parsePod), + }; +} + +export class WolframClient { + readonly #appId: string; + readonly #fetch: typeof fetch; + + constructor(appId: string, fetchImplementation: typeof fetch = fetch) { + this.#appId = appId; + this.#fetch = fetchImplementation; + } + + async query(input: string): Promise { + const url = new URL(WOLFRAM_QUERY_URL); + url.search = new URLSearchParams({ + input, + format: "image,plaintext", + output: "JSON", + appid: this.#appId, + }).toString(); + const response = await this.#fetch(url); + if (!response.ok) { + throw new Error(`Wolfram|Alpha returned HTTP status ${response.status}`); + } + return parseWolframResponse(await response.json()); + } +} diff --git a/discord/src/index.ts b/discord/src/index.ts new file mode 100644 index 0000000..9c9bb9a --- /dev/null +++ b/discord/src/index.ts @@ -0,0 +1,172 @@ +/** + * Composes the selected bot profile, shared services, features, and Discord platform router. + * This is the application's single composition root. + */ +import { Client, GatewayIntentBits, Partials, REST } from "discord.js"; +import { getBotProfile } from "./config.js"; +import { loadEnv } from "./env.js"; +import { createCodeFeature } from "./features/code/index.js"; +import { createMiscFeature } from "./features/misc/index.js"; +import { createPingFeature } from "./features/ping/index.js"; +import { createPrivacyFeature } from "./features/privacy/index.js"; +import { createTexFeature } from "./features/tex/index.js"; +import { createTranslateFeature } from "./features/translate/index.js"; +import { createWolframFeature } from "./features/wolfram/index.js"; +import { ErrorPresenter } from "./platform/discord/errorPresenter.js"; +import { ReplyCoordinator } from "./platform/discord/replyCoordinator.js"; +import { DiscordRouter } from "./platform/discord/router.js"; +import { getFixedT } from "./shared/i18n.js"; +import { createLogger } from "./shared/logger.js"; +import { + LocalFileState, + OptOutUsers, + S3ObjectState, + type StateBackend, +} from "./shared/state.js"; +import { UsageStats } from "./shared/usageStats.js"; +import type { Feature, FeatureFactory, FeatureId } from "./types.js"; + +const featureFactories: Record = { + tex: createTexFeature, + code: createCodeFeature, + privacy: createPrivacyFeature, + wolfram: createWolframFeature, + misc: createMiscFeature, + translate: createTranslateFeature, + ping: createPingFeature, +}; + +export async function bootstrap(): Promise { + const env = loadEnv(); + const profile = getBotProfile(env.botName); + const logger = createLogger(); + getFixedT(profile.defaultLocale); + + let stateBackend: StateBackend; + let usageStatsBackend: StateBackend; + if (env.state.backend === "local") { + stateBackend = new LocalFileState(env.state.filePath); + usageStatsBackend = new LocalFileState(env.state.usageStatsFilePath); + } else { + stateBackend = new S3ObjectState({ + endpoint: env.state.endpoint, + region: env.state.region, + bucket: env.state.bucket, + key: env.state.key, + credentials: { + accessKeyId: env.state.accessKeyId, + secretAccessKey: env.state.secretAccessKey, + }, + }); + usageStatsBackend = new S3ObjectState({ + endpoint: env.state.endpoint, + region: env.state.region, + bucket: env.state.bucket, + key: env.state.usageStatsKey, + credentials: { + accessKeyId: env.state.accessKeyId, + secretAccessKey: env.state.secretAccessKey, + }, + }); + } + const optOutUsers = new OptOutUsers(stateBackend); + await optOutUsers.init(); + const usageStats = new UsageStats(usageStatsBackend, { logger }); + await usageStats.init(); + const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.MessageContent, + ], + partials: [Partials.Channel, Partials.Message], + }); + let shuttingDown = false; + const shutdown = async (): Promise => { + if (shuttingDown) { + return; + } + shuttingDown = true; + try { + await usageStats.dispose(); + } catch (error) { + logger.error({ error }, "Failed to flush usage statistics on shutdown"); + } + // destroy() is async in discord.js v14; await it so the gateway close frame is sent. + await client.destroy().catch(() => undefined); + process.exit(0); + }; + process.once("SIGTERM", () => { + void shutdown(); + }); + process.once("SIGINT", () => { + void shutdown(); + }); + + const featureDependencies = { + optOutUsers, + usageStats, + logger, + env, + client, + }; + if (env.botName === "gaato-bot") { + if (env.wolframAppId === undefined) { + logger.warn( + { envName: "WOLFRAM_APPID", feature: "wolfram" }, + "gaato-bot feature disabled because an environment variable is missing", + ); + } + if (env.openAIApiKey === undefined) { + logger.warn( + { + envName: "OPENAI_API_KEY", + features: ["misc", "translate"], + }, + "gaato-bot features disabled because an environment variable is missing", + ); + } + } + const rest = new REST({ version: "10" }).setToken(env.token); + const features = [ + ...profile.features.map((id) => featureFactories[id](featureDependencies)), + featureFactories.ping(featureDependencies), + ].filter((feature): feature is Feature => feature !== undefined); + + for (const feature of features) { + await feature.init?.(); + } + + const replyCoordinator = new ReplyCoordinator(100); + const errorPresenter = new ErrorPresenter({ + client, + logger, + logChannelId: env.logChannelId, + supportServerLink: env.supportServerLink, + }); + const router = new DiscordRouter({ + client, + rest, + profile, + features, + replyCoordinator, + errorPresenter, + logger, + usageStats, + messageGate: (message) => !optOutUsers.has(message.author.id), + }); + router.bind(); + + try { + await client.login(env.token); + } catch (error) { + logger.error( + { error, tokenEnvName: env.tokenEnvName }, + "Discord login failed", + ); + throw error; + } +} + +await bootstrap(); diff --git a/discord/src/platform/discord/components.ts b/discord/src/platform/discord/components.ts new file mode 100644 index 0000000..ced1649 --- /dev/null +++ b/discord/src/platform/discord/components.ts @@ -0,0 +1,74 @@ +/** + * Builds reusable Discord Components V2 presentation primitives for the platform layer. + */ +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonStyle, + ContainerBuilder, + FileBuilder, + TextDisplayBuilder, +} from "discord.js"; + +export const SUCCESS_ACCENT = 0x2ecc71; +export const FAILURE_ACCENT = 0xe74c3c; +export const TEXT_FILE_CHARACTER_LIMIT = 1_000; +export const TEXT_FILE_LINE_LIMIT = 100; + +export function successContainer(text: string): ContainerBuilder { + return new ContainerBuilder() + .setAccentColor(SUCCESS_ACCENT) + .addTextDisplayComponents(new TextDisplayBuilder().setContent(text)); +} + +export function failureContainer(text: string): ContainerBuilder { + return new ContainerBuilder() + .setAccentColor(FAILURE_ACCENT) + .addTextDisplayComponents(new TextDisplayBuilder().setContent(text)); +} + +export function deleteButton(userId: string): ButtonBuilder { + return new ButtonBuilder() + .setCustomId(`delete:${userId}`) + .setLabel("Delete") + .setStyle(ButtonStyle.Danger); +} + +export function deleteButtonRow( + userId: string, +): ActionRowBuilder { + return new ActionRowBuilder().addComponents( + deleteButton(userId), + ); +} + +export interface TextComponentResult { + readonly component: TextDisplayBuilder | FileBuilder; + readonly files: readonly AttachmentBuilder[]; + readonly isFile: boolean; +} + +export function textComponent( + text: string, + filename = "output.txt", +): TextComponentResult { + const lineCount = text.length === 0 ? 0 : text.split("\n").length; + // Components V2 caps total text at 4,000 characters, so >1,000 chars or >100 lines use a file. + if ( + text.length > TEXT_FILE_CHARACTER_LIMIT || + lineCount > TEXT_FILE_LINE_LIMIT + ) { + return { + component: new FileBuilder().setURL(`attachment://${filename}`), + files: [new AttachmentBuilder(Buffer.from(text), { name: filename })], + isFile: true, + }; + } + + return { + component: new TextDisplayBuilder().setContent(text), + files: [], + isFile: false, + }; +} diff --git a/discord/src/platform/discord/context.ts b/discord/src/platform/discord/context.ts new file mode 100644 index 0000000..b5d032b --- /dev/null +++ b/discord/src/platform/discord/context.ts @@ -0,0 +1,51 @@ +/** + * Normalizes Discord interactions and messages into the platform's request context. + */ +import type { + ButtonInteraction, + ChatInputCommandInteraction, + ContextMenuCommandInteraction, + Message, + ModalSubmitInteraction, +} from "discord.js"; + +export type ReplyTarget = + | Message + | ChatInputCommandInteraction + | ContextMenuCommandInteraction + | ButtonInteraction + | ModalSubmitInteraction; + +export interface RequestContext { + readonly userId: string; + readonly locale: string; + readonly inGuild: boolean; + readonly replyTarget: ReplyTarget; +} + +type RequestInteraction = Exclude; + +export function contextForInteraction( + interaction: RequestInteraction, + defaultLocale: string, +): RequestContext { + return { + userId: interaction.user.id, + locale: + interaction.locale || interaction.guild?.preferredLocale || defaultLocale, + inGuild: interaction.inGuild(), + replyTarget: interaction, + }; +} + +export function contextForMessage( + message: Message, + defaultLocale: string, +): RequestContext { + return { + userId: message.author.id, + locale: message.guild?.preferredLocale ?? defaultLocale, + inGuild: message.inGuild(), + replyTarget: message, + }; +} diff --git a/discord/src/platform/discord/errorPresenter.ts b/discord/src/platform/discord/errorPresenter.ts new file mode 100644 index 0000000..8575afc --- /dev/null +++ b/discord/src/platform/discord/errorPresenter.ts @@ -0,0 +1,118 @@ +/** + * Converts handler failures into user replies and reports diagnostics through Discord. + * This platform service keeps error delivery separate from feature handlers. + */ +import { + AttachmentBuilder, + type Client, + DiscordAPIError, + RESTJSONErrorCodes, +} from "discord.js"; +import { getFixedT } from "../../shared/i18n.js"; +import type { AppLogger } from "../../shared/logger.js"; +import type { OutgoingReply } from "../../types.js"; +import { failureContainer } from "./components.js"; +import type { RequestContext } from "./context.js"; + +export class InvalidInputError extends Error {} + +export interface ErrorPresenterOptions { + readonly client: Client; + readonly logger: AppLogger; + readonly logChannelId: string; + readonly supportServerLink: string; +} + +export class ErrorPresenter { + readonly #client: Client; + readonly #logger: AppLogger; + readonly #logChannelId: string; + readonly #supportServerLink: string; + + constructor(options: ErrorPresenterOptions) { + this.#client = options.client; + this.#logger = options.logger; + this.#logChannelId = options.logChannelId; + this.#supportServerLink = options.supportServerLink; + } + + async present( + error: unknown, + context: RequestContext, + invocation: string, + ): Promise { + const normalized = + error instanceof Error ? error : new Error(String(error)); + await this.#logToDiscord(invocation, normalized); + + const t = getFixedT(context.locale); + const message = + error instanceof InvalidInputError + ? `## ${t("errors.invalidInput")}\n${error.message}` + : `## ${t("errors.unhandled")}\n${t("errors.report", { + supportLink: this.#supportServerLink, + })}`; + + return { + kind: "components-v2", + components: [failureContainer(message)], + }; + } + + async #logToDiscord(invocation: string, error: Error): Promise { + const stack = error.stack ?? `${error.name}: ${error.message}`; + // Try the channel cache, then an API fetch; any lookup or send failure falls back to Pino. + let channel = this.#client.channels.cache.get(this.#logChannelId); + + if (channel === undefined) { + try { + channel = + (await this.#client.channels.fetch(this.#logChannelId)) ?? undefined; + } catch (fetchError) { + this.#logger.error( + { + error: fetchError, + originalError: stack, + channelId: this.#logChannelId, + }, + "Unable to fetch Discord log channel", + ); + return; + } + } + + if (channel === undefined || !channel.isSendable()) { + this.#logger.error( + { originalError: stack, channelId: this.#logChannelId }, + "Discord log channel is not sendable", + ); + return; + } + + const content = `\`\`\`\n${invocation.slice(0, 1_900)}\n\`\`\``; + try { + await channel.send({ + content, + files: [ + new AttachmentBuilder(Buffer.from(stack), { + name: "error.txt", + }), + ], + }); + } catch (sendError) { + const forbidden = + sendError instanceof DiscordAPIError && + sendError.code === RESTJSONErrorCodes.MissingPermissions; + this.#logger.error( + { + error: sendError, + originalError: stack, + channelId: this.#logChannelId, + }, + forbidden + ? "Bot is missing access to Discord log channel" + : "Failed to send error to Discord log channel", + ); + } + } +} diff --git a/discord/src/platform/discord/paginator.ts b/discord/src/platform/discord/paginator.ts new file mode 100644 index 0000000..4bec702 --- /dev/null +++ b/discord/src/platform/discord/paginator.ts @@ -0,0 +1,133 @@ +/** + * Adds process-local, collector-driven pagination to platform reply components. + */ +import { randomUUID } from "node:crypto"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + type ContainerBuilder, + type Message, + MessageFlags, +} from "discord.js"; +import type { AppLogger } from "../../shared/logger.js"; +import type { OutgoingReply } from "../../types.js"; +import { deleteButtonRow } from "./components.js"; + +export const PAGINATOR_TIMEOUT_MS = 10 * 60 * 1_000; + +interface StoppableCollector { + stop(reason?: string): void; +} + +const activeCollectors = new WeakMap(); + +export function cancelPaginator(message: Message): void { + activeCollectors.get(message)?.stop("replaced"); + activeCollectors.delete(message); +} + +export interface PaginatorOptions { + readonly pages: readonly ContainerBuilder[]; + readonly userId: string; + readonly logger: AppLogger; + readonly timeoutMs?: number; +} + +function navigationRow( + token: string, + pageIndex: number, + pageCount: number, + disabled = false, +): ActionRowBuilder { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`pg:${token}:previous`) + .setLabel("◀") + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled || pageIndex === 0), + new ButtonBuilder() + .setCustomId(`pg:${token}:page`) + .setLabel(`${pageIndex + 1} / ${pageCount}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`pg:${token}:next`) + .setLabel("▶") + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled || pageIndex === pageCount - 1), + ); +} + +export function paginatedReply(options: PaginatorOptions): OutgoingReply { + if (options.pages.length === 0) { + throw new Error("paginator requires at least one page"); + } + + // The collector owns pg: interactions, so the reserved namespace bypasses the central router. + // Page state lives only in this process; after a restart, old buttons stop working by design. + const token = randomUUID(); + let pageIndex = 0; + const messageComponents = (disabled = false) => [ + options.pages[pageIndex] as ContainerBuilder, + navigationRow(token, pageIndex, options.pages.length, disabled), + deleteButtonRow(options.userId), + ]; + + return { + kind: "components-v2", + components: [ + options.pages[0] as ContainerBuilder, + navigationRow(token, 0, options.pages.length), + ], + async onDelivered(message) { + // A message-scoped collector keeps each paginator's mutable page index out of the router. + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (interaction) => + interaction.customId.startsWith(`pg:${token}:`), + time: options.timeoutMs ?? PAGINATOR_TIMEOUT_MS, + }); + activeCollectors.set(message, collector); + + collector.on("collect", (interaction) => { + void (async () => { + if (interaction.user.id !== options.userId) { + await interaction.deferUpdate(); + return; + } + + if (interaction.customId.endsWith(":previous")) { + pageIndex = Math.max(0, pageIndex - 1); + } else if (interaction.customId.endsWith(":next")) { + pageIndex = Math.min(options.pages.length - 1, pageIndex + 1); + } + await interaction.update({ components: messageComponents() }); + })().catch((error: unknown) => { + options.logger.warn({ error }, "Paginator interaction failed"); + }); + }); + + collector.on("end", (_collected, reason) => { + if (reason === "replaced") { + return; + } + if (activeCollectors.get(message) === collector) { + activeCollectors.delete(message); + } + void message + .edit({ + components: messageComponents(true), + flags: MessageFlags.IsComponentsV2, + }) + .catch((error: unknown) => { + options.logger.debug( + { error }, + "Unable to disable expired paginator", + ); + }); + }); + }, + }; +} diff --git a/discord/src/platform/discord/replyCoordinator.test.ts b/discord/src/platform/discord/replyCoordinator.test.ts new file mode 100644 index 0000000..82ea076 --- /dev/null +++ b/discord/src/platform/discord/replyCoordinator.test.ts @@ -0,0 +1,99 @@ +/** + * Exercises latest-wins generations and editable reply reuse in the Discord platform service. + */ +import type { MessageEditOptions } from "discord.js"; +import { describe, expect, it, vi } from "vitest"; +import type { ReplyTarget } from "./context.js"; +import { + type EditableReply, + ReplyCoordinator, + type ReplyTransport, +} from "./replyCoordinator.js"; + +function editableReply(): EditableReply { + return { + edit: vi.fn(async (_options: MessageEditOptions) => undefined), + delete: vi.fn(async () => undefined), + }; +} + +describe("ReplyCoordinator generations", () => { + it("drops late results from older generations and edits the tracked reply", async () => { + const reply = editableReply(); + const transport: ReplyTransport = { + send: vi.fn(async () => reply), + edit: vi.fn(async (target, payload) => { + await target.edit({ content: payload.content }); + }), + delete: vi.fn(async (target) => { + await target.delete(); + }), + }; + const coordinator = new ReplyCoordinator(100, transport); + const target = {} as ReplyTarget; + const firstGeneration = coordinator.begin("code", "source"); + const latestGeneration = coordinator.begin("code", "source"); + + await expect( + coordinator.deliver({ + featureId: "code", + target, + userId: "user", + outgoing: { kind: "plain", content: "old" }, + sourceMessageId: "source", + generation: firstGeneration, + }), + ).resolves.toBe("stale"); + expect(transport.send).not.toHaveBeenCalled(); + + await expect( + coordinator.deliver({ + featureId: "code", + target, + userId: "user", + outgoing: { kind: "plain", content: "latest" }, + sourceMessageId: "source", + generation: latestGeneration, + }), + ).resolves.toBe("sent"); + + const editedGeneration = coordinator.begin("code", "source"); + await expect( + coordinator.deliver({ + featureId: "code", + target, + userId: "user", + outgoing: { kind: "plain", content: "edited" }, + sourceMessageId: "source", + generation: editedGeneration, + }), + ).resolves.toBe("edited"); + + expect(transport.send).toHaveBeenCalledTimes(1); + expect(transport.edit).toHaveBeenCalledTimes(1); + }); + + it("removes tracked replies when the invocation is deleted", async () => { + const reply = editableReply(); + const transport: ReplyTransport = { + send: vi.fn(async () => reply), + edit: vi.fn(async () => undefined), + delete: vi.fn(async () => undefined), + }; + const coordinator = new ReplyCoordinator(100, transport); + const generation = coordinator.begin("tex", "source"); + await coordinator.deliver({ + featureId: "tex", + target: {} as ReplyTarget, + userId: "user", + outgoing: { kind: "plain", content: "result" }, + sourceMessageId: "source", + generation, + }); + + await coordinator.deleteReplies("source"); + + expect(transport.delete).toHaveBeenCalledWith(reply); + expect(coordinator.isTracked("tex", "source")).toBe(false); + }); +}); diff --git a/discord/src/platform/discord/replyCoordinator.ts b/discord/src/platform/discord/replyCoordinator.ts new file mode 100644 index 0000000..493494f --- /dev/null +++ b/discord/src/platform/discord/replyCoordinator.ts @@ -0,0 +1,253 @@ +/** + * Delivers feature reply values through Discord and tracks editable message replies. + * This platform service applies latest-wins coordination to message edits. + */ +import { + type BaseMessageOptions, + type InteractionEditReplyOptions, + type InteractionReplyOptions, + Message, + type MessageEditOptions, + MessageFlags, + type MessageReplyOptions, +} from "discord.js"; +import { LimitedSizeMap } from "../../shared/limitedMap.js"; +import type { FeatureId, OutgoingReply } from "../../types.js"; +import { deleteButtonRow } from "./components.js"; +import type { ReplyTarget } from "./context.js"; +import { cancelPaginator } from "./paginator.js"; + +export interface EditableReply { + edit(options: MessageEditOptions): Promise; + delete(): Promise; +} + +export interface ReplyPayload { + readonly kind: OutgoingReply["kind"]; + readonly content?: string; + readonly components: NonNullable; + readonly files?: BaseMessageOptions["files"]; + readonly ephemeral: boolean; + readonly replaceAttachments: boolean; +} + +export interface ReplyTransport { + send(target: ReplyTarget, payload: ReplyPayload): Promise; + edit(reply: EditableReply, payload: ReplyPayload): Promise; + delete(reply: EditableReply): Promise; +} + +function messageFlags(payload: ReplyPayload): number | undefined { + let flags = + payload.kind === "components-v2" ? MessageFlags.IsComponentsV2 : 0; + if (payload.ephemeral) { + flags |= MessageFlags.Ephemeral; + } + return flags === 0 ? undefined : flags; +} + +function editFlags( + payload: ReplyPayload, +): MessageFlags.IsComponentsV2 | undefined { + return payload.kind === "components-v2" + ? MessageFlags.IsComponentsV2 + : undefined; +} + +const discordReplyTransport: ReplyTransport = { + async send(target, payload) { + if (target instanceof Message) { + const options: MessageReplyOptions = { + content: payload.content, + components: payload.components, + files: payload.files, + flags: + payload.kind === "components-v2" + ? MessageFlags.IsComponentsV2 + : undefined, + }; + return target.reply(options); + } + + if (target.deferred || target.replied) { + const options: InteractionEditReplyOptions = { + content: payload.content, + components: payload.components, + files: payload.files, + // API v10 accepts an empty attachment set to replace files left by the prior reply. + attachments: payload.replaceAttachments ? [] : undefined, + flags: editFlags(payload), + }; + await target.editReply(options); + return target.fetchReply(); + } + + const options: InteractionReplyOptions = { + content: payload.content, + components: payload.components, + files: payload.files, + flags: messageFlags(payload), + }; + await target.reply(options); + return target.fetchReply(); + }, + + async edit(reply, payload) { + const options: MessageEditOptions = { + content: payload.content ?? null, + components: payload.components, + files: payload.files, + // API v10 attachment replacement needs [] or stale files survive a message edit. + attachments: [], + flags: editFlags(payload), + }; + await reply.edit(options); + }, + + async delete(reply) { + await reply.delete(); + }, +}; + +interface ReplyState { + readonly generation: number; + readonly reply?: EditableReply; + readonly kind?: OutgoingReply["kind"]; +} + +export interface DeliveryRequest { + readonly featureId: FeatureId; + readonly target: ReplyTarget; + readonly userId: string; + readonly outgoing: OutgoingReply; + readonly sourceMessageId?: string; + readonly generation?: number; +} + +export type DeliveryResult = "sent" | "edited" | "stale"; + +export class ReplyCoordinator { + readonly #capacity: number; + readonly #transport: ReplyTransport; + // Per-feature caps bound source-message tracking memory in long-running processes. + readonly #trackers = new Map>(); + + constructor( + capacity = 100, + transport: ReplyTransport = discordReplyTransport, + ) { + this.#capacity = capacity; + this.#transport = transport; + } + + begin(featureId: FeatureId, sourceMessageId: string): number { + const tracker = this.#tracker(featureId); + const current = tracker.get(sourceMessageId); + // Each source edit starts a generation; only the latest async invocation may publish. + const generation = (current?.generation ?? 0) + 1; + tracker.set(sourceMessageId, { ...current, generation }); + return generation; + } + + isTracked(featureId: FeatureId, sourceMessageId: string): boolean { + return this.#trackers.get(featureId)?.has(sourceMessageId) ?? false; + } + + async deliver(request: DeliveryRequest): Promise { + const payload = this.#payload(request.outgoing, request.userId); + if ( + request.sourceMessageId === undefined || + request.generation === undefined + ) { + const reply = await this.#transport.send(request.target, payload); + await this.#afterDelivery(request.outgoing, reply); + return "sent"; + } + + const sourceMessageId = request.sourceMessageId; + const generation = request.generation; + const tracker = this.#tracker(request.featureId); + const state = tracker.get(sourceMessageId); + // A newer invocation finished or remains in flight, so discard this stale result. + if (state?.generation !== generation) { + return "stale"; + } + + if (state.reply !== undefined) { + if (state.kind !== request.outgoing.kind) { + throw new Error( + `tracked reply kind cannot change from ${state.kind} to ${request.outgoing.kind}`, + ); + } + await this.#transport.edit(state.reply, payload); + await this.#afterDelivery(request.outgoing, state.reply); + return "edited"; + } + + const reply = await this.#transport.send(request.target, payload); + const latest = tracker.get(sourceMessageId); + if (latest?.generation !== generation) { + // The generation changed during send; delete the reply that lost the race. + await this.#transport.delete(reply); + return "stale"; + } + + tracker.set(sourceMessageId, { + generation: latest.generation, + reply, + kind: request.outgoing.kind, + }); + await this.#afterDelivery(request.outgoing, reply); + return "sent"; + } + + async deleteReplies(sourceMessageId: string): Promise { + const deletions: Promise[] = []; + for (const tracker of this.#trackers.values()) { + const state = tracker.get(sourceMessageId); + tracker.delete(sourceMessageId); + if (state?.reply !== undefined) { + deletions.push(this.#transport.delete(state.reply)); + } + } + await Promise.allSettled(deletions); + } + + #tracker(featureId: FeatureId): LimitedSizeMap { + let tracker = this.#trackers.get(featureId); + if (tracker === undefined) { + tracker = new LimitedSizeMap(this.#capacity); + this.#trackers.set(featureId, tracker); + } + return tracker; + } + + #payload(outgoing: OutgoingReply, userId: string): ReplyPayload { + // Ephemeral replies cannot be deleted via message.delete(); Discord + // provides its own dismiss affordance, so no delete button there. + const deleteRow = outgoing.ephemeral ? [] : [deleteButtonRow(userId)]; + const components = + outgoing.kind === "components-v2" + ? [...outgoing.components, ...deleteRow] + : deleteRow; + + return { + kind: outgoing.kind, + content: outgoing.kind === "plain" ? outgoing.content : undefined, + components, + files: outgoing.files, + ephemeral: outgoing.ephemeral ?? false, + replaceAttachments: true, + }; + } + + async #afterDelivery( + outgoing: OutgoingReply, + reply: EditableReply, + ): Promise { + if (reply instanceof Message) { + cancelPaginator(reply); + await outgoing.onDelivered?.(reply); + } + } +} diff --git a/discord/src/platform/discord/router.test.ts b/discord/src/platform/discord/router.test.ts new file mode 100644 index 0000000..b037d00 --- /dev/null +++ b/discord/src/platform/discord/router.test.ts @@ -0,0 +1,309 @@ +/** + * Tests startup route validation, prefix parsing, and anonymous dispatch instrumentation. + */ +import { + type Client, + Events, + type REST, + SlashCommandBuilder, +} from "discord.js"; +import { describe, expect, it, vi } from "vitest"; +import type { BotProfile } from "../../config.js"; +import type { AppLogger } from "../../shared/logger.js"; +import type { UsageStats } from "../../shared/usageStats.js"; +import type { Feature } from "../../types.js"; +import type { ErrorPresenter } from "./errorPresenter.js"; +import type { ReplyCoordinator } from "./replyCoordinator.js"; +import { + buildRouteRegistry, + DiscordRouter, + parsePrefixCommand, +} from "./router.js"; + +describe("buildRouteRegistry", () => { + it("fails fast when customId namespaces overlap across handler kinds", () => { + const modalFeature: Feature = { + id: "code", + modalHandlers: { + shared: async () => undefined, + }, + }; + const componentFeature: Feature = { + id: "tex", + componentHandlers: { + shared: async () => undefined, + }, + }; + + expect(() => buildRouteRegistry([modalFeature, componentFeature])).toThrow( + "duplicate customId namespace route: shared", + ); + }); + + it("rejects central and collector-owned namespaces", () => { + const feature: Feature = { + id: "code", + componentHandlers: { + pg: async () => undefined, + }, + }; + + expect(() => buildRouteRegistry([feature])).toThrow( + "customId namespace is reserved: pg", + ); + }); +}); + +describe("parsePrefixCommand", () => { + it("preserves the raw rest argument after one separator", () => { + expect(parsePrefixCommand("]run python\nprint(1)", "]")).toEqual({ + name: "run", + args: " python\nprint(1)", + }); + }); +}); + +describe("DiscordRouter usage statistics", () => { + it("records successful slash, prefix, modal, and button dispatches", async () => { + const slashExecute = vi.fn().mockResolvedValue(undefined); + const prefixExecute = vi.fn().mockResolvedValue(undefined); + const modalHandler = vi.fn().mockResolvedValue(undefined); + const buttonHandler = vi.fn().mockResolvedValue(undefined); + const feature: Feature = { + id: "code", + slashCommands: [ + { + data: new SlashCommandBuilder() + .setName("test") + .setDescription("Test command"), + execute: slashExecute, + }, + ], + prefixCommands: [ + { + name: "run", + aliases: ["r"], + execute: prefixExecute, + }, + ], + modalHandlers: { form: modalHandler }, + componentHandlers: { action: buttonHandler }, + }; + const harness = createHarness([feature]); + + harness.dispatch( + Events.InteractionCreate, + interaction({ + commandName: "test", + isChatInputCommand: () => true, + toString: () => "/test", + }), + ); + await waitForCalls(harness.recordCommand, 1); + + harness.dispatch(Events.MessageCreate, { + id: "message", + content: "]r python print(1)", + author: { id: "user", bot: false }, + guild: null, + inGuild: () => false, + channel: { sendTyping: vi.fn().mockResolvedValue(undefined) }, + }); + await waitForCalls(harness.recordCommand, 2); + + harness.dispatch( + Events.InteractionCreate, + interaction({ + customId: "form:value", + isModalSubmit: () => true, + }), + ); + await waitForCalls(harness.recordCommand, 3); + + harness.dispatch( + Events.InteractionCreate, + interaction({ + customId: "action:value", + isButton: () => true, + }), + ); + await waitForCalls(harness.recordCommand, 4); + + expect(harness.recordCommand.mock.calls).toEqual([ + ["slash", "test", "ok"], + ["prefix", "run", "ok"], + ["modal", "form", "ok"], + ["button", "action", "ok"], + ]); + }); + + it("records a slash handler failure even when the error is presented", async () => { + const error = new Error("handler failed"); + const feature: Feature = { + id: "code", + slashCommands: [ + { + data: new SlashCommandBuilder() + .setName("test") + .setDescription("Test command"), + execute: vi.fn().mockRejectedValue(error), + }, + ], + }; + const harness = createHarness([feature]); + + harness.dispatch( + Events.InteractionCreate, + interaction({ + commandName: "test", + isChatInputCommand: () => true, + toString: () => "/test", + }), + ); + + await vi.waitFor(() => { + expect(harness.presentError).toHaveBeenCalledOnce(); + expect(harness.deliver).toHaveBeenCalledOnce(); + }); + expect(harness.recordCommand).toHaveBeenCalledOnce(); + expect(harness.recordCommand).toHaveBeenCalledWith( + "slash", + "test", + "error", + ); + }); + + it("does not record reserved delete or pagination buttons", async () => { + const harness = createHarness([]); + const deleteMessage = vi.fn().mockResolvedValue(undefined); + const deferUpdate = vi.fn().mockResolvedValue(undefined); + + harness.dispatch( + Events.InteractionCreate, + interaction({ + customId: "pg:next", + isButton: () => true, + }), + ); + harness.dispatch( + Events.InteractionCreate, + interaction({ + customId: "delete:user", + isButton: () => true, + deferUpdate, + message: { delete: deleteMessage }, + }), + ); + + await vi.waitFor(() => expect(deleteMessage).toHaveBeenCalledOnce()); + expect(deferUpdate).toHaveBeenCalledOnce(); + expect(harness.recordCommand).not.toHaveBeenCalled(); + }); + + it("records guild counts on ready, join, and leave events", async () => { + const harness = createHarness([]); + harness.guildCache.set("one", {}); + harness.guildCache.set("two", {}); + + harness.dispatch(Events.ClientReady, harness.client); + await vi.waitFor(() => expect(harness.put).toHaveBeenCalledOnce()); + harness.guildCache.set("three", {}); + harness.dispatch(Events.GuildCreate, {}); + harness.guildCache.delete("three"); + harness.dispatch(Events.GuildDelete, {}); + + expect(harness.recordGuildCount.mock.calls).toEqual([[2], [3], [2]]); + }); +}); + +type Listener = (...args: never[]) => void; + +function createHarness(features: readonly Feature[]) { + const listeners = new Map(); + const guildCache = new Map(); + const client = { + application: { id: "application" }, + guilds: { cache: guildCache }, + once: vi.fn((event: string, listener: Listener) => { + listeners.set(event, listener); + }), + on: vi.fn((event: string, listener: Listener) => { + listeners.set(event, listener); + }), + }; + const put = vi.fn().mockResolvedValue(undefined); + const deliver = vi.fn().mockResolvedValue("sent"); + const presentError = vi.fn().mockResolvedValue({ + kind: "plain", + content: "error", + }); + const recordCommand = vi.fn(); + const recordGuildCount = vi.fn(); + const profile: BotProfile = { + name: "coderunbot", + prefix: "]", + defaultLocale: "en", + features: [], + }; + const router = new DiscordRouter({ + client: client as unknown as Client, + rest: { put } as unknown as REST, + profile, + features, + replyCoordinator: { + begin: vi.fn().mockReturnValue(1), + deliver, + deleteReplies: vi.fn().mockResolvedValue(undefined), + } as unknown as ReplyCoordinator, + errorPresenter: { present: presentError } as unknown as ErrorPresenter, + logger: { + info: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + } as unknown as AppLogger, + usageStats: { + recordCommand, + recordGuildCount, + } as unknown as UsageStats, + }); + router.bind(); + + return { + client, + deliver, + dispatch(event: string, ...args: unknown[]) { + const listener = listeners.get(event); + if (listener === undefined) { + throw new Error(`event is not bound: ${event}`); + } + listener(...(args as never[])); + }, + guildCache, + presentError, + put, + recordCommand, + recordGuildCount, + }; +} + +function interaction(overrides: Record): unknown { + return { + user: { id: "user" }, + locale: "en-US", + guild: null, + deferred: false, + replied: false, + inGuild: () => false, + isAutocomplete: () => false, + isChatInputCommand: () => false, + isContextMenuCommand: () => false, + isButton: () => false, + isModalSubmit: () => false, + toString: () => "interaction", + ...overrides, + }; +} + +async function waitForCalls(mock: ReturnType, count: number) { + await vi.waitFor(() => expect(mock).toHaveBeenCalledTimes(count)); +} diff --git a/discord/src/platform/discord/router.ts b/discord/src/platform/discord/router.ts new file mode 100644 index 0000000..eccd5a1 --- /dev/null +++ b/discord/src/platform/discord/router.ts @@ -0,0 +1,513 @@ +/** + * Registers feature routes and dispatches Discord events in the platform layer. + */ +import { + ApplicationIntegrationType, + type AutocompleteInteraction, + type ButtonInteraction, + type ChatInputCommandInteraction, + type Client, + type ContextMenuCommandInteraction, + Events, + type Interaction, + InteractionContextType, + type Message, + type ModalSubmitInteraction, + type PartialMessage, + type REST, + Routes, +} from "discord.js"; +import type { BotProfile } from "../../config.js"; +import type { AppLogger } from "../../shared/logger.js"; +import type { UsageStats } from "../../shared/usageStats.js"; +import type { + ComponentHandler, + Feature, + FeatureId, + ModalHandler, + PrefixCommand, + SlashCommand, +} from "../../types.js"; +import { + contextForInteraction, + contextForMessage, + type RequestContext, +} from "./context.js"; +import type { ErrorPresenter } from "./errorPresenter.js"; +import type { ReplyCoordinator } from "./replyCoordinator.js"; + +interface RegisteredSlash { + readonly featureId: FeatureId; + readonly command: SlashCommand; +} + +interface RegisteredPrefix { + readonly featureId: FeatureId; + readonly command: PrefixCommand; +} + +type CustomRoute = + | { + readonly kind: "modal"; + readonly featureId: FeatureId; + readonly handler: ModalHandler; + } + | { + readonly kind: "component"; + readonly featureId: FeatureId; + readonly handler: ComponentHandler; + }; + +export interface RouteRegistry { + readonly slash: ReadonlyMap; + readonly prefix: ReadonlyMap; + readonly custom: ReadonlyMap; +} + +function addUnique( + map: Map, + route: string, + value: Value, + routeType: string, +): void { + // Registry construction happens in the router constructor, so duplicates fail at startup. + if (map.has(route)) { + throw new Error(`duplicate ${routeType} route: ${route}`); + } + map.set(route, value); +} + +export function buildRouteRegistry( + features: readonly Feature[], +): RouteRegistry { + const slash = new Map(); + const prefix = new Map(); + // customIds use namespace:...; the first colon-delimited segment selects the route. + const custom = new Map(); + const featureIds = new Set(); + + for (const feature of features) { + if (featureIds.has(feature.id)) { + throw new Error(`duplicate feature id: ${feature.id}`); + } + featureIds.add(feature.id); + + for (const command of feature.slashCommands ?? []) { + addUnique( + slash, + command.data.name, + { featureId: feature.id, command }, + "application command", + ); + } + + for (const command of feature.prefixCommands ?? []) { + for (const name of [command.name, ...(command.aliases ?? [])]) { + addUnique( + prefix, + name.toLowerCase(), + { featureId: feature.id, command }, + "prefix command", + ); + } + } + + for (const [namespace, handler] of Object.entries( + feature.modalHandlers ?? {}, + )) { + addUnique( + custom, + namespace, + { kind: "modal", featureId: feature.id, handler }, + "customId namespace", + ); + } + + for (const [namespace, handler] of Object.entries( + feature.componentHandlers ?? {}, + )) { + addUnique( + custom, + namespace, + { kind: "component", featureId: feature.id, handler }, + "customId namespace", + ); + } + } + + // Delete handling and collector-based pagination own these platform namespaces. + for (const reserved of ["delete", "pg"]) { + if (custom.has(reserved)) { + throw new Error(`customId namespace is reserved: ${reserved}`); + } + } + + return { slash, prefix, custom }; +} + +export interface ParsedPrefixCommand { + readonly name: string; + readonly args: string; +} + +export function parsePrefixCommand( + content: string, + prefix: string, +): ParsedPrefixCommand | undefined { + if (!content.startsWith(prefix)) { + return undefined; + } + + const input = content.slice(prefix.length); + const separator = input.search(/\s/u); + const name = ( + separator === -1 ? input : input.slice(0, separator) + ).toLowerCase(); + if (name.length === 0) { + return undefined; + } + + return { + name, + // Remove only the command separator. The rest remains byte-for-byte intact. + args: separator === -1 ? "" : input.slice(separator + 1), + }; +} + +export type MessageGate = (message: Message) => boolean | Promise; + +export interface DiscordRouterOptions { + readonly client: Client; + readonly rest: REST; + readonly profile: BotProfile; + readonly features: readonly Feature[]; + readonly replyCoordinator: ReplyCoordinator; + readonly errorPresenter: ErrorPresenter; + readonly logger: AppLogger; + readonly usageStats?: UsageStats; + readonly messageGate?: MessageGate; +} + +export class DiscordRouter { + readonly #client: Client; + readonly #rest: REST; + readonly #profile: BotProfile; + readonly #features: readonly Feature[]; + readonly #routes: RouteRegistry; + readonly #replyCoordinator: ReplyCoordinator; + readonly #errorPresenter: ErrorPresenter; + readonly #logger: AppLogger; + readonly #usageStats?: UsageStats; + readonly #messageGate: MessageGate; + + constructor(options: DiscordRouterOptions) { + this.#client = options.client; + this.#rest = options.rest; + this.#profile = options.profile; + this.#features = options.features; + this.#routes = buildRouteRegistry(options.features); + this.#replyCoordinator = options.replyCoordinator; + this.#errorPresenter = options.errorPresenter; + this.#logger = options.logger; + this.#usageStats = options.usageStats; + this.#messageGate = options.messageGate ?? (() => true); + } + + bind(): void { + this.#client.once(Events.ClientReady, (client) => { + void this.#onReady(client); + }); + this.#client.on(Events.InteractionCreate, (interaction) => { + void this.#onInteraction(interaction); + }); + this.#client.on(Events.MessageCreate, (message) => { + void this.#onMessage(message); + }); + this.#client.on(Events.MessageUpdate, (before, after) => { + void this.#onMessageUpdate(before, after); + }); + this.#client.on(Events.MessageDelete, (message) => { + void this.#replyCoordinator.deleteReplies(message.id); + }); + this.#client.on(Events.GuildCreate, () => { + this.#usageStats?.recordGuildCount(this.#client.guilds.cache.size); + }); + this.#client.on(Events.GuildDelete, () => { + this.#usageStats?.recordGuildCount(this.#client.guilds.cache.size); + }); + } + + async #onReady(client: Client): Promise { + this.#usageStats?.recordGuildCount(client.guilds.cache.size); + const body = [...this.#routes.slash.values()].map(({ command }) => ({ + ...command.data.toJSON(), + // User-installable apps require both install types and all supported invocation contexts. + integration_types: [ + ApplicationIntegrationType.GuildInstall, + ApplicationIntegrationType.UserInstall, + ], + contexts: [ + InteractionContextType.Guild, + InteractionContextType.BotDM, + InteractionContextType.PrivateChannel, + ], + })); + + try { + // ClientReady provides the application ID; REST.put bulk-overwrites commands idempotently. + await this.#rest.put(Routes.applicationCommands(client.application.id), { + body, + }); + this.#logger.info( + { commandCount: body.length, bot: this.#profile.name }, + "Registered Discord application commands", + ); + } catch (error) { + this.#logger.error({ error }, "Failed to register application commands"); + } + } + + async #onInteraction(interaction: Interaction): Promise { + if (interaction.isAutocomplete()) { + await this.#onAutocomplete(interaction); + return; + } + + if ( + interaction.isChatInputCommand() || + interaction.isContextMenuCommand() + ) { + await this.#onApplicationCommand(interaction); + return; + } + + if (interaction.isButton()) { + await this.#onButton(interaction); + return; + } + + if (interaction.isModalSubmit()) { + await this.#onModal(interaction); + } + } + + async #onApplicationCommand( + interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction, + ): Promise { + const route = this.#routes.slash.get(interaction.commandName); + if (route === undefined) { + return; + } + const context = contextForInteraction( + interaction, + this.#profile.defaultLocale, + ); + await this.#invoke( + route.featureId, + context, + interaction.toString(), + () => route.command.execute(interaction, context), + { + kind: interaction.isChatInputCommand() ? "slash" : "context-menu", + name: interaction.commandName, + }, + ); + } + + async #onAutocomplete(interaction: AutocompleteInteraction): Promise { + const route = this.#routes.slash.get(interaction.commandName); + if (route?.command.autocomplete === undefined) { + return; + } + + try { + await route.command.autocomplete(interaction); + } catch (error) { + const context = contextForInteraction( + interaction as unknown as ChatInputCommandInteraction, + this.#profile.defaultLocale, + ); + await this.#errorPresenter.present( + error, + context, + interaction.toString(), + ); + if (!interaction.responded) { + await interaction.respond([]); + } + } + } + + async #onButton(interaction: ButtonInteraction): Promise { + const namespace = interaction.customId.split(":", 1)[0] ?? ""; + if (namespace === "pg") { + return; + } + if (namespace === "delete") { + const ownerId = interaction.customId.slice("delete:".length); + if (ownerId === interaction.user.id) { + await interaction.deferUpdate(); + await interaction.message.delete(); + } else if (!interaction.deferred && !interaction.replied) { + await interaction.deferUpdate(); + } + return; + } + + const route = this.#routes.custom.get(namespace); + if (route?.kind !== "component") { + return; + } + const context = contextForInteraction( + interaction, + this.#profile.defaultLocale, + ); + await this.#invoke( + route.featureId, + context, + interaction.customId, + () => route.handler(interaction, context), + { kind: "button", name: namespace }, + ); + } + + async #onModal(interaction: ModalSubmitInteraction): Promise { + const namespace = interaction.customId.split(":", 1)[0] ?? ""; + const route = this.#routes.custom.get(namespace); + if (route?.kind !== "modal") { + return; + } + const context = contextForInteraction( + interaction, + this.#profile.defaultLocale, + ); + await this.#invoke( + route.featureId, + context, + interaction.customId, + () => route.handler(interaction, context), + { kind: "modal", name: namespace }, + ); + } + + async #onMessage(message: Message): Promise { + if (message.author.bot || !(await this.#messageGate(message))) { + return; + } + + const context = contextForMessage(message, this.#profile.defaultLocale); + const handledFeatures = new Set(); + const parsed = parsePrefixCommand(message.content, this.#profile.prefix); + const prefixRoute = parsed && this.#routes.prefix.get(parsed.name); + + if (prefixRoute !== undefined && parsed !== undefined) { + handledFeatures.add(prefixRoute.featureId); + try { + // Messages have no defer API; typing gives feedback while interactions must + // acknowledge within three seconds. + if ("sendTyping" in message.channel) { + await message.channel.sendTyping(); + } + } catch (error) { + this.#logger.debug({ error }, "Unable to send typing indicator"); + } + await this.#invokeMessage( + prefixRoute.featureId, + context, + message.content, + () => prefixRoute.command.execute(message, parsed.args, context), + { kind: "prefix", name: prefixRoute.command.name }, + ); + } + + for (const feature of this.#features) { + if (feature.onMessage === undefined || handledFeatures.has(feature.id)) { + continue; + } + const onMessage = feature.onMessage; + await this.#invokeMessage(feature.id, context, message.content, () => + onMessage(message, context), + ); + } + } + + async #onMessageUpdate( + before: Message | PartialMessage, + after: Message | PartialMessage, + ): Promise { + if (before.partial || after.partial || before.content === after.content) { + return; + } + await this.#onMessage(after); + } + + async #invokeMessage( + featureId: FeatureId, + context: RequestContext, + invocation: string, + handler: () => ReturnType, + stat?: { readonly kind: string; readonly name: string }, + ): Promise { + const sourceMessageId = (context.replyTarget as Message).id; + const generation = this.#replyCoordinator.begin(featureId, sourceMessageId); + await this.#invoke(featureId, context, invocation, handler, stat, { + sourceMessageId, + generation, + }); + } + + async #invoke( + featureId: FeatureId, + context: RequestContext, + invocation: string, + handler: () => ReturnType, + stat?: { readonly kind: string; readonly name: string }, + tracking?: { + readonly sourceMessageId: string; + readonly generation: number; + }, + ): Promise { + try { + const outgoing = await handler(); + if (outgoing === undefined) { + if (stat !== undefined) { + this.#usageStats?.recordCommand(stat.kind, stat.name, "ok"); + } + return; + } + await this.#replyCoordinator.deliver({ + featureId, + target: context.replyTarget, + userId: context.userId, + outgoing, + ...tracking, + }); + if (stat !== undefined) { + this.#usageStats?.recordCommand(stat.kind, stat.name, "ok"); + } + } catch (error) { + if (stat !== undefined) { + this.#usageStats?.recordCommand(stat.kind, stat.name, "error"); + } + try { + const outgoing = await this.#errorPresenter.present( + error, + context, + invocation, + ); + await this.#replyCoordinator.deliver({ + featureId, + target: context.replyTarget, + userId: context.userId, + outgoing, + ...tracking, + }); + } catch (presentationError) { + this.#logger.error( + { error: presentationError, originalError: error }, + "Failed to present handler error", + ); + } + } + } +} diff --git a/discord/src/shared/i18n.ts b/discord/src/shared/i18n.ts new file mode 100644 index 0000000..0994e4f --- /dev/null +++ b/discord/src/shared/i18n.ts @@ -0,0 +1,160 @@ +/** + * Owns shared translation resources and request-scoped translator creation. + */ +import i18next from "i18next"; + +const resources = { + en: { + translation: { + errors: { + unhandled: "An unexpected error occurred.", + invalidInput: "Invalid input", + report: "Please report this problem to us: {{supportLink}}", + }, + privacy: { + source: + "This bot is free software licensed under the GNU AGPL v3 or later. The full source code is available at {{url}}", + optOut: { + already: + "Your message content is already off-track. To use other commands, please use the /opt-in command.", + success: + "This bot will not track your message content from now on. Most commands will no longer respond.", + }, + optIn: { + already: "This bot is already tracking your message content.", + success: + "This bot will now track the content of your messages. It will only be used to provide commands. Use the /privacy-policy command to view the privacy policy.", + }, + }, + tex: { + hintHeading: "Hint", + multilineHint: "You can use the gather or align environment.", + codeHeading: "Code", + errorHeading: "Rendering Error", + modal: { + title: "LaTeX to Image", + codeLabel: "Code", + codePlaceholder: "Enter TeX code here", + }, + }, + code: { + escape: { + noText: "That message has no text to escape.", + }, + supportedLanguagesHeading: "The following languages are supported", + noLanguagesAvailable: + "The language list is temporarily unavailable. Please try again later.", + resultHeading: "Result ({{compiler}})", + codeHeading: "Code", + modal: { + title: "Run code", + codeLabel: "Code", + codePlaceholder: "Write code here", + stdinLabel: "Standard Input", + }, + errors: { + heading: "Wandbox Error", + connection: "Could not connect to Wandbox. Please try again later.", + http: "Wandbox returned HTTP status {{status}}.", + nonJson: + "Wandbox returned a non-JSON response. Please check the service status and try again later.", + responsePreview: "Response Preview", + }, + }, + wolfram: { + errorHeading: "Wolfram|Alpha Error", + notUnderstood: "Wolfram|Alpha could not understand that input.", + noResults: "Wolfram|Alpha returned no result pages.", + }, + translate: { + invalidLanguage: + "Invalid language. Enter an ISO 639-1 code or language name.", + originalHeading: "Original", + translatedHeading: "Translated to {{language}}", + }, + }, + }, + ja: { + translation: { + errors: { + unhandled: "予期しないエラーが発生しました。", + invalidInput: "入力が正しくありません", + report: "この問題を報告してください: {{supportLink}}", + }, + privacy: { + source: + "このボットは GNU AGPL v3 以降のライセンスで公開されている自由ソフトウェアです。ソースコードはこちら: {{url}}", + optOut: { + already: + "メッセージ内容はすでに処理対象外です。他のコマンドを使用するには /opt-in コマンドを使用してください。", + success: + "今後、このボットはあなたのメッセージ内容を処理しません。ほとんどのコマンドは応答しなくなります。", + }, + optIn: { + already: "このボットはすでにあなたのメッセージ内容を処理しています。", + success: + "このボットは今後、あなたのメッセージ内容をコマンド提供のためだけに処理します。プライバシーポリシーは /privacy-policy コマンドで確認できます。", + }, + }, + tex: { + hintHeading: "ヒント", + multilineHint: "gather または align 環境を利用できます。", + codeHeading: "コード", + errorHeading: "レンダリングエラー", + modal: { + title: "LaTeX を画像に変換", + codeLabel: "コード", + codePlaceholder: "TeX コードを入力してください", + }, + }, + code: { + escape: { + noText: "そのメッセージにはエスケープできるテキストがありません。", + }, + supportedLanguagesHeading: "対応している言語", + noLanguagesAvailable: + "言語一覧を一時的に取得できません。しばらくしてからもう一度お試しください。", + resultHeading: "実行結果 ({{compiler}})", + codeHeading: "コード", + modal: { + title: "コードを実行", + codeLabel: "コード", + codePlaceholder: "コードを入力してください", + stdinLabel: "標準入力", + }, + errors: { + heading: "Wandbox エラー", + connection: + "Wandbox に接続できませんでした。しばらくしてからもう一度お試しください。", + http: "Wandbox が HTTP ステータス {{status}} を返しました。", + nonJson: + "Wandbox から JSON ではない応答が返されました。サービスの状態を確認して、しばらくしてからもう一度お試しください。", + responsePreview: "応答のプレビュー", + }, + }, + wolfram: { + errorHeading: "Wolfram|Alpha エラー", + notUnderstood: "Wolfram|Alpha は、その入力を理解できませんでした。", + noResults: "Wolfram|Alpha から結果ページが返されませんでした。", + }, + translate: { + invalidLanguage: + "無効な言語です。ISO 639-1 の2文字コードまたは言語名を入力してください。", + originalHeading: "原文", + translatedHeading: "翻訳結果({{language}})", + }, + }, + }, +} as const; + +const instance = i18next.createInstance(); +await instance.init({ + fallbackLng: "en", + interpolation: { escapeValue: false }, + resources, +}); + +export function getFixedT(locale: string) { + // A fixed translator avoids global changeLanguage calls bleeding across concurrent requests. + return instance.getFixedT(locale); +} diff --git a/discord/src/shared/limitedMap.test.ts b/discord/src/shared/limitedMap.test.ts new file mode 100644 index 0000000..e7fab94 --- /dev/null +++ b/discord/src/shared/limitedMap.test.ts @@ -0,0 +1,40 @@ +/** + * Verifies the shared bounded map's validation and oldest-entry eviction behavior. + */ +import { describe, expect, it } from "vitest"; +import { LimitedSizeMap } from "./limitedMap.js"; + +describe("LimitedSizeMap", () => { + it("evicts the oldest insertion when capacity is exceeded", () => { + const map = new LimitedSizeMap(2); + map.set("first", 1).set("second", 2).set("third", 3); + + expect([...map]).toEqual([ + ["second", 2], + ["third", 3], + ]); + }); + + it("keeps the original insertion order when an existing key is updated", () => { + const map = new LimitedSizeMap(2); + map.set("first", 1).set("second", 2).set("first", 10).set("third", 3); + + expect(map.has("first")).toBe(false); + expect([...map]).toEqual([ + ["second", 2], + ["third", 3], + ]); + }); + + it("supports undefined as a key", () => { + const map = new LimitedSizeMap(1); + map.set(undefined, 1).set("next", 2); + + expect([...map]).toEqual([["next", 2]]); + }); + + it("rejects invalid capacities", () => { + expect(() => new LimitedSizeMap(0)).toThrow(RangeError); + expect(() => new LimitedSizeMap(1.5)).toThrow(RangeError); + }); +}); diff --git a/discord/src/shared/limitedMap.ts b/discord/src/shared/limitedMap.ts new file mode 100644 index 0000000..adb74d1 --- /dev/null +++ b/discord/src/shared/limitedMap.ts @@ -0,0 +1,49 @@ +/** + * Provides a shared insertion-ordered map with a fixed upper bound. + */ +export class LimitedSizeMap implements Iterable<[Key, Value]> { + readonly #entries = new Map(); + readonly #capacity: number; + + constructor(capacity: number) { + if (!Number.isInteger(capacity) || capacity < 1) { + throw new RangeError("capacity must be a positive integer"); + } + this.#capacity = capacity; + } + + get size(): number { + return this.#entries.size; + } + + get(key: Key): Value | undefined { + return this.#entries.get(key); + } + + has(key: Key): boolean { + return this.#entries.has(key); + } + + set(key: Key, value: Value): this { + this.#entries.set(key, value); + if (this.#entries.size > this.#capacity) { + const oldest = this.#entries.keys().next(); + if (!oldest.done) { + this.#entries.delete(oldest.value); + } + } + return this; + } + + delete(key: Key): boolean { + return this.#entries.delete(key); + } + + clear(): void { + this.#entries.clear(); + } + + [Symbol.iterator](): MapIterator<[Key, Value]> { + return this.#entries[Symbol.iterator](); + } +} diff --git a/discord/src/shared/logger.ts b/discord/src/shared/logger.ts new file mode 100644 index 0000000..295b3a1 --- /dev/null +++ b/discord/src/shared/logger.ts @@ -0,0 +1,10 @@ +/** + * Exposes the shared application logger type and its Pino-backed factory. + */ +import pino, { type Logger, type LoggerOptions } from "pino"; + +export type AppLogger = Logger; + +export function createLogger(options?: LoggerOptions): AppLogger { + return pino(options); +} diff --git a/discord/src/shared/state.test.ts b/discord/src/shared/state.test.ts new file mode 100644 index 0000000..6527800 --- /dev/null +++ b/discord/src/shared/state.test.ts @@ -0,0 +1,102 @@ +/** + * Tests the discord.js-free state adapters and serialized opt-out persistence directly. + */ +import { mkdtemp, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { LocalFileState, OptOutUsers, type StateBackend } from "./state.js"; + +class MemoryState implements StateBackend { + readonly writes: string[] = []; + + constructor(readonly value = "") {} + + async read(): Promise { + return this.value; + } + + async write(value: string): Promise { + this.writes.push(value); + } +} + +describe("OptOutUsers", () => { + it("parses newline-delimited string snowflakes and ignores blank lines", async () => { + const state = new MemoryState( + " 123456789012345678\r\n\n987654321098765432\n", + ); + const users = new OptOutUsers(state); + + await users.init(); + + expect(users.has("123456789012345678")).toBe(true); + expect(users.has("987654321098765432")).toBe(true); + expect(users.has("123456789012345679")).toBe(false); + }); + + it("serializes sorted IDs with a trailing newline", async () => { + const state = new MemoryState(); + const users = new OptOutUsers(state); + await users.init(); + + await users.add("9"); + await users.add("10"); + await users.remove("9"); + + expect(state.writes).toEqual(["9\n", "10\n9\n", "10\n"]); + }); + + it("serializes concurrent writes in call order", async () => { + const writes: string[] = []; + let releaseFirstWrite: (() => void) | undefined; + const firstWriteBlocked = new Promise((resolve) => { + releaseFirstWrite = resolve; + }); + let writeCount = 0; + const state: StateBackend = { + async read() { + return ""; + }, + async write(value) { + writeCount += 1; + if (writeCount === 1) { + await firstWriteBlocked; + } + writes.push(value); + }, + }; + const users = new OptOutUsers(state); + await users.init(); + + const first = users.add("2"); + const second = users.add("1"); + await Promise.resolve(); + expect(writeCount).toBe(1); + + releaseFirstWrite?.(); + await Promise.all([first, second]); + + expect(writes).toEqual(["2\n", "1\n2\n"]); + }); +}); + +describe("LocalFileState", () => { + it("returns an empty string for a missing file", async () => { + const directory = await mkdtemp(join(tmpdir(), "coderunbot-state-")); + const state = new LocalFileState(join(directory, "missing", "state.txt")); + + await expect(state.read()).resolves.toBe(""); + }); + + it("creates parent directories and writes UTF-8 text", async () => { + const directory = await mkdtemp(join(tmpdir(), "coderunbot-state-")); + const filePath = join(directory, "nested", "state.txt"); + const state = new LocalFileState(filePath); + + await state.write("123\n"); + + await expect(readFile(filePath, "utf8")).resolves.toBe("123\n"); + await expect(state.read()).resolves.toBe("123\n"); + }); +}); diff --git a/discord/src/shared/state.ts b/discord/src/shared/state.ts new file mode 100644 index 0000000..2f17318 --- /dev/null +++ b/discord/src/shared/state.ts @@ -0,0 +1,160 @@ +/** + * Adapts local files or S3-compatible storage to the shared persistent state contract. + * The adapter is discord.js-free and unit-tested directly. + */ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import { + GetObjectCommand, + NoSuchKey, + PutObjectCommand, + S3Client, + type S3ClientConfig, +} from "@aws-sdk/client-s3"; + +export interface StateBackend { + read(): Promise; + write(value: string): Promise; +} + +export class LocalFileState implements StateBackend { + readonly #filePath: string; + + constructor(filePath: string) { + this.#filePath = filePath; + } + + async read(): Promise { + try { + return await readFile(this.#filePath, "utf8"); + } catch (error) { + if (isNodeError(error) && error.code === "ENOENT") { + return ""; + } + throw error; + } + } + + async write(value: string): Promise { + await mkdir(dirname(this.#filePath), { recursive: true }); + await writeFile(this.#filePath, value, "utf8"); + } +} + +export interface S3ObjectStateOptions { + readonly endpoint: string; + readonly region: string; + readonly bucket: string; + readonly key: string; + readonly credentials: S3ClientConfig["credentials"]; +} + +export class S3ObjectState implements StateBackend { + readonly #client: S3Client; + readonly #bucket: string; + readonly #key: string; + + constructor(options: S3ObjectStateOptions) { + this.#client = new S3Client({ + endpoint: options.endpoint, + region: options.region, + credentials: options.credentials, + // OCI Object Storage exposes an S3 endpoint that requires path-style bucket addressing. + forcePathStyle: true, + }); + this.#bucket = options.bucket; + this.#key = options.key; + } + + async read(): Promise { + try { + const response = await this.#client.send( + new GetObjectCommand({ Bucket: this.#bucket, Key: this.#key }), + ); + return (await response.Body?.transformToString("utf-8")) ?? ""; + } catch (error) { + if (!isMissingS3Object(error)) { + throw error; + } + // NoSuchKey represents an uninitialized store, so seed the empty state once. + await this.write(""); + return ""; + } + } + + async write(value: string): Promise { + await this.#client.send( + new PutObjectCommand({ + Bucket: this.#bucket, + Key: this.#key, + Body: value, + ContentType: "text/plain; charset=utf-8", + }), + ); + } +} + +export class OptOutUsers { + readonly #backend: StateBackend; + // Discord snowflakes exceed Number.MAX_SAFE_INTEGER, so user IDs remain strings end to end. + #users = new Set(); + // This promise-chain mutex serializes writes within the bot's single writer process. + #writeChain: Promise = Promise.resolve(); + + constructor(backend: StateBackend) { + this.#backend = backend; + } + + async init(): Promise { + this.#users = parseUsers(await this.#backend.read()); + } + + has(userId: string): boolean { + return this.#users.has(userId); + } + + add(userId: string): Promise { + this.#users.add(userId); + return this.#enqueueWrite(serializeUsers(this.#users)); + } + + remove(userId: string): Promise { + this.#users.delete(userId); + return this.#enqueueWrite(serializeUsers(this.#users)); + } + + #enqueueWrite(value: string): Promise { + const write = this.#writeChain.then(() => this.#backend.write(value)); + this.#writeChain = write.catch(() => undefined); + return write; + } +} + +function parseUsers(value: string): Set { + return new Set( + value + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.length > 0), + ); +} + +function serializeUsers(users: ReadonlySet): string { + const sorted = [...users].sort(); + return sorted.length === 0 ? "" : `${sorted.join("\n")}\n`; +} + +function isNodeError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && "code" in error; +} + +function isMissingS3Object(error: unknown): boolean { + if (error instanceof NoSuchKey) { + return true; + } + if (!(error instanceof Error) || !("$metadata" in error)) { + return false; + } + const metadata = error.$metadata as { readonly httpStatusCode?: number }; + return error.name === "NoSuchKey" && metadata.httpStatusCode === 404; +} diff --git a/discord/src/shared/usageStats.test.ts b/discord/src/shared/usageStats.test.ts new file mode 100644 index 0000000..c6d94d6 --- /dev/null +++ b/discord/src/shared/usageStats.test.ts @@ -0,0 +1,162 @@ +/** + * Tests anonymous usage aggregation, recovery, serialization, and restart persistence. + */ +import { describe, expect, it, vi } from "vitest"; +import type { AppLogger } from "./logger.js"; +import type { StateBackend } from "./state.js"; +import { UsageStats } from "./usageStats.js"; + +class MemoryState implements StateBackend { + readonly writes: string[] = []; + + constructor(public value = "") {} + + async read(): Promise { + return this.value; + } + + async write(value: string): Promise { + this.value = value; + this.writes.push(value); + } +} + +describe("UsageStats", () => { + it("buckets command outcomes and languages by UTC month", async () => { + const state = new MemoryState(); + let now = new Date("2026-07-31T23:59:59.000Z"); + const stats = new UsageStats(state, { now: () => now }); + await stats.init(); + + stats.recordCommand("slash", "tex", "ok"); + stats.recordCommand("slash", "tex", "error"); + stats.recordCommand("slash", "tex", "ok"); + stats.recordRunLanguage("python"); + now = new Date("2026-08-01T00:00:00.000Z"); + stats.recordCommand("prefix", "run", "ok"); + stats.recordRunLanguage("cpp"); + await stats.flush(); + + expect(JSON.parse(state.value)).toEqual({ + version: 1, + months: { + "2026-07": { + commands: { "slash/tex": { ok: 2, error: 1 } }, + runLanguages: { python: 1 }, + guilds: {}, + }, + "2026-08": { + commands: { "prefix/run": { ok: 1, error: 0 } }, + runLanguages: { cpp: 1 }, + guilds: {}, + }, + }, + }); + await stats.dispose(); + }); + + it("overwrites the guild snapshot within a UTC day", async () => { + const state = new MemoryState(); + let now = new Date("2026-07-18T00:00:01.000Z"); + const stats = new UsageStats(state, { now: () => now }); + await stats.init(); + + stats.recordGuildCount(40); + now = new Date("2026-07-18T23:59:59.000Z"); + stats.recordGuildCount(42); + now = new Date("2026-07-19T00:00:00.000Z"); + stats.recordGuildCount(41); + await stats.flush(); + + expect(JSON.parse(state.value).months["2026-07"].guilds).toEqual({ + "2026-07-18": 42, + "2026-07-19": 41, + }); + await stats.dispose(); + }); + + it("recovers from corrupt JSON and warns through the optional logger", async () => { + const state = new MemoryState("{not json"); + const warn = vi.fn(); + const logger = { warn } as unknown as AppLogger; + const stats = new UsageStats(state, { + now: () => new Date("2026-07-18T12:00:00.000Z"), + logger, + }); + + await stats.init(); + stats.recordCommand("slash", "run", "ok"); + await stats.flush(); + + expect(warn).toHaveBeenCalledOnce(); + expect(JSON.parse(state.value)).toEqual({ + version: 1, + months: { + "2026-07": { + commands: { "slash/run": { ok: 1, error: 0 } }, + runLanguages: {}, + guilds: {}, + }, + }, + }); + await stats.dispose(); + }); + + it("writes pretty-printed JSON with a trailing newline only when dirty", async () => { + const state = new MemoryState(); + const stats = new UsageStats(state, { + now: () => new Date("2026-07-18T12:00:00.000Z"), + }); + await stats.init(); + + await stats.flush(); + expect(state.writes).toEqual([]); + stats.recordRunLanguage("python"); + await stats.flush(); + + expect(state.writes).toEqual([ + `${JSON.stringify( + { + version: 1, + months: { + "2026-07": { + commands: {}, + runLanguages: { python: 1 }, + guilds: {}, + }, + }, + }, + null, + 2, + )}\n`, + ]); + await stats.dispose(); + }); + + it("loads persisted counters and continues them after a restart", async () => { + const state = new MemoryState(); + const now = () => new Date("2026-07-18T12:00:00.000Z"); + const first = new UsageStats(state, { now }); + await first.init(); + first.recordCommand("slash", "tex", "ok"); + first.recordRunLanguage("python"); + await first.dispose(); + + const second = new UsageStats(state, { now }); + await second.init(); + second.recordCommand("slash", "tex", "error"); + second.recordRunLanguage("python"); + await second.dispose(); + + expect(JSON.parse(state.value)).toEqual({ + version: 1, + months: { + "2026-07": { + commands: { "slash/tex": { ok: 1, error: 1 } }, + runLanguages: { python: 2 }, + guilds: {}, + }, + }, + }); + }); +}); diff --git a/discord/src/shared/usageStats.ts b/discord/src/shared/usageStats.ts new file mode 100644 index 0000000..3b8c57f --- /dev/null +++ b/discord/src/shared/usageStats.ts @@ -0,0 +1,199 @@ +/** + * Stores anonymous monthly usage counters through the shared state backend. + * The collector is discord.js-free and keeps no invocation-level identifiers or content. + */ +import type { AppLogger } from "./logger.js"; +import type { StateBackend } from "./state.js"; + +interface CommandCounts { + ok: number; + error: number; +} + +interface MonthStats { + commands: Record; + runLanguages: Record; + guilds: Record; +} + +interface UsageStatsDocument { + version: 1; + months: Record; +} + +export interface UsageStatsOptions { + readonly flushIntervalMs?: number; + readonly now?: () => Date; + readonly logger?: AppLogger; +} + +const DEFAULT_FLUSH_INTERVAL_MS = 300_000; + +export class UsageStats { + readonly #backend: StateBackend; + readonly #flushIntervalMs: number; + readonly #now: () => Date; + readonly #logger?: AppLogger; + #state: UsageStatsDocument = emptyDocument(); + #dirty = false; + #flushTimer?: NodeJS.Timeout; + // This promise-chain mutex serializes writes within the bot's single writer process. + #writeChain: Promise = Promise.resolve(); + + constructor(backend: StateBackend, options: UsageStatsOptions = {}) { + this.#backend = backend; + this.#flushIntervalMs = + options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS; + this.#now = options.now ?? (() => new Date()); + this.#logger = options.logger; + } + + async init(): Promise { + const value = await this.#backend.read(); + if (value.trim().length === 0) { + this.#state = emptyDocument(); + } else { + try { + this.#state = parseDocument(value); + } catch (error) { + this.#logger?.warn( + { error }, + "Usage statistics state is corrupt; starting with empty counters", + ); + this.#state = emptyDocument(); + } + } + this.#dirty = false; + this.#flushTimer = setInterval(() => { + void this.flush().catch((error) => { + this.#logger?.error( + { error }, + "Failed to flush usage statistics state", + ); + }); + }, this.#flushIntervalMs); + this.#flushTimer.unref(); + } + + recordCommand(kind: string, name: string, outcome: "ok" | "error"): void { + const month = this.#currentMonth(); + const key = `${kind}/${name}`; + let counts = month.commands[key]; + if (counts === undefined) { + counts = { ok: 0, error: 0 }; + month.commands[key] = counts; + } + counts[outcome] += 1; + this.#dirty = true; + } + + recordRunLanguage(languageKey: string): void { + const month = this.#currentMonth(); + month.runLanguages[languageKey] = + (month.runLanguages[languageKey] ?? 0) + 1; + this.#dirty = true; + } + + recordGuildCount(count: number): void { + const now = this.#now(); + const month = this.#month(now); + month.guilds[now.toISOString().slice(0, 10)] = count; + this.#dirty = true; + } + + async flush(): Promise { + if (!this.#dirty) { + await this.#writeChain; + return; + } + + this.#dirty = false; + const value = `${JSON.stringify(this.#state, null, 2)}\n`; + const write = this.#enqueueWrite(value); + try { + await write; + } catch (error) { + this.#dirty = true; + throw error; + } + } + + async dispose(): Promise { + if (this.#flushTimer !== undefined) { + clearInterval(this.#flushTimer); + this.#flushTimer = undefined; + } + await this.flush(); + } + + #currentMonth(): MonthStats { + return this.#month(this.#now()); + } + + #month(now: Date): MonthStats { + const key = now.toISOString().slice(0, 7); + let month = this.#state.months[key]; + if (month === undefined) { + month = emptyMonth(); + this.#state.months[key] = month; + } + return month; + } + + #enqueueWrite(value: string): Promise { + const write = this.#writeChain.then(() => this.#backend.write(value)); + this.#writeChain = write.catch(() => undefined); + return write; + } +} + +function emptyDocument(): UsageStatsDocument { + return { version: 1, months: {} }; +} + +function emptyMonth(): MonthStats { + return { commands: {}, runLanguages: {}, guilds: {} }; +} + +function parseDocument(value: string): UsageStatsDocument { + const parsed: unknown = JSON.parse(value); + if (!isRecord(parsed) || parsed.version !== 1 || !isRecord(parsed.months)) { + throw new Error("usage statistics state has an unsupported shape"); + } + + for (const month of Object.values(parsed.months)) { + if ( + !isRecord(month) || + !isRecord(month.commands) || + !isRecord(month.runLanguages) || + !isRecord(month.guilds) + ) { + throw new Error("usage statistics month has an unsupported shape"); + } + for (const counts of Object.values(month.commands)) { + if ( + !isRecord(counts) || + !isCounter(counts.ok) || + !isCounter(counts.error) + ) { + throw new Error("usage statistics command counts are invalid"); + } + } + if ( + !Object.values(month.runLanguages).every(isCounter) || + !Object.values(month.guilds).every(isCounter) + ) { + throw new Error("usage statistics aggregate counts are invalid"); + } + } + + return parsed as unknown as UsageStatsDocument; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isCounter(value: unknown): value is number { + return Number.isSafeInteger(value) && (value as number) >= 0; +} diff --git a/discord/src/types.ts b/discord/src/types.ts new file mode 100644 index 0000000..0b8079b --- /dev/null +++ b/discord/src/types.ts @@ -0,0 +1,99 @@ +/** + * Defines contracts shared across features, platform code, and the composition root. + */ +import type { + AutocompleteInteraction, + BaseMessageOptions, + ButtonInteraction, + ChatInputCommandInteraction, + ContextMenuCommandBuilder, + ContextMenuCommandInteraction, + Message, + ModalSubmitInteraction, + SlashCommandBuilder, +} from "discord.js"; +import type { Env } from "./env.js"; +import type { RequestContext } from "./platform/discord/context.js"; +import type { AppLogger } from "./shared/logger.js"; +import type { OptOutUsers } from "./shared/state.js"; +import type { UsageStats } from "./shared/usageStats.js"; + +export type FeatureId = + | "tex" + | "code" + | "privacy" + | "wolfram" + | "misc" + | "translate" + | "ping"; + +type HandlerResult = Promise; + +export interface SlashCommand { + readonly data: SlashCommandBuilder | ContextMenuCommandBuilder; + execute( + interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction, + context: RequestContext, + ): HandlerResult; + autocomplete?(interaction: AutocompleteInteraction): Promise; +} + +export interface PrefixCommand { + readonly name: string; + readonly aliases?: readonly string[]; + execute( + message: Message, + args: string, + context: RequestContext, + ): HandlerResult; +} + +export type ModalHandler = ( + interaction: ModalSubmitInteraction, + context: RequestContext, +) => HandlerResult; + +export type ComponentHandler = ( + interaction: ButtonInteraction, + context: RequestContext, +) => HandlerResult; + +export interface Feature { + readonly id: FeatureId; + readonly slashCommands?: readonly SlashCommand[]; + readonly prefixCommands?: readonly PrefixCommand[]; + readonly modalHandlers?: Readonly>; + readonly componentHandlers?: Readonly>; + onMessage?(message: Message, context: RequestContext): HandlerResult; + init?(): Promise; +} + +export interface FeatureDependencies { + readonly optOutUsers: OptOutUsers; + readonly usageStats: UsageStats; + readonly logger: AppLogger; + readonly env: Env; + readonly client: import("discord.js").Client; +} + +export type FeatureFactory = ( + dependencies: FeatureDependencies, +) => Feature | undefined; + +interface ReplyBase { + readonly files?: BaseMessageOptions["files"]; + readonly ephemeral?: boolean; + readonly onDelivered?: (message: Message) => void | Promise; +} + +export interface ComponentsV2Reply extends ReplyBase { + readonly kind: "components-v2"; + readonly components: NonNullable; +} + +export interface PlainContentReply extends ReplyBase { + readonly kind: "plain"; + readonly content: string; +} + +export type OutgoingReply = ComponentsV2Reply | PlainContentReply; diff --git a/discord/tsconfig.json b/discord/tsconfig.json new file mode 100644 index 0000000..f9b1acf --- /dev/null +++ b/discord/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "outDir": "dist", + "rootDir": "src", + "sourceMap": true, + "esModuleInterop": true, + "declaration": false + }, + "include": ["src/**/*.ts"] +} diff --git a/discord/uv.lock b/discord/uv.lock deleted file mode 100644 index 3b19a93..0000000 --- a/discord/uv.lock +++ /dev/null @@ -1,1015 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.14" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, -] - -[[package]] -name = "aiohttp" -version = "3.13.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, -] - -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "certifi" -version = "2026.4.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - -[[package]] -name = "circuitbreaker" -version = "2.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787, upload-time = "2025-03-31T08:12:08.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737, upload-time = "2025-03-31T08:12:07.802Z" }, -] - -[[package]] -name = "coderunbot-discord" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "aiohttp" }, - { name = "google-api-python-client" }, - { name = "google-auth-oauthlib" }, - { name = "iso639-lang" }, - { name = "oci" }, - { name = "openai" }, - { name = "py-cord", extra = ["voice"] }, - { name = "python-dotenv" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiohttp" }, - { name = "google-api-python-client" }, - { name = "google-auth-oauthlib" }, - { name = "iso639-lang" }, - { name = "oci" }, - { name = "openai" }, - { name = "py-cord", extras = ["voice"], specifier = ">=2.6" }, - { name = "python-dotenv" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, -] - -[[package]] -name = "google-api-core" -version = "2.30.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, -] - -[[package]] -name = "google-api-python-client" -version = "2.194.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, - { name = "google-auth-httplib2" }, - { name = "httplib2" }, - { name = "uritemplate" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/ab/e83af0eb043e4ccc49571ca7a6a49984e9d00f4e9e6e6f1238d60bc84dce/google_api_python_client-2.194.0.tar.gz", hash = "sha256:db92647bd1a90f40b79c9618461553c2b20b6a43ce7395fa6de07132dc14f023", size = 14443469, upload-time = "2026-04-08T23:07:35.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/34/5a624e49f179aa5b0cb87b2ce8093960299030ff40423bfbde09360eb908/google_api_python_client-2.194.0-py3-none-any.whl", hash = "sha256:61eaaac3b8fc8fdf11c08af87abc3d1342d1b37319cc1b57405f86ef7697e717", size = 15016514, upload-time = "2026-04-08T23:07:33.093Z" }, -] - -[[package]] -name = "google-auth" -version = "2.49.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyasn1-modules" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, -] - -[[package]] -name = "google-auth-httplib2" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "httplib2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/99/107612bef8d24b298bb5a7c8466f908ecda791d43f9466f5c3978f5b24c1/google_auth_httplib2-0.3.1.tar.gz", hash = "sha256:0af542e815784cb64159b4469aa5d71dd41069ba93effa006e1916b1dcd88e55", size = 11152, upload-time = "2026-03-30T22:50:26.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/e9/93afb14d23a949acaa3f4e7cc51a0024671174e116e35f42850764b99634/google_auth_httplib2-0.3.1-py3-none-any.whl", hash = "sha256:682356a90ef4ba3d06548c37e9112eea6fc00395a11b0303a644c1a86abc275c", size = 9534, upload-time = "2026-03-30T22:49:03.384Z" }, -] - -[[package]] -name = "google-auth-oauthlib" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "requests-oauthlib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/82/62482931dcbe5266a2680d0da17096f2aab983ecb320277d9556700ce00e/google_auth_oauthlib-1.3.1.tar.gz", hash = "sha256:14c22c7b3dd3d06dbe44264144409039465effdd1eef94f7ce3710e486cc4bfa", size = 21663, upload-time = "2026-03-30T22:49:56.408Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e0/cb454a95f460903e39f101e950038ec24a072ca69d0a294a6df625cc1627/google_auth_oauthlib-1.3.1-py3-none-any.whl", hash = "sha256:1a139ef23f1318756805b0e95f655c238bffd29655329a2978218248da4ee7f8", size = 19247, upload-time = "2026-03-30T20:02:23.894Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.74.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httplib2" -version = "0.31.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, -] - -[[package]] -name = "iso639-lang" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/5a/49bbf16d155192255e7bb37e403b2ac360144992d0d112a865afc62e457f/iso639_lang-2.6.3.tar.gz", hash = "sha256:078ddb7cd0182dcc04367691acc8022ddf7158b6cb09f08f798af823fa864265", size = 319391, upload-time = "2025-07-23T09:04:53.568Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/c7/f6fd3db6c33a164631c39dce2ca26a3794e3abf91b875cc99a43a5565d88/iso639_lang-2.6.3-py3-none-any.whl", hash = "sha256:a6c2fb9f739dca180dc7f48b098880f303bcce2cdf93a4ca3152ed8bbbb94fbb", size = 324990, upload-time = "2025-07-23T09:04:52.221Z" }, -] - -[[package]] -name = "jiter" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, - { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, - { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, - { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, - { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, - { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, - { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, - { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, - { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, -] - -[[package]] -name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, - { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, - { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, - { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, - { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, - { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, - { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, - { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, - { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, - { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, - { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - -[[package]] -name = "oci" -version = "2.172.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "circuitbreaker" }, - { name = "cryptography" }, - { name = "pyopenssl" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/ed/e9c0fa954f0fa77946993a46de68862c984f7ae8e9e651406a776214d1c9/oci-2.172.0.tar.gz", hash = "sha256:4453ab363e4f9d4b9890a3030f082f1841398f2cea66b63e44dd51cf6be0d347", size = 17086041, upload-time = "2026-04-21T04:41:35.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/e5/193bca3a95b1fb272a0f0fb02d966a9ba1d10dd264d21143d85625c55de8/oci-2.172.0-py3-none-any.whl", hash = "sha256:61f07c086b0140033ccd57a0632bdd747ce2dbc3cedc198639116dd78510e3ca", size = 34825199, upload-time = "2026-04-21T04:41:26.297Z" }, -] - -[[package]] -name = "openai" -version = "2.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, -] - -[[package]] -name = "proto-plus" -version = "1.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, -] - -[[package]] -name = "protobuf" -version = "7.34.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, - { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, - { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, - { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, - { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, -] - -[[package]] -name = "py-cord" -version = "2.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/75/5b510b4f30c0fe9ae0c440d968e32df1d8857562e49cc652e4f491871384/py_cord-2.7.2.tar.gz", hash = "sha256:86d2172c5b0de33d3aa5611d940dd98acfb1b95adfa10ae7b749e6aaf923ffff", size = 1132471, upload-time = "2026-04-14T11:21:20.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/4c/1d623000e5a91236895d89bfc2a2947db328182715ac2d7e7ef727938e77/py_cord-2.7.2-py3-none-any.whl", hash = "sha256:e25e5e9392f297b3d8fb8f9f31e57989276c1e2b12ebed5a7b1040dd5891d972", size = 1208198, upload-time = "2026-04-14T11:21:18.741Z" }, -] - -[package.optional-dependencies] -voice = [ - { name = "pynacl" }, -] - -[[package]] -name = "pyasn1" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pydantic" -version = "2.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, - { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, - { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, - { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, - { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, - { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, - { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, - { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, - { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, - { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, -] - -[[package]] -name = "pynacl" -version = "1.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" }, - { url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" }, - { url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" }, - { url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" }, - { url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" }, - { url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, - { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, - { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, -] - -[[package]] -name = "pyopenssl" -version = "26.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, -] - -[[package]] -name = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, -] - -[[package]] -name = "pytz" -version = "2026.1.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, -] - -[[package]] -name = "requests" -version = "2.33.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "uritemplate" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "yarl" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, -] diff --git a/discord/vitest.config.ts b/discord/vitest.config.ts new file mode 100644 index 0000000..5bfa2d5 --- /dev/null +++ b/discord/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + passWithNoTests: true, + }, +}); diff --git a/docs/en/index.md b/docs/en/index.md index b7dc52f..5b9019d 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -1,118 +1,69 @@ -# Bot help -## Index -- [Introduction](#Intoroduction) -- [Commands](#Commands) - - [run](#run) - - [Example](#e1) - - [Languages](#Languages) - - [save](#save) - - [tex](#tex) - - [Example](#e2) - - [texp](#texp) - - [Example](#e3) - -## Intoroduction -This Bot is made by Gart ([@gaato__](https://twitter.com/gaato__)). - -[Here](https://discord.gg/qRpYRTgvXM) is the discord server. +# How to use CodeRunBot + +## Introduction + +This bot is made by gaato ([@gaato__](https://twitter.com/gaato__)). + +- Official Discord server: [here](https://discord.gg/qRpYRTgvXM) +- [Invite link](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot) + +## Installation + +There are two ways to use the bot. + +- **Add to a server**: adds slash commands plus `]`-prefixed text commands. +- **User install**: add the app to your own account and use the slash commands + (`/run`, `/tex`, ...) in DMs and in servers that don't have the bot. + ## Commands -### run + +### /run + +Pick a language (with autocomplete) and a form opens for your code and +standard input. The code runs on [Wandbox](https://wandbox.org). + +Supported languages are fetched dynamically from Wandbox — whatever appears in +the autocomplete is what is available. + +### ]run (server only) + ``` ]run language code ``` -runs your code. -And the bot ignores "```" for a code block. -#### Example +Code fences (```) are ignored. + ``` ]run python print('hello') ``` -#### Languages -- Bash (bash) -- C (c) -- C# (c#) -- CLISP (clisp) -- CoffeeScript (coffeescript) -- Crystal (crystal) -- D (d) -- Elixir (elixir) -- Erlang (erlang) -- F# (f#) -- Free Pascal (fpc) -- Go (go) -- Groovy (groovy) -- Haskell (haskell) -- Java (java) -- JavaScript (javascript) -- Lazy K (lazyk) -- Lua (lua) -- Nim (nim) -- OCaml (ocaml) -- OpenSSL (openssl) -- Perl (perl) -- PHP (php) -- Pony (pony) -- PyPy (pypy) -- Python (python) -- R (r) -- Rill (rill) -- Ruby (ruby) -- Rust (rust) -- Scala (scala) -- SQL (sql) -- Swift (swift) -- TypeScript (typescript) -- VimScript (vimscript) - -### save -``` -]save language -code -``` -saves your code. -You can call it by sending as follows. -``` -]run saved -standard input -``` -### tex -``` -]tex -math commands -``` -images your math commands. +### /tex -And the bot ignores "```" for a code block. +Opens a form for LaTeX input. The `env` option wraps your input in an +align / gather environment, and `spoiler` posts the image as a spoiler. + +### ]tex / ]stex (server only) -It uses LaTeX gather* environment. -#### Example -``` -]tex -\int_0^1 f(x)\,dx -``` -### texp ``` -]texp -LaTeX text +]tex x^2 + y^2 = r^2 ``` -images your LaTeX text. -And the bot ignores "```" for a code block. +Renders LaTeX (math mode) as an image. `]stex` posts it as a spoiler. -It uses LaTeX text mode. -#### Example -``` -]texp -For real constants $a,b,c$ the solution of the quadratic equation -\[ - ax^2+bx+c=0 -\] -for complex number $x$ is -\[ - x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} -\] -. -``` +### Right-click a message → Apps → escape + +Shows the message content with Markdown and mentions escaped (visible only to you). + +### /privacy-policy, /opt-out, /opt-in + +Shows the privacy policy and lets you opt out of / back into message content +processing. See the [privacy policy](../../discord/config/privacy-policy.md) +for details. + +## Handy behaviors + +- Every bot reply has a **Delete button** that only the invoking user can press. +- If you **edit** the message that invoked `]run` or `]tex`, the bot's reply is + updated automatically. diff --git a/docs/ja/index.md b/docs/ja/index.md index 014595b..0e1dbfc 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -1,183 +1,67 @@ -# Bot の使い方 - -## 目次 - -- [はじめに](#はじめに) -- [コマンド](#コマンド) - - [run](#run) - - [例](#e1) - - [言語一覧](#言語一覧) - - [save](#save) - - [tex](#tex) - - [例](#e2) - - [texp](#texp) - - [例](#e3) - - [stex](#stex) - - [stexp](#stexp) - - [texpdf](#texpdf) - - [例](#e4) +# CodeRunBot の使い方 ## はじめに この Bot はがーと([@gaato__](https://twitter.com/gaato__))によって作られました. -公式 Discord サーバーは[こちら](https://discord.gg/qRpYRTgvXM) +- 公式 Discord サーバーは[こちら](https://discord.gg/qRpYRTgvXM) +- [招待リンク](https://discord.com/api/oauth2/authorize?client_id=761428259241328680&permissions=0&scope=bot) -## コマンド +## 導入方法 -### run +2 つの使い方があります. -``` -]run 言語 -コード -``` -であなたのコードを実行します. - -なおコードブロックのための "```" は無視されます. - -#### 例 - -``` -]run python -print('hello') -``` - -#### 言語一覧 - -- Bash (bash) -- C (c) -- C# (c#) -- CLISP (clisp) -- CoffeeScript (coffeescript) -- Crystal (crystal) -- D (d) -- Elixir (elixir) -- Erlang (erlang) -- F# (f#) -- Free Pascal (fpc) -- Go (go) -- Groovy (groovy) -- Haskell (haskell) -- Java (java) -- JavaScript (javascript) -- Lazy K (lazyk) -- Lua (lua) -- Nim (nim) -- OCaml (ocaml) -- OpenSSL (openssl) -- Perl (perl) -- PHP (php) -- Pony (pony) -- PyPy (pypy) -- Python (python) -- R (r) -- Rill (rill) -- Ruby (ruby) -- Rust (rust) -- Scala (scala) -- SQL (sql) -- Swift (swift) -- TypeScript (typescript) -- VimScript (vimscript) - -### save - -``` -]save 言語 -コード -``` -であなたのコードを保存します. -``` -]run saved -標準入力 -``` -で呼び出せます. +- **サーバーに追加**: 招待リンクからサーバーに Bot を追加すると,スラッシュコマンドに加えて + `]` プレフィックスのテキストコマンドが使えます. +- **ユーザーインストール**: 自分のアカウントにアプリとして追加すると,Bot がいないサーバーや + DM でもスラッシュコマンド(`/run`,`/tex` など)が使えます. -### tex +## コマンド -``` -]tex -数式コマンド -``` -であなたの数式コマンドを画像化します. +### /run -なおコードブロックのための "```" は無視されます. +言語を選ぶ(オートコンプリート対応)とコード入力用のフォームが開きます. +コードと標準入力を入れて送信すると [Wandbox](https://wandbox.org) で実行されます. -LaTeX の数式モード(gather*環境)を使っています. +対応言語は Wandbox から動的に取得されるため,オートコンプリートに出るものがすべてです. -#### 例 +### ]run(サーバー専用) ``` -]tex -\int_0^1 f(x)\,dx +]run 言語 +コード ``` -### texp +コードブロックのための ``` は無視されます. ``` -]texp -LaTeXによるテキスト +]run python +print('hello') ``` -であなたの LaTeX テキストを画像化します. -なおコードブロックのための "```" は無視されます. +### /tex -LaTeX のテキストモードを使っています. +数式入力用のフォームが開きます.`env` オプションで align / gather 環境を選べ, +`spoiler` オプションでスポイラー画像にできます. -#### 例 +### ]tex / ]stex(サーバー専用) ``` -]texp -実定数$a,b,c$に対して,複素数$x$に関する二次方程式 -\[ - ax^2+bx+c=0 -\] -の解は -\[ - x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} -\] -と表される. +]tex x^2 + y^2 = r^2 ``` -### stex +LaTeX(数式モード)を画像にします.`]stex` はスポイラー画像になります. -``` -]stex -数式コマンド -``` -[tex](#tex) コマンドでスポイラーをかけます. +### メッセージを右クリック → アプリ → escape -### stexp - -``` -]stexp -LaTeXによるテキスト -``` -[stex](#stex) + [texp](#texp) +メッセージの Markdown とメンションをエスケープした生テキストを表示します(自分にのみ表示). -### texpdf +### /privacy-policy, /opt-out, /opt-in -``` -]texpdf -LaTeX文書 -``` -で LaTeX により PDF を作成します. +プライバシーポリシーの表示と,メッセージ内容の処理のオプトアウト/再開ができます. +詳しくは[プライバシーポリシー](../../discord/config/privacy-policy.md)を参照してください. -#### 例 +## 便利な挙動 -``` -]texpdf -\documentclass[uplatex]{jsarticle} -\usepackage{amsmath} -\begin{document} -実定数$a,b,c$に対して,複素数$x$に関する二次方程式 -\[ - ax^2+bx+c=0 -\] -の解は -\[ - x=\frac{-b\pm\sqrt{b^2-4ac}}{2a} -\] -と表される. -\end{document} -``` \ No newline at end of file +- Bot の返信には **Delete ボタン**が付き,コマンドを実行した本人だけが削除できます. +- `]run` や `]tex` の呼び出しメッセージを**編集すると,Bot の返信も自動で更新**されます. diff --git a/tex/.gitignore b/tex/.gitignore deleted file mode 100644 index c6bba59..0000000 --- a/tex/.gitignore +++ /dev/null @@ -1,130 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* diff --git a/tex/Dockerfile b/tex/Dockerfile deleted file mode 100644 index 37d467b..0000000 --- a/tex/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:18-alpine - -WORKDIR /usr/src/app - -RUN apk --no-cache add font-noto-cjk-extra - -COPY package*.json ./ -RUN npm install - -COPY . . - -EXPOSE 80 - -CMD [ "node", "server.js" ] diff --git a/tex/README-ja.md b/tex/README-ja.md deleted file mode 100644 index 41bb944..0000000 --- a/tex/README-ja.md +++ /dev/null @@ -1,43 +0,0 @@ -# tex.gaato.net - -tex.gaato.net は、LaTeX 文字列を SVG や PNG 画像としてレンダリングするためのエンドポイントを提供します。 - -## エンドポイント - -### POST `/render/png` - -LaTeX 文字列を PNG 画像としてレンダリングします。 - -#### リクエストパラメータ - -| パラメータ | 型 | 説明 | -|------------|--------|------------------------| -| latex | string | レンダリングする LaTeX 文字列 | - -#### レスポンス - -- **200 OK** - - `image/png`: PNG 画像 -- **400 Bad Request** - - `text/plain`: LaTeX 文字列が不足している場合、または LaTeX の構文エラーが発生した場合のエラーメッセージ -- **500 Internal Server Error** - - `text/plain`: その他のエラーが発生した場合のエラーメッセージ - -### POST `/render/svg` - -LaTeX 文字列を SVG 画像としてレンダリングします。 - -#### リクエストパラメータ - -| パラメータ | 型 | 説明 | -|------------|--------|------------------------| -| latex | string | レンダリングする LaTeX 文字列 | - -#### レスポンス - -- **200 OK** - - `image/svg+xml`: SVG 画像 -- **400 Bad Request** - - `text/plain`: LaTeX 文字列が不足している場合、または LaTeX の構文エラーが発生した場合のエラーメッセージ -- **500 Internal Server Error** - - `text/plain`: その他のエラーが発生した場合のエラーメッセージ diff --git a/tex/README.md b/tex/README.md deleted file mode 100644 index e52d811..0000000 --- a/tex/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# tex.gaato.net - -tex.gaato.net provides endpoints to render LaTeX strings as SVG or PNG images. - -## Endpoints - -### POST `/render/png` - -Renders a LaTeX string as a PNG image. - -#### Request Parameters - -| Parameter | Type | Description | -|-----------|--------|-------------------------------| -| latex | string | LaTeX string to be rendered | - -#### Response - -- **200 OK** - - `image/png`: PNG image -- **400 Bad Request** - - `text/plain`: Error message if the LaTeX string is missing or if there's a LaTeX syntax error -- **500 Internal Server Error** - - `text/plain`: Error message if any other error occurs - -### POST `/render/svg` - -Renders a LaTeX string as an SVG image. - -#### Request Parameters - -| Parameter | Type | Description | -|-----------|--------|-------------------------------| -| latex | string | LaTeX string to be rendered | - -#### Response - -- **200 OK** - - `image/svg+xml`: SVG image -- **400 Bad Request** - - `text/plain`: Error message if the LaTeX string is missing or if there's a LaTeX syntax error -- **500 Internal Server Error** - - `text/plain`: Error message if any other error occurs diff --git a/tex/package-lock.json b/tex/package-lock.json deleted file mode 100644 index 5fdb00f..0000000 --- a/tex/package-lock.json +++ /dev/null @@ -1,1628 +0,0 @@ -{ - "name": "tex.gaato.net", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "express": "^4.18.2", - "jsdom": "^22.1.0", - "mathjax": "^3.2.2", - "mathjax-full": "^3.2.2", - "sharp": "^0.32.5" - } - }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz", - "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", - "dependencies": { - "rrweb-cssom": "^0.6.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" - }, - "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", - "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mathjax": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz", - "integrity": "sha512-Bt+SSVU8eBG27zChVewOicYs7Xsdt40qm4+UpHyX7k0/O9NliPc+x77k1/FEsPsjKPZGJvtRZM1vO+geW0OhGw==" - }, - "node_modules/mathjax-full": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", - "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", - "dependencies": { - "esm": "^3.2.25", - "mhchemparser": "^4.1.0", - "mj-context-menu": "^0.6.1", - "speech-rule-engine": "^4.0.6" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mhchemparser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", - "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==" - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mj-context-menu": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", - "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==" - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", - "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" - }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/prebuild-install/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, - "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/sharp": { - "version": "0.32.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz", - "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/speech-rule-engine": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz", - "integrity": "sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g==", - "dependencies": { - "commander": "9.2.0", - "wicked-good-xpath": "1.3.0", - "xmldom-sre": "0.1.31" - }, - "bin": { - "sre": "bin/sre" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamx": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", - "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/wicked-good-xpath": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", - "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, - "node_modules/xmldom-sre": { - "version": "0.1.31", - "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz", - "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==", - "engines": { - "node": ">=0.1" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/tex/package.json b/tex/package.json deleted file mode 100644 index 45f87df..0000000 --- a/tex/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "express": "^4.18.2", - "jsdom": "^22.1.0", - "mathjax": "^3.2.2", - "mathjax-full": "^3.2.2", - "sharp": "^0.32.5" - } -} diff --git a/tex/server.js b/tex/server.js deleted file mode 100644 index 1abfd38..0000000 --- a/tex/server.js +++ /dev/null @@ -1,99 +0,0 @@ -const { mathjax } = require('mathjax-full/js/mathjax.js'); -const { TeX } = require('mathjax-full/js/input/tex.js'); -const { SVG } = require('mathjax-full/js/output/svg.js'); -const { liteAdaptor } = require('mathjax-full/js/adaptors/liteAdaptor.js'); -const { RegisterHTMLHandler } = require('mathjax-full/js/handlers/html.js'); -const { AllPackages } = require('mathjax-full/js/input/tex/AllPackages.js'); -const { JSDOM } = require('jsdom'); -const express = require('express'); -const bodyParser = require('body-parser'); -const sharp = require('sharp'); - - -const app = express(); -const host = process.env.HOST || '127.0.0.1'; -const port = Number.parseInt(process.env.PORT || '8080', 10); - -app.use(bodyParser.json()); - -const adaptor = liteAdaptor(); -RegisterHTMLHandler(adaptor); - -async function convertToSvg(latex) { - try { - const tex = new TeX({ packages: AllPackages }); - const svg = new SVG(); - const doc = mathjax.document('', { InputJax: tex, OutputJax: svg }); - const node = doc.convert(latex, { display: true, em: 16, ex: 8, containerWidth: 80 }); - - const svgString = adaptor.outerHTML(node); - const svgTag = svgString.match(/]*>[\s\S]*<\/svg>/g)[0]; - - if (svgTag.includes('data-mjx-error')) { - const errorTitle = svgTag.match(/title="([^"]+)"/)[1]; - throw new Error(errorTitle); - } - const dom = new JSDOM(svgTag); - const textElements = dom.window.document.querySelectorAll('text'); - textElements.forEach(text => { - text.setAttribute('font-family', 'Noto Serif CJK JP, serif'); - }); - const updatedSvgTag = dom.window.document.querySelector('svg').outerHTML; - - return updatedSvgTag; - - } catch (error) { - throw new Error(`LaTeX error: ${error.message}`); - } -} - -app.post('/render/png', async (req, res) => { - try { - const latex = req.body.latex; - if (!latex) { - return res.status(400).send('LaTeX string is required.'); - } - - const svgString = await convertToSvg(latex); - const padding = 20; // 余白のサイズ - - const image = await sharp(Buffer.from(svgString)) - .resize({ height: 500 }) - .flatten({ background: { r: 255, g: 255, b: 255 } }) - .extend({ top: padding, bottom: padding, left: padding, right: padding, background: { r: 255, g: 255, b: 255, alpha: 1 } }) // 余白を追加 - .png() - .toBuffer(); - - res.contentType('image/png'); - res.send(image); - } catch (error) { - handleRenderingError(error, res); - } -}); - -app.post('/render/svg', async (req, res) => { - try { - const latex = req.body.latex; - if (!latex) { - return res.status(400).send('LaTeX string is required.'); - } - - const svgString = await convertToSvg(latex); - res.contentType('image/svg+xml'); - res.send(svgString); - } catch (error) { - handleRenderingError(error, res); - } -}); - -function handleRenderingError(error, res) { - if (error.message.startsWith('LaTeX error:')) { - return res.status(400).send(error.message); - } - console.error(error); - res.status(500).send('An error occurred while rendering the LaTeX string.'); -} - -app.listen(port, host, () => { - console.log(`Server is running at http://${host}:${port}`); -});