mirror of
				https://github.com/nonebot/nonebot2.git
				synced 2025-10-31 06:56:39 +00:00 
			
		
		
		
	🏗️ change nonebot project structure
This commit is contained in:
		
							
								
								
									
										0
									
								
								packages/nonebot-adapter-mirai/nonebot/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								packages/nonebot-adapter-mirai/nonebot/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										661
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										661
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,661 @@ | ||||
|                     GNU AFFERO GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 19 November 2007 | ||||
|  | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
|                             Preamble | ||||
|  | ||||
|   The GNU Affero General Public License is a free, copyleft license for | ||||
| software and other kinds of works, specifically designed to ensure | ||||
| cooperation with the community in the case of network server software. | ||||
|  | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| our General Public Licenses are intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users. | ||||
|  | ||||
|   When we speak of free software, we are referring to freedom, not | ||||
| price.  Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
|  | ||||
|   Developers that use our General Public Licenses protect your rights | ||||
| with two steps: (1) assert copyright on the software, and (2) offer | ||||
| you this License which gives you legal permission to copy, distribute | ||||
| and/or modify the software. | ||||
|  | ||||
|   A secondary benefit of defending all users' freedom is that | ||||
| improvements made in alternate versions of the program, if they | ||||
| receive widespread use, become available for other developers to | ||||
| incorporate.  Many developers of free software are heartened and | ||||
| encouraged by the resulting cooperation.  However, in the case of | ||||
| software used on network servers, this result may fail to come about. | ||||
| The GNU General Public License permits making a modified version and | ||||
| letting the public access it on a server without ever releasing its | ||||
| source code to the public. | ||||
|  | ||||
|   The GNU Affero General Public License is designed specifically to | ||||
| ensure that, in such cases, the modified source code becomes available | ||||
| to the community.  It requires the operator of a network server to | ||||
| provide the source code of the modified version running there to the | ||||
| users of that server.  Therefore, public use of a modified version, on | ||||
| a publicly accessible server, gives the public access to the source | ||||
| code of the modified version. | ||||
|  | ||||
|   An older license, called the Affero General Public License and | ||||
| published by Affero, was designed to accomplish similar goals.  This is | ||||
| a different license, not a version of the Affero GPL, but Affero has | ||||
| released a new version of the Affero GPL which permits relicensing under | ||||
| this license. | ||||
|  | ||||
|   The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
|  | ||||
|                        TERMS AND CONDITIONS | ||||
|  | ||||
|   0. Definitions. | ||||
|  | ||||
|   "This License" refers to version 3 of the GNU Affero General Public License. | ||||
|  | ||||
|   "Copyright" also means copyright-like laws that apply to other kinds of | ||||
| works, such as semiconductor masks. | ||||
|  | ||||
|   "The Program" refers to any copyrightable work licensed under this | ||||
| License.  Each licensee is addressed as "you".  "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
|  | ||||
|   To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of an | ||||
| exact copy.  The resulting work is called a "modified version" of the | ||||
| earlier work or a work "based on" the earlier work. | ||||
|  | ||||
|   A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
|  | ||||
|   To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy.  Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
|  | ||||
|   To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies.  Mere interaction with a user through | ||||
| a computer network, with no transfer of a copy, is not conveying. | ||||
|  | ||||
|   An interactive user interface displays "Appropriate Legal Notices" | ||||
| to the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License.  If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
|  | ||||
|   1. Source Code. | ||||
|  | ||||
|   The "source code" for a work means the preferred form of the work | ||||
| for making modifications to it.  "Object code" means any non-source | ||||
| form of a work. | ||||
|  | ||||
|   A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
|  | ||||
|   The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form.  A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
|  | ||||
|   The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities.  However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work.  For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
|  | ||||
|   The Corresponding Source need not include anything that users | ||||
| can regenerate automatically from other parts of the Corresponding | ||||
| Source. | ||||
|  | ||||
|   The Corresponding Source for a work in source code form is that | ||||
| same work. | ||||
|  | ||||
|   2. Basic Permissions. | ||||
|  | ||||
|   All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met.  This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program.  The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work.  This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
|  | ||||
|   You may make, run and propagate covered works that you do not | ||||
| convey, without conditions so long as your license otherwise remains | ||||
| in force.  You may convey covered works to others for the sole purpose | ||||
| of having them make modifications exclusively for you, or provide you | ||||
| with facilities for running those works, provided that you comply with | ||||
| the terms of this License in conveying all material for which you do | ||||
| not control copyright.  Those thus making or running the covered works | ||||
| for you must do so exclusively on your behalf, under your direction | ||||
| and control, on terms that prohibit them from making any copies of | ||||
| your copyrighted material outside their relationship with you. | ||||
|  | ||||
|   Conveying under any other circumstances is permitted solely under | ||||
| the conditions stated below.  Sublicensing is not allowed; section 10 | ||||
| makes it unnecessary. | ||||
|  | ||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
|  | ||||
|   No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
|  | ||||
|   When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such circumvention | ||||
| is effected by exercising rights under this License with respect to | ||||
| the covered work, and you disclaim any intention to limit operation or | ||||
| modification of the work as a means of enforcing, against the work's | ||||
| users, your or third parties' legal rights to forbid circumvention of | ||||
| technological measures. | ||||
|  | ||||
|   4. Conveying Verbatim Copies. | ||||
|  | ||||
|   You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
|  | ||||
|   You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
|  | ||||
|   5. Conveying Modified Source Versions. | ||||
|  | ||||
|   You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these conditions: | ||||
|  | ||||
|     a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
|  | ||||
|     b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under section | ||||
|     7.  This requirement modifies the requirement in section 4 to | ||||
|     "keep intact all notices". | ||||
|  | ||||
|     c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy.  This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged.  This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
|  | ||||
|     d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
|  | ||||
|   A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit.  Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
|  | ||||
|   6. Conveying Non-Source Forms. | ||||
|  | ||||
|   You may convey a covered work in object code form under the terms | ||||
| of sections 4 and 5, provided that you also convey the | ||||
| machine-readable Corresponding Source under the terms of this License, | ||||
| in one of these ways: | ||||
|  | ||||
|     a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
|  | ||||
|     b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the | ||||
|     Corresponding Source from a network server at no charge. | ||||
|  | ||||
|     c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source.  This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
|  | ||||
|     d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge.  You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code.  If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source.  Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
|  | ||||
|     e) Convey the object code using peer-to-peer transmission, provided | ||||
|     you inform other peers where the object code and Corresponding | ||||
|     Source of the work are being offered to the general public at no | ||||
|     charge under subsection 6d. | ||||
|  | ||||
|   A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
|  | ||||
|   A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, family, | ||||
| or household purposes, or (2) anything designed or sold for incorporation | ||||
| into a dwelling.  In determining whether a product is a consumer product, | ||||
| doubtful cases shall be resolved in favor of coverage.  For a particular | ||||
| product received by a particular user, "normally used" refers to a | ||||
| typical or common use of that class of product, regardless of the status | ||||
| of the particular user or of the way in which the particular user | ||||
| actually uses, or expects or is expected to use, the product.  A product | ||||
| is a consumer product regardless of whether the product has substantial | ||||
| commercial, industrial or non-consumer uses, unless such uses represent | ||||
| the only significant mode of use of the product. | ||||
|  | ||||
|   "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to install | ||||
| and execute modified versions of a covered work in that User Product from | ||||
| a modified version of its Corresponding Source.  The information must | ||||
| suffice to ensure that the continued functioning of the modified object | ||||
| code is in no case prevented or interfered with solely because | ||||
| modification has been made. | ||||
|  | ||||
|   If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information.  But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
|  | ||||
|   The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or updates | ||||
| for a work that has been modified or installed by the recipient, or for | ||||
| the User Product in which it has been modified or installed.  Access to a | ||||
| network may be denied when the modification itself materially and | ||||
| adversely affects the operation of the network or violates the rules and | ||||
| protocols for communication across the network. | ||||
|  | ||||
|   Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
|  | ||||
|   7. Additional Terms. | ||||
|  | ||||
|   "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law.  If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
|  | ||||
|   When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it.  (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.)  You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders of | ||||
| that material) supplement the terms of this License with terms: | ||||
|  | ||||
|     a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
|  | ||||
|     b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
|  | ||||
|     c) Prohibiting misrepresentation of the origin of that material, or | ||||
|     requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
|  | ||||
|     d) Limiting the use for publicity purposes of names of licensors or | ||||
|     authors of the material; or | ||||
|  | ||||
|     e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
|  | ||||
|     f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions of | ||||
|     it) with contractual assumptions of liability to the recipient, for | ||||
|     any liability that these contractual assumptions directly impose on | ||||
|     those licensors and authors. | ||||
|  | ||||
|   All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10.  If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term.  If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
|  | ||||
|   If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
|  | ||||
|   Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; | ||||
| the above requirements apply either way. | ||||
|  | ||||
|   8. Termination. | ||||
|  | ||||
|   You may not propagate or modify a covered work except as expressly | ||||
| provided under this License.  Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
|  | ||||
|   However, if you cease all violation of this License, then your | ||||
| license from a particular copyright holder is reinstated (a) | ||||
| provisionally, unless and until the copyright holder explicitly and | ||||
| finally terminates your license, and (b) permanently, if the copyright | ||||
| holder fails to notify you of the violation by some reasonable means | ||||
| prior to 60 days after the cessation. | ||||
|  | ||||
|   Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
|  | ||||
|   Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License.  If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
|  | ||||
|   9. Acceptance Not Required for Having Copies. | ||||
|  | ||||
|   You are not required to accept this License in order to receive or | ||||
| run a copy of the Program.  Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance.  However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work.  These actions infringe copyright if you do | ||||
| not accept this License.  Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
|  | ||||
|   10. Automatic Licensing of Downstream Recipients. | ||||
|  | ||||
|   Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License.  You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
|  | ||||
|   An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations.  If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
|  | ||||
|   You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License.  For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
|  | ||||
|   11. Patents. | ||||
|  | ||||
|   A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based.  The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
|  | ||||
|   A contributor's "essential patent claims" are all patent claims | ||||
| owned or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version.  For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
|  | ||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
|  | ||||
|   In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement).  To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
|  | ||||
|   If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients.  "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
|  | ||||
|   If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
|  | ||||
|   A patent license is "discriminatory" if it does not include within | ||||
| the scope of its coverage, prohibits the exercise of, or is | ||||
| conditioned on the non-exercise of one or more of the rights that are | ||||
| specifically granted under this License.  You may not convey a covered | ||||
| work if you are a party to an arrangement with a third party that is | ||||
| in the business of distributing software, under which you make payment | ||||
| to the third party based on the extent of your activity of conveying | ||||
| the work, and under which the third party grants, to any of the | ||||
| parties who would receive the covered work from you, a discriminatory | ||||
| patent license (a) in connection with copies of the covered work | ||||
| conveyed by you (or copies made from those copies), or (b) primarily | ||||
| for and in connection with specific products or compilations that | ||||
| contain the covered work, unless you entered into that arrangement, | ||||
| or that patent license was granted, prior to 28 March 2007. | ||||
|  | ||||
|   Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
|  | ||||
|   12. No Surrender of Others' Freedom. | ||||
|  | ||||
|   If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License.  If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under this | ||||
| License and any other pertinent obligations, then as a consequence you may | ||||
| not convey it at all.  For example, if you agree to terms that obligate you | ||||
| to collect a royalty for further conveying from those to whom you convey | ||||
| the Program, the only way you could satisfy both those terms and this | ||||
| License would be to refrain entirely from conveying the Program. | ||||
|  | ||||
|   13. Remote Network Interaction; Use with the GNU General Public License. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, if you modify the | ||||
| Program, your modified version must prominently offer all users | ||||
| interacting with it remotely through a computer network (if your version | ||||
| supports such interaction) an opportunity to receive the Corresponding | ||||
| Source of your version by providing access to the Corresponding Source | ||||
| from a network server at no charge, through some standard or customary | ||||
| means of facilitating copying of software.  This Corresponding Source | ||||
| shall include the Corresponding Source for any work covered by version 3 | ||||
| of the GNU General Public License that is incorporated pursuant to the | ||||
| following paragraph. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU General Public License into a single | ||||
| combined work, and to convey the resulting work.  The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the work with which it is combined will remain governed by version | ||||
| 3 of the GNU General Public License. | ||||
|  | ||||
|   14. Revised Versions of this License. | ||||
|  | ||||
|   The Free Software Foundation may publish revised and/or new versions of | ||||
| the GNU Affero General Public License from time to time.  Such new versions | ||||
| will be similar in spirit to the present version, but may differ in detail to | ||||
| address new problems or concerns. | ||||
|  | ||||
|   Each version is given a distinguishing version number.  If the | ||||
| Program specifies that a certain numbered version of the GNU Affero General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation.  If the Program does not specify a version number of the | ||||
| GNU Affero General Public License, you may choose any version ever published | ||||
| by the Free Software Foundation. | ||||
|  | ||||
|   If the Program specifies that a proxy can decide which future | ||||
| versions of the GNU Affero General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
|  | ||||
|   Later license versions may give you additional or different | ||||
| permissions.  However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
|  | ||||
|   15. Disclaimer of Warranty. | ||||
|  | ||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
|  | ||||
|   16. Limitation of Liability. | ||||
|  | ||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||
| SUCH DAMAGES. | ||||
|  | ||||
|   17. Interpretation of Sections 15 and 16. | ||||
|  | ||||
|   If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
|  | ||||
|                      END OF TERMS AND CONDITIONS | ||||
|  | ||||
|             How to Apply These Terms to Your New Programs | ||||
|  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
|  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| state the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
|  | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
|  | ||||
|     This program is free software: you can redistribute it and/or modify | ||||
|     it under the terms of the GNU Affero General Public License as published by | ||||
|     the Free Software Foundation, either version 3 of the License, or | ||||
|     (at your option) any later version. | ||||
|  | ||||
|     This program is distributed in the hope that it will be useful, | ||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|     GNU Affero General Public License for more details. | ||||
|  | ||||
|     You should have received a copy of the GNU Affero General Public License | ||||
|     along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
|   If your software can interact with users remotely through a computer | ||||
| network, you should also make sure that it provides a way for users to | ||||
| get its source.  For example, if your program is a web application, its | ||||
| interface could display a "Source" link that leads users to an archive | ||||
| of the code.  There are many ways you could offer source, and different | ||||
| solutions will be better for different programs; see section 13 for the | ||||
| specific requirements. | ||||
|  | ||||
|   You should also get your employer (if you work as a programmer) or school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||
| For more information on this, and how to apply and follow the GNU AGPL, see | ||||
| <https://www.gnu.org/licenses/>. | ||||
| @@ -0,0 +1,33 @@ | ||||
| """ | ||||
| Mirai-API-HTTP 协议适配 | ||||
| ============================ | ||||
|  | ||||
| 协议详情请看: `mirai-api-http 文档`_  | ||||
|  | ||||
| \:\:\: tip | ||||
| 该Adapter目前仍然处在早期实验性阶段, 并未经过充分测试 | ||||
|  | ||||
| 如果你在使用过程中遇到了任何问题, 请前往 `Issue页面`_ 为我们提供反馈 | ||||
| \:\:\: | ||||
|  | ||||
| \:\:\: danger | ||||
| Mirai-API-HTTP 的适配器以 `AGPLv3许可`_ 单独开源 | ||||
|  | ||||
| 这意味着在使用该适配器时需要 **以该许可开源您的完整程序代码** | ||||
| \:\:\: | ||||
|  | ||||
| .. _mirai-api-http 文档: | ||||
|     https://github.com/project-mirai/mirai-api-http/tree/master/docs | ||||
|  | ||||
| .. _Issue页面: | ||||
|     https://github.com/nonebot/nonebot2/issues | ||||
|  | ||||
| .. _AGPLv3许可: | ||||
|     https://opensource.org/licenses/AGPL-3.0 | ||||
|  | ||||
| """ | ||||
|  | ||||
| from .bot import Bot | ||||
| from .bot_ws import WebsocketBot | ||||
| from .event import * | ||||
| from .message import MessageChain, MessageSegment | ||||
							
								
								
									
										726
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										726
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,726 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from io import BytesIO | ||||
| from ipaddress import IPv4Address | ||||
| from typing import Any, Dict, List, NoReturn, Optional, Tuple, Union | ||||
|  | ||||
| import httpx | ||||
|  | ||||
| from nonebot.adapters import Bot as BaseBot | ||||
| from nonebot.config import Config | ||||
| from nonebot.drivers import Driver, WebSocket | ||||
| from nonebot.exception import ApiNotAvailable, RequestDenied | ||||
| from nonebot.typing import overrides | ||||
|  | ||||
| from .config import Config as MiraiConfig | ||||
| from .event import Event, FriendMessage, GroupMessage, TempMessage | ||||
| from .message import MessageChain, MessageSegment | ||||
| from .utils import Log, argument_validation, catch_network_error, process_event | ||||
|  | ||||
|  | ||||
| class SessionManager: | ||||
|     """Bot会话管理器, 提供API主动调用接口""" | ||||
|     sessions: Dict[int, Tuple[str, datetime, httpx.AsyncClient]] = {} | ||||
|     session_expiry: timedelta = timedelta(minutes=15) | ||||
|  | ||||
|     def __init__(self, session_key: str, client: httpx.AsyncClient): | ||||
|         self.session_key, self.client = session_key, client | ||||
|  | ||||
|     @catch_network_error | ||||
|     async def post(self, | ||||
|                    path: str, | ||||
|                    *, | ||||
|                    params: Optional[Dict[str, Any]] = None) -> Any: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           以POST方式主动提交API请求 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``path: str``: 对应API路径 | ||||
|           * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|           - ``Dict[str, Any]``: API 返回值 | ||||
|         """ | ||||
|         response = await self.client.post( | ||||
|             path, | ||||
|             json={ | ||||
|                 **(params or {}), | ||||
|                 'sessionKey': self.session_key, | ||||
|             }, | ||||
|             timeout=3, | ||||
|         ) | ||||
|         response.raise_for_status() | ||||
|         return response.json() | ||||
|  | ||||
|     @catch_network_error | ||||
|     async def request(self, | ||||
|                       path: str, | ||||
|                       *, | ||||
|                       params: Optional[Dict[str, Any]] = None) -> Any: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           以GET方式主动提交API请求 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``path: str``: 对应API路径 | ||||
|           * ``params: Optional[Dict[str, Any]]``: 请求参数 (无需sessionKey) | ||||
|         """ | ||||
|         response = await self.client.get( | ||||
|             path, | ||||
|             params={ | ||||
|                 **(params or {}), | ||||
|                 'sessionKey': self.session_key, | ||||
|             }, | ||||
|             timeout=3, | ||||
|         ) | ||||
|         response.raise_for_status() | ||||
|         return response.json() | ||||
|  | ||||
|     @catch_network_error | ||||
|     async def upload(self, path: str, *, params: Dict[str, Any]) -> Any: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|             以表单(``multipart/form-data``)形式主动提交API请求 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|             * ``path: str``: 对应API路径 | ||||
|             * ``params: Dict[str, Any]``: 请求参数 (无需sessionKey) | ||||
|         """ | ||||
|         files = {k: v for k, v in params.items() if isinstance(v, BytesIO)} | ||||
|         form = {k: v for k, v in params.items() if k not in files} | ||||
|         response = await self.client.post( | ||||
|             path, | ||||
|             data=form, | ||||
|             files=files, | ||||
|             timeout=6, | ||||
|         ) | ||||
|         response.raise_for_status() | ||||
|         return response.json() | ||||
|  | ||||
|     @classmethod | ||||
|     async def new(cls, self_id: int, *, host: IPv4Address, port: int, | ||||
|                   auth_key: str) -> "SessionManager": | ||||
|         session = cls.get(self_id) | ||||
|         if session is not None: | ||||
|             return session | ||||
|  | ||||
|         client = httpx.AsyncClient(base_url=f'http://{host}:{port}') | ||||
|         response = await client.post('/auth', json={'authKey': auth_key}) | ||||
|         response.raise_for_status() | ||||
|         auth = response.json() | ||||
|         assert auth['code'] == 0 | ||||
|         session_key = auth['session'] | ||||
|         response = await client.post('/verify', | ||||
|                                      json={ | ||||
|                                          'sessionKey': session_key, | ||||
|                                          'qq': self_id | ||||
|                                      }) | ||||
|         assert response.json()['code'] == 0 | ||||
|         cls.sessions[self_id] = session_key, datetime.now(), client | ||||
|  | ||||
|         return cls(session_key, client) | ||||
|  | ||||
|     @classmethod | ||||
|     def get(cls, | ||||
|             self_id: int, | ||||
|             check_expire: bool = True) -> Optional["SessionManager"]: | ||||
|         if self_id not in cls.sessions: | ||||
|             return None | ||||
|         key, time, client = cls.sessions[self_id] | ||||
|         if check_expire and (datetime.now() - time > cls.session_expiry): | ||||
|             return None | ||||
|         return cls(key, client) | ||||
|  | ||||
|  | ||||
| class Bot(BaseBot): | ||||
|     """ | ||||
|     mirai-api-http 协议 Bot 适配。 | ||||
|  | ||||
|     \:\:\: warning | ||||
|     API中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 | ||||
|  | ||||
|     部分字段可能与文档在符号上不一致 | ||||
|     \:\:\: | ||||
|  | ||||
|     """ | ||||
|  | ||||
|     @overrides(BaseBot) | ||||
|     def __init__(self, | ||||
|                  connection_type: str, | ||||
|                  self_id: str, | ||||
|                  *, | ||||
|                  websocket: Optional[WebSocket] = None): | ||||
|         super().__init__(connection_type, self_id, websocket=websocket) | ||||
|  | ||||
|     @property | ||||
|     @overrides(BaseBot) | ||||
|     def type(self) -> str: | ||||
|         return "mirai" | ||||
|  | ||||
|     @property | ||||
|     def alive(self) -> bool: | ||||
|         return not self.websocket.closed | ||||
|  | ||||
|     @property | ||||
|     def api(self) -> SessionManager: | ||||
|         """返回该Bot对象的会话管理实例以提供API主动调用""" | ||||
|         api = SessionManager.get(self_id=int(self.self_id)) | ||||
|         assert api is not None, 'SessionManager has not been initialized' | ||||
|         return api | ||||
|  | ||||
|     @classmethod | ||||
|     @overrides(BaseBot) | ||||
|     async def check_permission(cls, driver: "Driver", connection_type: str, | ||||
|                                headers: dict, body: Optional[dict]) -> str: | ||||
|         if connection_type == 'ws': | ||||
|             raise RequestDenied( | ||||
|                 status_code=501, | ||||
|                 reason='Websocket connection is not implemented') | ||||
|         self_id: Optional[str] = headers.get('bot') | ||||
|         if self_id is None: | ||||
|             raise RequestDenied(status_code=400, | ||||
|                                 reason='Header `Bot` is required.') | ||||
|         self_id = str(self_id).strip() | ||||
|         await SessionManager.new( | ||||
|             int(self_id), | ||||
|             host=cls.mirai_config.host,  # type: ignore | ||||
|             port=cls.mirai_config.port,  #type: ignore | ||||
|             auth_key=cls.mirai_config.auth_key)  # type: ignore | ||||
|         return self_id | ||||
|  | ||||
|     @classmethod | ||||
|     @overrides(BaseBot) | ||||
|     def register(cls, driver: "Driver", config: "Config"): | ||||
|         cls.mirai_config = MiraiConfig(**config.dict()) | ||||
|         if (cls.mirai_config.auth_key and cls.mirai_config.host and | ||||
|                 cls.mirai_config.port) is None: | ||||
|             raise ApiNotAvailable('mirai') | ||||
|         super().register(driver, config) | ||||
|  | ||||
|     @overrides(BaseBot) | ||||
|     async def handle_message(self, message: dict): | ||||
|         Log.debug(f'received message {message}') | ||||
|         try: | ||||
|             await process_event( | ||||
|                 bot=self, | ||||
|                 event=Event.new({ | ||||
|                     **message, | ||||
|                     'self_id': self.self_id, | ||||
|                 }), | ||||
|             ) | ||||
|         except Exception as e: | ||||
|             Log.error(f'Failed to handle message: {message}', e) | ||||
|  | ||||
|     @overrides(BaseBot) | ||||
|     async def call_api(self, api: str, **data) -> NoReturn: | ||||
|         """ | ||||
|         \:\:\: danger | ||||
|         由于Mirai的HTTP API特殊性, 该API暂时无法实现 | ||||
|         \:\:\: | ||||
|          | ||||
|         \:\:\: tip | ||||
|         你可以使用 ``MiraiBot.api`` 中提供的调用方法来代替 | ||||
|         \:\:\: | ||||
|         """ | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @overrides(BaseBot) | ||||
|     def __getattr__(self, key: str) -> NoReturn: | ||||
|         """由于Mirai的HTTP API特殊性, 该API暂时无法实现""" | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @overrides(BaseBot) | ||||
|     @argument_validation | ||||
|     async def send(self, | ||||
|                    event: Event, | ||||
|                    message: Union[MessageChain, MessageSegment, str], | ||||
|                    at_sender: bool = False): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           根据 ``event`` 向触发事件的主体发送信息 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``event: Event``: Event对象 | ||||
|           * ``message: Union[MessageChain, MessageSegment, str]``: 要发送的消息 | ||||
|           * ``at_sender: bool``: 是否 @ 事件主体 | ||||
|         """ | ||||
|         if not isinstance(message, MessageChain): | ||||
|             message = MessageChain(message) | ||||
|         if isinstance(event, FriendMessage): | ||||
|             return await self.send_friend_message(target=event.sender.id, | ||||
|                                                   message_chain=message) | ||||
|         elif isinstance(event, GroupMessage): | ||||
|             if at_sender: | ||||
|                 message = MessageSegment.at(event.sender.id) + message | ||||
|             return await self.send_group_message(group=event.sender.group.id, | ||||
|                                                  message_chain=message) | ||||
|         elif isinstance(event, TempMessage): | ||||
|             return await self.send_temp_message(qq=event.sender.id, | ||||
|                                                 group=event.sender.group.id, | ||||
|                                                 message_chain=message) | ||||
|         else: | ||||
|             raise ValueError(f'Unsupported event type {event!r}.') | ||||
|  | ||||
|     @argument_validation | ||||
|     async def send_friend_message(self, target: int, | ||||
|                                   message_chain: MessageChain): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法向指定好友发送消息 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 发送消息目标好友的 QQ 号 | ||||
|           * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 | ||||
|         """ | ||||
|         return await self.api.post('sendFriendMessage', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'messageChain': message_chain.export() | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def send_temp_message(self, qq: int, group: int, | ||||
|                                 message_chain: MessageChain): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法向临时会话对象发送消息 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``qq: int``: 临时会话对象 QQ 号 | ||||
|           * ``group: int``: 临时会话群号 | ||||
|           * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 | ||||
|         """ | ||||
|         return await self.api.post('sendTempMessage', | ||||
|                                    params={ | ||||
|                                        'qq': qq, | ||||
|                                        'group': group, | ||||
|                                        'messageChain': message_chain.export() | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def send_group_message(self, | ||||
|                                  group: int, | ||||
|                                  message_chain: MessageChain, | ||||
|                                  quote: Optional[int] = None): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法向指定群发送消息 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``group: int``: 发送消息目标群的群号 | ||||
|           * ``message_chain: MessageChain``: 消息链,是一个消息对象构成的数组 | ||||
|           * ``quote: Optional[int]``: 引用一条消息的 message_id 进行回复 | ||||
|         """ | ||||
|         return await self.api.post('sendGroupMessage', | ||||
|                                    params={ | ||||
|                                        'group': group, | ||||
|                                        'messageChain': message_chain.export(), | ||||
|                                        'quote': quote | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def recall(self, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法撤回指定消息。对于bot发送的消息,有2分钟时间限制。对于撤回群聊中群员的消息,需要有相应权限 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 需要撤回的消息的message_id | ||||
|         """ | ||||
|         return await self.api.post('recall', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def send_image_message(self, target: int, qq: int, group: int, | ||||
|                                  urls: List[str]) -> List[str]: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法向指定对象(群或好友)发送图片消息 | ||||
|           除非需要通过此手段获取image_id,否则不推荐使用该接口 | ||||
|  | ||||
|           > 当qq和group同时存在时,表示发送临时会话图片,qq为临时会话对象QQ号,group为临时会话发起的群号 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 发送对象的QQ号或群号,可能存在歧义 | ||||
|           * ``qq: int``: 发送对象的QQ号 | ||||
|           * ``group: int``: 发送对象的群号 | ||||
|           * ``urls: List[str]``: 是一个url字符串构成的数组 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|           - ``List[str]``: 一个包含图片imageId的数组 | ||||
|         """ | ||||
|         return await self.api.post('sendImageMessage', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'qq': qq, | ||||
|                                        'group': group, | ||||
|                                        'urls': urls | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def upload_image(self, type: str, img: BytesIO): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|            使用此方法上传图片文件至服务器并返回Image_id | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``type: str``: "friend" 或 "group" 或 "temp" | ||||
|           * ``img: BytesIO``: 图片的BytesIO对象 | ||||
|         """ | ||||
|         return await self.api.upload('uploadImage', | ||||
|                                      params={ | ||||
|                                          'type': type, | ||||
|                                          'img': img | ||||
|                                      }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def upload_voice(self, type: str, voice: BytesIO): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法上传语音文件至服务器并返回voice_id | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``type: str``: 当前仅支持 "group" | ||||
|           * ``voice: BytesIO``: 语音的BytesIO对象 | ||||
|         """ | ||||
|         return await self.api.upload('uploadVoice', | ||||
|                                      params={ | ||||
|                                          'type': type, | ||||
|                                          'voice': voice | ||||
|                                      }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def fetch_message(self, count: int = 10): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot接收到的最老消息和最老各类事件 | ||||
|           (会从MiraiApiHttp消息记录中删除) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``count: int``: 获取消息和事件的数量 | ||||
|         """ | ||||
|         return await self.api.request('fetchMessage', params={'count': count}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def fetch_latest_message(self, count: int = 10): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot接收到的最新消息和最新各类事件 | ||||
|           (会从MiraiApiHttp消息记录中删除) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``count: int``: 获取消息和事件的数量 | ||||
|         """ | ||||
|         return await self.api.request('fetchLatestMessage', | ||||
|                                       params={'count': count}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def peek_message(self, count: int = 10): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot接收到的最老消息和最老各类事件 | ||||
|           (不会从MiraiApiHttp消息记录中删除)  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``count: int``: 获取消息和事件的数量 | ||||
|         """ | ||||
|         return await self.api.request('peekMessage', params={'count': count}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def peek_latest_message(self, count: int = 10): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot接收到的最新消息和最新各类事件 | ||||
|           (不会从MiraiApiHttp消息记录中删除) | ||||
|          | ||||
|         :参数: | ||||
|  | ||||
|           * ``count: int``: 获取消息和事件的数量 | ||||
|         """ | ||||
|         return await self.api.request('peekLatestMessage', | ||||
|                                       params={'count': count}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def messsage_from_id(self, id: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           通过messageId获取一条被缓存的消息 | ||||
|           使用此方法获取bot接收到的消息和各类事件 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``id: int``: 获取消息的message_id | ||||
|         """ | ||||
|         return await self.api.request('messageFromId', params={'id': id}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def count_message(self): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot接收并缓存的消息总数,注意不包含被删除的 | ||||
|         """ | ||||
|         return await self.api.request('countMessage') | ||||
|  | ||||
|     @argument_validation | ||||
|     async def friend_list(self) -> List[Dict[str, Any]]: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot的好友列表 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|           - ``List[Dict[str, Any]]``: 返回的好友列表数据 | ||||
|         """ | ||||
|         return await self.api.request('friendList') | ||||
|  | ||||
|     @argument_validation | ||||
|     async def group_list(self) -> List[Dict[str, Any]]: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot的群列表 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|           - ``List[Dict[str, Any]]``: 返回的群列表数据 | ||||
|         """ | ||||
|         return await self.api.request('groupList') | ||||
|  | ||||
|     @argument_validation | ||||
|     async def member_list(self, target: int) -> List[Dict[str, Any]]: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取bot指定群种的成员列表 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|           - ``List[Dict[str, Any]]``: 返回的群成员列表数据 | ||||
|         """ | ||||
|         return await self.api.request('memberList', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def mute(self, target: int, member_id: int, time: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法指定群禁言指定群员(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``member_id: int``: 指定群员QQ号 | ||||
|           * ``time: int``: 禁言时长,单位为秒,最多30天 | ||||
|         """ | ||||
|         return await self.api.post('mute', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'memberId': member_id, | ||||
|                                        'time': time | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def unmute(self, target: int, member_id: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法指定群解除群成员禁言(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``member_id: int``: 指定群员QQ号 | ||||
|         """ | ||||
|         return await self.api.post('unmute', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'memberId': member_id | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def kick(self, target: int, member_id: int, msg: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法移除指定群成员(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``member_id: int``: 指定群员QQ号 | ||||
|           * ``msg: str``: 信息 | ||||
|         """ | ||||
|         return await self.api.post('kick', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'memberId': member_id, | ||||
|                                        'msg': msg | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def quit(self, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法使Bot退出群聊  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 退出的群号 | ||||
|         """ | ||||
|         return await self.api.post('quit', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def mute_all(self, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法令指定群进行全体禁言(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|         """ | ||||
|         return await self.api.post('muteAll', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def unmute_all(self, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法令指定群解除全体禁言(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|         """ | ||||
|         return await self.api.post('unmuteAll', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def group_config(self, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取群设置 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|         .. code-block:: json | ||||
|  | ||||
|             { | ||||
|                 "name": "群名称", | ||||
|                 "announcement": "群公告", | ||||
|                 "confessTalk": true, | ||||
|                 "allowMemberInvite": true, | ||||
|                 "autoApprove": true, | ||||
|                 "anonymousChat": true | ||||
|             } | ||||
|         """ | ||||
|         return await self.api.request('groupConfig', params={'target': target}) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def modify_group_config(self, target: int, config: Dict[str, Any]): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法修改群设置(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``config: Dict[str, Any]``: 群设置, 格式见 ``group_config`` 的返回值 | ||||
|         """ | ||||
|         return await self.api.post('groupConfig', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'config': config | ||||
|                                    }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def member_info(self, target: int, member_id: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法获取群员资料 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``member_id: int``: 群员QQ号 | ||||
|  | ||||
|         :返回: | ||||
|  | ||||
|         .. code-block:: json | ||||
|  | ||||
|             { | ||||
|                 "name": "群名片", | ||||
|                 "specialTitle": "群头衔" | ||||
|             } | ||||
|         """ | ||||
|         return await self.api.request('memberInfo', | ||||
|                                       params={ | ||||
|                                           'target': target, | ||||
|                                           'memberId': member_id | ||||
|                                       }) | ||||
|  | ||||
|     @argument_validation | ||||
|     async def modify_member_info(self, target: int, member_id: int, | ||||
|                                  info: Dict[str, Any]): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           使用此方法修改群员资料(需要有相关权限) | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 指定群的群号 | ||||
|           * ``member_id: int``: 群员QQ号 | ||||
|           * ``info: Dict[str, Any]``: 群员资料, 格式见 ``member_info`` 的返回值 | ||||
|         """ | ||||
|         return await self.api.post('memberInfo', | ||||
|                                    params={ | ||||
|                                        'target': target, | ||||
|                                        'memberId': member_id, | ||||
|                                        'info': info | ||||
|                                    }) | ||||
							
								
								
									
										192
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/bot_ws.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,192 @@ | ||||
| import asyncio | ||||
| import json | ||||
| from ipaddress import IPv4Address | ||||
| from typing import (Any, Callable, Coroutine, Dict, NoReturn, Optional, Set, | ||||
|                     TypeVar) | ||||
|  | ||||
| import httpx | ||||
| import websockets | ||||
|  | ||||
| from nonebot.config import Config | ||||
| from nonebot.drivers import Driver | ||||
| from nonebot.drivers import WebSocket as BaseWebSocket | ||||
| from nonebot.exception import RequestDenied | ||||
| from nonebot.log import logger | ||||
| from nonebot.typing import overrides | ||||
|  | ||||
| from .bot import SessionManager, Bot | ||||
|  | ||||
| WebsocketHandlerFunction = Callable[[Dict[str, Any]], Coroutine[Any, Any, None]] | ||||
| WebsocketHandler_T = TypeVar('WebsocketHandler_T', | ||||
|                              bound=WebsocketHandlerFunction) | ||||
|  | ||||
|  | ||||
| class WebSocket(BaseWebSocket): | ||||
|  | ||||
|     @classmethod | ||||
|     async def new(cls, *, host: IPv4Address, port: int, | ||||
|                   session_key: str) -> "WebSocket": | ||||
|         listen_address = httpx.URL(f'ws://{host}:{port}/all', | ||||
|                                    params={'sessionKey': session_key}) | ||||
|         websocket = await websockets.connect(uri=str(listen_address)) | ||||
|         await (await websocket.ping()) | ||||
|         return cls(websocket) | ||||
|  | ||||
|     @overrides(BaseWebSocket) | ||||
|     def __init__(self, websocket: websockets.WebSocketClientProtocol): | ||||
|         self.event_handlers: Set[WebsocketHandlerFunction] = set() | ||||
|         super().__init__(websocket) | ||||
|  | ||||
|     @property | ||||
|     @overrides(BaseWebSocket) | ||||
|     def websocket(self) -> websockets.WebSocketClientProtocol: | ||||
|         return self._websocket | ||||
|  | ||||
|     @property | ||||
|     @overrides(BaseWebSocket) | ||||
|     def closed(self) -> bool: | ||||
|         return self.websocket.closed | ||||
|  | ||||
|     @overrides(BaseWebSocket) | ||||
|     async def send(self, data: Dict[str, Any]): | ||||
|         return await self.websocket.send(json.dumps(data)) | ||||
|  | ||||
|     @overrides(BaseWebSocket) | ||||
|     async def receive(self) -> Dict[str, Any]: | ||||
|         received = await self.websocket.recv() | ||||
|         return json.loads(received) | ||||
|  | ||||
|     async def _dispatcher(self): | ||||
|         while not self.closed: | ||||
|             try: | ||||
|                 data = await self.receive() | ||||
|             except websockets.ConnectionClosedOK: | ||||
|                 logger.debug(f'Websocket connection {self.websocket} closed') | ||||
|                 break | ||||
|             except websockets.ConnectionClosedError: | ||||
|                 logger.exception(f'Websocket connection {self.websocket} ' | ||||
|                                  'connection closed abnormally:') | ||||
|                 break | ||||
|             except json.JSONDecodeError as e: | ||||
|                 logger.exception(f'Websocket client listened {self.websocket} ' | ||||
|                                  f'failed to decode data: {e}') | ||||
|                 continue | ||||
|             asyncio.gather( | ||||
|                 *map(lambda f: f(data), self.event_handlers),  #type: ignore | ||||
|                 return_exceptions=True) | ||||
|  | ||||
|     @overrides(BaseWebSocket) | ||||
|     async def accept(self): | ||||
|         asyncio.create_task(self._dispatcher()) | ||||
|  | ||||
|     @overrides(BaseWebSocket) | ||||
|     async def close(self): | ||||
|         await self.websocket.close() | ||||
|  | ||||
|     def handle(self, callable: WebsocketHandler_T) -> WebsocketHandler_T: | ||||
|         self.event_handlers.add(callable) | ||||
|         return callable | ||||
|  | ||||
|  | ||||
| class WebsocketBot(Bot): | ||||
|     """ | ||||
|     mirai-api-http 正向 Websocket 协议 Bot 适配。 | ||||
|     """ | ||||
|  | ||||
|     @overrides(Bot) | ||||
|     def __init__(self, connection_type: str, self_id: str, *, | ||||
|                  websocket: WebSocket): | ||||
|         super().__init__(connection_type, self_id, websocket=websocket) | ||||
|  | ||||
|     @property | ||||
|     @overrides(Bot) | ||||
|     def type(self) -> str: | ||||
|         return "mirai-ws" | ||||
|  | ||||
|     @property | ||||
|     def alive(self) -> bool: | ||||
|         return not self.websocket.closed | ||||
|  | ||||
|     @property | ||||
|     def api(self) -> SessionManager: | ||||
|         api = SessionManager.get(self_id=int(self.self_id), check_expire=False) | ||||
|         assert api is not None, 'SessionManager has not been initialized' | ||||
|         return api | ||||
|  | ||||
|     @classmethod | ||||
|     @overrides(Bot) | ||||
|     async def check_permission(cls, driver: "Driver", connection_type: str, | ||||
|                                headers: dict, body: Optional[dict]) -> NoReturn: | ||||
|         raise RequestDenied( | ||||
|             status_code=501, | ||||
|             reason=f'Connection {connection_type} not implented') | ||||
|  | ||||
|     @classmethod | ||||
|     @overrides(Bot) | ||||
|     def register(cls, driver: "Driver", config: "Config", qq: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           注册该Adapter  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``driver: Driver``: 程序所使用的``Driver`` | ||||
|           * ``config: Config``: 程序配置对象 | ||||
|           * ``qq: int``: 要使用的Bot的QQ号 **注意: 在使用正向Websocket时必须指定该值!** | ||||
|         """ | ||||
|         super().register(driver, config) | ||||
|         cls.active = True | ||||
|  | ||||
|         async def _bot_connection(): | ||||
|             session: SessionManager = await SessionManager.new( | ||||
|                 qq, | ||||
|                 host=cls.mirai_config.host,  # type: ignore | ||||
|                 port=cls.mirai_config.port,  # type: ignore | ||||
|                 auth_key=cls.mirai_config.auth_key  # type: ignore | ||||
|             ) | ||||
|             websocket = await WebSocket.new( | ||||
|                 host=cls.mirai_config.host,  # type: ignore | ||||
|                 port=cls.mirai_config.port,  # type: ignore | ||||
|                 session_key=session.session_key) | ||||
|             bot = cls(connection_type='forward_ws', | ||||
|                       self_id=str(qq), | ||||
|                       websocket=websocket) | ||||
|             websocket.handle(bot.handle_message) | ||||
|             await websocket.accept() | ||||
|             return bot | ||||
|  | ||||
|         async def _connection_ensure(): | ||||
|             self_id = str(qq) | ||||
|             if self_id not in driver._clients: | ||||
|                 bot = await _bot_connection() | ||||
|                 driver._bot_connect(bot) | ||||
|             else: | ||||
|                 bot = driver._clients[self_id] | ||||
|             if not bot.alive: | ||||
|                 driver._bot_disconnect(bot) | ||||
|             return | ||||
|  | ||||
|         @driver.on_startup | ||||
|         async def _startup(): | ||||
|  | ||||
|             async def _checker(): | ||||
|                 while cls.active: | ||||
|                     try: | ||||
|                         await _connection_ensure() | ||||
|                     except Exception as e: | ||||
|                         logger.opt(colors=True).warning( | ||||
|                             'Failed to create mirai connection to ' | ||||
|                             f'<y>{qq}</y>, reason: <r>{e}</r>. ' | ||||
|                             'Will retry after 3 seconds') | ||||
|                     await asyncio.sleep(3) | ||||
|  | ||||
|             asyncio.create_task(_checker()) | ||||
|  | ||||
|         @driver.on_shutdown | ||||
|         async def _shutdown(): | ||||
|             cls.active = False | ||||
|             bot = driver._clients.pop(str(qq), None) | ||||
|             if bot is None: | ||||
|                 return | ||||
|             await bot.websocket.close()  #type:ignore | ||||
| @@ -0,0 +1,22 @@ | ||||
| from ipaddress import IPv4Address | ||||
| from typing import Optional | ||||
|  | ||||
| from pydantic import BaseModel, Extra, Field | ||||
|  | ||||
|  | ||||
| class Config(BaseModel): | ||||
|     """ | ||||
|     Mirai 配置类 | ||||
|  | ||||
|     :必填: | ||||
|  | ||||
|       - ``auth_key`` / ``mirai_auth_key``: mirai-api-http 的 auth_key | ||||
|       - ``mirai_host``: mirai-api-http 的地址 | ||||
|       - ``mirai_port``: mirai-api-http 的端口 | ||||
|     """ | ||||
|     auth_key: Optional[str] = Field(None, alias='mirai_auth_key') | ||||
|     host: Optional[IPv4Address] = Field(None, alias='mirai_host') | ||||
|     port: Optional[int] = Field(None, alias='mirai_port') | ||||
|  | ||||
|     class Config: | ||||
|         extra = Extra.ignore | ||||
| @@ -0,0 +1,29 @@ | ||||
| """ | ||||
| \:\:\: warning  | ||||
| 事件中为了使代码更加整洁, 我们采用了与PEP8相符的命名规则取代Mirai原有的驼峰命名 | ||||
|  | ||||
| 部分字段可能与文档在符号上不一致 | ||||
| \:\:\: | ||||
| """ | ||||
| from .base import (Event, GroupChatInfo, GroupInfo, PrivateChatInfo, | ||||
|                    UserPermission) | ||||
| from .message import * | ||||
| from .notice import * | ||||
| from .request import * | ||||
|  | ||||
| __all__ = [ | ||||
|     'Event', 'GroupChatInfo', 'GroupInfo', 'PrivateChatInfo', 'UserPermission', | ||||
|     'MessageSource', 'MessageEvent', 'GroupMessage', 'FriendMessage', | ||||
|     'TempMessage', 'NoticeEvent', 'MuteEvent', 'BotMuteEvent', 'BotUnmuteEvent', | ||||
|     'MemberMuteEvent', 'MemberUnmuteEvent', 'BotJoinGroupEvent', | ||||
|     'BotLeaveEventActive', 'BotLeaveEventKick', 'MemberJoinEvent', | ||||
|     'MemberLeaveEventKick', 'MemberLeaveEventQuit', 'FriendRecallEvent', | ||||
|     'GroupRecallEvent', 'GroupStateChangeEvent', 'GroupNameChangeEvent', | ||||
|     'GroupEntranceAnnouncementChangeEvent', 'GroupMuteAllEvent', | ||||
|     'GroupAllowAnonymousChatEvent', 'GroupAllowConfessTalkEvent', | ||||
|     'GroupAllowMemberInviteEvent', 'MemberStateChangeEvent', | ||||
|     'MemberCardChangeEvent', 'MemberSpecialTitleChangeEvent', | ||||
|     'BotGroupPermissionChangeEvent', 'MemberPermissionChangeEvent', | ||||
|     'RequestEvent', 'NewFriendRequestEvent', 'MemberJoinRequestEvent', | ||||
|     'BotInvitedJoinGroupRequestEvent' | ||||
| ] | ||||
| @@ -0,0 +1,133 @@ | ||||
| import json | ||||
| from enum import Enum | ||||
| from typing import Any, Dict, Optional, Type | ||||
|  | ||||
| from pydantic import BaseModel, Field, ValidationError | ||||
| from typing_extensions import Literal | ||||
|  | ||||
| from nonebot.adapters import Event as BaseEvent | ||||
| from nonebot.adapters import Message as BaseMessage | ||||
| from nonebot.log import logger | ||||
| from nonebot.typing import overrides | ||||
|  | ||||
|  | ||||
| class UserPermission(str, Enum): | ||||
|     """ | ||||
|     :说明: | ||||
|      | ||||
|     用户权限枚举类 | ||||
|  | ||||
|       * ``OWNER``: 群主 | ||||
|       * ``ADMINISTRATOR``: 群管理 | ||||
|       * ``MEMBER``: 普通群成员 | ||||
|     """ | ||||
|     OWNER = 'OWNER' | ||||
|     ADMINISTRATOR = 'ADMINISTRATOR' | ||||
|     MEMBER = 'MEMBER' | ||||
|  | ||||
|  | ||||
| class GroupInfo(BaseModel): | ||||
|     id: int | ||||
|     name: str | ||||
|     permission: UserPermission | ||||
|  | ||||
|  | ||||
| class GroupChatInfo(BaseModel): | ||||
|     id: int | ||||
|     name: str = Field(alias='memberName') | ||||
|     permission: UserPermission | ||||
|     group: GroupInfo | ||||
|  | ||||
|  | ||||
| class PrivateChatInfo(BaseModel): | ||||
|     id: int | ||||
|     nickname: str | ||||
|     remark: str | ||||
|  | ||||
|  | ||||
| class Event(BaseEvent): | ||||
|     """ | ||||
|     mirai-api-http 协议事件,字段与 mirai-api-http 一致。各事件字段参考 `mirai-api-http 事件类型`_ | ||||
|  | ||||
|     .. _mirai-api-http 事件类型: | ||||
|         https://github.com/project-mirai/mirai-api-http/blob/master/docs/EventType.md | ||||
|     """ | ||||
|     self_id: int | ||||
|     type: str | ||||
|  | ||||
|     @classmethod | ||||
|     def new(cls, data: Dict[str, Any]) -> "Event": | ||||
|         """ | ||||
|         此事件类的工厂函数, 能够通过事件数据选择合适的子类进行序列化 | ||||
|         """ | ||||
|         type = data['type'] | ||||
|  | ||||
|         def all_subclasses(cls: Type[Event]): | ||||
|             return set(cls.__subclasses__()).union( | ||||
|                 [s for c in cls.__subclasses__() for s in all_subclasses(c)]) | ||||
|  | ||||
|         event_class: Optional[Type[Event]] = None | ||||
|         for subclass in all_subclasses(cls): | ||||
|             if subclass.__name__ != type: | ||||
|                 continue | ||||
|             event_class = subclass | ||||
|  | ||||
|         if event_class is None: | ||||
|             return Event.parse_obj(data) | ||||
|  | ||||
|         while issubclass(event_class, Event): | ||||
|             try: | ||||
|                 return event_class.parse_obj(data) | ||||
|             except ValidationError as e: | ||||
|                 logger.info( | ||||
|                     f'Failed to parse {data} to class {event_class.__name__}: ' | ||||
|                     f'{e.errors()!r}. Fallback to parent class.') | ||||
|                 event_class = event_class.__base__ | ||||
|  | ||||
|         raise ValueError(f'Failed to serialize {data}.') | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_type(self) -> Literal["message", "notice", "request", "meta_event"]: | ||||
|         from . import message, notice, request, meta | ||||
|         if isinstance(self, message.MessageEvent): | ||||
|             return 'message' | ||||
|         elif isinstance(self, notice.NoticeEvent): | ||||
|             return 'notice' | ||||
|         elif isinstance(self, request.RequestEvent): | ||||
|             return 'request' | ||||
|         else: | ||||
|             return 'meta_event' | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_event_name(self) -> str: | ||||
|         return self.type | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_event_description(self) -> str: | ||||
|         return str(self.normalize_dict()) | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_message(self) -> BaseMessage: | ||||
|         raise ValueError("Event has no message!") | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_plaintext(self) -> str: | ||||
|         raise ValueError("Event has no message!") | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_user_id(self) -> str: | ||||
|         raise ValueError("Event has no message!") | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def get_session_id(self) -> str: | ||||
|         raise ValueError("Event has no message!") | ||||
|  | ||||
|     @overrides(BaseEvent) | ||||
|     def is_tome(self) -> bool: | ||||
|         return False | ||||
|  | ||||
|     def normalize_dict(self, **kwargs) -> Dict[str, Any]: | ||||
|         """ | ||||
|         返回可以被json正常反序列化的结构体 | ||||
|         """ | ||||
|         return json.loads(self.json(**kwargs)) | ||||
| @@ -0,0 +1,85 @@ | ||||
| from datetime import datetime | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from pydantic import BaseModel, Field | ||||
|  | ||||
| from nonebot.typing import overrides | ||||
|  | ||||
| from ..message import MessageChain | ||||
| from .base import Event, GroupChatInfo, PrivateChatInfo | ||||
|  | ||||
|  | ||||
| class MessageSource(BaseModel): | ||||
|     id: int | ||||
|     time: datetime | ||||
|  | ||||
|  | ||||
| class MessageEvent(Event): | ||||
|     """消息事件基类""" | ||||
|     message_chain: MessageChain = Field(alias='messageChain') | ||||
|     source: Optional[MessageSource] = None | ||||
|     sender: Any | ||||
|  | ||||
|     @overrides(Event) | ||||
|     def get_message(self) -> MessageChain: | ||||
|         return self.message_chain | ||||
|  | ||||
|     @overrides(Event) | ||||
|     def get_plaintext(self) -> str: | ||||
|         return self.message_chain.extract_plain_text() | ||||
|  | ||||
|     @overrides(Event) | ||||
|     def get_user_id(self) -> str: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @overrides(Event) | ||||
|     def get_session_id(self) -> str: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| class GroupMessage(MessageEvent): | ||||
|     """群消息事件""" | ||||
|     sender: GroupChatInfo | ||||
|     to_me: bool = False | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def get_session_id(self) -> str: | ||||
|         return f'group_{self.sender.group.id}_' + self.get_user_id() | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def get_user_id(self) -> str: | ||||
|         return str(self.sender.id) | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def is_tome(self) -> bool: | ||||
|         return self.to_me | ||||
|  | ||||
|  | ||||
| class FriendMessage(MessageEvent): | ||||
|     """好友消息事件""" | ||||
|     sender: PrivateChatInfo | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def get_user_id(self) -> str: | ||||
|         return str(self.sender.id) | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def get_session_id(self) -> str: | ||||
|         return 'friend_' + self.get_user_id() | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def is_tome(self) -> bool: | ||||
|         return True | ||||
|  | ||||
|  | ||||
| class TempMessage(MessageEvent): | ||||
|     """临时会话消息事件""" | ||||
|     sender: GroupChatInfo | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def get_session_id(self) -> str: | ||||
|         return f'temp_{self.sender.group.id}_' + self.get_user_id() | ||||
|  | ||||
|     @overrides(MessageEvent) | ||||
|     def is_tome(self) -> bool: | ||||
|         return True | ||||
| @@ -0,0 +1,31 @@ | ||||
| from .base import Event | ||||
|  | ||||
|  | ||||
| class MetaEvent(Event): | ||||
|     """元事件基类""" | ||||
|     qq: int | ||||
|  | ||||
|  | ||||
| class BotOnlineEvent(MetaEvent): | ||||
|     """Bot登录成功""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotOfflineEventActive(MetaEvent): | ||||
|     """Bot主动离线""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotOfflineEventForce(MetaEvent): | ||||
|     """Bot被挤下线""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotOfflineEventDropped(MetaEvent): | ||||
|     """Bot被服务器断开或因网络问题而掉线""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotReloginEvent(MetaEvent): | ||||
|     """Bot主动重新登录""" | ||||
|     pass | ||||
| @@ -0,0 +1,156 @@ | ||||
| from typing import Any, Optional | ||||
|  | ||||
| from pydantic import Field | ||||
|  | ||||
| from .base import Event, GroupChatInfo, GroupInfo, UserPermission | ||||
|  | ||||
|  | ||||
| class NoticeEvent(Event): | ||||
|     """通知事件基类""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class MuteEvent(NoticeEvent): | ||||
|     """禁言类事件基类""" | ||||
|     operator: GroupChatInfo | ||||
|  | ||||
|  | ||||
| class BotMuteEvent(MuteEvent): | ||||
|     """Bot被禁言""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotUnmuteEvent(MuteEvent): | ||||
|     """Bot被取消禁言""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class MemberMuteEvent(MuteEvent): | ||||
|     """群成员被禁言事件(该成员不是Bot)""" | ||||
|     duration_seconds: int = Field(alias='durationSeconds') | ||||
|     member: GroupChatInfo | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class MemberUnmuteEvent(MuteEvent): | ||||
|     """群成员被取消禁言事件(该成员不是Bot)""" | ||||
|     member: GroupChatInfo | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class BotJoinGroupEvent(NoticeEvent): | ||||
|     """Bot加入了一个新群""" | ||||
|     group: GroupInfo | ||||
|  | ||||
|  | ||||
| class BotLeaveEventActive(BotJoinGroupEvent): | ||||
|     """Bot主动退出一个群""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class BotLeaveEventKick(BotJoinGroupEvent): | ||||
|     """Bot被踢出一个群""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class MemberJoinEvent(NoticeEvent): | ||||
|     """新人入群的事件""" | ||||
|     member: GroupChatInfo | ||||
|  | ||||
|  | ||||
| class MemberLeaveEventKick(MemberJoinEvent): | ||||
|     """成员被踢出群(该成员不是Bot)""" | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class MemberLeaveEventQuit(MemberJoinEvent): | ||||
|     """成员主动离群(该成员不是Bot)""" | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class FriendRecallEvent(NoticeEvent): | ||||
|     """好友消息撤回""" | ||||
|     author_id: int = Field(alias='authorId') | ||||
|     message_id: int = Field(alias='messageId') | ||||
|     time: int | ||||
|     operator: int | ||||
|  | ||||
|  | ||||
| class GroupRecallEvent(FriendRecallEvent): | ||||
|     """群消息撤回""" | ||||
|     group: GroupInfo | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class GroupStateChangeEvent(NoticeEvent): | ||||
|     """群变化事件基类""" | ||||
|     origin: Any | ||||
|     current: Any | ||||
|     group: GroupInfo | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class GroupNameChangeEvent(GroupStateChangeEvent): | ||||
|     """某个群名改变""" | ||||
|     origin: str | ||||
|     current: str | ||||
|  | ||||
|  | ||||
| class GroupEntranceAnnouncementChangeEvent(GroupStateChangeEvent): | ||||
|     """某群入群公告改变""" | ||||
|     origin: str | ||||
|     current: str | ||||
|  | ||||
|  | ||||
| class GroupMuteAllEvent(GroupStateChangeEvent): | ||||
|     """全员禁言""" | ||||
|     origin: bool | ||||
|     current: bool | ||||
|  | ||||
|  | ||||
| class GroupAllowAnonymousChatEvent(GroupStateChangeEvent): | ||||
|     """匿名聊天""" | ||||
|     origin: bool | ||||
|     current: bool | ||||
|  | ||||
|  | ||||
| class GroupAllowConfessTalkEvent(GroupStateChangeEvent): | ||||
|     """坦白说""" | ||||
|     origin: bool | ||||
|     current: bool | ||||
|  | ||||
|  | ||||
| class GroupAllowMemberInviteEvent(GroupStateChangeEvent): | ||||
|     """允许群员邀请好友加群""" | ||||
|     origin: bool | ||||
|     current: bool | ||||
|  | ||||
|  | ||||
| class MemberStateChangeEvent(NoticeEvent): | ||||
|     """群成员变化事件基类""" | ||||
|     member: GroupChatInfo | ||||
|     operator: Optional[GroupChatInfo] = None | ||||
|  | ||||
|  | ||||
| class MemberCardChangeEvent(MemberStateChangeEvent): | ||||
|     """群名片改动""" | ||||
|     origin: str | ||||
|     current: str | ||||
|  | ||||
|  | ||||
| class MemberSpecialTitleChangeEvent(MemberStateChangeEvent): | ||||
|     """群头衔改动(只有群主有操作限权)""" | ||||
|     origin: str | ||||
|     current: str | ||||
|  | ||||
|  | ||||
| class BotGroupPermissionChangeEvent(MemberStateChangeEvent): | ||||
|     """Bot在群里的权限被改变""" | ||||
|     origin: UserPermission | ||||
|     current: UserPermission | ||||
|  | ||||
|  | ||||
| class MemberPermissionChangeEvent(MemberStateChangeEvent): | ||||
|     """成员权限改变的事件(该成员不是Bot)""" | ||||
|     origin: UserPermission | ||||
|     current: UserPermission | ||||
| @@ -0,0 +1,170 @@ | ||||
| from typing import TYPE_CHECKING | ||||
|  | ||||
| from pydantic import Field | ||||
| from typing_extensions import Literal | ||||
|  | ||||
| from .base import Event | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from ..bot import Bot | ||||
|  | ||||
|  | ||||
| class RequestEvent(Event): | ||||
|     """请求事件基类""" | ||||
|     event_id: int = Field(alias='eventId') | ||||
|     message: str | ||||
|     nick: str | ||||
|  | ||||
|  | ||||
| class NewFriendRequestEvent(RequestEvent): | ||||
|     """添加好友申请""" | ||||
|     from_id: int = Field(alias='fromId') | ||||
|     group_id: int = Field(0, alias='groupId') | ||||
|  | ||||
|     async def approve(self, bot: "Bot"): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           通过此人的好友申请 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|         """ | ||||
|         return await bot.api.post('/resp/newFriendRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': 0 | ||||
|                                   }) | ||||
|  | ||||
|     async def reject(self, | ||||
|                      bot: "Bot", | ||||
|                      operate: Literal[1, 2] = 1, | ||||
|                      message: str = ''): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           拒绝此人的好友申请  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|           * ``operate: Literal[1, 2]``: 响应的操作类型 | ||||
|  | ||||
|             * ``1``: 拒绝添加好友 | ||||
|             * ``2``: 拒绝添加好友并添加黑名单,不再接收该用户的好友申请 | ||||
|  | ||||
|           * ``message: str``: 回复的信息 | ||||
|         """ | ||||
|         assert operate > 0 | ||||
|         return await bot.api.post('/resp/newFriendRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': operate, | ||||
|                                       'message': message | ||||
|                                   }) | ||||
|  | ||||
|  | ||||
| class MemberJoinRequestEvent(RequestEvent): | ||||
|     """用户入群申请(Bot需要有管理员权限)""" | ||||
|     from_id: int = Field(alias='fromId') | ||||
|     group_id: int = Field(alias='groupId') | ||||
|     group_name: str = Field(alias='groupName') | ||||
|  | ||||
|     async def approve(self, bot: "Bot"): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           通过此人的加群申请 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|         """ | ||||
|         return await bot.api.post('/resp/memberJoinRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': 0 | ||||
|                                   }) | ||||
|  | ||||
|     async def reject(self, | ||||
|                      bot: "Bot", | ||||
|                      operate: Literal[1, 2, 3, 4] = 1, | ||||
|                      message: str = ''): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           拒绝(忽略)此人的加群申请 | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|           * ``operate: Literal[1, 2, 3, 4]``: 响应的操作类型 | ||||
|  | ||||
|             * ``1``: 拒绝入群 | ||||
|             * ``2``: 忽略请求 | ||||
|             * ``3``: 拒绝入群并添加黑名单,不再接收该用户的入群申请 | ||||
|             * ``4``: 忽略入群并添加黑名单,不再接收该用户的入群申请 | ||||
|  | ||||
|           * ``message: str``: 回复的信息 | ||||
|         """ | ||||
|         assert operate > 0 | ||||
|         return await bot.api.post('/resp/memberJoinRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': operate, | ||||
|                                       'message': message | ||||
|                                   }) | ||||
|  | ||||
|  | ||||
| class BotInvitedJoinGroupRequestEvent(RequestEvent): | ||||
|     """Bot被邀请入群申请""" | ||||
|     from_id: int = Field(alias='fromId') | ||||
|     group_id: int = Field(alias='groupId') | ||||
|     group_name: str = Field(alias='groupName') | ||||
|  | ||||
|     async def approve(self, bot: "Bot"): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           通过这份被邀请入群申请  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|         """ | ||||
|         return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': 0 | ||||
|                                   }) | ||||
|  | ||||
|     async def reject(self, bot: "Bot", message: str = ""): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           拒绝这份被邀请入群申请  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``bot: Bot``: 当前的 ``Bot`` 对象 | ||||
|           * ``message: str``: 邀请消息 | ||||
|         """ | ||||
|         return await bot.api.post('/resp/botInvitedJoinGroupRequestEvent', | ||||
|                                   params={ | ||||
|                                       'eventId': self.event_id, | ||||
|                                       'groupId': self.group_id, | ||||
|                                       'fromId': self.from_id, | ||||
|                                       'operate': 1, | ||||
|                                       'message': message | ||||
|                                   }) | ||||
							
								
								
									
										343
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/message.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/message.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,343 @@ | ||||
| from enum import Enum | ||||
| from typing import Any, Dict, Iterable, List, Optional, Union | ||||
|  | ||||
| from pydantic import validate_arguments | ||||
|  | ||||
| from nonebot.adapters import Message as BaseMessage | ||||
| from nonebot.adapters import MessageSegment as BaseMessageSegment | ||||
| from nonebot.typing import overrides | ||||
|  | ||||
|  | ||||
| class MessageType(str, Enum): | ||||
|     """消息类型枚举类""" | ||||
|     SOURCE = 'Source' | ||||
|     QUOTE = 'Quote' | ||||
|     AT = 'At' | ||||
|     AT_ALL = 'AtAll' | ||||
|     FACE = 'Face' | ||||
|     PLAIN = 'Plain' | ||||
|     IMAGE = 'Image' | ||||
|     FLASH_IMAGE = 'FlashImage' | ||||
|     VOICE = 'Voice' | ||||
|     XML = 'Xml' | ||||
|     JSON = 'Json' | ||||
|     APP = 'App' | ||||
|     POKE = 'Poke' | ||||
|  | ||||
|  | ||||
| class MessageSegment(BaseMessageSegment): | ||||
|     """ | ||||
|     CQHTTP 协议 MessageSegment 适配。具体方法参考 `mirai-api-http 消息类型`_ | ||||
|  | ||||
|     .. _mirai-api-http 消息类型: | ||||
|         https://github.com/project-mirai/mirai-api-http/blob/master/docs/MessageType.md | ||||
|     """ | ||||
|  | ||||
|     type: MessageType | ||||
|     data: Dict[str, Any] | ||||
|  | ||||
|     @overrides(BaseMessageSegment) | ||||
|     @validate_arguments | ||||
|     def __init__(self, type: MessageType, **data): | ||||
|         super().__init__(type=type, | ||||
|                          data={k: v for k, v in data.items() if v is not None}) | ||||
|  | ||||
|     @overrides(BaseMessageSegment) | ||||
|     def __str__(self) -> str: | ||||
|         return self.data['text'] if self.is_text() else repr(self) | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         return '[mirai:%s]' % ','.join([ | ||||
|             self.type.value, | ||||
|             *map( | ||||
|                 lambda s: '%s=%r' % s, | ||||
|                 self.data.items(), | ||||
|             ), | ||||
|         ]) | ||||
|  | ||||
|     @overrides(BaseMessageSegment) | ||||
|     def __add__(self, other) -> "MessageChain": | ||||
|         return MessageChain(self) + other | ||||
|  | ||||
|     @overrides(BaseMessageSegment) | ||||
|     def __radd__(self, other) -> "MessageChain": | ||||
|         return MessageChain(other) + self | ||||
|  | ||||
|     @overrides(BaseMessageSegment) | ||||
|     def is_text(self) -> bool: | ||||
|         return self.type == MessageType.PLAIN | ||||
|  | ||||
|     def as_dict(self) -> Dict[str, Any]: | ||||
|         """导出可以被正常json序列化的结构体""" | ||||
|         return {'type': self.type.value, **self.data} | ||||
|  | ||||
|     @classmethod | ||||
|     def source(cls, id: int, time: int): | ||||
|         return cls(type=MessageType.SOURCE, id=id, time=time) | ||||
|  | ||||
|     @classmethod | ||||
|     def quote(cls, id: int, group_id: int, sender_id: int, target_id: int, | ||||
|               origin: "MessageChain"): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           生成回复引用消息段  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``id: int``: 被引用回复的原消息的message_id | ||||
|           * ``group_id: int``: 被引用回复的原消息所接收的群号,当为好友消息时为0 | ||||
|           * ``sender_id: int``: 被引用回复的原消息的发送者的QQ号 | ||||
|           * ``target_id: int``: 被引用回复的原消息的接收者者的QQ号(或群号) | ||||
|           * ``origin: MessageChain``: 被引用回复的原消息的消息链对象 | ||||
|         """ | ||||
|         return cls(type=MessageType.QUOTE, | ||||
|                    id=id, | ||||
|                    groupId=group_id, | ||||
|                    senderId=sender_id, | ||||
|                    targetId=target_id, | ||||
|                    origin=origin.export()) | ||||
|  | ||||
|     @classmethod | ||||
|     def at(cls, target: int): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           @某个人  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``target: int``: 群员QQ号 | ||||
|         """ | ||||
|         return cls(type=MessageType.AT, target=target) | ||||
|  | ||||
|     @classmethod | ||||
|     def at_all(cls): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           @全体成员 | ||||
|         """ | ||||
|         return cls(type=MessageType.AT_ALL) | ||||
|  | ||||
|     @classmethod | ||||
|     def face(cls, face_id: Optional[int] = None, name: Optional[str] = None): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           发送QQ表情  | ||||
|          | ||||
|         :参数: | ||||
|  | ||||
|           * ``face_id: Optional[int]``: QQ表情编号,可选,优先高于name | ||||
|           * ``name: Optional[str]``: QQ表情拼音,可选 | ||||
|         """ | ||||
|         return cls(type=MessageType.FACE, faceId=face_id, name=name) | ||||
|  | ||||
|     @classmethod | ||||
|     def plain(cls, text: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           纯文本消息  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``text: str``: 文字消息 | ||||
|         """ | ||||
|         return cls(type=MessageType.PLAIN, text=text) | ||||
|  | ||||
|     @classmethod | ||||
|     def image(cls, | ||||
|               image_id: Optional[str] = None, | ||||
|               url: Optional[str] = None, | ||||
|               path: Optional[str] = None): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           图片消息  | ||||
|          | ||||
|         :参数: | ||||
|  | ||||
|           * ``image_id: Optional[str]``: 图片的image_id,群图片与好友图片格式不同。不为空时将忽略url属性 | ||||
|           * ``url: Optional[str]``: 图片的URL,发送时可作网络图片的链接 | ||||
|           * ``path: Optional[str]``: 图片的路径,发送本地图片 | ||||
|         """ | ||||
|         return cls(type=MessageType.IMAGE, imageId=image_id, url=url, path=path) | ||||
|  | ||||
|     @classmethod | ||||
|     def flash_image(cls, | ||||
|                     image_id: Optional[str] = None, | ||||
|                     url: Optional[str] = None, | ||||
|                     path: Optional[str] = None): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           闪照消息  | ||||
|          | ||||
|         :参数: | ||||
|            | ||||
|           同 ``image`` | ||||
|         """ | ||||
|         return cls(type=MessageType.FLASH_IMAGE, | ||||
|                    imageId=image_id, | ||||
|                    url=url, | ||||
|                    path=path) | ||||
|  | ||||
|     @classmethod | ||||
|     def voice(cls, | ||||
|               voice_id: Optional[str] = None, | ||||
|               url: Optional[str] = None, | ||||
|               path: Optional[str] = None): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           语音消息 | ||||
|  | ||||
|         :参数:  | ||||
|  | ||||
|           * ``voice_id: Optional[str]``: 语音的voice_id,不为空时将忽略url属性 | ||||
|           * ``url: Optional[str]``: 语音的URL,发送时可作网络语音的链接 | ||||
|           * ``path: Optional[str]``: 语音的路径,发送本地语音 | ||||
|         """ | ||||
|         return cls(type=MessageType.FLASH_IMAGE, | ||||
|                    imageId=voice_id, | ||||
|                    url=url, | ||||
|                    path=path) | ||||
|  | ||||
|     @classmethod | ||||
|     def xml(cls, xml: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           XML消息  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``xml: str``: XML文本 | ||||
|         """ | ||||
|         return cls(type=MessageType.XML, xml=xml) | ||||
|  | ||||
|     @classmethod | ||||
|     def json(cls, json: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           Json消息  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``json: str``: Json文本 | ||||
|         """ | ||||
|         return cls(type=MessageType.JSON, json=json) | ||||
|  | ||||
|     @classmethod | ||||
|     def app(cls, content: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           应用程序消息  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``content: str``: 内容 | ||||
|         """ | ||||
|         return cls(type=MessageType.APP, content=content) | ||||
|  | ||||
|     @classmethod | ||||
|     def poke(cls, name: str): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           戳一戳消息  | ||||
|  | ||||
|         :参数: | ||||
|  | ||||
|           * ``name: str``: 戳一戳的类型 | ||||
|  | ||||
|             * ``Poke``: 戳一戳 | ||||
|             * ``ShowLove``: 比心 | ||||
|             * ``Like``: 点赞 | ||||
|             * ``Heartbroken``: 心碎 | ||||
|             * ``SixSixSix``: 666 | ||||
|             * ``FangDaZhao``: 放大招 | ||||
|  | ||||
|         """ | ||||
|         return cls(type=MessageType.POKE, name=name) | ||||
|  | ||||
|  | ||||
| class MessageChain(BaseMessage): | ||||
|     """ | ||||
|     Mirai 协议 Message 适配 | ||||
|      | ||||
|     由于Mirai协议的Message实现较为特殊, 故使用MessageChain命名 | ||||
|     """ | ||||
|  | ||||
|     @overrides(BaseMessage) | ||||
|     def __init__(self, message: Union[List[Dict[str, | ||||
|                                                 Any]], Iterable[MessageSegment], | ||||
|                                       MessageSegment, str], **kwargs): | ||||
|         super().__init__(**kwargs) | ||||
|         if isinstance(message, MessageSegment): | ||||
|             self.append(message) | ||||
|         elif isinstance(message, str): | ||||
|             self.append(MessageSegment.plain(text=message)) | ||||
|         elif isinstance(message, Iterable): | ||||
|             self.extend(self._construct(message)) | ||||
|         else: | ||||
|             raise ValueError( | ||||
|                 f'Type {type(message).__name__} is not supported in mirai adapter.' | ||||
|             ) | ||||
|  | ||||
|     @overrides(BaseMessage) | ||||
|     def reduce(self): | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           忽略为空的消息段, 合并相邻的纯文本消息段 | ||||
|         """ | ||||
|         for index, segment in enumerate(self): | ||||
|             segment: MessageSegment | ||||
|             if segment.is_text() and not str(segment).strip(): | ||||
|                 self.pop(index) | ||||
|         super().reduce() | ||||
|  | ||||
|     @overrides(BaseMessage) | ||||
|     def _construct( | ||||
|         self, message: Union[List[Dict[str, Any]], Iterable[MessageSegment]] | ||||
|     ) -> List[MessageSegment]: | ||||
|         if isinstance(message, str): | ||||
|             raise ValueError( | ||||
|                 "String operation is not supported in mirai adapter") | ||||
|         return [ | ||||
|             *map( | ||||
|                 lambda x: x | ||||
|                 if isinstance(x, MessageSegment) else MessageSegment(**x), | ||||
|                 message) | ||||
|         ] | ||||
|  | ||||
|     def export(self) -> List[Dict[str, Any]]: | ||||
|         """导出为可以被正常json序列化的数组""" | ||||
|         return [ | ||||
|             *map(lambda segment: segment.as_dict(), self.copy())  # type: ignore | ||||
|         ] | ||||
|  | ||||
|     def extract_first(self, *type: MessageType) -> Optional[MessageSegment]: | ||||
|         """ | ||||
|         :说明: | ||||
|  | ||||
|           弹出该消息链的第一个消息 | ||||
|          | ||||
|         :参数: | ||||
|  | ||||
|           * `*type: MessageType`: 指定的消息类型, 当指定后如类型不匹配不弹出 | ||||
|         """ | ||||
|         if not len(self): | ||||
|             return None | ||||
|         first: MessageSegment = self[0] | ||||
|         if (not type) or (first.type in type): | ||||
|             return self.pop(0) | ||||
|         return None | ||||
|  | ||||
|     def __repr__(self) -> str: | ||||
|         return f'<{self.__class__.__name__} {[*self.copy()]}>' | ||||
							
								
								
									
										178
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								packages/nonebot-adapter-mirai/nonebot/adapters/mirai/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| import re | ||||
| from functools import wraps | ||||
| from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional, TypeVar | ||||
|  | ||||
| import httpx | ||||
| from pydantic import Extra, ValidationError, validate_arguments | ||||
|  | ||||
| import nonebot.exception as exception | ||||
| from nonebot.log import logger | ||||
| from nonebot.message import handle_event | ||||
| from nonebot.utils import escape_tag, logger_wrapper | ||||
|  | ||||
| from .event import Event, GroupMessage, MessageEvent, MessageSource | ||||
| from .message import MessageType | ||||
|  | ||||
| if TYPE_CHECKING: | ||||
|     from .bot import Bot | ||||
|  | ||||
| _AsyncCallable = TypeVar("_AsyncCallable", bound=Callable[..., Coroutine]) | ||||
| _AnyCallable = TypeVar("_AnyCallable", bound=Callable) | ||||
|  | ||||
|  | ||||
| class Log: | ||||
|  | ||||
|     @staticmethod | ||||
|     def log(level: str, message: str, exception: Optional[Exception] = None): | ||||
|         logger = logger_wrapper('MIRAI') | ||||
|         message = '<e>' + escape_tag(message) + '</e>' | ||||
|         logger(level=level.upper(), message=message, exception=exception) | ||||
|  | ||||
|     @classmethod | ||||
|     def info(cls, message: Any): | ||||
|         cls.log('INFO', str(message)) | ||||
|  | ||||
|     @classmethod | ||||
|     def debug(cls, message: Any): | ||||
|         cls.log('DEBUG', str(message)) | ||||
|  | ||||
|     @classmethod | ||||
|     def warn(cls, message: Any): | ||||
|         cls.log('WARNING', str(message)) | ||||
|  | ||||
|     @classmethod | ||||
|     def error(cls, message: Any, exception: Optional[Exception] = None): | ||||
|         cls.log('ERROR', str(message), exception=exception) | ||||
|  | ||||
|  | ||||
| class ActionFailed(exception.ActionFailed): | ||||
|     """ | ||||
|     :说明: | ||||
|  | ||||
|       API 请求成功返回数据,但 API 操作失败。 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         super().__init__('mirai') | ||||
|         self.data = kwargs.copy() | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return self.__class__.__name__ + '(%s)' % ', '.join( | ||||
|             map(lambda m: '%s=%r' % m, self.data.items())) | ||||
|  | ||||
|  | ||||
| class InvalidArgument(exception.AdapterException): | ||||
|     """ | ||||
|     :说明: | ||||
|      | ||||
|       调用API的参数出错 | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, **kwargs): | ||||
|         super().__init__('mirai') | ||||
|  | ||||
|  | ||||
| def catch_network_error(function: _AsyncCallable) -> _AsyncCallable: | ||||
|     """ | ||||
|     :说明: | ||||
|  | ||||
|       捕捉函数抛出的httpx网络异常并释放 ``NetworkError`` 异常 | ||||
|  | ||||
|       处理返回数据, 在code不为0时释放 ``ActionFailed`` 异常 | ||||
|  | ||||
|     \:\:\: warning | ||||
|     此装饰器只支持使用了httpx的异步函数 | ||||
|     \:\:\: | ||||
|     """ | ||||
|  | ||||
|     @wraps(function) | ||||
|     async def wrapper(*args, **kwargs): | ||||
|         try: | ||||
|             data = await function(*args, **kwargs) | ||||
|         except httpx.HTTPError: | ||||
|             raise exception.NetworkError('mirai') | ||||
|         logger.opt(colors=True).debug('<b>Mirai API returned data:</b> ' | ||||
|                                       f'<y>{escape_tag(str(data))}</y>') | ||||
|         if isinstance(data, dict): | ||||
|             if data.get('code', 0) != 0: | ||||
|                 raise ActionFailed(**data) | ||||
|         return data | ||||
|  | ||||
|     return wrapper  # type: ignore | ||||
|  | ||||
|  | ||||
| def argument_validation(function: _AnyCallable) -> _AnyCallable: | ||||
|     """ | ||||
|     :说明: | ||||
|  | ||||
|       通过函数签名中的类型注解来对传入参数进行运行时校验 | ||||
|        | ||||
|       会在参数出错时释放 ``InvalidArgument`` 异常 | ||||
|     """ | ||||
|     function = validate_arguments(config={ | ||||
|         'arbitrary_types_allowed': True, | ||||
|         'extra': Extra.forbid | ||||
|     })(function) | ||||
|  | ||||
|     @wraps(function) | ||||
|     def wrapper(*args, **kwargs): | ||||
|         try: | ||||
|             return function(*args, **kwargs) | ||||
|         except ValidationError: | ||||
|             raise InvalidArgument | ||||
|  | ||||
|     return wrapper  # type: ignore | ||||
|  | ||||
|  | ||||
| def process_source(bot: "Bot", event: MessageEvent) -> MessageEvent: | ||||
|     source = event.message_chain.extract_first(MessageType.SOURCE) | ||||
|     if source is not None: | ||||
|         event.source = MessageSource.parse_obj(source.data) | ||||
|     return event | ||||
|  | ||||
|  | ||||
| def process_at(bot: "Bot", event: GroupMessage) -> GroupMessage: | ||||
|     at = event.message_chain.extract_first(MessageType.AT) | ||||
|     if at is not None: | ||||
|         if at.data['target'] == event.self_id: | ||||
|             event.to_me = True | ||||
|         else: | ||||
|             event.message_chain.insert(0, at) | ||||
|     return event | ||||
|  | ||||
|  | ||||
| def process_nick(bot: "Bot", event: GroupMessage) -> GroupMessage: | ||||
|     plain = event.message_chain.extract_first(MessageType.PLAIN) | ||||
|     if plain is not None: | ||||
|         text = str(plain) | ||||
|         nick_regex = '|'.join(filter(lambda x: x, bot.config.nickname)) | ||||
|         matched = re.search(rf"^({nick_regex})([\s,,]*|$)", text, re.IGNORECASE) | ||||
|         if matched is not None: | ||||
|             event.to_me = True | ||||
|             nickname = matched.group(1) | ||||
|             Log.info(f'User is calling me {nickname}') | ||||
|             plain.data['text'] = text[matched.end():] | ||||
|         event.message_chain.insert(0, plain) | ||||
|     return event | ||||
|  | ||||
|  | ||||
| def process_reply(bot: "Bot", event: GroupMessage) -> GroupMessage: | ||||
|     reply = event.message_chain.extract_first(MessageType.QUOTE) | ||||
|     if reply is not None: | ||||
|         if reply.data['senderId'] == event.self_id: | ||||
|             event.to_me = True | ||||
|         else: | ||||
|             event.message_chain.insert(0, reply) | ||||
|     return event | ||||
|  | ||||
|  | ||||
| async def process_event(bot: "Bot", event: Event) -> None: | ||||
|     if isinstance(event, MessageEvent): | ||||
|         event.message_chain.reduce() | ||||
|         Log.debug(event.message_chain) | ||||
|         event = process_source(bot, event) | ||||
|         if isinstance(event, GroupMessage): | ||||
|             event = process_nick(bot, event) | ||||
|             event = process_at(bot, event) | ||||
|             event = process_reply(bot, event) | ||||
|     await handle_event(bot, event) | ||||
		Reference in New Issue
	
	Block a user