├── .github └── workflows │ └── python-publish.yml ├── LICENSE ├── README.md ├── mirai ├── __init__.py ├── application.py ├── depend.py ├── entities │ ├── __init__.py │ ├── builtins.py │ ├── friend.py │ └── group.py ├── event │ ├── __init__.py │ ├── builtins.py │ ├── enums.py │ ├── external │ │ ├── __init__.py │ │ └── enums.py │ └── message │ │ ├── __init__.py │ │ ├── base.py │ │ ├── chain.py │ │ ├── components.py │ │ └── models.py ├── exceptions.py ├── face.py ├── file.py ├── image.py ├── logger.py ├── misc.py ├── network.py ├── protocol.py ├── utilles │ ├── __init__.py │ └── dependencies.py └── voice.py ├── setup.cfg └── setup.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | workflow_dispatch: 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | run: | 31 | python setup.py sdist bdist_wheel 32 | twine upload dist/* 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mirai-python-sdk 2 | 基于 kuriyama(Python SDK v3)的修改版本 3 | 4 | ### 这是什么? 5 | 以 OICQ(QQ) 协议驱动的高性能机器人开发框架 [Mirai](https://github.com/mamoe/mirai) 的 Python 接口, 通过其提供的 `HTTP API` 与无头客户端 `Mirai` 交互. 6 | 7 | ### 开始使用 8 | #### 从 Pypi 安装 9 | ``` bash 10 | pip install kuriyama-lxnet 11 | ``` 12 | 13 | #### 开始开发 14 | 15 | 由于 `python-mirai` 依赖于 `mirai` 提供的 `mirai-http-api` 插件, 所以你需要先运行一个 `mirai-core` 或是 `mirai-console` 实例以支撑你的应用运行. 16 | 17 | 仓库地址: https://github.com/Lxns-Network/mirai-python-sdk 18 | 19 | ### 依赖版本 20 | - mirai-core-all *v2.6.5*:https://github.com/mamoe/mirai 21 | - mirai-api-http *v1.11.0*:https://github.com/project-mirai/mirai-api-http 22 | ### 语音组件 23 | #### 第三方依赖 24 | ffmpeg 环境:https://ffmpeg.org/ 25 | #### 使用方法 26 | MessageChain:`Voice.fromFileSystem(Path, convert_type="silk")` 27 | ### 示例 28 | ```python 29 | from mirai import Mirai, Plain, MessageChain, Friend, Group, Member, Source, BotInvitedJoinGroupRequestEvent 30 | import asyncio 31 | 32 | app = Mirai( 33 | host = "127.0.0.1", 34 | port = "8880", 35 | authKey = "INITKEY", 36 | qq = "114514", 37 | websocket = True 38 | ) 39 | 40 | @app.receiver("FriendMessage") 41 | async def _(app: Mirai, friend: Friend, message: MessageChain): 42 | pass 43 | 44 | @app.receiver("GroupMessage") 45 | async def _(app: Mirai, group: Group, member: Member, message: MessageChain, source: Source): 46 | await app.sendGroupMessage(group, [ 47 | Plain(text="收到消息:" + message.toString()) 48 | ], quoteSource=source) 49 | return True 50 | 51 | @app.receiver("BotInvitedJoinGroupRequestEvent") 52 | async def _(app: Mirai, event: BotInvitedJoinGroupRequestEvent): 53 | await app.respondRequest(event, 1) # 自动同意入群邀请 54 | return True 55 | 56 | @app.receiver("AppInitEvent") 57 | async def _(app: Mirai): 58 | print("应用初始化完成,您可以在此直接获取到 app") 59 | 60 | if __name__ == "__main__": 61 | app.run() 62 | ``` 63 | 64 | ### 许可证 65 | 我们使用 [`GNU AGPLv3`](https://choosealicense.com/licenses/agpl-3.0/) 作为本项目的开源许可证, 而由于原项目 [`mirai`](https://github.com/mamoe/mirai) 同样使用了 `GNU AGPLv3` 作为开源许可证, 因此你在使用时需要遵守相应的规则. 66 | -------------------------------------------------------------------------------- /mirai/__init__.py: -------------------------------------------------------------------------------- 1 | import mirai.logger 2 | from mirai.misc import ( 3 | ImageType 4 | ) 5 | from mirai.face import QQFaces 6 | from mirai.exceptions import NetworkError, Cancelled 7 | from mirai.depend import Depend 8 | 9 | import mirai.event.message.base 10 | from mirai.event.message.components import ( 11 | At, 12 | Plain, 13 | Source, 14 | AtAll, 15 | Face, 16 | Quote, 17 | Json as JsonMessage, 18 | Xml as XmlMessage, 19 | App as LightApp, 20 | Image, 21 | FlashImage, 22 | Voice, 23 | Forward, 24 | File 25 | ) 26 | from mirai.event.message.chain import ( 27 | MessageChain 28 | ) 29 | from mirai.event.message.models import ( 30 | GroupMessage, 31 | FriendMessage, 32 | BotMessage 33 | ) 34 | 35 | from mirai.event import ( 36 | InternalEvent, 37 | ExternalEvent 38 | ) 39 | 40 | from mirai.event.external import ( 41 | AppInitEvent, 42 | 43 | BotOnlineEvent, 44 | BotOfflineEventActive, 45 | BotOfflineEventForce, 46 | BotOfflineEventDropped, 47 | BotReloginEvent, 48 | BotGroupPermissionChangeEvent, 49 | BotMuteEvent, 50 | BotUnmuteEvent, 51 | BotJoinGroupEvent, 52 | BotLeaveEventActive, 53 | 54 | GroupRecallEvent, 55 | FriendRecallEvent, 56 | 57 | GroupNameChangeEvent, 58 | GroupEntranceAnnouncementChangeEvent, 59 | GroupMuteAllEvent, 60 | 61 | # 群设置被修改事件 62 | GroupAllowAnonymousChatEvent, 63 | GroupAllowConfessTalkEvent, 64 | GroupAllowMemberInviteEvent, 65 | 66 | # 群事件(被 Bot 监听到的, 为"被动事件", 其中 Bot 身份为第三方.) 67 | MemberJoinEvent, 68 | MemberLeaveEventKick, 69 | MemberLeaveEventQuit, 70 | MemberCardChangeEvent, 71 | MemberSpecialTitleChangeEvent, 72 | MemberPermissionChangeEvent, 73 | MemberMuteEvent, 74 | MemberUnmuteEvent, 75 | 76 | BotInvitedJoinGroupRequestEvent, 77 | NewFriendRequestEvent, 78 | MemberJoinRequestEvent, 79 | 80 | NudgeEvent 81 | ) 82 | from mirai.event.enums import ( 83 | BotInvitedJoinGroupRequestResponseOperate as BotInvitedJoinGroupRequestResp, # 新增 84 | NewFriendRequestResponseOperate as NewFriendRequestResp, 85 | MemberJoinRequestResponseOperate as MemberJoinRequestResp 86 | ) 87 | 88 | from mirai.entities.friend import ( 89 | Friend 90 | ) 91 | from mirai.entities.group import ( 92 | Group, 93 | Member, 94 | MemberChangeableSetting, 95 | Permission, 96 | GroupSetting 97 | ) 98 | 99 | import mirai.network 100 | import mirai.protocol 101 | 102 | from mirai.application import Mirai 103 | from mirai.event.builtins import ( 104 | UnexpectedException 105 | ) 106 | from mirai.event.external.enums import ExternalEvents -------------------------------------------------------------------------------- /mirai/application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import inspect 4 | import traceback 5 | from contextlib import AsyncExitStack 6 | from functools import lru_cache 7 | from functools import partial 8 | from typing import ( 9 | Any, Awaitable, Callable, Dict, List, NamedTuple, Optional) 10 | from urllib import parse 11 | 12 | import aiohttp 13 | import pydantic 14 | from async_lru import alru_cache 15 | from mirai import exceptions 16 | from mirai.depend import Depend 17 | from mirai.entities.builtins import ExecutorProtocol 18 | from mirai.entities.friend import Friend 19 | from mirai.entities.group import Group, Member 20 | from mirai.event import ExternalEvent, InternalEvent 21 | from mirai.event.message import MessageChain, components 22 | from mirai.event.message.models import ( 23 | FriendMessage, GroupMessage, TempMessage, 24 | MessageItemType, MessageTypes 25 | ) 26 | from mirai.logger import ( 27 | Event as EventLogger, 28 | Session as SessionLogger, 29 | Network as NetworkLogger 30 | ) 31 | from mirai.misc import argument_signature, raiser, TRACEBACKED 32 | from mirai.protocol import MiraiProtocol 33 | 34 | 35 | class Mirai(MiraiProtocol): 36 | event: Dict[ 37 | str, List[Callable[[Any], Awaitable]] 38 | ] = {} 39 | subroutines: List[Callable] = [] 40 | lifecycle: Dict[str, List[Callable]] = { 41 | "start": [], 42 | "end": [], 43 | "around": [] 44 | } 45 | useWebsocket = False 46 | listening_exceptions: List[Exception] = [] 47 | 48 | extensite_config: Dict 49 | global_dependencies: List[Depend] 50 | global_middlewares: List 51 | 52 | def __init__(self, 53 | url: Optional[str] = None, 54 | 55 | host: Optional[str] = None, 56 | port: Optional[int] = None, 57 | authKey: Optional[str] = None, 58 | qq: Optional[int] = None, 59 | 60 | websocket: bool = False, 61 | extensite_config: dict = None, 62 | global_dependencies: List[Depend] = None, 63 | global_middlewares: List = None 64 | ): 65 | self.extensite_config = extensite_config or {} 66 | self.global_dependencies = global_dependencies or [] 67 | self.global_middlewares = global_middlewares or [] 68 | self.useWebsocket = websocket 69 | 70 | if url: 71 | urlinfo = parse.urlparse(url) 72 | if urlinfo: 73 | query_info = parse.parse_qs(urlinfo.query) 74 | if all([ 75 | urlinfo.scheme == "mirai", 76 | urlinfo.path in ["/", "/ws"], 77 | 78 | "authKey" in query_info and query_info["authKey"], 79 | "qq" in query_info and query_info["qq"] 80 | ]): 81 | if urlinfo.path == "/ws": 82 | self.useWebsocket = True 83 | else: 84 | self.useWebsocket = websocket 85 | 86 | authKey = query_info["authKey"][0] 87 | 88 | self.baseurl = f"http://{urlinfo.netloc}" 89 | self.auth_key = authKey 90 | self.qq = int(query_info["qq"][0]) 91 | else: 92 | raise ValueError("invaild url: wrong format") 93 | else: 94 | raise ValueError("invaild url") 95 | else: 96 | if all([host, port, authKey, qq]): 97 | self.baseurl = f"http://{host}:{port}" 98 | self.auth_key = authKey 99 | self.qq = int(qq) 100 | else: 101 | raise ValueError("invaild arguments") 102 | 103 | async def enable_session(self): 104 | auth_response = await self.auth() 105 | if all([ 106 | "code" in auth_response and auth_response['code'] == 0, 107 | "session" in auth_response and auth_response['session'] 108 | ]): 109 | if "msg" in auth_response and auth_response['msg']: 110 | self.session_key = auth_response['msg'] 111 | else: 112 | self.session_key = auth_response['session'] 113 | 114 | await self.verify() 115 | else: 116 | if "code" in auth_response and auth_response['code'] == 1: 117 | raise ValueError("invaild authKey") 118 | else: 119 | raise ValueError('invaild args: unknown response') 120 | 121 | self.enabled = True 122 | return self 123 | 124 | def receiver(self, 125 | event_name, 126 | dependencies: List[Depend] = None, 127 | use_middlewares: List[Callable] = None 128 | ): 129 | def receiver_warpper(func: Callable): 130 | if not inspect.iscoroutinefunction(func): 131 | raise TypeError("event body must be a coroutine function.") 132 | 133 | self.event.setdefault(event_name, []) 134 | self.event[event_name].append(ExecutorProtocol( 135 | callable=func, 136 | dependencies=(dependencies or []) + self.global_dependencies, 137 | middlewares=(use_middlewares or []) + self.global_middlewares 138 | )) 139 | return func 140 | 141 | return receiver_warpper 142 | 143 | async def message_polling(self, count=10): 144 | while True: 145 | await asyncio.sleep(0.5) 146 | 147 | try: 148 | result = \ 149 | await super().fetchMessage(count) 150 | except pydantic.ValidationError: 151 | continue 152 | last_length = len(result) 153 | latest_result = [] 154 | while True: 155 | if last_length == count: 156 | latest_result = await super().fetchMessage(count) 157 | last_length = len(latest_result) 158 | result += latest_result 159 | continue 160 | break 161 | 162 | for message_index in range(len(result)): 163 | item = result[message_index] 164 | await self.queue.put( 165 | InternalEvent( 166 | name=self.getEventCurrentName(type(item)), 167 | body=item 168 | ) 169 | ) 170 | 171 | async def ws_message(self): 172 | async with aiohttp.ClientSession() as session: 173 | async with session.ws_connect( 174 | f"{self.baseurl}/message?sessionKey={self.session_key}" 175 | ) as ws_connection: 176 | while True: 177 | try: 178 | received_data = await ws_connection.receive_json() 179 | except TypeError: 180 | continue 181 | if received_data: 182 | NetworkLogger.debug("received", received_data) 183 | try: 184 | received_data['messageChain'] = MessageChain.parse_obj(received_data['messageChain']) 185 | received_data = MessageTypes[received_data['type']].parse_obj(received_data) 186 | except pydantic.ValidationError: 187 | SessionLogger.error(f"parse failed: {received_data}") 188 | traceback.print_exc() 189 | else: 190 | await self.queue.put(InternalEvent( 191 | name=self.getEventCurrentName(type(received_data)), 192 | body=received_data 193 | )) 194 | 195 | async def ws_event(self): 196 | from mirai.event.external.enums import ExternalEvents 197 | async with aiohttp.ClientSession() as session: 198 | async with session.ws_connect( 199 | f"{self.baseurl}/event?sessionKey={self.session_key}" 200 | ) as ws_connection: 201 | while True: 202 | try: 203 | received_data = await ws_connection.receive_json() 204 | except TypeError: 205 | continue 206 | if received_data: 207 | try: 208 | if hasattr(ExternalEvents, received_data['type']): 209 | received_data = \ 210 | ExternalEvents[received_data['type']] \ 211 | .value \ 212 | .parse_obj(received_data) 213 | else: 214 | raise exceptions.UnknownEvent( 215 | f"a unknown event has been received, it's '{received_data['type']}'") 216 | except pydantic.ValidationError: 217 | SessionLogger.error(f"parse failed: {received_data}") 218 | traceback.print_exc() 219 | else: 220 | await self.queue.put(InternalEvent( 221 | name=self.getEventCurrentName(type(received_data)), 222 | body=received_data 223 | )) 224 | 225 | async def event_runner(self): 226 | while True: 227 | try: 228 | event_context: NamedTuple[InternalEvent] = await asyncio.wait_for(self.queue.get(), 3) 229 | except asyncio.TimeoutError: 230 | continue 231 | 232 | if event_context.name in self.registeredEventNames: 233 | EventLogger.info(f"handling a event: {event_context.name}") 234 | for event_body in list(self.event.values()) \ 235 | [self.registeredEventNames.index(event_context.name)]: 236 | if event_body: 237 | running_loop = asyncio.get_running_loop() 238 | running_loop.create_task(self.executor(event_body, event_context)) 239 | 240 | @staticmethod 241 | def sort_middlewares(iterator): 242 | return { 243 | "async": [ 244 | i for i in iterator if all([ 245 | hasattr(i, "__aenter__"), 246 | hasattr(i, "__aexit__") 247 | ]) 248 | ], 249 | "normal": [ 250 | i for i in iterator if all([ 251 | hasattr(i, "__enter__"), 252 | hasattr(i, "__exit__") 253 | ]) 254 | ] 255 | } 256 | 257 | async def put_exception(self, event_context, exception): 258 | from mirai.event.builtins import UnexpectedException 259 | if event_context.name != "UnexpectedException": 260 | if exception.__class__ in self.listening_exceptions: 261 | EventLogger.error( 262 | f"threw a exception by {event_context.name}, Exception: {exception.__class__.__name__}, and it has been catched.") 263 | else: 264 | EventLogger.error( 265 | f"threw a exception by {event_context.name}, Exception: {exception.__class__.__name__}, and it hasn't been catched!") 266 | traceback.print_exc() 267 | await self.queue.put(InternalEvent( 268 | name="UnexpectedException", 269 | body=UnexpectedException( 270 | error=exception, 271 | event=event_context, 272 | application=self 273 | ) 274 | )) 275 | else: 276 | EventLogger.critical( 277 | f"threw a exception in a exception handler by {event_context.name}, Exception: {exception.__class__.__name__}.") 278 | 279 | async def executor_with_middlewares(self, 280 | callable, raw_middlewares, 281 | event_context, 282 | lru_cache_sets=None 283 | ): 284 | middlewares = self.sort_middlewares(raw_middlewares) 285 | try: 286 | async with AsyncExitStack() as stack: 287 | for async_middleware in middlewares['async']: 288 | await stack.enter_async_context(async_middleware) 289 | for normal_middleware in middlewares['normal']: 290 | stack.enter_context(normal_middleware) 291 | 292 | result = await self.executor( 293 | ExecutorProtocol( 294 | callable=callable, 295 | dependencies=self.global_dependencies, 296 | middlewares=[] 297 | ), 298 | event_context, 299 | lru_cache_sets=lru_cache_sets 300 | ) 301 | if result is TRACEBACKED: 302 | return TRACEBACKED 303 | except exceptions.Cancelled: 304 | return TRACEBACKED 305 | except (NameError, TypeError): 306 | EventLogger.error( 307 | f"threw a exception by {event_context.name}, it's about Annotations Checker, please report to developer.") 308 | traceback.print_exc() 309 | except Exception as exception: 310 | if type(exception) not in self.listening_exceptions: 311 | EventLogger.error( 312 | f"threw a exception by {event_context.name} in a depend, and it's {exception}, body has been cancelled.") 313 | raise 314 | else: 315 | await self.put_exception( 316 | event_context, 317 | exception 318 | ) 319 | return TRACEBACKED 320 | 321 | async def executor(self, 322 | executor_protocol: ExecutorProtocol, 323 | event_context, 324 | extra_parameter={}, 325 | lru_cache_sets=None 326 | ): 327 | lru_cache_sets = lru_cache_sets or {} 328 | executor_protocol: ExecutorProtocol 329 | for depend in executor_protocol.dependencies: 330 | if not inspect.isclass(depend.func): 331 | depend_func = depend.func 332 | elif hasattr(depend.func, "__call__"): 333 | depend_func = depend.func.__call__ 334 | else: 335 | raise TypeError("must be callable.") 336 | 337 | if depend_func in lru_cache_sets and depend.cache: 338 | depend_func = lru_cache_sets[depend_func] 339 | else: 340 | if depend.cache: 341 | original = depend_func 342 | if inspect.iscoroutinefunction(depend_func): 343 | depend_func = alru_cache(depend_func) 344 | else: 345 | depend_func = lru_cache(depend_func) 346 | lru_cache_sets[original] = depend_func 347 | 348 | result = await self.executor_with_middlewares( 349 | depend_func, depend.middlewares, event_context, lru_cache_sets 350 | ) 351 | if result is TRACEBACKED: 352 | return TRACEBACKED 353 | 354 | ParamSignatures = argument_signature(executor_protocol.callable) 355 | PlaceAnnotation = self.get_annotations_mapping() 356 | CallParams = {} 357 | for name, annotation, default in ParamSignatures: 358 | if default: 359 | if isinstance(default, Depend): 360 | if not inspect.isclass(default.func): 361 | depend_func = default.func 362 | elif hasattr(default.func, "__call__"): 363 | depend_func = default.func.__call__ 364 | else: 365 | raise TypeError("must be callable.") 366 | 367 | if depend_func in lru_cache_sets and default.cache: 368 | depend_func = lru_cache_sets[depend_func] 369 | else: 370 | if default.cache: 371 | original = depend_func 372 | if inspect.iscoroutinefunction(depend_func): 373 | depend_func = alru_cache(depend_func) 374 | else: 375 | depend_func = lru_cache(depend_func) 376 | lru_cache_sets[original] = depend_func 377 | 378 | CallParams[name] = await self.executor_with_middlewares( 379 | depend_func, default.middlewares, event_context, lru_cache_sets 380 | ) 381 | continue 382 | else: 383 | raise RuntimeError("checked a unexpected default value.") 384 | else: 385 | if annotation in PlaceAnnotation: 386 | CallParams[name] = PlaceAnnotation[annotation](event_context) 387 | continue 388 | else: 389 | if name not in extra_parameter: 390 | raise RuntimeError(f"checked a unexpected annotation: {annotation}") 391 | 392 | try: 393 | async with AsyncExitStack() as stack: 394 | sorted_middlewares = self.sort_middlewares(executor_protocol.middlewares) 395 | for async_middleware in sorted_middlewares['async']: 396 | await stack.enter_async_context(async_middleware) 397 | for normal_middleware in sorted_middlewares['normal']: 398 | stack.enter_context(normal_middleware) 399 | 400 | return await self.run_func(executor_protocol.callable, **CallParams, **extra_parameter) 401 | except exceptions.Cancelled: 402 | return TRACEBACKED 403 | except Exception as e: 404 | await self.put_exception(event_context, e) 405 | return TRACEBACKED 406 | 407 | def getRestraintMapping(self): 408 | from mirai.event.external.enums import ExternalEvents 409 | return { 410 | Mirai: lambda k: True, 411 | GroupMessage: lambda k: k.__class__.__name__ == "GroupMessage", 412 | FriendMessage: lambda k: k.__class__.__name__ == "FriendMessage", 413 | TempMessage: lambda k: k.__class__.__name__ == "TempMessage", 414 | MessageChain: lambda k: k.__class__.__name__ in MessageTypes, 415 | components.Source: lambda k: k.__class__.__name__ in MessageTypes, 416 | Group: lambda k: k.__class__.__name__ in ["GroupMessage", "TempMessage"], 417 | Friend: lambda k: k.__class__.__name__ == "FriendMessage", 418 | Member: lambda k: k.__class__.__name__ in ["GroupMessage", "TempMessage"], 419 | "Sender": lambda k: k.__class__.__name__ in MessageTypes, 420 | "Type": lambda k: k.__class__.__name__, 421 | **({ 422 | event_class.value: partial( 423 | (lambda a, b: a == b.__class__.__name__), 424 | copy.copy(event_name) 425 | ) 426 | for event_name, event_class in \ 427 | ExternalEvents.__members__.items() 428 | }) 429 | } 430 | 431 | def checkEventBodyAnnotations(self): 432 | event_bodys: Dict[Callable, List[str]] = {} 433 | for event_name in self.event: 434 | event_body_list = self.event[event_name] 435 | for i in event_body_list: 436 | event_bodys.setdefault(i.callable, []) 437 | event_bodys[i.callable].append(event_name) 438 | 439 | restraint_mapping = self.getRestraintMapping() 440 | for func in event_bodys: 441 | self.checkFuncAnnotations(func) 442 | 443 | def getFuncRegisteredEvents(self, callable_target: Callable): 444 | result = [] 445 | for event_name in self.event: 446 | if callable_target in [i.callable for i in self.event[event_name]]: 447 | result.append(event_name) 448 | return result 449 | 450 | def checkFuncAnnotations(self, callable_target: Callable): 451 | restraint_mapping = self.getRestraintMapping() 452 | registered_events = self.getFuncRegisteredEvents(callable_target) 453 | for name, annotation, default in argument_signature(callable_target): 454 | if not default: 455 | if not registered_events: 456 | raise ValueError(f"error in annotations checker: {callable_target} is invaild.") 457 | for event_name in registered_events: 458 | try: 459 | if not restraint_mapping[annotation](type(event_name, (object,), {})()): 460 | raise ValueError( 461 | f"error in annotations checker: {callable_target}.[{name}:{annotation}]: {event_name}") 462 | except KeyError: 463 | raise ValueError( 464 | f"error in annotations checker: {callable_target}.[{name}:{annotation}] is invaild.") 465 | except ValueError: 466 | raise 467 | 468 | def checkDependencies(self, depend_target: Depend): 469 | self.checkEventBodyAnnotations() 470 | for name, annotation, default in argument_signature(depend_target.func): 471 | if type(default) == Depend: 472 | self.checkDependencies(default) 473 | 474 | def checkEventDependencies(self): 475 | for event_name, event_bodys in self.event.items(): 476 | for i in event_bodys: 477 | for depend in i.dependencies: 478 | if type(depend) != Depend: 479 | raise TypeError(f"error in dependencies checker: {i['func']}: {event_name}") 480 | else: 481 | self.checkDependencies(depend) 482 | 483 | def exception_handler(self, exception_class=None): 484 | from .event.builtins import UnexpectedException 485 | def receiver_warpper(func: Callable): 486 | event_name = "UnexpectedException" 487 | 488 | if not inspect.iscoroutinefunction(func): 489 | raise TypeError("event body must be a coroutine function.") 490 | 491 | async def func_warpper_inout(context: UnexpectedException, *args, **kwargs): 492 | if type(context.error) == exception_class: 493 | return await func(context, *args, **kwargs) 494 | 495 | func_warpper_inout.__annotations__.update(func.__annotations__) 496 | 497 | self.event.setdefault(event_name, []) 498 | self.event[event_name].append(ExecutorProtocol( 499 | callable=func_warpper_inout, 500 | dependencies=self.global_dependencies, 501 | middlewares=self.global_middlewares 502 | )) 503 | 504 | if exception_class: 505 | if exception_class not in self.listening_exceptions: 506 | self.listening_exceptions.append(exception_class) 507 | return func 508 | 509 | return receiver_warpper 510 | 511 | def gen_event_anno(self): 512 | from mirai.event.external.enums import ExternalEvents 513 | 514 | def warpper(name, event_context): 515 | if name != event_context.name: 516 | raise ValueError("cannot look up a non-listened event.") 517 | return event_context.body 518 | 519 | return { 520 | event_class.value: partial(warpper, copy.copy(event_name)) \ 521 | for event_name, event_class in ExternalEvents.__members__.items() 522 | } 523 | 524 | def get_annotations_mapping(self): 525 | return { 526 | Mirai: lambda k: self, 527 | GroupMessage: lambda k: k.body \ 528 | if self.getEventCurrentName(k.body) == "GroupMessage" else \ 529 | raiser(ValueError("you cannot setting a unbind argument.")), 530 | FriendMessage: lambda k: k.body \ 531 | if self.getEventCurrentName(k.body) == "FriendMessage" else \ 532 | raiser(ValueError("you cannot setting a unbind argument.")), 533 | TempMessage: lambda k: k.body \ 534 | if self.getEventCurrentName(k.body) == "TempMessage" else \ 535 | raiser(ValueError("you cannot setting a unbind argument.")), 536 | MessageChain: lambda k: k.body.messageChain \ 537 | if self.getEventCurrentName(k.body) in MessageTypes else \ 538 | raiser(ValueError("MessageChain is not enable in this type of event.")), 539 | components.Source: lambda k: k.body.messageChain.getSource() \ 540 | if self.getEventCurrentName(k.body) in MessageTypes else \ 541 | raiser(TypeError("Source is not enable in this type of event.")), 542 | Group: lambda k: k.body.sender.group \ 543 | if self.getEventCurrentName(k.body) in ["GroupMessage", "TempMessage"] else \ 544 | raiser(ValueError("Group is not enable in this type of event.")), 545 | Friend: lambda k: k.body.sender \ 546 | if self.getEventCurrentName(k.body) == "FriendMessage" else \ 547 | raiser(ValueError("Friend is not enable in this type of event.")), 548 | Member: lambda k: k.body.sender \ 549 | if self.getEventCurrentName(k.body) in ["GroupMessage", "TempMessage"] else \ 550 | raiser(ValueError("Group is not enable in this type of event.")), 551 | "Sender": lambda k: k.body.sender \ 552 | if self.getEventCurrentName(k.body) in MessageTypes else \ 553 | raiser(ValueError("Sender is not enable in this type of event.")), 554 | "Type": lambda k: self.getEventCurrentName(k.body), 555 | **self.gen_event_anno() 556 | } 557 | 558 | def getEventCurrentName(self, event_value): 559 | from .event.builtins import UnexpectedException 560 | from mirai.event.external.enums import ExternalEvents 561 | if inspect.isclass(event_value) and issubclass(event_value, ExternalEvent): # subclass 562 | return event_value.__name__ 563 | elif isinstance(event_value, ( # normal class 564 | UnexpectedException, 565 | GroupMessage, 566 | FriendMessage, 567 | TempMessage 568 | )): 569 | return event_value.__class__.__name__ 570 | elif event_value in [ # message 571 | GroupMessage, 572 | FriendMessage, 573 | TempMessage 574 | ]: 575 | return event_value.__name__ 576 | elif isinstance(event_value, ( # enum 577 | MessageItemType, 578 | ExternalEvents 579 | )): 580 | return event_value.name 581 | else: 582 | return event_value 583 | 584 | @property 585 | def registeredEventNames(self): 586 | return [self.getEventCurrentName(i) for i in self.event.keys()] 587 | 588 | def subroutine(self, func: Callable[["Mirai"], Any]): 589 | from .event.builtins import UnexpectedException 590 | async def warpper(app: "Mirai"): 591 | try: 592 | return await func(app) 593 | except Exception as e: 594 | await self.queue.put(InternalEvent( 595 | name="UnexpectedException", 596 | body=UnexpectedException( 597 | error=e, 598 | event=None, 599 | application=self 600 | ) 601 | )) 602 | 603 | self.subroutines.append(warpper) 604 | return func 605 | 606 | async def checkWebsocket(self, force=False): 607 | return (await self.getConfig())["enableWebsocket"] 608 | 609 | @staticmethod 610 | async def run_func(func, *args, **kwargs): 611 | if inspect.iscoroutinefunction(func): 612 | await func(*args, **kwargs) 613 | else: 614 | func(*args, **kwargs) 615 | 616 | def onStage(self, stage_name): 617 | def warpper(func): 618 | self.lifecycle.setdefault(stage_name, []) 619 | self.lifecycle[stage_name].append(func) 620 | return func 621 | 622 | return warpper 623 | 624 | def include_others(self, *args: List["Mirai"]): 625 | for other in args: 626 | for event_name, items in other.event.items(): 627 | if event_name in self.event: 628 | self.event[event_name] += items 629 | else: 630 | self.event[event_name] = items.copy() 631 | self.subroutines = other.subroutines 632 | for life_name, items in other.lifecycle: 633 | self.lifecycle.setdefault(life_name, []) 634 | self.lifecycle[life_name] += items 635 | self.listening_exceptions += other.listening_exceptions 636 | 637 | def run(self, loop=None, no_polling=False, no_forever=False): 638 | self.checkEventBodyAnnotations() 639 | self.checkEventDependencies() 640 | 641 | loop = loop or asyncio.get_event_loop() 642 | self.queue = asyncio.Queue(loop=loop) 643 | exit_signal = False 644 | loop.run_until_complete(self.enable_session()) 645 | if not no_polling: 646 | # check ws status 647 | if self.useWebsocket: 648 | SessionLogger.info("event receive method: websocket") 649 | else: 650 | SessionLogger.info("event receive method: http polling") 651 | 652 | result = loop.run_until_complete(self.checkWebsocket()) 653 | if not result: # we can use http, not ws. 654 | # should use http, but we can change it. 655 | if self.useWebsocket: 656 | SessionLogger.warning("catched wrong config: enableWebsocket=false, we will modify it.") 657 | loop.run_until_complete(self.setConfig(enableWebsocket=True)) 658 | loop.create_task(self.ws_event()) 659 | loop.create_task(self.ws_message()) 660 | else: 661 | loop.create_task(self.message_polling()) 662 | else: # we can use websocket, it's fine 663 | if self.useWebsocket: 664 | loop.create_task(self.ws_event()) 665 | loop.create_task(self.ws_message()) 666 | else: 667 | SessionLogger.warning("catched wrong config: enableWebsocket=true, we will modify it.") 668 | loop.run_until_complete(self.setConfig(enableWebsocket=False)) 669 | loop.create_task(self.message_polling()) 670 | loop.create_task(self.event_runner()) 671 | loop.run_until_complete(self.queue.put(InternalEvent( 672 | name=self.getEventCurrentName("AppInitEvent"), 673 | body={} 674 | ))) 675 | 676 | if not no_forever: 677 | for i in self.subroutines: 678 | loop.create_task(i(self)) 679 | 680 | try: 681 | for start_callable in self.lifecycle['start']: 682 | loop.run_until_complete(self.run_func(start_callable, self)) 683 | 684 | for around_callable in self.lifecycle['around']: 685 | loop.run_until_complete(self.run_func(around_callable, self)) 686 | 687 | loop.run_forever() 688 | except KeyboardInterrupt: 689 | SessionLogger.info("catched Ctrl-C, exiting..") 690 | except Exception as e: 691 | traceback.print_exc() 692 | finally: 693 | for around_callable in self.lifecycle['around']: 694 | loop.run_until_complete(self.run_func(around_callable, self)) 695 | 696 | for end_callable in self.lifecycle['end']: 697 | loop.run_until_complete(self.run_func(end_callable, self)) 698 | 699 | loop.run_until_complete(self.release()) 700 | -------------------------------------------------------------------------------- /mirai/depend.py: -------------------------------------------------------------------------------- 1 | class Depend: 2 | def __init__(self, func, middlewares=[], cache=True): 3 | self.func = func 4 | self.middlewares = middlewares 5 | self.cache = cache -------------------------------------------------------------------------------- /mirai/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/mirai-python-sdk/7908061b7b0e65bb79dd1b65dd12e57c7d68dd3d/mirai/entities/__init__.py -------------------------------------------------------------------------------- /mirai/entities/builtins.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | import typing as T 3 | from mirai.depend import Depend 4 | 5 | class ExecutorProtocol(BaseModel): 6 | callable: T.Callable 7 | dependencies: T.List[Depend] 8 | middlewares: T.List 9 | 10 | class Config: 11 | arbitrary_types_allowed = True -------------------------------------------------------------------------------- /mirai/entities/friend.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from pydantic import BaseModel 3 | from typing import Optional 4 | 5 | class Friend(BaseModel): 6 | id: int 7 | nickname: Optional[str] 8 | remark: Optional[str] 9 | 10 | def __repr__(self): 11 | return f"" 12 | 13 | def getAvatarUrl(self) -> str: 14 | return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140' 15 | 16 | -------------------------------------------------------------------------------- /mirai/entities/group.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from dataclasses import dataclass 3 | from pydantic import BaseModel 4 | 5 | import typing as T 6 | 7 | class Permission(Enum): 8 | Member = "MEMBER" 9 | Administrator = "ADMINISTRATOR" 10 | Owner = "OWNER" 11 | 12 | class Group(BaseModel): 13 | id: int 14 | name: str 15 | permission: Permission 16 | 17 | def __repr__(self): 18 | return f"" 19 | 20 | def getAvatarUrl(self) -> str: 21 | return f'https://p.qlogo.cn/gh/{self.id}/{self.id}/' 22 | 23 | class Member(BaseModel): 24 | id: int 25 | memberName: str 26 | permission: Permission 27 | group: Group 28 | 29 | def __repr__(self): 30 | return f"" 31 | 32 | def getAvatarUrl(self) -> str: 33 | return f'http://q4.qlogo.cn/g?b=qq&nk={self.id}&s=140' 34 | 35 | class MemberChangeableSetting(BaseModel): 36 | name: str 37 | specialTitle: str 38 | 39 | def modify(self, **kwargs): 40 | for i in ("name", "kwargs"): 41 | if i in kwargs: 42 | setattr(self, i, kwargs[i]) 43 | return self 44 | 45 | class GroupSetting(BaseModel): 46 | name: str 47 | announcement: str 48 | confessTalk: bool 49 | allowMemberInvite: bool 50 | autoApprove: bool 51 | anonymousChat: bool 52 | 53 | def modify(self, **kwargs): 54 | for i in ("name", 55 | "announcement", 56 | "confessTalk", 57 | "allowMemberInvite", 58 | "autoApprove", 59 | "anonymousChat" 60 | ): 61 | if i in kwargs: 62 | setattr(self, i, kwargs[i]) 63 | return self 64 | 65 | class GroupFile(BaseModel): 66 | name: str 67 | path: str 68 | id: str 69 | length: int 70 | downloadTimes: int 71 | uploaderId: int 72 | uploadTime: int 73 | lastModifyTime: int 74 | downloadUrl: str 75 | sha1: str 76 | md5: str 77 | 78 | class GroupFileShort(BaseModel): 79 | name: str 80 | id: str 81 | path: str 82 | isFile: bool 83 | 84 | class GroupFileList(BaseModel): 85 | __root__: T.List[GroupFileShort] = [] 86 | -------------------------------------------------------------------------------- /mirai/event/__init__.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import Any 3 | from enum import Enum 4 | from pydantic import BaseModel 5 | 6 | # 内部事件实现. 7 | InternalEvent = namedtuple("Event", ("name", "body")) 8 | 9 | from .enums import ExternalEventTypes 10 | class ExternalEvent(BaseModel): 11 | type: ExternalEventTypes -------------------------------------------------------------------------------- /mirai/event/builtins.py: -------------------------------------------------------------------------------- 1 | from . import InternalEvent 2 | from pydantic import BaseModel 3 | from mirai import Mirai 4 | 5 | class UnexpectedException(BaseModel): 6 | error: Exception 7 | event: InternalEvent 8 | application: Mirai 9 | 10 | class Config: 11 | arbitrary_types_allowed = True -------------------------------------------------------------------------------- /mirai/event/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class ExternalEventTypes(Enum): 4 | AppInitEvent = "AppInitEvent" 5 | 6 | BotOnlineEvent = "BotOnlineEvent" 7 | BotOfflineEventActive = "BotOfflineEventActive" 8 | BotOfflineEventForce = "BotOfflineEventForce" 9 | BotOfflineEventDropped = "BotOfflineEventDropped" 10 | BotReloginEvent = "BotReloginEvent" 11 | BotGroupPermissionChangeEvent = "BotGroupPermissionChangeEvent" 12 | BotMuteEvent = "BotMuteEvent" 13 | BotUnmuteEvent = "BotUnmuteEvent" 14 | BotJoinGroupEvent = "BotJoinGroupEvent" 15 | BotLeaveEventActive = "BotLeaveEventActive" 16 | 17 | GroupRecallEvent = "GroupRecallEvent" 18 | FriendRecallEvent = "FriendRecallEvent" 19 | 20 | GroupNameChangeEvent = "GroupNameChangeEvent" 21 | GroupEntranceAnnouncementChangeEvent = "GroupEntranceAnnouncementChangeEvent" 22 | GroupMuteAllEvent = "GroupMuteAllEvent" 23 | 24 | # 群设置被修改事件 25 | GroupAllowAnonymousChatEvent = "GroupAllowAnonymousChatEvent" # 群设置 是否允许匿名聊天 被修改 26 | GroupAllowConfessTalkEvent = "GroupAllowConfessTalkEvent" # 坦白说 27 | GroupAllowMemberInviteEvent = "GroupAllowMemberInviteEvent" # 邀请进群 28 | 29 | # 群事件(被 Bot 监听到的, 为"被动事件", 其中 Bot 身份为第三方.) 30 | MemberJoinEvent = "MemberJoinEvent" 31 | MemberLeaveEventKick = "MemberLeaveEventKick" 32 | MemberLeaveEventQuit = "MemberLeaveEventQuit" 33 | MemberCardChangeEvent = "MemberCardChangeEvent" 34 | MemberSpecialTitleChangeEvent = "MemberSpecialTitleChangeEvent" 35 | MemberPermissionChangeEvent = "MemberPermissionChangeEvent" 36 | MemberMuteEvent = "MemberMuteEvent" 37 | MemberUnmuteEvent = "MemberUnmuteEvent" 38 | 39 | BotInvitedJoinGroupRequestEvent = "BotInvitedJoinGroupRequestEvent" 40 | NewFriendRequestEvent = "NewFriendRequestEvent" 41 | MemberJoinRequestEvent = "MemberJoinRequestEvent" 42 | 43 | NudgeEvent = "NudgeEvent" 44 | 45 | # python-mirai 自己提供的事件 46 | UnexceptedException = "UnexceptedException" 47 | 48 | class BotInvitedJoinGroupRequestResponseOperate(Enum): # 新增 49 | accept = 0 50 | refuse = 1 51 | 52 | class NewFriendRequestResponseOperate(Enum): 53 | accept = 0 54 | refuse = 1 55 | refuse_and_blacklist = 2 56 | 57 | class MemberJoinRequestResponseOperate(Enum): 58 | accept = 0 59 | refuse = 1 60 | ignore = 2 61 | refuse_and_blacklist = 3 62 | ignore_and_blacklist = 4 -------------------------------------------------------------------------------- /mirai/event/external/__init__.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from mirai.event import ExternalEvent 3 | from mirai.event.enums import ExternalEventTypes as EventType 4 | from mirai.entities.group import Permission, Group, Member 5 | from mirai.entities.friend import Friend 6 | import typing as T 7 | from datetime import datetime 8 | 9 | class AppInitEvent(ExternalEvent): 10 | type: EventType = EventType.AppInitEvent 11 | 12 | class BotOnlineEvent(ExternalEvent): 13 | type: EventType = EventType.BotOnlineEvent 14 | qq: int 15 | 16 | class BotOfflineEventActive(ExternalEvent): 17 | type: EventType = EventType.BotOfflineEventActive 18 | qq: int 19 | 20 | class BotOfflineEventForce(ExternalEvent): 21 | type: EventType = EventType.BotOfflineEventForce 22 | qq: int 23 | 24 | class BotOfflineEventDropped(ExternalEvent): 25 | type: EventType = EventType.BotOfflineEventDropped 26 | qq: int 27 | 28 | class BotReloginEvent(ExternalEvent): 29 | type: EventType = EventType.BotReloginEvent 30 | qq: int 31 | 32 | class BotGroupPermissionChangeEvent(ExternalEvent): 33 | type: EventType = EventType.BotGroupPermissionChangeEvent 34 | origin: Permission 35 | current: Permission 36 | group: Group 37 | 38 | class BotMuteEvent(ExternalEvent): 39 | type: EventType = EventType.BotMuteEvent 40 | durationSeconds: int 41 | operator: T.Optional[Member] 42 | 43 | class BotUnmuteEvent(ExternalEvent): 44 | type: EventType = EventType.BotUnmuteEvent 45 | operator: T.Optional[Member] 46 | 47 | class BotJoinGroupEvent(ExternalEvent): 48 | type: EventType = EventType.BotJoinGroupEvent 49 | group: Group 50 | 51 | class BotLeaveEventActive(ExternalEvent): 52 | type: EventType = EventType.BotLeaveEventActive 53 | group: Group 54 | 55 | class GroupRecallEvent(ExternalEvent): 56 | type: EventType = EventType.GroupRecallEvent 57 | authorId: int 58 | messageId: int 59 | time: datetime 60 | group: Group 61 | operator: T.Optional[Member] 62 | 63 | class FriendRecallEvent(ExternalEvent): 64 | type: EventType = EventType.FriendRecallEvent 65 | authorId: int 66 | messageId: int 67 | time: int 68 | operator: int 69 | 70 | class GroupNameChangeEvent(ExternalEvent): 71 | type: EventType = EventType.GroupNameChangeEvent 72 | origin: str 73 | current: str 74 | group: Group 75 | isByBot: bool 76 | 77 | class GroupEntranceAnnouncementChangeEvent(ExternalEvent): 78 | type: EventType = EventType.GroupEntranceAnnouncementChangeEvent 79 | origin: str 80 | current: str 81 | group: Group 82 | operator: T.Optional[Member] 83 | 84 | class GroupMuteAllEvent(ExternalEvent): 85 | type: EventType = EventType.GroupMuteAllEvent 86 | origin: bool 87 | current: bool 88 | group: Group 89 | operator: T.Optional[Member] 90 | 91 | class GroupAllowAnonymousChatEvent(ExternalEvent): 92 | type: EventType = EventType.GroupAllowAnonymousChatEvent 93 | origin: bool 94 | current: bool 95 | group: Group 96 | operator: T.Optional[Member] 97 | 98 | class GroupAllowConfessTalkEvent(ExternalEvent): 99 | type: EventType = EventType.GroupAllowAnonymousChatEvent 100 | origin: bool 101 | current: bool 102 | group: Group 103 | isByBot: bool 104 | 105 | class GroupAllowMemberInviteEvent(ExternalEvent): 106 | type: EventType = EventType.GroupAllowMemberInviteEvent 107 | origin: bool 108 | current: bool 109 | group: Group 110 | operator: T.Optional[Member] 111 | 112 | class MemberJoinEvent(ExternalEvent): 113 | type: EventType = EventType.MemberJoinEvent 114 | member: Member 115 | 116 | class MemberLeaveEventKick(ExternalEvent): 117 | type: EventType = EventType.MemberLeaveEventKick 118 | member: Member 119 | operator: T.Optional[Member] 120 | 121 | class MemberLeaveEventQuit(ExternalEvent): 122 | type: EventType = EventType.MemberLeaveEventQuit 123 | member: Member 124 | 125 | class MemberCardChangeEvent(ExternalEvent): 126 | type: EventType = EventType.MemberCardChangeEvent 127 | origin: str 128 | current: str 129 | member: Member 130 | operator: T.Optional[Member] 131 | 132 | class MemberSpecialTitleChangeEvent(ExternalEvent): 133 | type: EventType = EventType.MemberSpecialTitleChangeEvent 134 | origin: str 135 | current: str 136 | member: Member 137 | 138 | class MemberPermissionChangeEvent(ExternalEvent): 139 | type: EventType = EventType.MemberPermissionChangeEvent 140 | origin: str 141 | current: str 142 | member: Member 143 | 144 | class MemberMuteEvent(ExternalEvent): 145 | type: EventType = EventType.MemberMuteEvent 146 | durationSeconds: int 147 | member: Member 148 | operator: T.Optional[Member] 149 | 150 | class MemberUnmuteEvent(ExternalEvent): 151 | type: EventType = EventType.MemberUnmuteEvent 152 | member: Member 153 | operator: T.Optional[Member] 154 | 155 | class BotInvitedJoinGroupRequestEvent(ExternalEvent): # 新增 156 | type: EventType = EventType.BotInvitedJoinGroupRequestEvent 157 | requestId: int = Field(..., alias="eventId") 158 | supplicant: int = Field(..., alias="fromId") # 即请求方 QQ 159 | groupName: str = Field(..., alias="groupName") 160 | sourceGroup: T.Optional[int] = Field(..., alias="groupId") 161 | nickname: str = Field(..., alias="nick") 162 | 163 | class NewFriendRequestEvent(ExternalEvent): 164 | type: EventType = EventType.NewFriendRequestEvent 165 | requestId: int = Field(..., alias="eventId") 166 | supplicant: int = Field(..., alias="fromId") # 即请求方 QQ 167 | sourceGroup: T.Optional[int] = Field(..., alias="groupId") 168 | nickname: str = Field(..., alias="nick") 169 | 170 | class MemberJoinRequestEvent(ExternalEvent): 171 | type: EventType = EventType.MemberJoinRequestEvent 172 | requestId: int = Field(..., alias="eventId") 173 | supplicant: int = Field(..., alias="fromId") # 即请求方 QQ 174 | groupId: T.Optional[int] = Field(..., alias="groupId") 175 | groupName: str = Field(..., alias="groupName") 176 | nickname: str = Field(..., alias="nick") 177 | 178 | class NudgeEventSubject(BaseModel): 179 | id: int 180 | kind: str 181 | 182 | class NudgeEvent(ExternalEvent): 183 | type: EventType = EventType.NudgeEvent 184 | fromId: int 185 | target: int 186 | subject: NudgeEventSubject 187 | action: str 188 | suffix: str -------------------------------------------------------------------------------- /mirai/event/external/enums.py: -------------------------------------------------------------------------------- 1 | from . import * 2 | from ..builtins import UnexpectedException 3 | from enum import Enum 4 | 5 | class ExternalEvents(Enum): 6 | AppInitEvent = AppInitEvent 7 | 8 | BotOnlineEvent = BotOnlineEvent 9 | BotOfflineEventActive = BotOfflineEventActive 10 | BotOfflineEventForce = BotOfflineEventForce 11 | BotOfflineEventDropped = BotOfflineEventDropped 12 | BotReloginEvent = BotReloginEvent 13 | BotGroupPermissionChangeEvent = BotGroupPermissionChangeEvent 14 | BotMuteEvent = BotMuteEvent 15 | BotUnmuteEvent = BotUnmuteEvent 16 | BotJoinGroupEvent = BotJoinGroupEvent 17 | BotLeaveEventActive = BotLeaveEventActive 18 | 19 | GroupRecallEvent = GroupRecallEvent 20 | FriendRecallEvent = FriendRecallEvent 21 | 22 | GroupNameChangeEvent = GroupNameChangeEvent 23 | GroupEntranceAnnouncementChangeEvent = GroupEntranceAnnouncementChangeEvent 24 | GroupMuteAllEvent = GroupMuteAllEvent 25 | 26 | # 群设置被修改事件 27 | GroupAllowAnonymousChatEvent = GroupAllowAnonymousChatEvent # 群设置 是否允许匿名聊天 被修改 28 | GroupAllowConfessTalkEvent = GroupAllowConfessTalkEvent # 坦白说 29 | GroupAllowMemberInviteEvent = GroupAllowMemberInviteEvent # 邀请进群 30 | 31 | # 群事件(被 Bot 监听到的, 为被动事件, 其中 Bot 身份为第三方.) 32 | MemberJoinEvent = MemberJoinEvent 33 | MemberLeaveEventKick = MemberLeaveEventKick 34 | MemberLeaveEventQuit = MemberLeaveEventQuit 35 | MemberCardChangeEvent = MemberCardChangeEvent 36 | MemberSpecialTitleChangeEvent = MemberSpecialTitleChangeEvent 37 | MemberPermissionChangeEvent = MemberPermissionChangeEvent 38 | MemberMuteEvent = MemberMuteEvent 39 | MemberUnmuteEvent = MemberUnmuteEvent 40 | 41 | BotInvitedJoinGroupRequestEvent = BotInvitedJoinGroupRequestEvent 42 | NewFriendRequestEvent = NewFriendRequestEvent 43 | MemberJoinRequestEvent = MemberJoinRequestEvent 44 | 45 | NudgeEvent = NudgeEvent 46 | 47 | UnexpectedException = UnexpectedException -------------------------------------------------------------------------------- /mirai/event/message/__init__.py: -------------------------------------------------------------------------------- 1 | from .components import * 2 | from .chain import MessageChain 3 | from .models import MessageTypes -------------------------------------------------------------------------------- /mirai/event/message/base.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pydantic import BaseModel 3 | 4 | __all__ = [ 5 | "MessageComponentTypes", 6 | "BaseMessageComponent" 7 | ] 8 | 9 | class MessageComponentTypes(Enum): 10 | Source = "Source" 11 | Plain = "Plain" 12 | Face = "Face" 13 | At = "At" 14 | AtAll = "AtAll" 15 | Image = "Image" 16 | Quote = "Quote" 17 | Xml = "Xml" 18 | Json = "Json" 19 | App = "App" 20 | Poke = "Poke" 21 | FlashImage = "FlashImage" 22 | Unknown = "Unknown" 23 | Voice = "Voice" 24 | Forward = "Forward" 25 | File = "File" 26 | 27 | class BaseMessageComponent(BaseModel): 28 | type: MessageComponentTypes 29 | 30 | def toString(self): 31 | return self.__repr__() -------------------------------------------------------------------------------- /mirai/event/message/chain.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from pydantic import BaseModel 3 | 4 | from .base import BaseMessageComponent 5 | from mirai.misc import raiser, printer, if_error_print_arg 6 | from .components import Source 7 | from mirai.logger import Protocol 8 | 9 | class MessageChain(BaseModel): 10 | __root__: T.List[BaseMessageComponent] = [] 11 | 12 | def __add__(self, value): 13 | if isinstance(value, BaseMessageComponent): 14 | self.__root__.append(value) 15 | return self 16 | elif isinstance(value, MessageChain): 17 | self.__root__ += value.__root__ 18 | return self 19 | 20 | def toString(self) -> str: 21 | return "".join([i.toString() for i in self.__root__]) 22 | 23 | @classmethod 24 | def parse_obj(cls, obj): 25 | from .components import MessageComponents 26 | result = [] 27 | for i in obj: 28 | if not isinstance(i, dict): 29 | raise TypeError("invaild value") 30 | try: 31 | if i['type'] != "Xml": 32 | result.append(MessageComponents[i['type']].parse_obj(i)) 33 | except: 34 | Protocol.error(f"error throwed by message serialization: {i['type']}, it's {i}") 35 | raise 36 | return cls(__root__=result) 37 | 38 | def __iter__(self): 39 | yield from self.__root__ 40 | 41 | def __getitem__(self, index): 42 | return self.__root__[index] 43 | 44 | def hasComponent(self, component_class) -> bool: 45 | for i in self: 46 | if type(i) == component_class: 47 | return True 48 | else: 49 | return False 50 | 51 | def __len__(self) -> int: 52 | return len(self.__root__) 53 | 54 | def getFirstComponent(self, component_class) -> T.Optional[BaseMessageComponent]: 55 | for i in self: 56 | if type(i) == component_class: 57 | return i 58 | 59 | def getAllofComponent(self, component_class) -> T.List[BaseMessageComponent]: 60 | return [i for i in self if type(i) == component_class] 61 | 62 | def getSource(self) -> Source: 63 | return self.getFirstComponent(Source) 64 | 65 | __contains__ = hasComponent 66 | __getitem__ = getAllofComponent -------------------------------------------------------------------------------- /mirai/event/message/components.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import shutil 3 | import datetime 4 | import tempfile 5 | import subprocess 6 | import typing as T 7 | from mirai.event.message.base import BaseMessageComponent, MessageComponentTypes 8 | from pydantic import Field, validator, HttpUrl 9 | from io import BytesIO 10 | from pathlib import Path 11 | from mirai.image import ( 12 | LocalImage, 13 | IOImage, 14 | Base64Image, BytesImage, 15 | ) 16 | from mirai.voice import LocalVoice 17 | from mirai.file import LocalFile 18 | from mirai.logger import Protocol as ProtocolLogger 19 | from aiohttp import ClientSession 20 | 21 | 22 | __all__ = [ 23 | "Plain", 24 | "Source", 25 | "At", 26 | "AtAll", 27 | "Face", 28 | "Image", 29 | "Unknown", 30 | "Quote", 31 | "FlashImage", 32 | "Forward", 33 | "File" 34 | ] 35 | 36 | 37 | class Plain(BaseMessageComponent): 38 | type: MessageComponentTypes = "Plain" 39 | text: str 40 | 41 | def __init__(self, text, **_): 42 | if len(text) > 128: 43 | ProtocolLogger.warn(f"mirai does not support for long string: now its length is {len(text)}") 44 | super().__init__(text=text, type="Plain") 45 | 46 | def toString(self): 47 | return self.text 48 | 49 | class Source(BaseMessageComponent): 50 | type: MessageComponentTypes = "Source" 51 | id: int 52 | time: datetime.datetime 53 | 54 | def toString(self): 55 | return "" 56 | 57 | from .chain import MessageChain 58 | 59 | class Quote(BaseMessageComponent): 60 | type: MessageComponentTypes = "Quote" 61 | id: T.Optional[int] 62 | groupId: T.Optional[int] 63 | senderId: T.Optional[int] 64 | targetId: T.Optional[int] 65 | origin: MessageChain 66 | 67 | @validator("origin", always=True, pre=True) 68 | @classmethod 69 | def origin_formater(cls, v): 70 | return MessageChain.parse_obj(v) 71 | 72 | def __init__(self, id: int, groupId: int, senderId: int, origin: int, **_): 73 | super().__init__( 74 | id=id, 75 | groupId=groupId, 76 | senderId=senderId, 77 | origin=origin 78 | ) 79 | 80 | def toString(self): 81 | return "" 82 | 83 | class At(BaseMessageComponent): 84 | type: MessageComponentTypes = "At" 85 | target: int 86 | display: T.Optional[str] = None 87 | 88 | def __init__(self, target, display=None, **_): 89 | super().__init__(target=target, display=display) 90 | 91 | def toString(self): 92 | return f"[At::target={self.target}]" 93 | 94 | class AtAll(BaseMessageComponent): 95 | type: MessageComponentTypes = "AtAll" 96 | 97 | def __init__(self, **_): 98 | super().__init__() 99 | 100 | def toString(self): 101 | return f"[AtAll]" 102 | 103 | class Face(BaseMessageComponent): 104 | type: MessageComponentTypes = "Face" 105 | faceId: int 106 | name: T.Optional[str] 107 | 108 | def __init__(self, faceId, name=None, **_): 109 | super().__init__(faceId=faceId, name=name) 110 | 111 | def toString(self): 112 | return f"[Face::name={self.name}]" 113 | 114 | class Image(BaseMessageComponent): 115 | type: MessageComponentTypes = "Image" 116 | imageId: T.Optional[str] 117 | url: T.Optional[HttpUrl] = None 118 | 119 | @validator("imageId", always=True, pre=True) 120 | @classmethod 121 | def imageId_formater(cls, v): 122 | length = len(v) 123 | if length == 44: 124 | # group 125 | return v[1:-7] 126 | elif length == 37: 127 | return v[1:] 128 | else: 129 | return v 130 | 131 | def __init__(self, imageId, url=None, **_): 132 | super().__init__(imageId=imageId, url=url) 133 | 134 | def toString(self): 135 | return f"[Image::{self.imageId}]" 136 | 137 | def asGroupImage(self) -> str: 138 | return self.imageId.upper() 139 | 140 | def asFriendImage(self) -> str: 141 | return self.imageId.upper() 142 | 143 | def asFlashImage(self) -> "FlashImage": 144 | return FlashImage(self.imageId, self.url) 145 | 146 | @staticmethod 147 | async def fromRemote(url, **extra) -> BytesImage: 148 | async with ClientSession() as session: 149 | async with session.get(url, **extra) as response: 150 | return BytesImage(await response.read()) 151 | 152 | @staticmethod 153 | def fromFileSystem(path: T.Union[Path, str]) -> LocalImage: 154 | return LocalImage(path) 155 | 156 | async def toBytes(self, chunk_size=256) -> BytesIO: 157 | async with ClientSession() as session: 158 | async with session.get(self.url) as response: 159 | result = BytesIO() 160 | while True: 161 | chunk = await response.content.read(chunk_size) 162 | if not chunk: 163 | break 164 | result.write(chunk) 165 | return result 166 | 167 | @staticmethod 168 | def fromBytes(data) -> BytesImage: 169 | return BytesImage(data) 170 | 171 | @staticmethod 172 | def fromBase64(base64_str) -> Base64Image: 173 | return Base64Image(base64_str) 174 | 175 | @staticmethod 176 | def fromIO(IO) -> IOImage: 177 | return IOImage(IO) 178 | 179 | class Xml(BaseMessageComponent): 180 | type: MessageComponentTypes = "Xml" 181 | XML: str 182 | 183 | def __init__(self, xml): 184 | super().__init__(XML=xml) 185 | 186 | class Json(BaseMessageComponent): 187 | type: MessageComponentTypes = "Json" 188 | Json: dict = Field(..., alias="json") 189 | 190 | def __init__(self, json: dict, **_): 191 | super().__init__(Json=json) 192 | 193 | class App(BaseMessageComponent): 194 | type: MessageComponentTypes = "App" 195 | content: str 196 | 197 | def __init__(self, content: str, **_): 198 | super().__init__(content=content) 199 | 200 | class Poke(BaseMessageComponent): 201 | type: MessageComponentTypes = "Poke" 202 | name: str 203 | 204 | def __init__(self, name: str, **_): 205 | super().__init__(name=name) 206 | 207 | class Unknown(BaseMessageComponent): 208 | type: MessageComponentTypes = "Unknown" 209 | text: str 210 | 211 | def toString(self): 212 | return "" 213 | 214 | class FlashImage(BaseMessageComponent): 215 | type: MessageComponentTypes = "FlashImage" 216 | imageId: T.Optional[str] 217 | url: T.Optional[HttpUrl] = None 218 | 219 | @validator("imageId", always=True, pre=True) 220 | @classmethod 221 | def imageId_formater(cls, v): 222 | length = len(v) 223 | if length == 44: 224 | # group 225 | return v[1:-7] 226 | elif length == 37: 227 | return v[1:] 228 | else: 229 | return v 230 | 231 | def __init__(self, imageId, url=None, **_): 232 | super().__init__(imageId=imageId, url=url) 233 | 234 | def toString(self): 235 | return f"[FlashImage::{self.imageId}]" 236 | 237 | def asGroupImage(self) -> str: 238 | return f"{{{self.imageId.upper()}}}.mirai" 239 | 240 | def asFriendImage(self) -> str: 241 | return f"/{self.imageId.lower()}" 242 | 243 | def asNormal(self) -> Image: 244 | return Image(self.imageId, self.url) 245 | 246 | @staticmethod 247 | def fromFileSystem(path: T.Union[Path, str]) -> LocalImage: 248 | return LocalImage(path, flash=True) 249 | 250 | async def toBytes(self, chunk_size=256) -> BytesIO: 251 | async with ClientSession() as session: 252 | async with session.get(self.url) as response: 253 | result = BytesIO() 254 | while True: 255 | chunk = await response.content.read(chunk_size) 256 | if not chunk: 257 | break 258 | result.write(chunk) 259 | return result 260 | 261 | @staticmethod 262 | def fromBytes(data) -> BytesImage: 263 | return BytesImage(data, flash=True) 264 | 265 | @staticmethod 266 | def fromBase64(base64_str) -> Base64Image: 267 | return Base64Image(base64_str, flash=True) 268 | 269 | @staticmethod 270 | def fromIO(IO) -> IOImage: 271 | return IOImage(IO, flash=True) 272 | 273 | 274 | class Voice(BaseMessageComponent): 275 | type: MessageComponentTypes = "Voice" 276 | voiceId: T.Optional[str] 277 | url: T.Optional[HttpUrl] = None 278 | 279 | def __init__(self, voiceId, url=None, **_): 280 | super().__init__(voiceId=voiceId, url=url) 281 | 282 | def toString(self): 283 | return f"[Voice::{self.voiceId}]" 284 | 285 | def asGroupVoice(self) -> str: 286 | return self.voiceId 287 | 288 | @staticmethod 289 | def fromFileSystem(path: T.Union[Path, str], convert_type="silk") -> LocalVoice: 290 | if not (path.endswith("amr") and path.endswith("silk")): 291 | if not shutil.which("ffmpeg"): 292 | raise FileNotFoundError("ffmpeg is not exists") 293 | temp_voiceId = uuid.uuid4() 294 | if convert_type == "amr": 295 | pc = subprocess.Popen(["ffmpeg", "-i", path, 296 | '-ar', '8000', '-ac', "1", '-ab', "12.2k", 297 | f"{tempfile.gettempdir()}/{temp_voiceId}.amr"]) 298 | if pc.wait() != 0: 299 | raise ProcessLookupError(pc.returncode) 300 | return LocalVoice(f"{tempfile.gettempdir()}/{temp_voiceId}.amr") 301 | elif convert_type == "silk": 302 | if not shutil.which("silk_v3_encoder"): 303 | raise FileNotFoundError("silk_v3_encoder is not exists") 304 | pc = subprocess.Popen(["ffmpeg", "-i", path, "-f", "s16le", "-ar", "24000", "-ac", "1", 305 | f"{tempfile.gettempdir()}/{temp_voiceId}.pcm"]) 306 | if pc.wait() != 0: 307 | raise ProcessLookupError(pc.returncode) 308 | pc = subprocess.Popen(["silk_v3_encoder", f"{tempfile.gettempdir()}/{temp_voiceId}.pcm", 309 | f"{tempfile.gettempdir()}/{temp_voiceId}.silk", "-rate", "24000", "-quiet", "-tencent"]) 310 | if pc.wait() != 0: 311 | raise ProcessLookupError(pc.returncode) 312 | return LocalVoice(f"{tempfile.gettempdir()}/{temp_voiceId}.silk") 313 | else: 314 | return LocalVoice(path) 315 | 316 | 317 | from .chain import MessageChain 318 | from pydantic import BaseModel 319 | class ForwardNodeMessage(BaseModel): 320 | senderId: int 321 | time: datetime.datetime 322 | senderName: str 323 | messageChain: MessageChain 324 | 325 | class Forward(BaseMessageComponent): 326 | type: MessageComponentTypes = "Forward" 327 | title: str 328 | brief: str 329 | source: str 330 | summary: str 331 | nodeList: T.List[ForwardNodeMessage] 332 | 333 | class File(BaseMessageComponent): 334 | type: MessageComponentTypes = "File" 335 | id: str 336 | internalId: T.Optional[int] 337 | name: T.Optional[str] 338 | size: T.Optional[int] 339 | 340 | def toString(self): 341 | return f"[File::{self.id}]" 342 | 343 | @staticmethod 344 | def fromFileSystem(path: T.Union[Path, str]) -> LocalFile: 345 | return LocalFile(path) 346 | 347 | MessageComponents = { 348 | "At": At, 349 | "AtAll": AtAll, 350 | "Face": Face, 351 | "Plain": Plain, 352 | "Image": Image, 353 | "Source": Source, 354 | "Quote": Quote, 355 | "Xml": Xml, 356 | "Json": Json, 357 | "App": App, 358 | "Poke": Poke, 359 | "Voice": Voice, 360 | "FlashImage": FlashImage, 361 | "Forward": Forward, 362 | "File": File, 363 | "Unknown": Unknown 364 | } 365 | -------------------------------------------------------------------------------- /mirai/event/message/models.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | from enum import Enum 3 | from .base import MessageComponentTypes 4 | from mirai.entities.friend import Friend 5 | from mirai.entities.group import Group, Member 6 | from pydantic import BaseModel 7 | from .chain import MessageChain 8 | 9 | class MessageItemType(Enum): 10 | FriendMessage = "FriendMessage" 11 | GroupMessage = "GroupMessage" 12 | TempMessage = "TempMessage" 13 | BotMessage = "BotMessage" 14 | 15 | class FriendMessage(BaseModel): 16 | type: MessageItemType = "FriendMessage" 17 | messageChain: T.Optional[MessageChain] 18 | sender: Friend 19 | 20 | def toString(self): 21 | if self.messageChain: 22 | return self.messageChain.toString() 23 | 24 | class GroupMessage(BaseModel): 25 | type: MessageItemType = "GroupMessage" 26 | messageChain: T.Optional[MessageChain] 27 | sender: Member 28 | 29 | def toString(self): 30 | if self.messageChain: 31 | return self.messageChain.toString() 32 | 33 | class TempMessage(BaseModel): 34 | type: MessageItemType = "TempMessage" 35 | messageChain: T.Optional[MessageChain] 36 | sender: Member 37 | 38 | def toString(self): 39 | if self.messageChain: 40 | return self.messageChain.toString() 41 | 42 | class BotMessage(BaseModel): 43 | type: MessageItemType = 'BotMessage' 44 | messageId: int 45 | 46 | 47 | MessageTypes = { 48 | "GroupMessage": GroupMessage, 49 | "FriendMessage": FriendMessage, 50 | "TempMessage": TempMessage 51 | } -------------------------------------------------------------------------------- /mirai/exceptions.py: -------------------------------------------------------------------------------- 1 | class NetworkError(Exception): 2 | pass 3 | 4 | class Cancelled(Exception): 5 | pass 6 | 7 | class UnknownTarget(Exception): 8 | pass 9 | 10 | class UnknownEvent(Exception): 11 | pass 12 | 13 | class LoginException(Exception): 14 | "你忘记在mirai-console登录就这种错误." 15 | pass 16 | 17 | class AuthenticateError(Exception): 18 | pass 19 | 20 | class InvaildSession(Exception): 21 | pass 22 | 23 | class ValidatedSession(Exception): 24 | "一般来讲 这种情况出现时需要刷新session" 25 | 26 | class UnknownReceiverTarget(Exception): 27 | pass 28 | 29 | class CallDevelopers(Exception): 30 | '还愣着干啥?开ISSUE啊!' 31 | pass 32 | 33 | class NonEnabledError(Exception): 34 | pass 35 | 36 | class BotMutedError(Exception): 37 | pass 38 | 39 | class TooLargeMessageError(Exception): 40 | pass -------------------------------------------------------------------------------- /mirai/face.py: -------------------------------------------------------------------------------- 1 | QQFaces = { 2 | "unknown": 0xff, 3 | "jingya": 0, 4 | "piezui": 1, 5 | "se": 2, 6 | "fadai": 3, 7 | "deyi": 4, 8 | "liulei": 5, 9 | "haixiu": 6, 10 | "bizui": 7, 11 | "shui": 8, 12 | "daku": 9, 13 | "ganga": 10, 14 | "fanu": 11, 15 | "tiaopi": 12, 16 | "ciya": 13, 17 | "weixiao": 14, 18 | "nanguo": 15, 19 | "ku": 16, 20 | "zhuakuang": 18, 21 | "tu": 19, 22 | "touxiao": 20, 23 | "keai": 21, 24 | "baiyan": 22, 25 | "aoman": 23, 26 | "ji_e": 24, 27 | "kun": 25, 28 | "jingkong": 26, 29 | "liuhan": 27, 30 | "hanxiao": 28, 31 | "dabing": 29, 32 | "fendou": 30, 33 | "zhouma": 31, 34 | "yiwen": 32, 35 | "yun": 34, 36 | "zhemo": 35, 37 | "shuai": 36, 38 | "kulou": 37, 39 | "qiaoda": 38, 40 | "zaijian": 39, 41 | "fadou": 41, 42 | "aiqing": 42, 43 | "tiaotiao": 43, 44 | "zhutou": 46, 45 | "yongbao": 49, 46 | "dan_gao": 53, 47 | "shandian": 54, 48 | "zhadan": 55, 49 | "dao": 56, 50 | "zuqiu": 57, 51 | "bianbian": 59, 52 | "kafei": 60, 53 | "fan": 61, 54 | "meigui": 63, 55 | "diaoxie": 64, 56 | "aixin": 66, 57 | "xinsui": 67, 58 | "liwu": 69, 59 | "taiyang": 74, 60 | "yueliang": 75, 61 | "qiang": 76, 62 | "ruo": 77, 63 | "woshou": 78, 64 | "shengli": 79, 65 | "feiwen": 85, 66 | "naohuo": 86, 67 | "xigua": 89, 68 | "lenghan": 96, 69 | "cahan": 97, 70 | "koubi": 98, 71 | "guzhang": 99, 72 | "qiudale": 100, 73 | "huaixiao": 101, 74 | "zuohengheng": 102, 75 | "youhengheng": 103, 76 | "haqian": 104, 77 | "bishi": 105, 78 | "weiqu": 106, 79 | "kuaikule": 107, 80 | "yinxian": 108, 81 | "qinqin": 109, 82 | "xia": 110, 83 | "kelian": 111, 84 | "caidao": 112, 85 | "pijiu": 113, 86 | "lanqiu": 114, 87 | "pingpang": 115, 88 | "shiai": 116, 89 | "piaochong": 117, 90 | "baoquan": 118, 91 | "gouyin": 119, 92 | "quantou": 120, 93 | "chajin": 121, 94 | "aini": 122, 95 | "bu": 123, 96 | "hao": 124, 97 | "zhuanquan": 125, 98 | "ketou": 126, 99 | "huitou": 127, 100 | "tiaosheng": 128, 101 | "huishou": 129, 102 | "jidong": 130, 103 | "jiewu": 131, 104 | "xianwen": 132, 105 | "zuotaiji": 133, 106 | "youtaiji": 134, 107 | "shuangxi": 136, 108 | "bianpao": 137, 109 | "denglong": 138, 110 | "facai": 139, 111 | "K_ge": 140, 112 | "gouwu": 141, 113 | "youjian": 142, 114 | "shuai_qi": 143, 115 | "hecai": 144, 116 | "qidao": 145, 117 | "baojin": 146, 118 | "bangbangtang": 147, 119 | "he_nai": 148, 120 | "xiamian": 149, 121 | "xiangjiao": 150, 122 | "feiji": 151, 123 | "kaiche": 152, 124 | "gaotiezuochetou": 153, 125 | "chexiang": 154, 126 | "gaotieyouchetou": 155, 127 | "duoyun": 156, 128 | "xiayu": 157, 129 | "chaopiao": 158, 130 | "xiongmao": 159, 131 | "dengpao": 160, 132 | "fengche": 161, 133 | "naozhong": 162, 134 | "dasan": 163, 135 | "caiqiu": 164, 136 | "zuanjie": 165, 137 | "shafa": 166, 138 | "zhijin": 167, 139 | "yao": 168, 140 | "shouqiang": 169, 141 | "qingwa": 170, 142 | "cha": 171, 143 | "zhayan": 172, 144 | "leibeng": 173, 145 | "wunai": 174, 146 | "maimeng": 175, 147 | "xiaojiujie": 176, 148 | "penxue": 177, 149 | "xieyanxiao": 178, 150 | "dog": 179, 151 | "jinxi": 180, 152 | "saorao": 181, 153 | "xiaoku": 182, 154 | "wozuimei": 183, 155 | "hexie": 184, 156 | "yangtuo": 185, 157 | "banli": 186, 158 | "youling": 187, 159 | "dan": 188, 160 | "mofang": 189, 161 | "juhua": 190, 162 | "feizao": 191, 163 | "hongbao": 192, 164 | "daxiao": 193, 165 | "bukaixin": 194, 166 | "zhenjing": 195, 167 | "ganga": 196, 168 | "lenmo": 197, 169 | "ye": 198, 170 | "haobang": 199, 171 | "baituo": 200, 172 | "dianzan": 201, 173 | "wuliao": 202, 174 | "tuolian": 203, 175 | "chi": 204, 176 | "songhua": 205, 177 | "haipa": 206, 178 | "huachi": 207, 179 | "xiaoyang": 208, 180 | "unknown2": 209,#暂时不知道 181 | "biaolei": 210, 182 | "wobukan": 211, 183 | "tuosai": 212, 184 | "unknown3": 213,#暂时不知道 185 | #214-247表情在电脑版qq9.2.3无法显示 186 | "bobo": 214, 187 | "hulian": 215, 188 | "paitou": 216, 189 | "cheyiche": 217, 190 | "tianyitian": 218, 191 | "cengyiceng": 219, 192 | "zhaozhatian": 220, 193 | "dingguagua": 221, 194 | "baobao": 222, 195 | "baoji": 223, 196 | "kaiqiang": 224, 197 | "liaoyiliao": 225, 198 | "paizhuo": 226, 199 | "paishou": 227, 200 | "gongxi": 228, 201 | "ganbei": 229, 202 | "chaofeng": 230, 203 | "hen": 231, 204 | "foxi": 232, 205 | "jingdai": 234, 206 | "chandou": 235, 207 | "jiaotou": 236, 208 | "toukan": 237, 209 | "shanlian": 238, 210 | "yuanliang": 239, 211 | "penlian": 240, 212 | "shengrikuaile": 241, 213 | "touzhuangji": 242, 214 | "shuaitou": 243, 215 | "renggou": 244, 216 | "jiayoubisheng": 245, 217 | "jiayoubaobao": 246, 218 | "kouzhaohuti": 247, 219 | #248-255未定义 220 | "jinya": 256, 221 | "piezei": 257, 222 | "se": 258, 223 | "fadai": 259, 224 | "deyi": 260, 225 | "liulei": 261, 226 | "haixiu": 262, 227 | "bizui": 263, 228 | "shui": 264, 229 | "daku": 265, 230 | "ganga": 266, 231 | "falu": 267, 232 | "tiaopi": 268, 233 | "ziya": 269, 234 | "weixiao": 270, 235 | "nanguo": 271, 236 | "ku": 272, 237 | "unknown4": 273,#暂时不知道,qq安卓版本8.2.8.4440不显示 238 | "zhuakuang": 274, 239 | "tu": 275, 240 | "touxiao": 276, 241 | "keai": 277, 242 | "baiyan": 278, 243 | "aoman": 279, 244 | "jie": 280, 245 | "kun": 281, 246 | "jingkong": 282, 247 | "liuhan": 283, 248 | "hanxiao": 284, 249 | "dabing": 285, 250 | "fendou": 286, 251 | "zhouma": 287, 252 | "yiwen": 288, 253 | "xu": 289, 254 | "yun": 290, 255 | "zhemo": 291, 256 | "shuai": 292, 257 | "kulou": 293, 258 | "qiaoda": 294, 259 | "zaijian": 295, 260 | "unknown5": 296,#安卓版本无显示 261 | "dadou": 297, 262 | "aiqing": 298, 263 | "tiaotiao": 299, 264 | "unknown6": 300,#暂时不知道 265 | "unknown7": 301,#暂时不知道 266 | "zhutou": 302, 267 | "mao": 303, 268 | "unknown8": 304,#暂时不知道 269 | "baobao": 305, 270 | "meiyuanfuhao": 306, 271 | "dengpao": 307,#安卓版本不显示 272 | "gaijiaobei": 308,#安卓版本不显示 273 | "dangao": 309, 274 | "shandian": 310, 275 | "zhadan": 311, 276 | "shiai": 321, 277 | "aixin": 322, 278 | "xinsui": 323, 279 | "zhuozi": 324,#安卓qq不显示 280 | "liwu": 325, 281 | } -------------------------------------------------------------------------------- /mirai/file.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class InternalFile(metaclass=ABCMeta): 5 | @abstractmethod 6 | def __init__(self): 7 | super().__init__() 8 | 9 | @abstractmethod 10 | def render(self) -> bytes: 11 | pass 12 | 13 | 14 | class LocalFile(InternalFile): 15 | path: Path 16 | 17 | def __init__(self, path): 18 | if isinstance(path, str): 19 | self.path = Path(path) 20 | elif isinstance(path, Path): 21 | self.path = path 22 | 23 | def render(self) -> bytes: 24 | return self.path.read_bytes() 25 | -------------------------------------------------------------------------------- /mirai/image.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from abc import ABCMeta, abstractmethod 3 | import base64 4 | 5 | class InternalImage(metaclass=ABCMeta): 6 | @abstractmethod 7 | def __init__(self): 8 | super().__init__() 9 | 10 | @abstractmethod 11 | def render(self) -> bytes: 12 | pass 13 | 14 | class LocalImage(InternalImage): 15 | path: Path 16 | flash: bool = False 17 | 18 | def __init__(self, path, flash: bool = False): 19 | if isinstance(path, str): 20 | self.path = Path(path) 21 | elif isinstance(path, Path): 22 | self.path = path 23 | self.flash = flash 24 | 25 | def render(self) -> bytes: 26 | return self.path.read_bytes() 27 | 28 | class IOImage(InternalImage): 29 | def __init__(self, IO, flash: bool = False): 30 | """make a object with 'read' method a image. 31 | 32 | IO - a object, must has a `read` method to return bytes. 33 | """ 34 | self.IO = IO 35 | self.flash = flash 36 | 37 | def render(self) -> bytes: 38 | return self.IO.read() 39 | 40 | class BytesImage(InternalImage): 41 | def __init__(self, data: bytes, flash: bool = False): 42 | self.data = data 43 | self.flash = flash 44 | 45 | def render(self) -> bytes: 46 | return self.data 47 | 48 | class Base64Image(InternalImage): 49 | def __init__(self, base64_str, flash: bool = False): 50 | self.base64_str = base64_str 51 | self.flash = flash 52 | 53 | def render(self) -> bytes: 54 | return base64.b64decode(self.base64_str) 55 | -------------------------------------------------------------------------------- /mirai/logger.py: -------------------------------------------------------------------------------- 1 | import logbook 2 | from logbook import Logger, StreamHandler 3 | from logbook import ( 4 | INFO, 5 | DEBUG 6 | ) 7 | import os 8 | import sys 9 | 10 | logbook.set_datetime_format('local') 11 | stream_handler = StreamHandler(sys.stdout, level=INFO if not os.environ.get("MIRAI_DEBUG") else DEBUG) 12 | stream_handler.format_string = '[{record.time:%Y-%m-%d %H:%M:%S}][Mirai] {record.level_name}: {record.channel}: {record.message}' 13 | stream_handler.push_application() 14 | 15 | Event = Logger('Event', level=INFO) 16 | Network = Logger("Network", level=DEBUG) 17 | Session = Logger("Session", level=INFO) 18 | Protocol = Logger("Protocol", level=INFO) 19 | -------------------------------------------------------------------------------- /mirai/misc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import os 4 | import random 5 | import re 6 | import traceback 7 | import typing as T 8 | from collections import namedtuple 9 | from enum import Enum 10 | from threading import Lock, Thread 11 | 12 | import aiohttp 13 | 14 | from . import exceptions 15 | from .logger import Protocol, Session 16 | 17 | 18 | def assertOperatorSuccess(result, raise_exception=False, return_as_is=False): 19 | if not result: 20 | if raise_exception: 21 | raise exceptions.InvaildSession("this method returned None, as sessionkey invaild...") 22 | else: 23 | return None 24 | if "code" in result: 25 | if not raise_exception: 26 | return result['code'] == 0 27 | else: 28 | if result['code'] != 0: 29 | print(result) 30 | raise { 31 | 1: exceptions.AuthenticateError, # 这种情况需要检查Authkey, 可能还是连错了. 32 | 2: exceptions.LoginException, # 嗯...你是不是忘记在mirai-console登录了?...算了 自动重连. 33 | 3: exceptions.InvaildSession, # 这种情况会自动重连. 34 | 4: exceptions.ValidatedSession, # 啊 smjb错误... 也会自动重连 35 | 5: exceptions.UnknownReceiverTarget, # 业务代码错误. 36 | 10: PermissionError, # 一般业务代码错误, 自行亦会 37 | 20: exceptions.BotMutedError, # 机器人被禁言 38 | 30: exceptions.TooLargeMessageError, 39 | 400: exceptions.CallDevelopers # 发生这个错误...你就给我提个ISSUE 40 | }[result['code']](f"""invaild stdin: { { 41 | 1: "wrong auth key", 42 | 2: "unknown qq account", 43 | 3: "invaild session key", 44 | 4: "disabled session key", 45 | 5: "unknown receiver target", 46 | 10: "permission denied", 47 | 20: "bot account has been muted", 48 | 30: "mirai backend cannot deal with so large message", 49 | 400: "wrong arguments" 50 | }[result['code']] }""") 51 | else: 52 | if return_as_is: 53 | return result 54 | else: 55 | return True 56 | if return_as_is: 57 | return result 58 | return False 59 | 60 | 61 | class ImageType(Enum): 62 | Friend = "friend" 63 | Group = "group" 64 | 65 | class VoiceType(Enum): 66 | Group = "group" 67 | 68 | class FileType(Enum): 69 | Group = "group" 70 | 71 | class NudgeType(Enum): 72 | Friend = "friend" 73 | Group = "group" 74 | 75 | 76 | Parameter = namedtuple("Parameter", ["name", "annotation", "default"]) 77 | 78 | TRACEBACKED = os.urandom(32) 79 | 80 | ImageRegex = { 81 | "group": r"(?<=\{)([0-9A-Z]{8})\-([0-9A-Z]{4})-([0-9A-Z]{4})-([0-9A-Z]{4})-([0-9A-Z]{12})(?=\}\..*?)", 82 | "friend": r"(?<=/)([0-9a-z]{8})\-([0-9a-z]{4})-([0-9a-z]{4})-([0-9a-z]{4})-([0-9a-z]{12})" 83 | } 84 | 85 | _windows_device_files = ( 86 | "CON", 87 | "AUX", 88 | "COM1", 89 | "COM2", 90 | "COM3", 91 | "COM4", 92 | "LPT1", 93 | "LPT2", 94 | "LPT3", 95 | "PRN", 96 | "NUL", 97 | ) 98 | _filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") 99 | 100 | 101 | def getMatchedString(regex_result): 102 | if regex_result: 103 | return regex_result.string[slice(*regex_result.span())] 104 | 105 | 106 | def findKey(mapping, value): 107 | try: 108 | index = list(mapping.values()).index(value) 109 | except ValueError: 110 | return "Unknown" 111 | return list(mapping.keys())[index] 112 | 113 | 114 | def raiser(error): 115 | raise error 116 | 117 | 118 | def printer(val): 119 | print(val) 120 | return val 121 | 122 | 123 | def justdo(call, val): 124 | print(call()) 125 | return val 126 | 127 | 128 | def randomNumberString(): 129 | return str(random.choice(range(100000000, 9999999999))) 130 | 131 | 132 | def randomRangedNumberString(length_range=(9,)): 133 | length = random.choice(length_range) 134 | return random.choice(range(10 ** (length - 1), int("9" * (length)))) 135 | 136 | 137 | def protocol_log(func): 138 | async def wrapper(*args, **kwargs): 139 | try: 140 | result = await func(*args, **kwargs) 141 | Protocol.info(f"protocol method {func.__name__} was called") 142 | return result 143 | except Exception as e: 144 | Protocol.error(f"protocol method {func.__name__} raised a error: {e.__class__.__name__}") 145 | raise e 146 | 147 | return wrapper 148 | 149 | 150 | def secure_filename(filename): 151 | if isinstance(filename, str): 152 | from unicodedata import normalize 153 | 154 | filename = normalize("NFKD", filename).encode("ascii", "ignore") 155 | filename = filename.decode("ascii") 156 | 157 | for sep in os.path.sep, os.path.altsep: 158 | if sep: 159 | filename = filename.replace(sep, " ") 160 | 161 | filename = \ 162 | str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip("._") 163 | 164 | if ( 165 | os.name == "nt" and filename and \ 166 | filename.split(".")[0].upper() in _windows_device_files 167 | ): 168 | filename = "_" + filename 169 | 170 | return filename 171 | 172 | 173 | def edge_case_handler(func): 174 | async def wrapper(self, *args, **kwargs): 175 | retry_times = 0 176 | while retry_times <= 5: 177 | retry_times += 1 178 | try: 179 | return await func(self, *args, **kwargs) 180 | except exceptions.AuthenticateError: 181 | Protocol.error("invaild authkey, please check your input.") 182 | exit(1) 183 | except exceptions.LoginException: 184 | Protocol.error("there is not such qq in headless client, we will try again after 5 seconds.") 185 | await asyncio.sleep(5) 186 | if func.__name__ != "verify": 187 | await self.verify() 188 | continue 189 | except (exceptions.InvaildSession, exceptions.ValidatedSession): 190 | Protocol.error("a unexpected session error, we will deal with it.") 191 | await self.enable_session() 192 | except aiohttp.client_exceptions.ClientError as e: 193 | Protocol.error(f"cannot connect to the headless client, will retry after 5 seconds.") 194 | await asyncio.sleep(5) 195 | continue 196 | except exceptions.CallDevelopers: 197 | Protocol.error("emmm, please contect me at github.") 198 | exit(-1) 199 | except: 200 | raise 201 | else: 202 | Protocol.error("we retried many times, but it doesn't send a success message to us...") 203 | 204 | wrapper.__name__ = func.__name__ 205 | return wrapper 206 | 207 | 208 | def throw_error_if_not_enable(func): 209 | def wrapper(self, *args, **kwargs): 210 | if not self.enabled: 211 | raise exceptions.NonEnabledError( 212 | f"you mustn't use any methods in MiraiProtocol...,\ 213 | if you want to access '{func.__name__}' before `app.run()`\ 214 | , use 'Subroutine'." 215 | ) 216 | return func(self, *args, **kwargs) 217 | 218 | wrapper.__name__ = func.__name__ 219 | wrapper.__annotations__ = func.__annotations__ 220 | return wrapper 221 | 222 | 223 | def if_error_print_arg(func): 224 | def wrapper(*args, **kwargs): 225 | try: 226 | return func(*args, **kwargs) 227 | except: 228 | print(args, kwargs) 229 | traceback.print_exc() 230 | 231 | return wrapper 232 | 233 | 234 | def argument_signature(callable_target) -> T.List[Parameter]: 235 | return [ 236 | Parameter( 237 | name=name, 238 | annotation=param.annotation if param.annotation != inspect._empty else None, 239 | default=param.default if param.default != inspect._empty else None 240 | ) 241 | for name, param in dict(inspect.signature(callable_target).parameters).items() 242 | ] 243 | -------------------------------------------------------------------------------- /mirai/network.py: -------------------------------------------------------------------------------- 1 | import json 2 | import mimetypes 3 | import typing as T 4 | from pathlib import Path 5 | from .logger import Network 6 | 7 | import aiohttp 8 | 9 | from mirai.exceptions import NetworkError 10 | 11 | class fetch: 12 | @staticmethod 13 | async def http_post(url, data_map): 14 | async with aiohttp.ClientSession() as session: 15 | async with session.post(url, json=data_map) as response: 16 | data = await response.text(encoding="utf-8") 17 | Network.debug(f"requested url={url}, by data_map={data_map}, and status={response.status}, data={data}") 18 | response.raise_for_status() 19 | try: 20 | return json.loads(data) 21 | except json.decoder.JSONDecodeError: 22 | Network.error(f"requested {url} with {data_map}, responsed {data}, decode failed...") 23 | 24 | @staticmethod 25 | async def http_get(url, params=None): 26 | async with aiohttp.ClientSession() as session: 27 | async with session.get(url, params=params) as response: 28 | response.raise_for_status() 29 | data = await response.text(encoding="utf-8") 30 | Network.debug(f"requested url={url}, by params={params}, and status={response.status}, data={data}") 31 | try: 32 | return json.loads(data) 33 | except json.decoder.JSONDecodeError: 34 | Network.error(f"requested {url} with {params}, responsed {data}, decode failed...") 35 | 36 | @staticmethod 37 | async def upload(url, filedata: bytes, addon_dict: dict): 38 | upload_data = aiohttp.FormData() 39 | upload_data.add_field("img", filedata) 40 | for item in addon_dict.items(): 41 | upload_data.add_fields(item) 42 | 43 | async with aiohttp.ClientSession() as session: 44 | async with session.post(url, data=upload_data) as response: 45 | response.raise_for_status() 46 | Network.debug(f"requested url={url}, and status={response.status}, addon_dict={addon_dict}") 47 | return await response.text("utf-8") 48 | 49 | @staticmethod 50 | async def upload_voice(url, filedata: bytes, addon_dict: dict): 51 | upload_data = aiohttp.FormData() 52 | upload_data.add_field("voice", filedata) 53 | for item in addon_dict.items(): 54 | upload_data.add_fields(item) 55 | 56 | async with aiohttp.ClientSession() as session: 57 | async with session.post(url, data=upload_data) as response: 58 | response.raise_for_status() 59 | Network.debug(f"requested url={url}, and status={response.status}, addon_dict={addon_dict}") 60 | return await response.text("utf-8") 61 | 62 | @staticmethod 63 | async def upload_file(url, filedata: bytes, addon_dict: dict): 64 | upload_data = aiohttp.FormData() 65 | upload_data.add_field("file", filedata) 66 | for item in addon_dict.items(): 67 | upload_data.add_fields(item) 68 | 69 | async with aiohttp.ClientSession() as session: 70 | async with session.post(url, data=upload_data) as response: 71 | response.raise_for_status() 72 | Network.debug(f"requested url={url}, and status={response.status}, addon_dict={addon_dict}") 73 | return await response.text("utf-8") -------------------------------------------------------------------------------- /mirai/protocol.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | import typing as T 4 | from datetime import timedelta 5 | 6 | import pydantic 7 | from mirai.entities.friend import Friend 8 | from mirai.entities.group import (Group, GroupSetting, Member, 9 | MemberChangeableSetting, GroupFile, GroupFileShort) 10 | from mirai.event import ExternalEvent 11 | from mirai.event import external as eem 12 | from mirai.event.enums import ( 13 | BotInvitedJoinGroupRequestResponseOperate, 14 | NewFriendRequestResponseOperate, 15 | MemberJoinRequestResponseOperate 16 | ) 17 | from mirai.event.message import components 18 | from mirai.event.message.base import BaseMessageComponent 19 | from mirai.event.message.chain import MessageChain 20 | from mirai.event.message.models import (BotMessage, FriendMessage, 21 | GroupMessage, MessageTypes) 22 | from mirai.image import InternalImage 23 | from mirai.voice import InternalVoice 24 | from mirai.file import InternalFile 25 | from mirai.logger import Protocol as ProtocolLogger 26 | from mirai.misc import (ImageType, VoiceType, FileType, NudgeType, assertOperatorSuccess, 27 | edge_case_handler, protocol_log, raiser, throw_error_if_not_enable) 28 | from mirai.network import fetch 29 | 30 | # 与 mirai 的 Command 部分将由 mirai.command 模块进行魔法支持, 31 | # 并尽量的兼容 mirai-console 的内部机制. 32 | 33 | 34 | class MiraiProtocol: 35 | qq: int 36 | baseurl: str 37 | session_key: str 38 | auth_key: str 39 | 40 | @protocol_log 41 | @edge_case_handler 42 | async def auth(self): 43 | return assertOperatorSuccess( 44 | await fetch.http_post(f"{self.baseurl}/auth", { 45 | "authKey": self.auth_key 46 | } 47 | ), raise_exception=True, return_as_is=True) 48 | 49 | @protocol_log 50 | @edge_case_handler 51 | async def verify(self): 52 | return assertOperatorSuccess( 53 | await fetch.http_post(f"{self.baseurl}/verify", { 54 | "sessionKey": self.session_key, 55 | "qq": self.qq 56 | } 57 | ), raise_exception=True, return_as_is=True) 58 | 59 | @throw_error_if_not_enable 60 | @protocol_log 61 | @edge_case_handler 62 | async def release(self): 63 | return assertOperatorSuccess( 64 | await fetch.http_post(f"{self.baseurl}/release", { 65 | "sessionKey": self.session_key, 66 | "qq": self.qq 67 | } 68 | ), raise_exception=True) 69 | 70 | @throw_error_if_not_enable 71 | @edge_case_handler 72 | async def getConfig(self) -> dict: 73 | return assertOperatorSuccess( 74 | await fetch.http_get(f"{self.baseurl}/config", { 75 | "sessionKey": self.session_key 76 | } 77 | ), raise_exception=True, return_as_is=True) 78 | 79 | @throw_error_if_not_enable 80 | @edge_case_handler 81 | async def setConfig(self, 82 | cacheSize=None, 83 | enableWebsocket=None 84 | ): 85 | return assertOperatorSuccess( 86 | await fetch.http_post(f"{self.baseurl}/config", { 87 | "sessionKey": self.session_key, 88 | **({ 89 | "cacheSize": cacheSize 90 | } if cacheSize else {}), 91 | **({ 92 | "enableWebsocket": enableWebsocket 93 | } if enableWebsocket else {}) 94 | } 95 | ), raise_exception=True, return_as_is=True) 96 | 97 | @throw_error_if_not_enable 98 | @protocol_log 99 | @edge_case_handler 100 | async def sendFriendMessage(self, 101 | friend: T.Union[Friend, int], 102 | message: T.Union[ 103 | MessageChain, 104 | BaseMessageComponent, 105 | T.List[T.Union[BaseMessageComponent, InternalImage]], 106 | str 107 | ] 108 | ) -> BotMessage: 109 | return BotMessage.parse_obj(assertOperatorSuccess( 110 | await fetch.http_post(f"{self.baseurl}/sendFriendMessage", { 111 | "sessionKey": self.session_key, 112 | "target": self.handleTargetAsFriend(friend), 113 | "messageChain": await self.handleMessageAsFriend(message) 114 | } 115 | ), raise_exception=True, return_as_is=True)) 116 | 117 | @throw_error_if_not_enable 118 | @protocol_log 119 | @edge_case_handler 120 | async def sendGroupMessage(self, 121 | group: T.Union[Group, int], 122 | message: T.Union[ 123 | MessageChain, 124 | BaseMessageComponent, 125 | T.List[T.Union[BaseMessageComponent, InternalImage, InternalVoice]], 126 | str 127 | ], 128 | quoteSource: T.Union[int, components.Source] = None 129 | ) -> BotMessage: 130 | return BotMessage.parse_obj(assertOperatorSuccess( 131 | await fetch.http_post(f"{self.baseurl}/sendGroupMessage", { 132 | "sessionKey": self.session_key, 133 | "target": self.handleTargetAsGroup(group), 134 | "messageChain": await self.handleMessageAsGroup(message), 135 | **({"quote": quoteSource.id \ 136 | if isinstance(quoteSource, components.Source) else quoteSource} \ 137 | if quoteSource else {}) 138 | } 139 | ), raise_exception=True, return_as_is=True)) 140 | 141 | @throw_error_if_not_enable 142 | @protocol_log 143 | @edge_case_handler 144 | async def sendTempMessage(self, 145 | group: T.Union[Group, int], 146 | member: T.Union[Member, int], 147 | message: T.Union[ 148 | MessageChain, 149 | BaseMessageComponent, 150 | T.List[T.Union[BaseMessageComponent, InternalImage]], 151 | str 152 | ], 153 | quoteSource: T.Union[int, components.Source] = None 154 | ) -> BotMessage: 155 | return BotMessage.parse_obj(assertOperatorSuccess( 156 | await fetch.http_post(f"{self.baseurl}/sendTempMessage", { 157 | "sessionKey": self.session_key, 158 | "qq": (member.id if isinstance(member, Member) else member), 159 | "group": (group.id if isinstance(group, Group) else group), 160 | "messageChain": await self.handleMessageForTempMessage(message), 161 | **({"quote": quoteSource.id \ 162 | if isinstance(quoteSource, components.Source) else quoteSource} \ 163 | if quoteSource else {}) 164 | } 165 | ), raise_exception=True, return_as_is=True)) 166 | 167 | @throw_error_if_not_enable 168 | @protocol_log 169 | @edge_case_handler 170 | async def revokeMessage(self, source: T.Union[components.Source, BotMessage, int]): 171 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/recall", { 172 | "sessionKey": self.session_key, 173 | "target": source if isinstance(source, int) else source.id \ 174 | if isinstance(source, components.Source) else source.messageId \ 175 | if isinstance(source, BotMessage) else \ 176 | raiser(TypeError("invaild message source")) 177 | }), raise_exception=True) 178 | 179 | @throw_error_if_not_enable 180 | @protocol_log 181 | @edge_case_handler 182 | async def groupList(self) -> T.List[Group]: 183 | return [Group.parse_obj(group_info) \ 184 | for group_info in await fetch.http_get(f"{self.baseurl}/groupList", { 185 | "sessionKey": self.session_key 186 | }) 187 | ] 188 | 189 | @throw_error_if_not_enable 190 | @protocol_log 191 | @edge_case_handler 192 | async def friendList(self) -> T.List[Friend]: 193 | return [Friend.parse_obj(friend_info) \ 194 | for friend_info in await fetch.http_get(f"{self.baseurl}/friendList", { 195 | "sessionKey": self.session_key 196 | }) 197 | ] 198 | 199 | @throw_error_if_not_enable 200 | @protocol_log 201 | @edge_case_handler 202 | async def memberList(self, target: int) -> T.List[Member]: 203 | return [Member.parse_obj(member_info) \ 204 | for member_info in await fetch.http_get(f"{self.baseurl}/memberList", { 205 | "sessionKey": self.session_key, 206 | "target": target 207 | }) 208 | ] 209 | 210 | @throw_error_if_not_enable 211 | @protocol_log 212 | @edge_case_handler 213 | async def groupMemberNumber(self, target: int) -> int: 214 | return len(await self.memberList(target)) + 1 215 | 216 | @throw_error_if_not_enable 217 | @protocol_log 218 | @edge_case_handler 219 | async def uploadImage(self, type: T.Union[str, ImageType], image: InternalImage): 220 | post_result = json.loads(await fetch.upload(f"{self.baseurl}/uploadImage", image.render(), { 221 | "sessionKey": self.session_key, 222 | "type": type if isinstance(type, str) else type.value 223 | })) 224 | return components.Image(**post_result) 225 | 226 | @throw_error_if_not_enable 227 | @protocol_log 228 | @edge_case_handler 229 | async def uploadVoice(self, type: T.Union[str, VoiceType], voice: InternalVoice): 230 | post_result = json.loads(await fetch.upload_voice(f"{self.baseurl}/uploadVoice", voice.render(), { 231 | "sessionKey": self.session_key, 232 | "type": type if isinstance(type, str) else type.value 233 | })) 234 | return components.Voice(**post_result) 235 | 236 | @throw_error_if_not_enable 237 | @protocol_log 238 | @edge_case_handler 239 | async def uploadFile(self, type: T.Union[str, FileType], file: InternalFile, target: int, path: str): 240 | type = type if isinstance(type, str) else type.value 241 | post_result = json.loads(await fetch.upload_file(f"{self.baseurl}/uploadFileAndSend", file.render(), { 242 | "sessionKey": self.session_key, 243 | "type": '{}{}'.format(type[0].upper(), type[1:]), # 不知道为什么现在需要首字母大写 244 | "target": target, 245 | "path": path 246 | })) 247 | 248 | return components.File(**post_result) 249 | 250 | @throw_error_if_not_enable 251 | @protocol_log 252 | @edge_case_handler 253 | async def groupFileList(self, target: int, dir: str = ""): 254 | return [GroupFileShort.parse_obj(file_info) \ 255 | for file_info in await fetch.http_get(f"{self.baseurl}/groupFileList", { 256 | "sessionKey": self.session_key, 257 | "target": target, 258 | "dir": dir 259 | } 260 | )] 261 | 262 | @throw_error_if_not_enable 263 | @protocol_log 264 | @edge_case_handler 265 | async def groupFileInfo(self, target: int, id: str): 266 | return GroupFile.parse_obj(await fetch.http_get(f"{self.baseurl}/groupFileInfo", { 267 | "sessionKey": self.session_key, 268 | "target": target, 269 | "id": id 270 | } 271 | )) 272 | 273 | @throw_error_if_not_enable 274 | @protocol_log 275 | @edge_case_handler 276 | async def groupRenameFile(self, target: int, id: str, name: str): 277 | return assertOperatorSuccess( 278 | await fetch.http_post(f"{self.baseurl}/groupFileRename", { 279 | "sessionKey": self.session_key, 280 | "target": target, 281 | "id": id, 282 | "rename": name 283 | } 284 | ), raise_exception=True) 285 | 286 | @throw_error_if_not_enable 287 | @protocol_log 288 | @edge_case_handler 289 | async def groupMkdir(self, target: int, dir_name: str): 290 | return assertOperatorSuccess( 291 | await fetch.http_post(f"{self.baseurl}/groupMkdir", { 292 | "sessionKey": self.session_key, 293 | "target": target, 294 | "dir": dir_name 295 | } 296 | ), raise_exception=True) 297 | 298 | @throw_error_if_not_enable 299 | @protocol_log 300 | @edge_case_handler 301 | async def groupMoveFile(self, target: int, id: str, path: str): 302 | return assertOperatorSuccess( 303 | await fetch.http_post(f"{self.baseurl}/groupFileMove", { 304 | "sessionKey": self.session_key, 305 | "target": target, 306 | "id": id, 307 | "movePath": path 308 | } 309 | ), raise_exception=True) 310 | 311 | @throw_error_if_not_enable 312 | @protocol_log 313 | @edge_case_handler 314 | async def groupDeleteFile(self, target: int, id: str): 315 | return assertOperatorSuccess( 316 | await fetch.http_post(f"{self.baseurl}/groupFileDelete", { 317 | "sessionKey": self.session_key, 318 | "target": target, 319 | "id": id 320 | } 321 | ), raise_exception=True) 322 | 323 | @protocol_log 324 | @edge_case_handler 325 | async def sendCommand(self, command, *args): 326 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/command/send", { 327 | "authKey": self.auth_key, 328 | "name": command, 329 | "args": args 330 | }), raise_exception=True, return_as_is=True) 331 | 332 | @throw_error_if_not_enable 333 | @edge_case_handler 334 | async def fetchMessage(self, count: int) -> T.List[T.Union[FriendMessage, GroupMessage, ExternalEvent]]: 335 | from mirai.event.external.enums import ExternalEvents 336 | result = assertOperatorSuccess( 337 | await fetch.http_get(f"{self.baseurl}/fetchMessage", { 338 | "sessionKey": self.session_key, 339 | "count": count 340 | } 341 | ), raise_exception=True, return_as_is=True)['data'] 342 | # 因为重新生成一个开销太大, 所以就直接在原数据内进行遍历替换 343 | try: 344 | for index in range(len(result)): 345 | # 判断当前项是否为 Message 346 | if result[index]['type'] in MessageTypes: 347 | if 'messageChain' in result[index]: 348 | result[index]['messageChain'] = MessageChain.parse_obj(result[index]['messageChain']) 349 | 350 | result[index] = \ 351 | MessageTypes[result[index]['type']].parse_obj(result[index]) 352 | 353 | elif hasattr(ExternalEvents, result[index]['type']): 354 | # 判断当前项为 Event 355 | result[index] = \ 356 | ExternalEvents[result[index]['type']].value.parse_obj(result[index]) 357 | except pydantic.ValidationError: 358 | ProtocolLogger.error(f"parse failed: {result}") 359 | traceback.print_exc() 360 | raise 361 | return result 362 | 363 | @protocol_log 364 | @edge_case_handler 365 | async def getManagers(self): 366 | return assertOperatorSuccess(await fetch.http_get(f"{self.baseurl}/managers")) 367 | 368 | @throw_error_if_not_enable 369 | @protocol_log 370 | @edge_case_handler 371 | async def messageFromId(self, sourceId: T.Union[components.Source, components.Quote, int]): 372 | if isinstance(sourceId, (components.Source, components.Quote)): 373 | sourceId = sourceId.id 374 | 375 | result = assertOperatorSuccess(await fetch.http_get(f"{self.baseurl}/messageFromId", { 376 | "sessionKey": self.session_key, 377 | "id": sourceId 378 | }), raise_exception=True, return_as_is=True) 379 | 380 | if result['type'] in MessageTypes: 381 | if "messageChain" in result: 382 | result['messageChain'] = MessageChain.custom_parse(result['messageChain']) 383 | 384 | return MessageTypes[result['type']].parse_obj(result) 385 | else: 386 | raise TypeError(f"unknown message, not found type.") 387 | 388 | @throw_error_if_not_enable 389 | @protocol_log 390 | @edge_case_handler 391 | async def muteAll(self, group: T.Union[Group, int]) -> bool: 392 | return assertOperatorSuccess( 393 | await fetch.http_post(f"{self.baseurl}/muteAll", { 394 | "sessionKey": self.session_key, 395 | "target": self.handleTargetAsGroup(group) 396 | } 397 | ), raise_exception=True) 398 | 399 | @throw_error_if_not_enable 400 | @protocol_log 401 | @edge_case_handler 402 | async def unmuteAll(self, group: T.Union[Group, int]) -> bool: 403 | return assertOperatorSuccess( 404 | await fetch.http_post(f"{self.baseurl}/unmuteAll", { 405 | "sessionKey": self.session_key, 406 | "target": self.handleTargetAsGroup(group) 407 | } 408 | ), raise_exception=True) 409 | 410 | @throw_error_if_not_enable 411 | @protocol_log 412 | @edge_case_handler 413 | async def memberInfo(self, 414 | group: T.Union[Group, int], 415 | member: T.Union[Member, int] 416 | ): 417 | return MemberChangeableSetting.parse_obj(assertOperatorSuccess( 418 | await fetch.http_get(f"{self.baseurl}/memberInfo", { 419 | "sessionKey": self.session_key, 420 | "target": self.handleTargetAsGroup(group), 421 | "memberId": self.handleTargetAsMember(member) 422 | } 423 | ), raise_exception=True, return_as_is=True)) 424 | 425 | @throw_error_if_not_enable 426 | @protocol_log 427 | @edge_case_handler 428 | async def botMemberInfo(self, 429 | group: T.Union[Group, int] 430 | ): 431 | return await self.memberInfo(group, self.qq) 432 | 433 | @throw_error_if_not_enable 434 | @protocol_log 435 | @edge_case_handler 436 | async def changeMemberInfo(self, 437 | group: T.Union[Group, int], 438 | member: T.Union[Member, int], 439 | setting: MemberChangeableSetting 440 | ) -> bool: 441 | return assertOperatorSuccess( 442 | await fetch.http_post(f"{self.baseurl}/memberInfo", { 443 | "sessionKey": self.session_key, 444 | "target": self.handleTargetAsGroup(group), 445 | "memberId": self.handleTargetAsMember(member), 446 | "info": json.loads(setting.json()) 447 | } 448 | ), raise_exception=True) 449 | 450 | @throw_error_if_not_enable 451 | @protocol_log 452 | @edge_case_handler 453 | async def groupConfig(self, group: T.Union[Group, int]) -> GroupSetting: 454 | return GroupSetting.parse_obj( 455 | await fetch.http_get(f"{self.baseurl}/groupConfig", { 456 | "sessionKey": self.session_key, 457 | "target": self.handleTargetAsGroup(group) 458 | }) 459 | ) 460 | 461 | @throw_error_if_not_enable 462 | @protocol_log 463 | @edge_case_handler 464 | async def changeGroupConfig(self, 465 | group: T.Union[Group, int], 466 | config: GroupSetting 467 | ) -> bool: 468 | return assertOperatorSuccess( 469 | await fetch.http_post(f"{self.baseurl}/groupConfig", { 470 | "sessionKey": self.session_key, 471 | "target": self.handleTargetAsGroup(group), 472 | "config": json.loads(config.json()) 473 | } 474 | ), raise_exception=True) 475 | 476 | @throw_error_if_not_enable 477 | @protocol_log 478 | @edge_case_handler 479 | async def mute(self, 480 | group: T.Union[Group, int], 481 | member: T.Union[Member, int], 482 | time: T.Union[timedelta, int] 483 | ): 484 | if isinstance(time, timedelta): 485 | time = int(time.total_seconds()) 486 | time = min(86400 * 30, max(60, time)) 487 | return assertOperatorSuccess( 488 | await fetch.http_post(f"{self.baseurl}/mute", { 489 | "sessionKey": self.session_key, 490 | "target": self.handleTargetAsGroup(group), 491 | "memberId": self.handleTargetAsMember(member), 492 | "time": time 493 | } 494 | ), raise_exception=True) 495 | 496 | @throw_error_if_not_enable 497 | @protocol_log 498 | @edge_case_handler 499 | async def unmute(self, 500 | group: T.Union[Group, int], 501 | member: T.Union[Member, int] 502 | ): 503 | return assertOperatorSuccess( 504 | await fetch.http_post(f"{self.baseurl}/unmute", { 505 | "sessionKey": self.session_key, 506 | "target": self.handleTargetAsGroup(group), 507 | "memberId": self.handleTargetAsMember(member), 508 | } 509 | ), raise_exception=True) 510 | 511 | @throw_error_if_not_enable 512 | @protocol_log 513 | @edge_case_handler 514 | async def kick(self, 515 | group: T.Union[Group, int], 516 | member: T.Union[Member, int], 517 | kickMessage: T.Optional[str] = None 518 | ): 519 | return assertOperatorSuccess( 520 | await fetch.http_post(f"{self.baseurl}/kick", { 521 | "sessionKey": self.session_key, 522 | "target": self.handleTargetAsGroup(group), 523 | "memberId": self.handleTargetAsMember(member), 524 | **({ 525 | "msg": kickMessage 526 | } if kickMessage else {}) 527 | } 528 | ), raise_exception=True) 529 | 530 | @throw_error_if_not_enable 531 | @protocol_log 532 | @edge_case_handler 533 | async def quit(self, 534 | group: T.Union[Group, int] 535 | ): 536 | return assertOperatorSuccess( 537 | await fetch.http_post(f"{self.baseurl}/quit", { 538 | "sessionKey": self.session_key, 539 | "target": self.handleTargetAsGroup(group) 540 | } 541 | ), raise_exception=True) 542 | 543 | @throw_error_if_not_enable 544 | @protocol_log 545 | @edge_case_handler 546 | async def nudge(self, type: T.Union[str, NudgeType], subject: int, target: int): 547 | type = type if isinstance(type, str) else type.value 548 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/sendNudge", { 549 | "sessionKey": self.session_key, 550 | "target": target, 551 | "subject": subject, 552 | "kind": '{}{}'.format(type[0].upper(), type[1:]) # 不知道为什么现在需要首字母大写 553 | }), raise_exception=True) 554 | 555 | @throw_error_if_not_enable 556 | @protocol_log 557 | @edge_case_handler 558 | async def setEssence(self, source: T.Union[components.Source, BotMessage, int]): 559 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/setEssence", { 560 | "sessionKey": self.session_key, 561 | "target": source if isinstance(source, int) else source.id \ 562 | if isinstance(source, components.Source) else source.messageId \ 563 | if isinstance(source, BotMessage) else \ 564 | raiser(TypeError("invaild message source")) 565 | }), raise_exception=True) 566 | 567 | @throw_error_if_not_enable 568 | @protocol_log 569 | @edge_case_handler 570 | async def respondRequest(self, 571 | request: T.Union[ 572 | eem.BotInvitedJoinGroupRequestEvent, 573 | eem.NewFriendRequestEvent, 574 | eem.MemberJoinRequestEvent 575 | ], 576 | operate: T.Union[ 577 | BotInvitedJoinGroupRequestResponseOperate, 578 | NewFriendRequestResponseOperate, 579 | MemberJoinRequestResponseOperate, 580 | int 581 | ], 582 | message: T.Optional[str] = "" 583 | ): 584 | """回应请求, 请求指 `添加好友请求` 或 `申请加群请求`.""" 585 | if isinstance(request, eem.BotInvitedJoinGroupRequestEvent): # 新增 586 | if not isinstance(operate, (BotInvitedJoinGroupRequestResponseOperate, int)): 587 | raise TypeError(f"unknown operate: {operate}") 588 | operate = (operate.value if isinstance(operate, BotInvitedJoinGroupRequestResponseOperate) else operate) 589 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/resp/botInvitedJoinGroupRequestEvent", { 590 | "sessionKey": self.session_key, 591 | "eventId": request.requestId, 592 | "fromId": request.supplicant, 593 | "groupId": request.sourceGroup, 594 | "operate": operate, 595 | "message": message 596 | }), raise_exception=True) 597 | elif isinstance(request, eem.NewFriendRequestEvent): 598 | if not isinstance(operate, (NewFriendRequestResponseOperate, int)): 599 | raise TypeError(f"unknown operate: {operate}") 600 | operate = (operate.value if isinstance(operate, NewFriendRequestResponseOperate) else operate) 601 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/resp/newFriendRequestEvent", { 602 | "sessionKey": self.session_key, 603 | "eventId": request.requestId, 604 | "fromId": request.supplicant, 605 | "groupId": request.sourceGroup, 606 | "operate": operate, 607 | "message": message 608 | }), raise_exception=True) 609 | elif isinstance(request, eem.MemberJoinRequestEvent): 610 | if not isinstance(operate, (MemberJoinRequestResponseOperate, int)): 611 | raise TypeError(f"unknown operate: {operate}") 612 | operate = (operate.value if isinstance(operate, MemberJoinRequestResponseOperate) else operate) 613 | return assertOperatorSuccess(await fetch.http_post(f"{self.baseurl}/resp/memberJoinRequestEvent", { 614 | "sessionKey": self.session_key, 615 | "eventId": request.requestId, 616 | "fromId": request.supplicant, 617 | "groupId": request.sourceGroup, 618 | "operate": operate, 619 | "message": message 620 | }), raise_exception=True) 621 | else: 622 | raise TypeError(f"unknown request: {request}") 623 | 624 | async def handleMessageAsGroup( 625 | self, 626 | message: T.Union[ 627 | MessageChain, 628 | BaseMessageComponent, 629 | T.List[T.Union[BaseMessageComponent, InternalImage]], 630 | str 631 | ]): 632 | if isinstance(message, MessageChain): 633 | return json.loads(message.json()) 634 | elif isinstance(message, BaseMessageComponent): 635 | return [json.loads(message.json())] 636 | elif isinstance(message, (tuple, list)): 637 | result = [] 638 | for i in message: 639 | if isinstance(i, InternalImage): 640 | result.append({ 641 | "type": "Image" if not i.flash else "FlashImage", 642 | "imageId": (await self.handleInternalImageAsGroup(i)).asGroupImage() 643 | }) 644 | elif isinstance(i, InternalVoice): 645 | result.append({ 646 | "type": "Voice", 647 | "voiceId": (await self.handleInternalVoiceForGroup(i)).asGroupVoice() 648 | }) 649 | elif isinstance(i, components.Image): 650 | result.append({ 651 | "type": "Image", 652 | "imageId": i.asGroupImage() 653 | }) 654 | elif isinstance(i, components.FlashImage): 655 | result.append({ 656 | "type": "FlashImage", 657 | "imageId": i.asGroupImage() 658 | }) 659 | else: 660 | result.append(json.loads(i.json())) 661 | return result 662 | elif isinstance(message, str): 663 | return [json.loads(components.Plain(text=message).json())] 664 | else: 665 | raise raiser(ValueError("invaild message.")) 666 | 667 | async def handleMessageAsFriend( 668 | self, 669 | message: T.Union[ 670 | MessageChain, 671 | BaseMessageComponent, 672 | T.List[BaseMessageComponent], 673 | str 674 | ]): 675 | if isinstance(message, MessageChain): 676 | return json.loads(message.json()) 677 | elif isinstance(message, BaseMessageComponent): 678 | return [json.loads(message.json())] 679 | elif isinstance(message, (tuple, list)): 680 | result = [] 681 | for i in message: 682 | if isinstance(i, InternalImage): 683 | result.append({ 684 | "type": "Image" if not i.flash else "FlashImage", 685 | "imageId": (await self.handleInternalImageAsFriend(i)).asFriendImage() 686 | }) 687 | elif isinstance(i, components.Image): 688 | result.append({ 689 | "type": "Image" if not i.flash else "FlashImage", 690 | "imageId": i.asFriendImage() 691 | }) 692 | else: 693 | result.append(json.loads(i.json())) 694 | return result 695 | elif isinstance(message, str): 696 | return [json.loads(components.Plain(text=message).json())] 697 | else: 698 | raise raiser(ValueError("invaild message.")) 699 | 700 | async def handleMessageForTempMessage( 701 | self, 702 | message: T.Union[ 703 | MessageChain, 704 | BaseMessageComponent, 705 | T.List[BaseMessageComponent], 706 | str 707 | ]): 708 | if isinstance(message, MessageChain): 709 | return json.loads(message.json()) 710 | elif isinstance(message, BaseMessageComponent): 711 | return [json.loads(message.json())] 712 | elif isinstance(message, (tuple, list)): 713 | result = [] 714 | for i in message: 715 | if isinstance(i, InternalImage): 716 | result.append({ 717 | "type": "Image" if not i.flash else "FlashImage", 718 | "imageId": (await self.handleInternalImageForTempMessage(i)).asFriendImage() 719 | }) 720 | elif isinstance(i, components.Image): 721 | result.append({ 722 | "type": "Image" if not i.flash else "FlashImage", 723 | "imageId": i.asFriendImage() 724 | }) 725 | else: 726 | result.append(json.loads(i.json())) 727 | return result 728 | elif isinstance(message, str): 729 | return [json.loads(components.Plain(text=message).json())] 730 | else: 731 | raise raiser(ValueError("invaild message.")) 732 | 733 | def handleTargetAsGroup(self, target: T.Union[Group, int]): 734 | return target if isinstance(target, int) else \ 735 | target.id if isinstance(target, Group) else \ 736 | raiser(ValueError("invaild target as group.")) 737 | 738 | def handleTargetAsFriend(self, target: T.Union[Friend, int]): 739 | return target if isinstance(target, int) else \ 740 | target.id if isinstance(target, Friend) else \ 741 | raiser(ValueError("invaild target as a friend obj.")) 742 | 743 | def handleTargetAsMember(self, target: T.Union[Member, int]): 744 | return target if isinstance(target, int) else \ 745 | target.id if isinstance(target, Member) else \ 746 | raiser(ValueError("invaild target as a member obj.")) 747 | 748 | async def handleInternalImageAsGroup(self, image: InternalImage): 749 | return await self.uploadImage("group", image) 750 | 751 | async def handleInternalImageAsFriend(self, image: InternalImage): 752 | return await self.uploadImage("friend", image) 753 | 754 | async def handleInternalImageForTempMessage(self, image: InternalImage): 755 | return await self.uploadImage("temp", image) 756 | 757 | async def handleInternalVoiceForGroup(self, voice: InternalVoice): 758 | return await self.uploadVoice("group", voice) 759 | 760 | async def handleInternalFileForGroup(self, file: InternalFile): 761 | return await self.uploadFile("group", file) 762 | 763 | async def handleInternalFileForFriend(self, file: InternalFile): 764 | return await self.uploadFile("friend", file) 765 | -------------------------------------------------------------------------------- /mirai/utilles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lxns-Network/mirai-python-sdk/7908061b7b0e65bb79dd1b65dd12e57c7d68dd3d/mirai/utilles/__init__.py -------------------------------------------------------------------------------- /mirai/utilles/dependencies.py: -------------------------------------------------------------------------------- 1 | """ python-mirai 自带的一些小型依赖注入设施. 2 | 3 | 各个函数皆返回 mirai.Depend 实例, 不需要进一步的包装. 4 | 5 | """ 6 | 7 | from mirai.depend import Depend 8 | from mirai import MessageChain, Cancelled, Image, Mirai, At, Group 9 | import re 10 | from typing import List, Union 11 | 12 | def RegexMatch(pattern): 13 | async def regex_depend_wrapper(message: MessageChain): 14 | if not re.match(pattern, message.toString()): 15 | raise Cancelled 16 | return Depend(regex_depend_wrapper) 17 | 18 | def StartsWith(string): 19 | async def startswith_wrapper(message: MessageChain): 20 | if not message.toString().startswith(string): 21 | raise Cancelled 22 | return Depend(startswith_wrapper) 23 | 24 | def WithPhoto(num=1): 25 | "断言消息中图片的数量" 26 | async def photo_wrapper(message: MessageChain): 27 | if len(message.getAllofComponent(Image)) < num: 28 | raise Cancelled 29 | return Depend(photo_wrapper) 30 | 31 | def AssertAt(qq=None): 32 | "断言是否at了某人, 如果没有给出则断言是否at了机器人" 33 | async def at_wrapper(app: Mirai, message: MessageChain): 34 | at_set: List[At] = message.getAllofComponent(At) 35 | qq = qq or app.qq 36 | if at_set: 37 | for at in at_set: 38 | if at.target == qq: 39 | return 40 | else: 41 | raise Cancelled 42 | return Depend(at_wrapper) 43 | 44 | def GroupsRestraint(*groups: List[Union[Group, int]]): 45 | "断言事件是否发生在某个群内" 46 | async def gr_wrapper(app: Mirai, group: Group): 47 | groups = [group if isinstance(group, int) else group.id for group in groups] 48 | if group.id not in groups: 49 | raise Cancelled 50 | return Depend(gr_wrapper) -------------------------------------------------------------------------------- /mirai/voice.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from abc import ABCMeta, abstractmethod 3 | 4 | class InternalVoice(metaclass=ABCMeta): 5 | @abstractmethod 6 | def __init__(self): 7 | super().__init__() 8 | 9 | @abstractmethod 10 | def render(self) -> bytes: 11 | pass 12 | 13 | 14 | class LocalVoice(InternalVoice): 15 | path: Path 16 | 17 | def __init__(self, path): 18 | if isinstance(path, str): 19 | self.path = Path(path) 20 | elif isinstance(path, Path): 21 | self.path = path 22 | 23 | def render(self) -> bytes: 24 | return self.path.read_bytes() 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import requests 3 | import os 4 | 5 | def md_to_rst(from_file, to_file): 6 | response = requests.post( 7 | url='http://c.docverter.com/convert', 8 | data={'to': 'rst', 'from': 'markdown'}, 9 | files={'input_files[]': open(from_file, 'rb')} 10 | ) 11 | 12 | if response.ok: 13 | with open(to_file, "wb") as f: 14 | f.write(response.content) 15 | 16 | md_to_rst("README.md", "README.rst") 17 | 18 | with open("README.rst", "r", encoding="utf-8") as fh: 19 | long_description = fh.read() 20 | 21 | setup( 22 | name="kuriyama-lxnet", 23 | version='1.3.0', 24 | description='A framework for OICQ(QQ, made by Tencent) headless client "Mirai".', 25 | author='lxnet', 26 | author_email="personnpc@gmail.com", 27 | url="https://github.com/NatriumLab/python-mirai", 28 | packages=find_packages(include=("mirai", "mirai.*")), 29 | python_requires='>=3.7', 30 | keywords=["oicq qq qqbot", ], 31 | install_requires=[ 32 | "aiohttp", 33 | "pydantic", 34 | "Logbook", 35 | "async_lru" 36 | ], 37 | long_description=long_description, 38 | long_description_content_type="text/x-rst", 39 | classifiers = [ 40 | 'Development Status :: 4 - Beta', 41 | 'Intended Audience :: Developers', 42 | 'Topic :: Software Development :: User Interfaces', 43 | 'License :: OSI Approved :: MIT License', 44 | 'Programming Language :: Python :: 3.7', 45 | "Operating System :: OS Independent" 46 | ] 47 | ) 48 | --------------------------------------------------------------------------------