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