├── .idea └── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── spring-boot-autoconfigure-itbaima-robot ├── pom.xml └── src │ └── main │ ├── java │ └── net │ │ └── itbaima │ │ └── robot │ │ └── autoconfigure │ │ ├── RobotAutoConfiguration.java │ │ ├── RobotProperties.java │ │ └── util │ │ ├── LoginType.java │ │ └── SignerType.java │ └── resources │ └── META-INF │ ├── additional-spring-configuration-metadata.json │ └── spring │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports ├── spring-boot-itbaima-robot ├── pom.xml └── src │ └── main │ ├── java │ ├── net │ │ └── itbaima │ │ │ └── robot │ │ │ ├── event │ │ │ ├── RobotEventPostProcessor.java │ │ │ ├── RobotListener.java │ │ │ ├── RobotListenerHandler.java │ │ │ └── handle │ │ │ │ ├── EventHandler.java │ │ │ │ └── HandlerResolver.java │ │ │ ├── listener │ │ │ ├── MessageListener.java │ │ │ └── util │ │ │ │ └── TrieTree.java │ │ │ └── service │ │ │ ├── RobotService.java │ │ │ └── impl │ │ │ └── RobotServiceImpl.java │ └── xyz │ │ └── cssxsh │ │ └── mirai │ │ └── tool │ │ ├── ProtocolVersionFixer.kt │ │ └── sign │ │ └── service │ │ ├── SignServiceConfig.kt │ │ ├── SignServiceFactory.kt │ │ └── impl │ │ ├── MagicSignerGuide.kt │ │ └── UnidbgFetchQsign.kt │ └── resources │ └── cmd.txt └── spring-boot-starter-itbaima-robot └── pom.xml /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /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 637 | by 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](/Users/nagocoler/Downloads/mirai.svg) 2 | 3 | 本项目基于 Mirai ,它是一个在全平台下运行,提供 QQ Android 协议支持的高效率机器人库。 4 | 5 | - mirai 是完全免费且开放源代码的软件,仅供学习和娱乐用途使用 6 | - mirai 不会通过任何方式强制收取费用,或对使用者提出物质条件 7 | - mirai 由整个开源社区维护,并不是属于某个个体的作品,所有贡献者都享有其作品的著作权。 8 | 9 | 原 Mirai 项目地址:https://github.com/mamoe/mirai 10 | 11 | ## itbaima-robot-starter 12 | 13 | **声明:本项目仅用于学习交流使用,请勿用于任何非法用途** 14 | 15 | 本项目因为基于`mirai` ,同样采用 `AGPLv3` 协议开源,为了整个社区的良性发展,我们**强烈建议**您做到以下几点: 16 | 17 | - **间接接触(包括但不限于使用 `Http API` 或 跨进程技术)到 `mirai` 的软件使用 `AGPLv3` 开源** 18 | - **不鼓励,不支持一切商业使用** 19 | 20 | 鉴于项目的特殊性,开发团队同样可能在任何时间**停止更新**或**删除项目**,本项目在孵化阶段为本团队内部使用项目,经过诸多测试和实践已经相对稳定,现已孵化结束进入正式更新版本。 21 | 22 | ## 使用问题 23 | 24 | 使用本项目遇到任何问题,首先判断是属于Mirai本身问题还是Starter处理问题: 25 | 26 | * 有关本Starter整合相关问题:可以在 [issues](https://github.com/itbaima-study/itbaima-robot-starter/issues) 中提出。 27 | 28 | * 如果对Mirai提供的API本身使用有任何疑问,可以在其官方站点查询: 29 | 30 | * **用户手册**: [UserManual](https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md) 31 | 32 | > 如果你希望快速部署一个 Mirai QQ 机器人,安装插件、并投入使用,请看这里 33 | 34 | * 论坛: [Mirai Forum](https://mirai.mamoe.net/) 35 | 36 | > Mirai 只有**唯一一个**官方论坛 Mirai Forum 37 | 38 | * 在线讨论: [Gitter](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 39 | 40 | ## 快速上手 41 | 42 | 在开始之前,请根据我们的指引完成对机器人相关的配置:https://github.com/itbaima-study/itbaima-robot-starter/wiki 当一切配置无误时,就可以开始体验了。 43 | 44 | 我们还准备了B站视频教程:https://www.bilibili.com/video/BV1Rp4y1J7kh/ 45 | 46 | ### 使用监听器 47 | 48 | 你可以使用注解快速编写监听器,监听即将到来的事件: 49 | 50 | ```java 51 | @RobotListener //添加@RobotListener注解表示这是一个监听器类 52 | public class MyListener { 53 | 54 | @RobotListenerHandler //监听器类中可以有很多处理器方法,用于处理对应的事件 55 | public void handleJoin(MemberJoinRequestEvent event) { //处理方法中需要填写事件类型对应的参数 56 | //这里处理的是MemberJoinRequestEvent新成员加入事件 57 | } 58 | } 59 | ``` 60 | 61 | 有了监听器,我们就可以快速处理事件了。比如我们想要实现在群成员发违禁词的情况下对其进行禁言操作,那么我们可以向下面这样编写: 62 | 63 | ```java 64 | @RobotListener 65 | public class NormalGroupListener extends MessageListener { 66 | @RobotListenerHandler 67 | public void handleMessage(GroupMessageEvent event) { 68 | String message = event.getMessage().contentToString(); 69 | if(message.contains("傻逼")) { //检测到违禁词 70 | Member sender = event.getSender(); //获取到消息发送者 71 | sender.mute(60); //直接禁言60秒 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | 当然,可能我们的QQ机器人加了很多个群,那么此时我们需要对监听的群进行限制,只处理我们指定群号的事件,比如下面这种情况: 78 | 79 | ```java 80 | @RobotListener 81 | public class NormalGroupListener extends MessageListener { 82 | @RobotListenerHandler(contactId = {123456789}) //只监听123456789这个群的消息 83 | public void handleMessage(GroupMessageEvent event) { 84 | //注意这种方式仅适用于那些与用户或群这类可以获取ID的事件 85 | } 86 | } 87 | ``` 88 | 89 | 有些时候可能会存在很多个监听器,或是很多个相同的事件处理方法,我们可以对其进行排序操作: 90 | 91 | ```java 92 | @RobotListener 93 | public class NormalGroupListener extends MessageListener { 94 | @RobotListenerHandler(order = 1) //order数值越小越优先,默认为0 95 | public void handleMessage(GroupMessageEvent event) { 96 | //此处理器优先进行 97 | } 98 | 99 | @RobotListenerHandler(order = 5) 100 | public void handleMessage(GroupMessageEvent event) { //同样是GroupMessageEvent的事件处理器 101 | //此处理器后进行 102 | } 103 | } 104 | ``` 105 | 106 | 正常情况下,所有的事件处理器都是按照顺序进行的,但是可能有些时候我们为了效率,希望事件处理器并发执行,我们可以直接配置: 107 | 108 | ```java 109 | public class NormalGroupListener extends MessageListener { 110 | @RobotListenerHandler(order = 1) 111 | public void handleMessage(GroupMessageEvent event) { 112 | Thread.sleep(3000); //这里的阻塞不会影响到并发执行的其他事件处理器 113 | } 114 | 115 | @RobotListenerHandler(order = 5, concurrency = true) //开启并发执行,无视顺序直接新开线程处理 116 | public void handleMessage(GroupMessageEvent event) { 117 | //此处理器与上面的处理器并发执行,不受影响 118 | } 119 | } 120 | ``` 121 | 122 | 有了监听器,我们对于Mirai中一些常见的事件可以很方便地进行处理了。 123 | 124 | ### 使用MessageListener类 125 | 126 | 考虑到各位小伙伴可能会在群里做一些常用的操作,比如判断违禁词之类的,我们封装了一个 MessageListener 类用于继承,它提供了大量的预设操作: 127 | 128 | ```java 129 | @RobotListener 130 | public class TestListener extends MessageListener { 131 | 132 | public TestListener(){ 133 | //构造时配置(也可以延迟设置)违禁词列表,并设置大小写敏感 134 | super(List.of("傻逼", "弱智", "脑残"), true); 135 | } 136 | 137 | @RobotListenerHandler 138 | public void handleMessage(GroupMessageEvent event){ 139 | String s = event.getMessage().contentToString(); 140 | if(this.invalidText(s)) { //直接使用预设的 invalidText 判断是否出现违禁词 141 | System.out.println("检测到违禁词"); 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | 除了违禁词检查,还支持撤回消息、快速回复某条消息、快速AT某个成员等,还请各位小伙伴自行阅读源码。 148 | 149 | ### 自动注册的Bean 150 | 151 | 在使用Starter后,一些对象会被自动注册为Bean,比如Mirai的机器人对象、IMirai对象等: 152 | 153 | ```java 154 | @RobotListener 155 | public class NormalGroupListener extends MessageListener { 156 | 157 | @Resource 158 | Bot bot; //可以直接注入 159 | 160 | @Resource 161 | IMirai mirai; //可以直接注入 162 | } 163 | ``` 164 | 165 | 我们也提供了一个专用于机器人操作的 Service 类用于处理各种常规操作: 166 | 167 | ```java 168 | public interface RobotService { 169 | MessageReceipt sendMessageToFriend(long user, Message message); 170 | void run(Consumer action); 171 | void runWithFriend(long group, Consumer action); 172 | void runWithGroup(long group, Consumer action); 173 | void runWithGroupMembers(long group, Consumer> action); 174 | void runWithProfile(long user, Consumer action); 175 | MessageReceipt sendMessageToFriend(long user, String message); 176 | void deleteFriend(long user); 177 | Friend getFriend(long user); 178 | MessageReceipt sendMessageToGroup(long group, Message message); 179 | MessageReceipt sendMessageToGroup(long group, String message); 180 | void deleteGroup(long group); 181 | Group getGroup(long group); 182 | boolean isGroupContainsUser(long group, long user); 183 | } 184 | ``` 185 | 186 | 它同样包含了大量日常机器人操作的方法,RobotService同样被自动注册为Bean,并且可以随时使用。 187 | 188 | ## 相关项目 189 | * https://github.com/cssxsh/fix-protocol-version 版本协议临时修复插件 190 | * https://github.com/fuqiuluo/unidbg-fetch-qsign fuqiuluo签名服务器 191 | * https://github.com/kiliokuara/magic-signer-guide kiliokuara签名服务器 192 | * https://github.com/mamoe/mirai Mirai核心 193 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | net.itbaima 7 | itbaima-robot-starter 8 | 1.0.2 9 | pom 10 | itbaima-robot-starter 11 | 12 | 13 | spring-boot-itbaima-robot 14 | spring-boot-starter-itbaima-robot 15 | spring-boot-autoconfigure-itbaima-robot 16 | 17 | 18 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.itbaima 9 | itbaima-robot-starter 10 | 1.0.2 11 | 12 | spring-boot-autoconfigure-itbaima-robot 13 | 1.0.2 14 | 15 | 16 | 17 17 | 17 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-autoconfigure 25 | 3.0.9 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-configuration-processor 31 | 3.0.9 32 | true 33 | 34 | 35 | 36 | net.itbaima 37 | spring-boot-itbaima-robot 38 | 1.0.2 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/java/net/itbaima/robot/autoconfigure/RobotAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.autoconfigure; 2 | 3 | import jakarta.annotation.Resource; 4 | import net.itbaima.robot.autoconfigure.RobotProperties.DataConfig; 5 | import net.itbaima.robot.event.RobotEventPostProcessor; 6 | import net.itbaima.robot.service.RobotService; 7 | import net.itbaima.robot.service.impl.RobotServiceImpl; 8 | import net.mamoe.mirai.Bot; 9 | import net.mamoe.mirai.BotFactory; 10 | import net.mamoe.mirai.IMirai; 11 | import net.mamoe.mirai.Mirai; 12 | import net.mamoe.mirai.auth.BotAuthorization; 13 | import net.mamoe.mirai.utils.BotConfiguration; 14 | import net.mamoe.mirai.utils.LoggerAdapters; 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 17 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Import; 21 | import xyz.cssxsh.mirai.tool.sign.service.SignServiceConfig; 22 | import xyz.cssxsh.mirai.tool.ProtocolVersionFixer; 23 | import xyz.cssxsh.mirai.tool.sign.service.SignServiceFactory; 24 | 25 | import java.io.File; 26 | 27 | @Configuration 28 | @Import(RobotEventPostProcessor.class) 29 | @EnableConfigurationProperties({RobotProperties.class}) 30 | public class RobotAutoConfiguration { 31 | 32 | static { 33 | LoggerAdapters.useLog4j2(); 34 | } 35 | 36 | @Resource 37 | RobotProperties properties; 38 | 39 | @Bean 40 | @ConditionalOnMissingBean 41 | public IMirai mirai(){ 42 | return Mirai.getInstance(); 43 | } 44 | 45 | @Bean 46 | public RobotService robotService(){ 47 | return new RobotServiceImpl(); 48 | } 49 | 50 | @Bean 51 | @ConditionalOnMissingBean 52 | @ConditionalOnProperty( 53 | prefix = RobotProperties.PREFIX, 54 | name = "login-type", 55 | havingValue = "password" 56 | ) 57 | public Bot createBotByPassword() { 58 | if(properties.getPassword() == null || properties.getUsername() == null) 59 | throw new RuntimeException("登录失败,请先配置QQ账号和密码"); 60 | DataConfig data = properties.getData(); 61 | this.createWorkDir(data.getWorkDir()); 62 | this.fixProtocolVersion(data.getWorkDir()); 63 | Bot bot = BotFactory.INSTANCE.newBot( 64 | properties.getUsername(), properties.getPassword(), this::configureRobot); 65 | bot.login(); 66 | return bot; 67 | } 68 | 69 | @Bean 70 | @ConditionalOnMissingBean 71 | @ConditionalOnProperty( 72 | prefix = RobotProperties.PREFIX, 73 | name = "login-type", 74 | havingValue = "qr_code", 75 | matchIfMissing = true 76 | ) 77 | public Bot createBotByQrcode() { 78 | if(properties.getUsername() == null) 79 | throw new RuntimeException("登录失败,请先配置QQ账号"); 80 | DataConfig data = properties.getData(); 81 | createWorkDir(data.getWorkDir()); 82 | Bot bot = BotFactory.INSTANCE.newBot( 83 | properties.getUsername(), BotAuthorization.byQRCode(), this::configureRobot); 84 | bot.login(); 85 | return bot; 86 | } 87 | 88 | private void fixProtocolVersion(String path){ 89 | RobotProperties.SignerConfig signer = properties.getSigner(); 90 | SignServiceFactory.initConfiguration(path, new SignServiceConfig( 91 | signer.getUrl(), 92 | signer.getType().toName(), 93 | signer.getKey(), 94 | signer.getServerIdentityKey(), 95 | signer.getAuthorizationKey() 96 | )); 97 | SignServiceFactory.install(); 98 | ProtocolVersionFixer.fetch(properties.getProtocol(), signer.getVersion()); 99 | } 100 | 101 | private void configureRobot(BotConfiguration configuration) { 102 | DataConfig data = properties.getData(); 103 | configuration.setWorkingDir(new File(data.getWorkDir())); 104 | configuration.setCacheDir(new File(data.getCacheDir())); 105 | configuration.setHeartbeatStrategy(properties.getStrategy()); 106 | configuration.setProtocol(properties.getProtocol()); 107 | if(data.isSaveDeviceId()) { 108 | configuration.fileBasedDeviceInfo("robot-device.json"); 109 | } 110 | if(data.isContactCache()) { 111 | configuration.enableContactCache(); 112 | } else { 113 | configuration.disableContactCache(); 114 | } 115 | } 116 | 117 | private void createWorkDir(String path) { 118 | File dir = new File(path); 119 | if(!dir.exists()) { 120 | if(!dir.mkdirs()) 121 | throw new RuntimeException("无法完成机器人启动,创建工作目录失败"); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/java/net/itbaima/robot/autoconfigure/RobotProperties.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.autoconfigure; 2 | 3 | import net.itbaima.robot.autoconfigure.util.LoginType; 4 | import net.itbaima.robot.autoconfigure.util.SignerType; 5 | import net.mamoe.mirai.utils.BotConfiguration; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | 8 | @ConfigurationProperties(RobotProperties.PREFIX) 9 | public class RobotProperties { 10 | public static final String PREFIX = "itbaima.robot"; 11 | 12 | private LoginType loginType = LoginType.QR_CODE; 13 | private Long username; 14 | private String password; 15 | private SignerConfig signer = new SignerConfig(); 16 | private DataConfig data = new DataConfig(); 17 | private BotConfiguration.HeartbeatStrategy strategy = BotConfiguration.HeartbeatStrategy.STAT_HB; 18 | private BotConfiguration.MiraiProtocol protocol = BotConfiguration.MiraiProtocol.ANDROID_WATCH; 19 | 20 | public String getPassword() { 21 | return password; 22 | } 23 | 24 | public Long getUsername() { 25 | return username; 26 | } 27 | 28 | public BotConfiguration.HeartbeatStrategy getStrategy() { 29 | return strategy; 30 | } 31 | 32 | public void setStrategy(BotConfiguration.HeartbeatStrategy strategy) { 33 | this.strategy = strategy; 34 | } 35 | 36 | public BotConfiguration.MiraiProtocol getProtocol() { 37 | return protocol; 38 | } 39 | 40 | public void setProtocol(BotConfiguration.MiraiProtocol protocol) { 41 | this.protocol = protocol; 42 | } 43 | 44 | public LoginType getLoginType() { 45 | return loginType; 46 | } 47 | 48 | public void setPassword(String password) { 49 | this.password = password; 50 | } 51 | 52 | public void setUsername(Long username) { 53 | this.username = username; 54 | } 55 | 56 | public void setLoginType(LoginType loginType) { 57 | this.loginType = loginType; 58 | } 59 | 60 | public SignerConfig getSigner() { 61 | return signer; 62 | } 63 | 64 | public void setSigner(SignerConfig signer) { 65 | this.signer = signer; 66 | } 67 | 68 | public DataConfig getData() { 69 | return data; 70 | } 71 | 72 | public void setData(DataConfig data) { 73 | this.data = data; 74 | } 75 | 76 | public static class DataConfig { 77 | private String workDir = "robot-data"; 78 | private String cacheDir = "cache"; 79 | private boolean saveDeviceId = true; 80 | 81 | public boolean isContactCache() { 82 | return contactCache; 83 | } 84 | 85 | public void setContactCache(boolean contactCache) { 86 | this.contactCache = contactCache; 87 | } 88 | 89 | private boolean contactCache = true; 90 | 91 | public String getWorkDir() { 92 | return workDir; 93 | } 94 | 95 | public void setWorkDir(String workDir) { 96 | this.workDir = workDir; 97 | } 98 | 99 | public String getCacheDir() { 100 | return cacheDir; 101 | } 102 | 103 | public void setCacheDir(String cacheDir) { 104 | this.cacheDir = cacheDir; 105 | } 106 | 107 | public boolean isSaveDeviceId() { 108 | return saveDeviceId; 109 | } 110 | 111 | public void setSaveDeviceId(boolean saveDeviceId) { 112 | this.saveDeviceId = saveDeviceId; 113 | } 114 | } 115 | 116 | public static class SignerConfig { 117 | private String version; 118 | private SignerType type; 119 | private String url; 120 | private String key = "114514"; 121 | private String serverIdentityKey = "vivo50"; 122 | private String authorizationKey = "kfc"; 123 | 124 | public String getVersion() { 125 | return version; 126 | } 127 | 128 | public void setVersion(String version) { 129 | this.version = version; 130 | } 131 | 132 | public SignerType getType() { 133 | return type; 134 | } 135 | 136 | public void setType(SignerType type) { 137 | this.type = type; 138 | } 139 | 140 | public String getUrl() { 141 | return url; 142 | } 143 | 144 | public void setUrl(String url) { 145 | this.url = url; 146 | } 147 | 148 | public String getKey() { 149 | return key; 150 | } 151 | 152 | public void setKey(String key) { 153 | this.key = key; 154 | } 155 | 156 | public String getServerIdentityKey() { 157 | return serverIdentityKey; 158 | } 159 | 160 | public void setServerIdentityKey(String serverIdentityKey) { 161 | this.serverIdentityKey = serverIdentityKey; 162 | } 163 | 164 | public String getAuthorizationKey() { 165 | return authorizationKey; 166 | } 167 | 168 | public void setAuthorizationKey(String authorizationKey) { 169 | this.authorizationKey = authorizationKey; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/java/net/itbaima/robot/autoconfigure/util/LoginType.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.autoconfigure.util; 2 | 3 | public enum LoginType { 4 | QR_CODE("qr_code"), PASSWORD("password"); 5 | 6 | private final String name; 7 | 8 | LoginType(String name) { 9 | this.name = name; 10 | } 11 | 12 | public String getName() { 13 | return name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/java/net/itbaima/robot/autoconfigure/util/SignerType.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.autoconfigure.util; 2 | 3 | public enum SignerType { 4 | FUQIULUO("fuqiuluo/unidbg-fetch-qsign"), 5 | KILIOKUARA("kiliokuara/magic-signer-guide"); 6 | 7 | String type; 8 | 9 | SignerType(String type) { 10 | this.type = type; 11 | } 12 | 13 | public String toName() { 14 | return type; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "itbaima.robot.login-type", 5 | "type": "java.lang.String", 6 | "defaultValue": "qr_code", 7 | "description": "登录类型,密码登录以及控制台打印二维码扫描登录,默认为二维码扫描登录.", 8 | "values": [ 9 | { 10 | "value": "password", 11 | "description": "使用密码登录." 12 | }, { 13 | "value": "qr_code", 14 | "description": "扫描控制台二维码登录." 15 | } 16 | ] 17 | }, { 18 | "name": "itbaima.robot.username", 19 | "type": "java.lang.Long", 20 | "description": "要登录的QQ账号." 21 | }, { 22 | "name": "itbaima.robot.password", 23 | "type": "java.lang.String", 24 | "description": "如果选择使用密码登录,请填写登录密码." 25 | }, { 26 | "name": "itbaima.robot.data.work-dir", 27 | "type": "java.lang.String", 28 | "description": "默认数据存储根目录." 29 | }, { 30 | "name": "itbaima.robot.data.cache-dir", 31 | "type": "java.lang.String", 32 | "description": "默认信息缓存目录,相对于根目录." 33 | }, { 34 | "name": "itbaima.robot.data.save-device-id", 35 | "type": "java.lang.Boolean", 36 | "description": "是否持久化设备信息,方便迁移." 37 | }, { 38 | "name": "itbaima.robot.signer.version", 39 | "type": "java.lang.String", 40 | "description": "签名服务器提供的版本." 41 | }, { 42 | "name": "itbaima.robot.signer.type", 43 | "type": "net.itbaima.robot.autoconfigure.util.SignerType", 44 | "description": "签名服务器类型." 45 | }, { 46 | "name": "itbaima.robot.signer.url", 47 | "type": "java.lang.String", 48 | "description": "签名服务器地址." 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /spring-boot-autoconfigure-itbaima-robot/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | net.itbaima.robot.autoconfigure.RobotAutoConfiguration 2 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.itbaima 9 | itbaima-robot-starter 10 | 1.0.2 11 | 12 | spring-boot-itbaima-robot 13 | 1.0.2 14 | 15 | 16 | 17 17 | 17 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 3.0.9 26 | 27 | 28 | org.jetbrains.kotlin 29 | kotlin-stdlib-jdk8 30 | 1.9.0 31 | 32 | 33 | net.mamoe 34 | mirai-core-jvm 35 | 2.15.0 36 | 37 | 38 | net.mamoe 39 | mirai-core-utils-jvm 40 | 2.15.0 41 | 42 | 43 | org.asynchttpclient 44 | async-http-client 45 | 2.12.3 46 | false 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.jetbrains.kotlin 54 | kotlin-maven-plugin 55 | 1.9.0 56 | 57 | 58 | compile 59 | process-sources 60 | 61 | compile 62 | 63 | 64 | 65 | 66 | 17 67 | 68 | kotlinx-serialization 69 | 70 | 71 | 72 | 73 | org.jetbrains.kotlin 74 | kotlin-maven-serialization 75 | 1.9.0 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/event/RobotEventPostProcessor.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.event; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import jakarta.annotation.Resource; 5 | import net.itbaima.robot.event.handle.HandlerResolver; 6 | import net.mamoe.mirai.Bot; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.BeansException; 11 | import org.springframework.beans.factory.BeanFactory; 12 | import org.springframework.beans.factory.BeanFactoryAware; 13 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; 14 | import org.springframework.beans.factory.config.BeanPostProcessor; 15 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 16 | import org.springframework.core.type.AnnotationMetadata; 17 | 18 | import java.lang.reflect.Method; 19 | 20 | /** 21 | * 处理所有机器人事件监听器的后置处理器 22 | */ 23 | public class RobotEventPostProcessor implements BeanPostProcessor, BeanFactoryAware { 24 | 25 | @Resource 26 | private Bot bot; 27 | 28 | private final Logger logger = LoggerFactory.getLogger(RobotEventPostProcessor.class); 29 | 30 | private ConfigurableListableBeanFactory factory; 31 | 32 | @PostConstruct 33 | public void init() { 34 | HandlerResolver.subscribe(bot); 35 | } 36 | 37 | @Override 38 | public void setBeanFactory(@NotNull BeanFactory factory) throws BeansException { 39 | this.factory = (ConfigurableListableBeanFactory) factory; 40 | } 41 | 42 | @Override 43 | public Object postProcessAfterInitialization(@NotNull Object bean, @NotNull String beanName) throws BeansException { 44 | if(factory.getBeanDefinition(beanName) instanceof AnnotatedBeanDefinition definition) { 45 | AnnotationMetadata metadata = definition.getMetadata(); 46 | if(metadata.hasAnnotation(RobotListener.class.getName())) { 47 | Class beanClass = bean.getClass(); 48 | Method[] declaredMethods = beanClass.getDeclaredMethods(); 49 | HandlerResolver resolver = new HandlerResolver(bean, factory, declaredMethods); 50 | logger.info("Register robot listener bean: {}, successfully, the listener will listen events {}", 51 | bean.getClass(), 52 | resolver.events()); 53 | } 54 | } 55 | return bean; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/event/RobotListener.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.event; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 添加此注解的类都是消息监听器 9 | * 10 | *

消息监听器可以对各种各样的事情进行监听,比如群里有成员发送消息、 11 | * 好友发送消息、有用户申请加群以及各种各样可能会发生的事情,使用监听器 12 | * 就可以实现消息快速监听。 13 | * 14 | *

每一个监听器内都需要对应的事件处理器,事件处理器以方法形式表示, 15 | * 每一个事件处理器都可以处理一个事件,只需要将对应的事件 {@link net.mamoe.mirai.event.Event} 16 | * 添加到方法中作为参数,最后在对应的方法上添加 {@link RobotListenerHandler} 17 | * 即可,在发生对应事件时,会自动调用一次此方法。 18 | * 19 | *

每一个监听器都会自动被扫描并注册为Bean进行管理,所以不需要配置 20 | * 其他的任何内容,比如包扫描之类的东西。 21 | * 22 | * @since 1.0.0 23 | * @author Ketuer 24 | * @see net.itbaima.robot.event.RobotListenerHandler 25 | */ 26 | @Target({ElementType.TYPE}) 27 | @Retention(RetentionPolicy.RUNTIME) 28 | @Documented 29 | @Component 30 | public @interface RobotListener { 31 | } 32 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/event/RobotListenerHandler.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.event; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 请在需要作为监听器对应Handler的方法上添加此注解 7 | * 8 | *

监听器类需要添加 {@link RobotListener} 注解表示,监听器类在加载时会自动将其中 9 | * 所有添加了 {@link RobotListenerHandler} 注解的方法作为对应事件发生时的回调函数, 10 | * 对于同一个事件可以注册多个Handler方法,顺序默认为监听器类的的扫描顺序,这是不确定的, 11 | * 因此可以使用 {@code order} 来手动指定优先级。 12 | * 13 | *

注意,对于Handler处理的优先级,仅在当前监听器内部生效,如果其他监听中同样存在监听此事件 14 | * 的Handler方法,那么会根据监听器的优先级决定,同时,如果Handler配置了并发执行,那么将直接无 15 | * 视此优先级。 16 | * 17 | *

作为监听器Handler的方法,其首个形参必须为 {@link net.mamoe.mirai.event.Event} 的子类, 18 | * 后续形参会自动从Spring提供的IoC容器中去寻找,如果找不到会抛出异常。每一个Handler有且只能有 19 | * 一个 {@link net.mamoe.mirai.event.Event} 及其子类类型的参数,就像下面这样: 20 | * 21 | *

对于一些特殊的事件,比如好友或群聊相关的事件,对应的类型为 {@link net.mamoe.mirai.event.events.GroupEvent} 22 | * 和 {@link net.mamoe.mirai.event.events.FriendEvent} 这些类型一般包含群号或QQ号, 23 | * 我们可以配置 {@code contactId} 属性从而只监听对应群号的消息,默认情况下监听所有的群号或QQ号消息。 24 | * 25 | *

一个标准的Handler方法如下: 26 | *

27 |  * @RobotListenerHandler(order = 1, contactId = 12345678)
28 |  * public void handleGroupMessage(GroupMessageEvent event, ...) {
29 |  *     System.out.println(event.getMessage());
30 |  * }
31 |  * 
32 | * 33 | * @since 1.0.0 34 | * @author Ketuer 35 | * @see #order() 36 | * @see #contactId() 37 | */ 38 | @Target({ElementType.METHOD}) 39 | @Retention(RetentionPolicy.RUNTIME) 40 | @Documented 41 | public @interface RobotListenerHandler { 42 | 43 | /** 44 | * 手动配置当前监听器内Handler方法的优先级,默认情况下为0,按照扫描顺序排列 45 | * @return 优先级 46 | */ 47 | int order() default 0; 48 | 49 | /** 50 | * 对于一些特殊类型,可以配置只监听指定的Id发生的事件 51 | * @return 监听Id列表 52 | */ 53 | long[] contactId() default {}; 54 | 55 | /** 56 | * 是否并发执行,无需等待其他Handler完成,当事件到来时直接并发执行 57 | * @return 是否并发执行 58 | */ 59 | boolean concurrency() default false; 60 | } 61 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/event/handle/EventHandler.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.event.handle; 2 | 3 | 4 | import net.itbaima.robot.event.RobotListenerHandler; 5 | import net.mamoe.mirai.event.Event; 6 | 7 | import java.util.function.Consumer; 8 | 9 | /** 10 | * 事件处理器 11 | * @param annotation 注解 12 | * @param consumer 事件消费函数 13 | */ 14 | public record EventHandler(RobotListenerHandler annotation, Consumer consumer) { 15 | public void accept(Event event) { 16 | consumer.accept(event); 17 | } 18 | 19 | public void acceptIfContainsId(long id, Event event) { 20 | if(annotation.contactId().length == 0) { 21 | consumer.accept(event); 22 | } else { 23 | boolean contains = false; 24 | for (long l : annotation.contactId()) { 25 | if(l == id) { 26 | contains = true; 27 | break; 28 | } 29 | } 30 | if(contains) consumer.accept(event); 31 | } 32 | } 33 | 34 | public int compareOrder(EventHandler another){ 35 | return Integer.compare(this.annotation.order(), another.annotation.order()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/event/handle/HandlerResolver.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.event.handle; 2 | 3 | import net.itbaima.robot.event.RobotListenerHandler; 4 | import net.mamoe.mirai.Bot; 5 | import net.mamoe.mirai.event.Event; 6 | import net.mamoe.mirai.event.events.GroupEvent; 7 | import net.mamoe.mirai.event.events.UserEvent; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.BeanFactory; 11 | 12 | import java.lang.reflect.Method; 13 | import java.lang.reflect.Parameter; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.PriorityQueue; 17 | import java.util.Set; 18 | import java.util.function.Consumer; 19 | 20 | /** 21 | * 解析所有监听器中的处理器方法,并统一完成通道订阅操作 22 | */ 23 | public class HandlerResolver { 24 | private final Object bean; 25 | private final BeanFactory factory; 26 | private final Method[] declaredMethods; 27 | private static final Map, PriorityQueue> handlers = new HashMap<>(); 28 | 29 | private final Logger logger = LoggerFactory.getLogger(HandlerResolver.class); 30 | 31 | public HandlerResolver(Object bean, BeanFactory factory, Method... declaredMethods){ 32 | this.bean = bean; 33 | this.factory = factory; 34 | this.declaredMethods = declaredMethods; 35 | this.resolve(); 36 | } 37 | 38 | public Set> events() { 39 | return handlers.keySet(); 40 | } 41 | 42 | public static void subscribe(Bot bot) { 43 | bot.getEventChannel().subscribeAlways(Event.class, event -> handlers.keySet().forEach(clazz -> { 44 | if(event.getClass().isAssignableFrom(clazz)) { 45 | handlers.get(clazz).forEach(handler -> { 46 | if(handler.annotation().concurrency()) { 47 | Thread thread = new Thread(() -> { 48 | if(event instanceof UserEvent userEvent) { 49 | handler.acceptIfContainsId(userEvent.getUser().getId(), event); 50 | } else if (event instanceof GroupEvent groupEvent){ 51 | handler.acceptIfContainsId(groupEvent.getGroup().getId(), event); 52 | } else { 53 | handler.accept(event); 54 | } 55 | }, "robot-handler-" + System.currentTimeMillis()); 56 | thread.start(); 57 | } else { 58 | if(event instanceof UserEvent userEvent) { 59 | handler.acceptIfContainsId(userEvent.getUser().getId(), event); 60 | } else if (event instanceof GroupEvent groupEvent){ 61 | handler.acceptIfContainsId(groupEvent.getGroup().getId(), event); 62 | } else { 63 | handler.accept(event); 64 | } 65 | } 66 | }); 67 | } 68 | })); 69 | } 70 | 71 | private void resolve(){ 72 | for (Method method : declaredMethods) { 73 | RobotListenerHandler annotation = method.getAnnotation(RobotListenerHandler.class); 74 | if(annotation == null) continue; 75 | Class event = this.methodEvent(method); 76 | this.addHandlerMethod(annotation, event, method); 77 | } 78 | } 79 | 80 | private Class methodEvent(Method method) { 81 | Parameter[] parameters = method.getParameters(); 82 | if(parameters.length < 1 || parameters[0].getType().isAssignableFrom(Event.class)) 83 | throw new IllegalArgumentException("监听器Handler方法的第一个参数必须是Event及其子类型!"); 84 | return parameters[0].getType().asSubclass(Event.class); 85 | } 86 | 87 | private void addHandlerMethod(RobotListenerHandler annotation, Class eventClazz, Method method) { 88 | Consumer invoke = event -> this.invokeMethod(method, event); 89 | if (!handlers.containsKey(eventClazz)) 90 | handlers.put(eventClazz, new PriorityQueue<>(EventHandler::compareOrder)); 91 | handlers.get(eventClazz).offer(new EventHandler(annotation, invoke)); 92 | } 93 | 94 | private void invokeMethod(Method method, Event event) { 95 | Object[] paramObjs = new Object[method.getParameterCount()]; 96 | paramObjs[0] = event; 97 | Parameter[] parameters = method.getParameters(); 98 | for (int i = 1; i < parameters.length; i++) { 99 | Object object = factory.getBean(parameters[i].getType()); 100 | paramObjs[i] = object; 101 | } 102 | try { 103 | method.invoke(bean, paramObjs); 104 | }catch (ReflectiveOperationException exception) { 105 | logger.error("执行机器人事件监听器Handler时出错,当前监听器: " + bean, exception); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/listener/MessageListener.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.listener; 2 | 3 | import net.itbaima.robot.listener.util.TrieTree; 4 | import net.mamoe.mirai.message.data.*; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | /** 10 | * 消息监听器通用抽象,包含一些封装好的消息管理相关方法,比如违禁词检测(基于AC自动机实现)等 11 | * 12 | *

如果您编写的是消息监听相关的监听器,那么可以直接继承此类并获得所有 13 | * 已经封装好的工具方法,可以直接使用。 14 | * 15 | * 16 | * @since 1.0.0 17 | * @author Ketuer 18 | */ 19 | public abstract class MessageListener { 20 | 21 | private TrieTree tree; 22 | private final boolean caseSensitive; 23 | 24 | /** 25 | * 构造普通的抽象消息监听器 26 | * @since 1.0.0 27 | */ 28 | public MessageListener() { 29 | this(Collections.emptyList(), false); 30 | } 31 | 32 | /** 33 | * 在构造时直接填写后续用于判断的违禁词列表 34 | * @since 1.0.0 35 | * @param caseSensitive 是否大小写敏感 36 | * @param words 违禁词列表 37 | */ 38 | public MessageListener(List words, boolean caseSensitive) { 39 | this.caseSensitive = caseSensitive; 40 | if(!words.isEmpty()) 41 | this.setProhibitedWords(words); 42 | } 43 | 44 | /** 45 | * 设置违禁词列表,后续可以使用 {@link #invalidTextWithCount} 方法快速进行违禁词判断 46 | * @since 1.0.0 47 | * @param words 违禁词 48 | */ 49 | protected void setProhibitedWords(List words){ 50 | tree = new TrieTree(); 51 | words.forEach(word -> tree.insert(caseSensitive ? word.toLowerCase() : word)); 52 | tree.buildFailureNode(); 53 | } 54 | 55 | /** 56 | * 判断一段文本中违禁词出现的次数(单个违禁词只会统计一次) 57 | * @since 1.0.0 58 | * @param text 文本内容 59 | * @return 出现违禁词次数 60 | */ 61 | protected int invalidTextWithCount(String text){ 62 | return tree.checkTextWithCount(caseSensitive ? text.toLowerCase() : text); 63 | } 64 | 65 | /** 66 | * 判断消息中是否出现违禁词,仅判断文本内容(单个违禁词只会统计一次) 67 | * @since 1.0.0 68 | * @param message Message 69 | * @return 出现违禁词次数 70 | */ 71 | protected int invalidTextWithCount(Message message){ 72 | return this.invalidTextWithCount(message.contentToString()); 73 | } 74 | 75 | /** 76 | * 判断一段文本中是否出现违禁词 77 | * @since 1.0.0 78 | * @param text 文本内容 79 | * @return 是否出现 80 | */ 81 | protected boolean invalidText(String text){ 82 | return tree.checkText(caseSensitive ? text.toLowerCase() : text); 83 | } 84 | 85 | /** 86 | * 判断消息中是否出现违禁词,仅判断文本内容 87 | * @since 1.0.0 88 | * @param message 消息 89 | * @return 是否出现 90 | */ 91 | protected boolean invalidText(Message message){ 92 | return this.invalidText(message.contentToString()); 93 | } 94 | 95 | /** 96 | * 撤回一条消息 97 | * @since 1.0.0 98 | * @param messages 消息 99 | */ 100 | protected void recallMessage(MessageChain messages){ 101 | MessageSource.recall(messages); 102 | } 103 | 104 | /** 105 | * 对一条消息进行引用,并根据给定的文本构造成一个引用回复消息 106 | * @since 1.0.0 107 | * @param quote 引用 108 | * @param message 回复内容 109 | * @return 构造完成的消息链 110 | */ 111 | protected MessageChain quoteWithMessages(MessageChain quote, String... message){ 112 | QuoteReply quoteReply = MessageSource.quote(quote); 113 | MessageChain chain = MessageUtils.newChain(quoteReply); 114 | for (String s : message) 115 | chain.plus(new PlainText(s)); 116 | return chain; 117 | } 118 | 119 | /** 120 | * 对一条消息进行引用,并根据给定的消息链进行合并得到最终的引用回复消息 121 | * @since 1.0.0 122 | * @param quote 引用 123 | * @param messages 回复消息 124 | * @return 构造完成的消息链 125 | */ 126 | protected MessageChain quoteWithMessages(MessageChain quote, Message... messages){ 127 | QuoteReply quoteReply = MessageSource.quote(quote); 128 | MessageChain chain = MessageUtils.newChain(quoteReply); 129 | for (Message message : messages) 130 | chain.plus(message); 131 | return chain; 132 | } 133 | 134 | /** 135 | * 对一个用户进行@操作,并根据给定的文本构造成一个引用回复消息 136 | * @since 1.0.0 137 | * @param user 用户 138 | * @param message 回复内容 139 | * @return 构造完成的消息链 140 | */ 141 | protected MessageChain atWithMessages(long user, String... message){ 142 | At at = new At(user); 143 | MessageChain chain = MessageUtils.newChain(at); 144 | for (String s : message) 145 | chain.plus(new PlainText(s)); 146 | return chain; 147 | } 148 | 149 | /** 150 | * 对一个用户进行@操作,并根据给定的消息链进行合并得到最终的引用回复消息 151 | * @since 1.0.0 152 | * @param user 用户 153 | * @param messages 回复消息 154 | * @return 构造完成的消息链 155 | */ 156 | protected MessageChain atWithMessages(long user, Message... messages){ 157 | At at = new At(user); 158 | MessageChain chain = MessageUtils.newChain(at); 159 | for (Message message : messages) 160 | chain.plus(message); 161 | return chain; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/listener/util/TrieTree.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.listener.util; 2 | 3 | import java.util.*; 4 | import java.util.regex.Pattern; 5 | 6 | /** 7 | * 内部使用,处理关键词匹配AC自动机算法 8 | */ 9 | public class TrieTree { 10 | private final TrieNode root; 11 | 12 | // 静态成员,预编译的正则表达式 13 | private static final Pattern INVALID_CHAR_PATTERN = Pattern.compile("[^a-zA-Z0-9\u4E00-\u9FA5]"); 14 | 15 | // 创建一个新的Trie树,根节点为空 16 | public TrieTree() { 17 | root = new TrieNode(); 18 | } 19 | 20 | // 插入一个新的关键词到Trie树中 21 | public void insert(String word) { 22 | TrieNode node = root; 23 | for (char c : word.toCharArray()) { 24 | node.children.putIfAbsent(c, new TrieNode()); // 如果该字符在当前节点的子节点中不存在,则创建一个新的子节点 25 | node = node.children.get(c); // 移动到下一个子节点 26 | } 27 | node.end = true; // 标记最后一个字符的节点为结束节点,表示一个完整的关键词 28 | } 29 | 30 | // 构建AC自动机的失效链接 31 | public void buildFailureNode() { 32 | Queue queue = new LinkedList<>(); 33 | for (TrieNode child : root.children.values()) { 34 | child.fail = root; // 根节点的子节点的失效链接都指向根节点 35 | queue.add(child); 36 | } 37 | while (!queue.isEmpty()) { 38 | TrieNode current = queue.poll(); 39 | for (char c : current.children.keySet()) { 40 | TrieNode child = current.children.get(c); 41 | queue.add(child); 42 | TrieNode failNode = current.fail; 43 | while (failNode != null && !failNode.children.containsKey(c)) 44 | failNode = failNode.fail; // 寻找失效链接的节点 45 | child.fail = failNode != null ? failNode.children.get(c) : root; // 如果找到了失效链接的节点,则指向该节点的对应子节点,否则指向根节点 46 | } 47 | } 48 | } 49 | 50 | // 检查文本中是否存在关键词 51 | public boolean checkText(String text) { 52 | TrieNode current = root; 53 | for (char c : text.toCharArray()) { 54 | if (isInvalidChar(c)) continue; // 如果字符无效,则跳过 55 | while (current != null && !current.children.containsKey(c)) 56 | current = current.fail; // 如果当前节点的子节点中不存在该字符,则跟随失效链接向上查找 57 | if (current == null) { 58 | current = root; // 如果没有找到,则回到根节点并继续查找 59 | continue; 60 | } 61 | current = current.children.get(c); // 如果找到了,则转到下一个子节点 62 | if (current.end) return true; // 如果找到了一个关键词的结束节点,则返回true 63 | } 64 | return false; // 如果没有找到任何关键词,则返回false 65 | } 66 | 67 | // 检查文本中的关键词数量 68 | public int checkTextWithCount(String text) { 69 | Set nodes = new HashSet<>(); 70 | int count = 0; 71 | TrieNode current = root; 72 | for (char c : text.toCharArray()) { 73 | if (isInvalidChar(c)) continue; // 无效字符直接跳过 74 | while (current != null && !current.children.containsKey(c)) 75 | current = current.fail; // 如果当前节点的子节点中不存在该字符,则跟随失效链接向上查找 76 | if (current == null) { 77 | current = root; // 如果没有找到,则回到根节点并继续查找 78 | continue; 79 | } 80 | current = current.children.get(c); // 如果找到了,则转到下一个子节点 81 | TrieNode tmp = current; 82 | while (tmp != null) { 83 | if(tmp.end && !nodes.contains(tmp)) { 84 | nodes.add(tmp); // 如果找到了一个关键词的结束节点,并且该节点还没有被计数过,则计数加1 85 | count++; 86 | } 87 | tmp = tmp.fail; // 向上跟随失效链接查找其他可能的关键词 88 | } 89 | } 90 | return count; // 返回找到的关键词数量 91 | } 92 | 93 | // 检查字符是否为无效字符,这里定义了无效字符为非英文、非数字和非中文的字符 94 | private boolean isInvalidChar(char c) { 95 | return INVALID_CHAR_PATTERN.matcher(String.valueOf(c)).matches(); 96 | } 97 | 98 | // Trie树的节点类,包含子节点、失效链接和结束标记 99 | private static class TrieNode { 100 | HashMap children; 101 | TrieNode fail; 102 | boolean end; 103 | 104 | public TrieNode() { 105 | children = new HashMap<>(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/service/RobotService.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.service; 2 | 3 | import net.mamoe.mirai.Bot; 4 | import net.mamoe.mirai.contact.ContactList; 5 | import net.mamoe.mirai.contact.Friend; 6 | import net.mamoe.mirai.contact.Group; 7 | import net.mamoe.mirai.contact.NormalMember; 8 | import net.mamoe.mirai.data.UserProfile; 9 | import net.mamoe.mirai.message.MessageReceipt; 10 | import net.mamoe.mirai.message.data.Message; 11 | 12 | import java.util.function.Consumer; 13 | 14 | public interface RobotService { 15 | MessageReceipt sendMessageToFriend(long user, Message message); 16 | 17 | void run(Consumer action); 18 | 19 | void runWithFriend(long group, Consumer action); 20 | 21 | void runWithGroup(long group, Consumer action); 22 | 23 | void runWithGroupMembers(long group, Consumer> action); 24 | 25 | void runWithProfile(long user, Consumer action); 26 | 27 | MessageReceipt sendMessageToFriend(long user, String message); 28 | void deleteFriend(long user); 29 | Friend getFriend(long user); 30 | MessageReceipt sendMessageToGroup(long group, Message message); 31 | MessageReceipt sendMessageToGroup(long group, String message); 32 | void deleteGroup(long group); 33 | Group getGroup(long group); 34 | boolean isGroupContainsUser(long group, long user); 35 | } 36 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/net/itbaima/robot/service/impl/RobotServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.itbaima.robot.service.impl; 2 | 3 | import jakarta.annotation.Resource; 4 | import net.itbaima.robot.service.RobotService; 5 | import net.mamoe.mirai.Bot; 6 | import net.mamoe.mirai.IMirai; 7 | import net.mamoe.mirai.contact.ContactList; 8 | import net.mamoe.mirai.contact.Friend; 9 | import net.mamoe.mirai.contact.Group; 10 | import net.mamoe.mirai.contact.NormalMember; 11 | import net.mamoe.mirai.data.UserProfile; 12 | import net.mamoe.mirai.message.MessageReceipt; 13 | import net.mamoe.mirai.message.data.Message; 14 | 15 | import java.util.function.Consumer; 16 | 17 | /** 18 | * 可以快速使用的RobotService服务类,包含大量通用操作 19 | * 20 | * @since 1.0.0 21 | * @author Ketuer 22 | */ 23 | public class RobotServiceImpl implements RobotService { 24 | 25 | @Resource 26 | private Bot bot; 27 | 28 | @Resource 29 | private IMirai mirai; 30 | 31 | /** 32 | * 在回调中进行聊天机器人相关操作 33 | * @param action 操作 34 | */ 35 | @Override 36 | public void run(Consumer action) { 37 | action.accept(bot); 38 | } 39 | 40 | /** 41 | * 在回调中对指定好友进行操作 42 | * @param fried 好友QQ号 43 | * @param action 操作 44 | */ 45 | @Override 46 | public void runWithFriend(long fried, Consumer action) { 47 | action.accept(this.getFriend(fried)); 48 | } 49 | 50 | /** 51 | * 在回调中对指定群聊进行操作 52 | * @param group 群号 53 | * @param action 操作 54 | */ 55 | @Override 56 | public void runWithGroup(long group, Consumer action) { 57 | action.accept(this.getGroup(group)); 58 | } 59 | 60 | /** 61 | * 在回调中对指定群成员列表进行操作 62 | * @param group 群号 63 | * @param action 操作 64 | */ 65 | @Override 66 | public void runWithGroupMembers(long group, Consumer> action) { 67 | action.accept(this.getGroup(group).getMembers()); 68 | } 69 | 70 | /** 71 | * 查询用户信息,并在回调中对指定用户信息进行操作 72 | * @param user 用户QQ号 73 | * @param action 操作 74 | */ 75 | @Override 76 | public void runWithProfile(long user, Consumer action){ 77 | action.accept(mirai.queryProfile(bot, user)); 78 | } 79 | 80 | /** 81 | * 向指定好友发送一条文本消息 82 | * @param friend 好友 83 | * @param message 文本消息 84 | * @return 消息回执 85 | */ 86 | @Override 87 | public MessageReceipt sendMessageToFriend(long friend, String message){ 88 | return this.findFriendById(friend).sendMessage(message); 89 | } 90 | 91 | /** 92 | * 向指定好友发送一条消息 93 | * @param friend 好友 94 | * @param message 消息 95 | * @return 消息回执 96 | */ 97 | @Override 98 | public MessageReceipt sendMessageToFriend(long friend, Message message){ 99 | return this.findFriendById(friend).sendMessage(message); 100 | } 101 | 102 | /** 103 | * 删除指定ID的好友 104 | * @param friend 好友 105 | */ 106 | @Override 107 | public void deleteFriend(long friend){ 108 | this.findFriendById(friend).delete(); 109 | } 110 | 111 | /** 112 | * 获取指定ID的好友,不存在时返回null 113 | * @param friend 好友 114 | */ 115 | @Override 116 | public Friend getFriend(long friend){ 117 | return bot.getFriend(friend); 118 | } 119 | 120 | /** 121 | * 发送一条消息到群聊 122 | * @param group 群号 123 | * @param message 消息 124 | * @return 消息回执 125 | */ 126 | @Override 127 | public MessageReceipt sendMessageToGroup(long group, Message message) { 128 | return this.findGroupById(group).sendMessage(message); 129 | } 130 | 131 | /** 132 | * 发送一条文本消息到群聊 133 | * @param group 群号 134 | * @param message 消息 135 | * @return 消息回执 136 | */ 137 | @Override 138 | public MessageReceipt sendMessageToGroup(long group, String message) { 139 | return this.findGroupById(group).sendMessage(message); 140 | } 141 | 142 | /** 143 | * 退出指定ID的群聊 144 | * @param group 群号 145 | */ 146 | @Override 147 | public void deleteGroup(long group) { 148 | this.findGroupById(group).quit(); 149 | } 150 | 151 | /** 152 | * 获取指定ID的群里,不存在时返回null 153 | * @param group 群号 154 | * @return 群 155 | */ 156 | @Override 157 | public Group getGroup(long group) { 158 | return bot.getGroup(group); 159 | } 160 | 161 | /** 162 | * 快速判断某个群内是否存在指定QQ号的用户 163 | * @param group 群号 164 | * @param user QQ号 165 | * @return 是否存在 166 | */ 167 | @Override 168 | public boolean isGroupContainsUser(long group, long user) { 169 | return this.findGroupById(group).contains(user); 170 | } 171 | 172 | private Friend findFriendById(long id){ 173 | return bot.getFriendOrFail(id); 174 | } 175 | 176 | private Group findGroupById(long id){ 177 | return bot.getGroupOrFail(id); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/ProtocolVersionFixer.kt: -------------------------------------------------------------------------------- 1 | package xyz.cssxsh.mirai.tool 2 | 3 | import kotlinx.serialization.json.* 4 | import net.mamoe.mirai.internal.utils.* 5 | import net.mamoe.mirai.utils.BotConfiguration 6 | import net.mamoe.mirai.utils.cast 7 | import net.mamoe.mirai.utils.hexToBytes 8 | import net.mamoe.mirai.utils.toUHexString 9 | import xyz.cssxsh.mirai.tool.sign.service.SignServiceFactory 10 | import java.io.File 11 | import java.net.URL 12 | import java.time.Instant 13 | import java.time.OffsetDateTime 14 | import java.time.ZoneId 15 | 16 | /** 17 | * 原名: FixProtocolVersion. 18 | */ 19 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 20 | object ProtocolVersionFixer { 21 | 22 | private val clazz = MiraiProtocolInternal::class.java 23 | 24 | private val constructor = clazz.constructors.single() 25 | 26 | @PublishedApi 27 | internal val protocols: MutableMap by lazy { 28 | try { 29 | MiraiProtocolInternal.protocols 30 | } catch (_: NoSuchMethodError) { 31 | with(MiraiProtocolInternal) { 32 | this::class.members 33 | .first { "protocols" == it.name } 34 | .call(this) 35 | }.cast() 36 | } 37 | } 38 | 39 | @PublishedApi 40 | internal fun MiraiProtocolInternal.field(name: String, default: T): T { 41 | @Suppress("UNCHECKED_CAST") 42 | return kotlin.runCatching { 43 | val field = clazz.getDeclaredField(name) 44 | field.isAccessible = true 45 | field.get(this) as T 46 | }.getOrElse { 47 | default 48 | } 49 | } 50 | 51 | @PublishedApi 52 | internal fun MiraiProtocolInternal.change(block: MiraiProtocolInternalBuilder.() -> Unit): MiraiProtocolInternal { 53 | val builder = MiraiProtocolInternalBuilder(this).apply(block) 54 | return when (constructor.parameterCount) { 55 | 10 -> constructor.newInstance( 56 | builder.apkId, 57 | builder.id, 58 | builder.ver, 59 | builder.sdkVer, 60 | builder.miscBitMap, 61 | builder.subSigMap, 62 | builder.mainSigMap, 63 | builder.sign, 64 | builder.buildTime, 65 | builder.ssoVersion 66 | ) 67 | 11 -> constructor.newInstance( 68 | builder.apkId, 69 | builder.id, 70 | builder.ver, 71 | builder.sdkVer, 72 | builder.miscBitMap, 73 | builder.subSigMap, 74 | builder.mainSigMap, 75 | builder.sign, 76 | builder.buildTime, 77 | builder.ssoVersion, 78 | builder.supportsQRLogin 79 | ) 80 | else -> this 81 | } as MiraiProtocolInternal 82 | } 83 | 84 | @PublishedApi 85 | internal class MiraiProtocolInternalBuilder(impl: MiraiProtocolInternal) { 86 | var apkId: String = impl.field("apkId", "") 87 | var id: Long = impl.field("id", 0) 88 | var ver: String = impl.field("ver", "") 89 | var sdkVer: String = impl.field("sdkVer", "") 90 | var miscBitMap: Int = impl.field("miscBitMap", 0) 91 | var subSigMap: Int = impl.field("subSigMap", 0) 92 | var mainSigMap: Int = impl.field("mainSigMap", 0) 93 | var sign: String = impl.field("sign", "") 94 | var buildTime: Long = impl.field("buildTime", 0) 95 | var ssoVersion: Int = impl.field("ssoVersion", 0) 96 | var supportsQRLogin: Boolean = impl.field("supportsQRLogin", false) 97 | } 98 | 99 | /** 100 | * fix-protocol-version 插件最初的功能 101 | * 102 | * 根据本地代码检查协议版本更新 103 | */ 104 | @JvmStatic 105 | fun update() { 106 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_PHONE) { _, impl -> 107 | when { 108 | null == impl -> null 109 | impl.runCatching { id }.isFailure -> impl.change { 110 | apkId = "com.tencent.mobileqq" 111 | id = 537163098 112 | ver = "8.9.58.11170" 113 | sdkVer = "6.0.0.2545" 114 | miscBitMap = 0x08F7_FF7C 115 | subSigMap = 0x0001_0400 116 | mainSigMap = 0x0214_10E0 117 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 118 | buildTime = 1684467300L 119 | ssoVersion = 20 120 | } 121 | impl.id < 537163098 -> impl.apply { 122 | apkId = "com.tencent.mobileqq" 123 | id = 537163098 124 | ver = "8.9.58" 125 | buildVer = "8.9.58.11170" 126 | sdkVer = "6.0.0.2545" 127 | miscBitMap = 0x08F7_FF7C 128 | subSigMap = 0x0001_0400 129 | mainSigMap = 0x0214_10E0 130 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 131 | buildTime = 1684467300L 132 | ssoVersion = 20 133 | appKey = "0S200MNJT807V3GE" 134 | supportsQRLogin = false 135 | } 136 | else -> impl 137 | } 138 | } 139 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_PAD) { _, impl -> 140 | when { 141 | null == impl -> null 142 | impl.runCatching { id }.isFailure -> impl.change { 143 | apkId = "com.tencent.mobileqq" 144 | id = 537161402 145 | ver = "8.9.58.11170" 146 | sdkVer = "6.0.0.2545" 147 | miscBitMap = 0x08F7_FF7C 148 | subSigMap = 0x0001_0400 149 | mainSigMap = 0x0214_10E0 150 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 151 | buildTime = 1684467300L 152 | ssoVersion = 20 153 | } 154 | impl.id < 537161402 -> impl.apply { 155 | apkId = "com.tencent.mobileqq" 156 | id = 537161402 157 | ver = "8.9.58" 158 | buildVer = "8.9.58.11170" 159 | sdkVer = "6.0.0.2545" 160 | miscBitMap = 0x08F7_FF7C 161 | subSigMap = 0x0001_0400 162 | mainSigMap = 0x0214_10E0 163 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 164 | buildTime = 1684467300L 165 | ssoVersion = 20 166 | appKey = "0S200MNJT807V3GE" 167 | supportsQRLogin = false 168 | } 169 | else -> impl 170 | } 171 | } 172 | protocols.compute(BotConfiguration.MiraiProtocol.ANDROID_WATCH) { _, impl -> 173 | when { 174 | null == impl -> null 175 | impl.runCatching { id }.isFailure -> impl.change { 176 | apkId = "com.tencent.qqlite" 177 | id = 537065138 178 | ver = "2.0.8" 179 | sdkVer = "6.0.0.2365" 180 | miscBitMap = 0x00F7_FF7C 181 | subSigMap = 0x0001_0400 182 | mainSigMap = 0x00FF_32F2 183 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 184 | buildTime = 1559564731L 185 | ssoVersion = 5 186 | supportsQRLogin = true 187 | } 188 | impl.id < 537065138 -> impl.apply { 189 | apkId = "com.tencent.qqlite" 190 | id = 537065138 191 | ver = "2.0.8" 192 | buildVer = "2.0.8" 193 | sdkVer = "6.0.0.2365" 194 | miscBitMap = 0x00F7_FF7C 195 | subSigMap = 0x0001_0400 196 | mainSigMap = 0x00FF_32F2 197 | sign = "A6 B7 45 BF 24 A2 C2 77 52 77 16 F6 F3 6E B6 8D" 198 | buildTime = 1559564731L 199 | ssoVersion = 5 200 | supportsQRLogin = true 201 | } 202 | else -> impl 203 | } 204 | } 205 | protocols.compute(BotConfiguration.MiraiProtocol.IPAD) { _, impl -> 206 | when { 207 | null == impl -> null 208 | impl.runCatching { id }.isFailure -> impl.change { 209 | apkId = "com.tencent.minihd.qq" 210 | id = 537155074 211 | ver = "8.9.50.611" 212 | sdkVer = "6.0.0.2535" 213 | miscBitMap = 0x08F7_FF7C 214 | subSigMap = 0x0001_0400 215 | mainSigMap = 0x001E_10E0 216 | sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7" 217 | buildTime = 1676531414L 218 | ssoVersion = 19 219 | } 220 | impl.id < 537155074 -> impl.apply { 221 | apkId = "com.tencent.minihd.qq" 222 | id = 537155074 223 | ver = "8.9.50" 224 | buildVer = "8.9.50.611" 225 | sdkVer = "6.0.0.2535" 226 | miscBitMap = 0x08F7_FF7C 227 | subSigMap = 0x0001_0400 228 | mainSigMap = 0x001E_10E0 229 | sign = "AA 39 78 F4 1F D9 6F F9 91 4A 66 9E 18 64 74 C7" 230 | buildTime = 1676531414L 231 | ssoVersion = 19 232 | appKey = "0S200MNJT807V3GE" 233 | supportsQRLogin = false 234 | } 235 | else -> impl 236 | } 237 | } 238 | protocols.compute(BotConfiguration.MiraiProtocol.MACOS) { _, impl -> 239 | when { 240 | null == impl -> null 241 | impl.runCatching { id }.isFailure -> impl.change { 242 | id = 537128930 243 | ver = "6.8.2.21241" 244 | buildTime = 1647227495 245 | } 246 | impl.id < 537128930 -> impl.apply { 247 | id = 537128930 248 | ver = "6.8.2" 249 | buildVer = "6.8.2.21241" 250 | buildTime = 1647227495 251 | } 252 | else -> impl 253 | } 254 | } 255 | } 256 | 257 | /** 258 | * 从 [RomiChan/protocol-versions](https://github.com/RomiChan/protocol-versions) 获取指定版本协议 259 | * 260 | * @since 1.9.6 261 | */ 262 | @JvmStatic 263 | fun fetch(protocol: BotConfiguration.MiraiProtocol, version: String) { 264 | if(this.existsLocalFile(protocol)) { 265 | this.load(protocol) 266 | } else { 267 | val (file, url) = when (protocol) { 268 | BotConfiguration.MiraiProtocol.ANDROID_PHONE -> { 269 | File(SignServiceFactory.workDir + "android_phone.json") to 270 | when (version) { 271 | "", "latest" -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone.json") 272 | else -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_phone/${version}.json") 273 | } 274 | } 275 | BotConfiguration.MiraiProtocol.ANDROID_PAD -> { 276 | File(SignServiceFactory.workDir + "android_pad.json") to 277 | when (version) { 278 | "", "latest" -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad.json") 279 | else -> URL("https://raw.githubusercontent.com/RomiChan/protocol-versions/master/android_pad/${version}.json") 280 | } 281 | } 282 | else -> throw IllegalArgumentException("不支持同步的协议: ${protocol.name}") 283 | } 284 | 285 | val json: JsonObject = kotlin.runCatching { 286 | url.openConnection() 287 | .apply { 288 | connectTimeout = 30_000 289 | readTimeout = 30_000 290 | } 291 | .getInputStream().use { it.readBytes() } 292 | .decodeToString() 293 | }.recoverCatching { throwable -> 294 | try { 295 | URL("https://ghproxy.com/$url").openConnection() 296 | .apply { 297 | connectTimeout = 30_000 298 | readTimeout = 30_000 299 | } 300 | .getInputStream().use { it.readBytes() } 301 | .decodeToString() 302 | } catch (cause: Throwable) { 303 | val exception = throwable.cause as? java.net.UnknownHostException ?: throwable 304 | exception.addSuppressed(cause) 305 | throw exception 306 | } 307 | }.fold( 308 | onSuccess = { text -> 309 | val online = Json.parseToJsonElement(text).jsonObject 310 | check(online.getValue("app_id").jsonPrimitive.long != 0L) { "载入的 ${protocol.name.lowercase()}.json 有误" } 311 | file.writeText(text) 312 | file.setLastModified(online.getValue("build_time").jsonPrimitive.long * 1000) 313 | online 314 | }, 315 | onFailure = { cause -> 316 | throw IllegalStateException("从 $url 下载协议失败", cause) 317 | } 318 | ) 319 | 320 | store(protocol, json) 321 | } 322 | } 323 | 324 | /** 325 | * 判断本地是否存在已经下载好的协议 326 | */ 327 | @JvmStatic 328 | private fun existsLocalFile(protocol: BotConfiguration.MiraiProtocol) : Boolean{ 329 | return File(SignServiceFactory.workDir + "${protocol.name.lowercase()}.json").exists() 330 | } 331 | 332 | /** 333 | * 从本地加载协议 334 | * 335 | * @since 1.8.0 336 | */ 337 | @JvmStatic 338 | private fun load(protocol: BotConfiguration.MiraiProtocol) { 339 | val file = File(SignServiceFactory.workDir + "${protocol.name.lowercase()}.json") 340 | val json: JsonObject = Json.parseToJsonElement(file.readText()).jsonObject 341 | store(protocol, json) 342 | } 343 | 344 | @JvmStatic 345 | private fun store(protocol: BotConfiguration.MiraiProtocol, json: JsonObject) { 346 | check(json.getValue("app_id").jsonPrimitive.long != 0L) { "载入的 ${protocol.name.lowercase()}.json 有误" } 347 | protocols.compute(protocol) { _, impl -> 348 | when { 349 | null == impl -> null 350 | impl.runCatching { id }.isFailure -> impl.change { 351 | apkId = json.getValue("apk_id").jsonPrimitive.content 352 | id = json.getValue("app_id").jsonPrimitive.long 353 | ver = json.getValue("sort_version_name").jsonPrimitive.content 354 | sdkVer = json.getValue("sdk_version").jsonPrimitive.content 355 | miscBitMap = json.getValue("misc_bitmap").jsonPrimitive.int 356 | subSigMap = json.getValue("sub_sig_map").jsonPrimitive.int 357 | mainSigMap = json.getValue("main_sig_map").jsonPrimitive.int 358 | sign = json.getValue("apk_sign").jsonPrimitive.content.hexToBytes().toUHexString(" ") 359 | buildTime = json.getValue("build_time").jsonPrimitive.long 360 | ssoVersion = json.getValue("sso_version").jsonPrimitive.int 361 | } 362 | else -> impl.apply { 363 | apkId = json.getValue("apk_id").jsonPrimitive.content 364 | id = json.getValue("app_id").jsonPrimitive.long 365 | buildVer = json.getValue("sort_version_name").jsonPrimitive.content 366 | ver = buildVer.substringBeforeLast(".") 367 | sdkVer = json.getValue("sdk_version").jsonPrimitive.content 368 | miscBitMap = json.getValue("misc_bitmap").jsonPrimitive.int 369 | subSigMap = json.getValue("sub_sig_map").jsonPrimitive.int 370 | mainSigMap = json.getValue("main_sig_map").jsonPrimitive.int 371 | sign = json.getValue("apk_sign").jsonPrimitive.content.hexToBytes().toUHexString(" ") 372 | buildTime = json.getValue("build_time").jsonPrimitive.long 373 | ssoVersion = json.getValue("sso_version").jsonPrimitive.int 374 | appKey = json.getValue("app_key").jsonPrimitive.content 375 | } 376 | } 377 | } 378 | } 379 | 380 | /** 381 | * 协议版本信息 382 | * 383 | * @since 1.6.0 384 | */ 385 | @JvmStatic 386 | fun info(): Map { 387 | return protocols.mapValues { (protocol, info) -> 388 | val version = info.field("buildVer", null as String?) ?: info.field("ver", "???") 389 | val epochSecond = info.field("buildTime", 0L) 390 | val datetime = OffsetDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), ZoneId.systemDefault()) 391 | 392 | "%-13s %-12s %s".format(protocol, version, datetime) 393 | } 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/SignServiceConfig.kt: -------------------------------------------------------------------------------- 1 | package xyz.cssxsh.mirai.tool.sign.service 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.json.JsonNames 7 | 8 | @Serializable 9 | @OptIn(ExperimentalSerializationApi::class) 10 | data class SignServiceConfig( 11 | @SerialName("base_url") 12 | val base: String, 13 | @SerialName("type") 14 | val type: String = "", 15 | @SerialName("key") 16 | val key: String = "", 17 | @SerialName("server_identity_key") 18 | @JsonNames("serverIdentityKey") 19 | val serverIdentityKey: String = "", 20 | @SerialName("authorization_key") 21 | @JsonNames("authorizationKey") 22 | val authorizationKey: String = "" 23 | ) 24 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/SignServiceFactory.kt: -------------------------------------------------------------------------------- 1 | package xyz.cssxsh.mirai.tool.sign.service 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.job 5 | import net.mamoe.mirai.internal.spi.EncryptService 6 | import net.mamoe.mirai.internal.spi.EncryptServiceContext 7 | import net.mamoe.mirai.internal.utils.* 8 | import net.mamoe.mirai.utils.BotConfiguration 9 | import net.mamoe.mirai.utils.MiraiLogger 10 | import net.mamoe.mirai.utils.Services 11 | import xyz.cssxsh.mirai.tool.sign.service.impl.MagicSignerGuide 12 | import xyz.cssxsh.mirai.tool.sign.service.impl.UnidbgFetchQsign 13 | import java.net.ConnectException 14 | import java.net.URL 15 | 16 | /** 17 | * 原名: KFCFactory 18 | */ 19 | class SignServiceFactory : EncryptService.Factory { 20 | 21 | companion object { 22 | @JvmStatic 23 | internal var signServiceConfig: SignServiceConfig? = null 24 | 25 | @JvmStatic 26 | internal var workDir: String = "" 27 | 28 | @JvmStatic 29 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(SignServiceFactory::class) 30 | 31 | @JvmStatic 32 | fun install() { 33 | Services.register( 34 | EncryptService.Factory::class.qualifiedName!!, 35 | SignServiceFactory::class.qualifiedName!!, 36 | ::SignServiceFactory 37 | ) 38 | } 39 | 40 | @JvmStatic 41 | internal val created: MutableSet = java.util.concurrent.ConcurrentHashMap.newKeySet() 42 | 43 | @JvmStatic 44 | fun initConfiguration(path: String, config: SignServiceConfig) { 45 | workDir = "$path/" 46 | signServiceConfig = config 47 | } 48 | } 49 | 50 | override fun createForBot(context: EncryptServiceContext, serviceSubScope: CoroutineScope): EncryptService { 51 | if (created.add(context.id).not()) { 52 | throw UnsupportedOperationException("repeated create EncryptService") 53 | } 54 | serviceSubScope.coroutineContext.job.invokeOnCompletion { 55 | created.remove(context.id) 56 | } 57 | try { 58 | org.asynchttpclient.Dsl.config() 59 | } catch (cause: NoClassDefFoundError) { 60 | throw RuntimeException("请参照 https://search.maven.org/artifact/org.asynchttpclient/async-http-client/2.12.3/jar 添加依赖", cause) 61 | } 62 | return when (val protocol = context.extraArgs[EncryptServiceContext.KEY_BOT_PROTOCOL]) { 63 | BotConfiguration.MiraiProtocol.ANDROID_PHONE, BotConfiguration.MiraiProtocol.ANDROID_PAD -> { 64 | @Suppress("INVISIBLE_MEMBER") 65 | val version = MiraiProtocolInternal[protocol].ver 66 | val config = signServiceConfig!! 67 | when (val type = config.type.ifEmpty { throw IllegalArgumentException("need server type") }) { 68 | "fuqiuluo/unidbg-fetch-qsign", "fuqiuluo", "unidbg-fetch-qsign" -> { 69 | try { 70 | val about = URL(config.base).readText() 71 | logger.info("unidbg-fetch-qsign by ${config.base} about " + about.replace("\n", "").replace(" ", "")) 72 | when { 73 | "version" !in about -> { 74 | // 低于等于 1.1.3 的的版本 requestToken 不工作 75 | System.setProperty(UnidbgFetchQsign.REQUEST_TOKEN_INTERVAL, "0") 76 | logger.warning("请更新 unidbg-fetch-qsign") 77 | } 78 | version !in about -> { 79 | throw IllegalStateException("unidbg-fetch-qsign by ${config.base} 的版本与 ${protocol}(${version}) 似乎不匹配") 80 | } 81 | } 82 | } catch (cause: ConnectException) { 83 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause) 84 | } catch (cause: java.io.FileNotFoundException) { 85 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause) 86 | } 87 | UnidbgFetchQsign( 88 | server = config.base, 89 | key = config.key, 90 | coroutineContext = serviceSubScope.coroutineContext 91 | ) 92 | } 93 | "kiliokuara/magic-signer-guide", "kiliokuara", "magic-signer-guide", "vivo50" -> { 94 | try { 95 | val about = URL(config.base).readText() 96 | logger.info("magic-signer-guide by ${config.base} about \n" + about) 97 | when { 98 | "void" == about.trim() -> { 99 | logger.warning("请更新 magic-signer-guide 的 docker 镜像") 100 | } 101 | version !in about -> { 102 | throw IllegalStateException("magic-signer-guide by ${config.base} 与 ${protocol}(${version}) 似乎不匹配") 103 | } 104 | } 105 | } catch (cause: ConnectException) { 106 | throw RuntimeException("请检查 magic-signer-guide by ${config.base} 的可用性", cause) 107 | } catch (cause: java.io.FileNotFoundException) { 108 | throw RuntimeException("请检查 unidbg-fetch-qsign by ${config.base} 的可用性", cause) 109 | } 110 | MagicSignerGuide( 111 | server = config.base, 112 | serverIdentityKey = config.serverIdentityKey, 113 | authorizationKey = config.authorizationKey, 114 | coroutineContext = serviceSubScope.coroutineContext 115 | ) 116 | } 117 | else -> throw UnsupportedOperationException(type) 118 | } 119 | } 120 | 121 | else -> throw UnsupportedOperationException(protocol.name) 122 | } 123 | } 124 | 125 | override fun toString(): String { 126 | return "EncryptServiceFactory(config=$signServiceConfig)" 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/impl/MagicSignerGuide.kt: -------------------------------------------------------------------------------- 1 | package xyz.cssxsh.mirai.tool.sign.service.impl 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.serialization.DeserializationStrategy 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.builtins.ListSerializer 8 | import kotlinx.serialization.builtins.serializer 9 | import kotlinx.serialization.json.* 10 | import net.mamoe.mirai.Bot 11 | import net.mamoe.mirai.event.broadcast 12 | import net.mamoe.mirai.event.events.BotOfflineEvent 13 | import net.mamoe.mirai.internal.spi.EncryptService 14 | import net.mamoe.mirai.internal.spi.EncryptServiceContext 15 | import net.mamoe.mirai.internal.utils.* 16 | import net.mamoe.mirai.utils.* 17 | import org.asynchttpclient.DefaultAsyncHttpClientConfig 18 | import org.asynchttpclient.Dsl 19 | import org.asynchttpclient.ListenableFuture 20 | import org.asynchttpclient.Response 21 | import org.asynchttpclient.ws.WebSocket 22 | import org.asynchttpclient.ws.WebSocketListener 23 | import org.asynchttpclient.ws.WebSocketUpgradeHandler 24 | import java.security.KeyFactory 25 | import java.security.KeyPair 26 | import java.security.KeyPairGenerator 27 | import java.security.Signature 28 | import java.security.spec.X509EncodedKeySpec 29 | import java.util.* 30 | import java.util.concurrent.CompletableFuture 31 | import java.util.concurrent.ConcurrentHashMap 32 | import java.util.concurrent.TimeUnit 33 | import java.util.concurrent.TimeoutException 34 | import javax.crypto.Cipher 35 | import javax.crypto.spec.SecretKeySpec 36 | import kotlin.coroutines.CoroutineContext 37 | 38 | /** 39 | * kiliokuara/magic-signer-guide 的加密服务实现. 40 | * 原名: ViVo50. 41 | */ 42 | class MagicSignerGuide( 43 | private val server: String, 44 | private val serverIdentityKey: String, 45 | private val authorizationKey: String, 46 | coroutineContext: CoroutineContext 47 | ) : EncryptService, CoroutineScope { 48 | 49 | companion object { 50 | @JvmStatic 51 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(MagicSignerGuide::class) 52 | 53 | @JvmStatic 54 | val SESSION_EXCEPT_TIMEOUT: String = "xyz.cssxsh.mirai.tool.sign.service.impl.MagicSignerGuide.Session.timeout" 55 | } 56 | 57 | override val coroutineContext: CoroutineContext = 58 | coroutineContext + SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { context, exception -> 59 | when (exception) { 60 | is CancellationException -> { 61 | // ... 62 | } 63 | else -> { 64 | logger.warning({ "with ${context[CoroutineName]}" }, exception) 65 | } 66 | } 67 | } 68 | 69 | private val client = Dsl.asyncHttpClient( 70 | DefaultAsyncHttpClientConfig.Builder() 71 | .setKeepAlive(true) 72 | .setUserAgent("curl/7.61.0") 73 | .setRequestTimeout(30_000) 74 | .setConnectTimeout(30_000) 75 | .setReadTimeout(180_000) 76 | ) 77 | 78 | private val sharedKey = SecretKeySpec(UUID.randomUUID().toString().substring(0, 16).encodeToByteArray(), "AES") 79 | 80 | private val rsaKeyPair: KeyPair = KeyPairGenerator.getInstance("RSA") 81 | .apply { initialize(4096) } 82 | .generateKeyPair() 83 | 84 | private val sessions = ConcurrentHashMap() 85 | 86 | private val white = ConcurrentHashMap.newKeySet() 87 | 88 | private fun ListenableFuture.getBody(deserializer: DeserializationStrategy): T { 89 | val response = get() 90 | return Json.decodeFromString(deserializer, response.responseBody) 91 | } 92 | 93 | private fun EncryptServiceContext.session(): Session { 94 | return sessions[id] ?: throw NoSuchElementException("Session(bot=${id})") 95 | } 96 | 97 | override fun initialize(context: EncryptServiceContext) { 98 | val device = context.extraArgs[EncryptServiceContext.KEY_DEVICE_INFO] 99 | val qimei36 = context.extraArgs[EncryptServiceContext.KEY_QIMEI36] 100 | val protocol = context.extraArgs[EncryptServiceContext.KEY_BOT_PROTOCOL] 101 | val cache = context.extraArgs[EncryptServiceContext.KEY_BOT_CACHING_DIR] 102 | val channel = context.extraArgs[EncryptServiceContext.KEY_CHANNEL_PROXY] 103 | 104 | logger.info("Bot(${context.id}) initialize by $server") 105 | 106 | val cmd = java.io.File(cache, "cmd.txt") 107 | 108 | sessions.computeIfAbsent(context.id) { 109 | val token = handshake(uin = context.id) 110 | val session = Session(token = token, bot = context.id, channel = channel) 111 | session.websocket() 112 | coroutineContext.job.invokeOnCompletion { 113 | sessions.remove(context.id, session) 114 | session.close() 115 | } 116 | try { 117 | if (cmd.exists()) { 118 | white.addAll(cmd.readText().split("\n")) 119 | } 120 | } catch (cause: Throwable) { 121 | logger.warning("Session(bot=${context.id}) cmd_white_list cache read fail", cause) 122 | cmd.delete() 123 | } 124 | session 125 | }.apply { 126 | sendCommand(type = "rpc.initialize", deserializer = JsonElement.serializer()) { 127 | putJsonObject("extArgs") { 128 | put("KEY_QIMEI36", qimei36) 129 | putJsonObject("BOT_PROTOCOL") { 130 | putJsonObject("protocolValue") { 131 | @Suppress("INVISIBLE_MEMBER") 132 | put("ver", MiraiProtocolInternal[protocol].ver) 133 | } 134 | } 135 | } 136 | putJsonObject("device") { 137 | put("display", device.display.toUHexString("")) 138 | put("product", device.product.toUHexString("")) 139 | put("device", device.device.toUHexString("")) 140 | put("board", device.board.toUHexString("")) 141 | put("brand", device.brand.toUHexString("")) 142 | put("model", device.model.toUHexString("")) 143 | put("bootloader", device.bootloader.toUHexString("")) 144 | put("fingerprint", device.fingerprint.toUHexString("")) 145 | put("bootId", device.bootId.toUHexString("")) 146 | put("procVersion", device.procVersion.toUHexString("")) 147 | put("baseBand", device.baseBand.toUHexString("")) 148 | putJsonObject("version") { 149 | put("incremental", device.version.incremental.toUHexString("")) 150 | put("release", device.version.release.toUHexString("")) 151 | put("codename", device.version.codename.toUHexString("")) 152 | put("sdk", device.version.sdk) 153 | } 154 | put("simInfo", device.simInfo.toUHexString("")) 155 | put("osType", device.osType.toUHexString("")) 156 | put("macAddress", device.macAddress.toUHexString("")) 157 | put("wifiBSSID", device.wifiBSSID.toUHexString("")) 158 | put("wifiSSID", device.wifiSSID.toUHexString("")) 159 | put("imsiMd5", device.imsiMd5.toUHexString("")) 160 | put("imei", device.imei) 161 | put("apn", device.apn.toUHexString("")) 162 | put("androidId", device.androidId.toUHexString("")) 163 | @OptIn(MiraiInternalApi::class) 164 | put("guid", device.guid.toUHexString("")) 165 | } 166 | } 167 | sendCommand(type = "rpc.get_cmd_white_list", deserializer = ListSerializer(String.serializer())).also { 168 | val list = checkNotNull(it) { "get_cmd_white_list is null" } 169 | white.clear() 170 | white.addAll(list) 171 | } 172 | } 173 | 174 | launch(CoroutineName(name = "Session(bot=${context.id})")) { 175 | cmd.writeText(white.joinToString("\n")) 176 | } 177 | 178 | logger.info("Bot(${context.id}) initialize complete") 179 | } 180 | 181 | private fun handshake(uin: Long): String { 182 | val config = client.prepareGet("${server}/service/rpc/handshake/config") 183 | .execute().getBody(HandshakeConfig.serializer()) 184 | 185 | val pKeyRsaSha1 = (serverIdentityKey + config.publicKey) 186 | .toByteArray().sha1().toUHexString("").lowercase() 187 | val clientKeySignature = (pKeyRsaSha1 + serverIdentityKey) 188 | .toByteArray().sha1().toUHexString("").lowercase() 189 | 190 | check(clientKeySignature == config.keySignature) { 191 | "请检查 serverIdentityKey 是否正确。(client calculated key signature doesn't match the server provides.)" 192 | } 193 | 194 | val secret = buildJsonObject { 195 | put("authorizationKey", authorizationKey) 196 | put("sharedKey", sharedKey.encoded.decodeToString()) 197 | put("botid", uin) 198 | }.let { 199 | val text = Json.encodeToString(JsonElement.serializer(), it) 200 | val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding") 201 | val publicKey = KeyFactory.getInstance("RSA") 202 | .generatePublic(X509EncodedKeySpec(Base64.getDecoder().decode(config.publicKey))) 203 | cipher.init(Cipher.ENCRYPT_MODE, publicKey) 204 | 205 | cipher.doFinal(text.encodeToByteArray()) 206 | } 207 | 208 | 209 | val result = client.preparePost("${server}/service/rpc/handshake/handshake") 210 | .setBody(Json.encodeToString(JsonElement.serializer(), buildJsonObject { 211 | put("clientRsa", Base64.getEncoder().encodeToString(rsaKeyPair.public.encoded)) 212 | put("secret", Base64.getEncoder().encodeToString(secret)) 213 | })) 214 | .execute().getBody(HandshakeResult.serializer()) 215 | 216 | check(result.status == 200) { result.reason } 217 | 218 | return Base64.getDecoder().decode(result.token).decodeToString() 219 | } 220 | 221 | private fun signature(): Pair { 222 | val current = System.currentTimeMillis().toString() 223 | val privateSignature = Signature.getInstance("SHA256withRSA") 224 | privateSignature.initSign(rsaKeyPair.private) 225 | privateSignature.update(current.encodeToByteArray()) 226 | 227 | return current to Base64.getEncoder().encodeToString(privateSignature.sign()) 228 | } 229 | 230 | override fun encryptTlv(context: EncryptServiceContext, tlvType: Int, payload: ByteArray): ByteArray? { 231 | val command = context.extraArgs[EncryptServiceContext.KEY_COMMAND_STR] 232 | 233 | val response = context.session().sendCommand(type = "rpc.tlv", deserializer = String.serializer()) { 234 | put("tlvType", tlvType) 235 | putJsonObject("extArgs") { 236 | put("KEY_COMMAND_STR", command) 237 | } 238 | put("content", payload.toUHexString("")) 239 | } ?: return null 240 | 241 | return response.hexToBytes() 242 | } 243 | 244 | override fun qSecurityGetSign( 245 | context: EncryptServiceContext, 246 | sequenceId: Int, 247 | commandName: String, 248 | payload: ByteArray 249 | ): EncryptService.SignResult? { 250 | if (white.isEmpty().not() && commandName !in white) return null 251 | 252 | logger.debug("Bot(${context.id}) sign $commandName") 253 | 254 | val response = context.session().sendCommand(type = "rpc.sign", deserializer = RpcSignResult.serializer()) { 255 | put("seqId", sequenceId) 256 | put("command", commandName) 257 | putJsonObject("extArgs") { 258 | // ... 259 | } 260 | put("content", payload.toUHexString("")) 261 | } ?: return null 262 | 263 | return EncryptService.SignResult( 264 | sign = response.sign.hexToBytes(), 265 | extra = response.extra.hexToBytes(), 266 | token = response.token.hexToBytes(), 267 | ) 268 | } 269 | 270 | override fun toString(): String { 271 | return "MagicSignerGuideService(server=${server}, sessions=${sessions.keys})" 272 | } 273 | 274 | private inner class Session(val bot: Long, val token: String, val channel: EncryptService.ChannelProxy) : 275 | WebSocketListener, AutoCloseable { 276 | private var websocket0: WebSocket? = null 277 | private var cause0: Throwable? = null 278 | private val packet: MutableMap> = ConcurrentHashMap() 279 | private val timeout: Long = System.getProperty(SESSION_EXCEPT_TIMEOUT, "60000").toLong() 280 | 281 | override fun onOpen(websocket: WebSocket) { 282 | websocket0 = websocket 283 | cause0 = null 284 | logger.info("Session(bot=${bot}) opened") 285 | } 286 | 287 | override fun onClose(websocket: WebSocket, code: Int, reason: String?) { 288 | websocket0 = null 289 | if (code != 1_000) logger.warning("Session(bot=${bot}) closed, $code - $reason") 290 | } 291 | 292 | override fun onError(cause: Throwable) { 293 | when (val websocket = websocket0) { 294 | null -> { 295 | cause0 = cause 296 | } 297 | else -> { 298 | websocket0 = null 299 | val wrapper = IllegalStateException("Session(bot=${bot}) error", cause) 300 | if (websocket.isOpen) { 301 | try { 302 | websocket.sendCloseFrame() 303 | } catch (cause: Throwable) { 304 | wrapper.addSuppressed(cause) 305 | } 306 | } 307 | logger.error("Session(bot=${bot}) error", wrapper) 308 | } 309 | } 310 | } 311 | 312 | override fun onBinaryFrame(payload: ByteArray, finalFragment: Boolean, rsv: Int) { 313 | val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") 314 | cipher.init(Cipher.DECRYPT_MODE, sharedKey) 315 | val text = cipher.doFinal(payload).decodeToString() 316 | 317 | val json = Json.parseToJsonElement(text).jsonObject 318 | val id = json["packetId"]!!.jsonPrimitive.content 319 | packet.remove(id)?.complete(json) 320 | 321 | when (json["packetType"]?.jsonPrimitive?.content) { 322 | "rpc.service.send" -> { 323 | val uin = json["botUin"]!!.jsonPrimitive.long 324 | val cmd = json["command"]!!.jsonPrimitive.content 325 | launch(CoroutineName(name = "Session(bot=${bot})")) { 326 | logger.verbose("Bot(${bot}) sendMessage <- $cmd") 327 | 328 | val result = channel.sendMessage( 329 | remark = json["remark"]!!.jsonPrimitive.content, 330 | commandName = cmd, 331 | uin = uin, 332 | data = json["data"]!!.jsonPrimitive.content.hexToBytes() 333 | ) 334 | 335 | if (result == null) { 336 | logger.debug("Bot(${bot}) ChannelResult is null") 337 | return@launch 338 | } 339 | logger.verbose("Bot(${bot}) sendMessage -> ${result.cmd}") 340 | 341 | sendPacket(type = "rpc.service.send", id = id) { 342 | put("command", result.cmd) 343 | put("data", result.data.toUHexString("")) 344 | } 345 | } 346 | } 347 | "service.interrupt" -> { 348 | logger.error("Session(bot=${bot}) $text") 349 | } 350 | else -> { 351 | // ... 352 | } 353 | } 354 | } 355 | 356 | private fun open(): WebSocket { 357 | val (timestamp, signature) = signature() 358 | return client.prepareGet("${server}/service/rpc/session".replace("http", "ws")) 359 | .addHeader("Authorization", token) 360 | .addHeader("X-SEC-Time", timestamp) 361 | .addHeader("X-SEC-Signature", signature) 362 | .execute( 363 | WebSocketUpgradeHandler 364 | .Builder() 365 | .addWebSocketListener(this) 366 | .build() 367 | ) 368 | .get() ?: throw IllegalStateException("Session(bot=${bot}) open fail", cause0) 369 | } 370 | 371 | private fun check(): WebSocket? { 372 | val (timestamp, signature) = signature() 373 | val response = client.prepareGet("${server}/service/rpc/session/check") 374 | .addHeader("Authorization", token) 375 | .addHeader("X-SEC-Time", timestamp) 376 | .addHeader("X-SEC-Signature", signature) 377 | .execute().get() 378 | 379 | return when (response.statusCode) { 380 | 204 -> websocket0 381 | 404 -> null 382 | else -> { 383 | sessions.remove(bot, this) 384 | val cause = IllegalStateException("Session(bot=${bot}) ${response.responseBody}") 385 | launch(CoroutineName(name = "Dropped(bot=${bot})")) { 386 | @OptIn(MiraiInternalApi::class) 387 | BotOfflineEvent.Dropped( 388 | bot = Bot.getInstance(qq = bot), 389 | cause = cause 390 | ).broadcast() 391 | } 392 | throw cause 393 | } 394 | } 395 | } 396 | 397 | private fun delete() { 398 | val (timestamp, signature) = signature() 399 | val response = client.prepareDelete("${server}/service/rpc/session") 400 | .addHeader("Authorization", token) 401 | .addHeader("X-SEC-Time", timestamp) 402 | .addHeader("X-SEC-Signature", signature) 403 | .execute().get() 404 | 405 | when (response.statusCode) { 406 | 204 -> websocket0 = null 407 | 404 -> throw NoSuchElementException("Session(bot=${bot}) ${response.responseBody}") 408 | else -> throw IllegalStateException("Session(bot=${bot}) ${response.responseBody}") 409 | } 410 | } 411 | 412 | override fun close() { 413 | try { 414 | delete() 415 | } catch (cause: NoSuchElementException) { 416 | logger.warning(cause) 417 | } catch (cause: Throwable) { 418 | logger.error(cause) 419 | } 420 | try { 421 | websocket0?.takeIf { it.isOpen }?.sendCloseFrame() 422 | } catch (cause: Throwable) { 423 | logger.error(cause) 424 | } 425 | } 426 | 427 | @Synchronized 428 | fun websocket(): WebSocket { 429 | coroutineContext.ensureActive() 430 | return check() ?: open() 431 | } 432 | 433 | fun sendPacket(type: String, id: String, block: JsonObjectBuilder.() -> Unit) { 434 | val packet = buildJsonObject { 435 | put("packetId", id) 436 | put("packetType", type) 437 | block.invoke(this) 438 | } 439 | val text = Json.encodeToString(JsonElement.serializer(), packet) 440 | val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding") 441 | cipher.init(Cipher.ENCRYPT_MODE, sharedKey) 442 | websocket().sendBinaryFrame(cipher.doFinal(text.encodeToByteArray())) 443 | } 444 | 445 | fun sendCommand( 446 | type: String, 447 | deserializer: DeserializationStrategy, 448 | block: JsonObjectBuilder.() -> Unit = {} 449 | ): T? { 450 | 451 | val uuid = UUID.randomUUID().toString() 452 | val future = CompletableFuture() 453 | packet[uuid] = future 454 | 455 | sendPacket(type = type, id = uuid, block = block) 456 | 457 | val json = try { 458 | future.get(timeout, TimeUnit.MILLISECONDS) 459 | } catch (cause: TimeoutException) { 460 | logger.warning("Session(bot=${bot}) $type timeout ${timeout}ms", cause) 461 | return null 462 | } 463 | 464 | json["message"]?.let { 465 | throw IllegalStateException("Session(bot=${bot}) $type error: $it") 466 | } 467 | 468 | val response = json["response"] ?: return null 469 | 470 | return Json.decodeFromJsonElement(deserializer, response) 471 | } 472 | 473 | override fun toString(): String { 474 | return "Session(bot=${bot}, token=${token})" 475 | } 476 | } 477 | } 478 | 479 | @Serializable 480 | private data class HandshakeConfig( 481 | @SerialName("publicKey") 482 | val publicKey: String, 483 | @SerialName("timeout") 484 | val timeout: Long, 485 | @SerialName("keySignature") 486 | val keySignature: String 487 | ) 488 | 489 | @Serializable 490 | private data class HandshakeResult( 491 | @SerialName("status") 492 | val status: Int, 493 | @SerialName("reason") 494 | val reason: String = "", 495 | @SerialName("token") 496 | val token: String = "" 497 | ) 498 | 499 | @Serializable 500 | private data class RpcSignResult( 501 | @SerialName("sign") 502 | val sign: String, 503 | @SerialName("token") 504 | val token: String, 505 | @SerialName("extra") 506 | val extra: String, 507 | ) 508 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/java/xyz/cssxsh/mirai/tool/sign/service/impl/UnidbgFetchQsign.kt: -------------------------------------------------------------------------------- 1 | package xyz.cssxsh.mirai.tool.sign.service.impl 2 | 3 | import kotlinx.coroutines.* 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.builtins.ListSerializer 8 | import kotlinx.serialization.builtins.serializer 9 | import kotlinx.serialization.json.Json 10 | import kotlinx.serialization.json.JsonElement 11 | import kotlinx.serialization.json.JsonNames 12 | import net.mamoe.mirai.Bot 13 | import net.mamoe.mirai.event.broadcast 14 | import net.mamoe.mirai.event.events.BotOfflineEvent 15 | import net.mamoe.mirai.internal.spi.EncryptService 16 | import net.mamoe.mirai.internal.spi.EncryptServiceContext 17 | import net.mamoe.mirai.utils.* 18 | import org.asynchttpclient.DefaultAsyncHttpClientConfig 19 | import org.asynchttpclient.Dsl 20 | import kotlin.coroutines.CoroutineContext 21 | 22 | /** 23 | * fuqiuluo/unidbg-fetch-qsign 的加密服务实现. 24 | */ 25 | class UnidbgFetchQsign(private val server: String, private val key: String, coroutineContext: CoroutineContext) : 26 | EncryptService, CoroutineScope { 27 | 28 | companion object { 29 | @JvmStatic 30 | internal val logger: MiraiLogger = MiraiLogger.Factory.create(UnidbgFetchQsign::class) 31 | 32 | @JvmStatic 33 | val REQUEST_TOKEN_INTERVAL: String = "xyz.cssxsh.mirai.tool.sign.service.impl.UnidbgFetchQsign.token.interval" 34 | 35 | @JvmStatic 36 | internal val CMD_WHITE_LIST = UnidbgFetchQsign::class.java.getResource("/cmd.txt")!!.readText().lines() 37 | } 38 | 39 | override val coroutineContext: CoroutineContext = 40 | coroutineContext + SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { context, exception -> 41 | when (exception) { 42 | is CancellationException -> { 43 | // ... 44 | } 45 | else -> { 46 | logger.warning({ "with ${context[CoroutineName]}" }, exception) 47 | } 48 | } 49 | } 50 | 51 | private val client = Dsl.asyncHttpClient( 52 | DefaultAsyncHttpClientConfig.Builder() 53 | .setKeepAlive(true) 54 | .setUserAgent("curl/7.61.0") 55 | .setRequestTimeout(90_000) 56 | .setConnectTimeout(30_000) 57 | .setReadTimeout(180_000) 58 | ) 59 | 60 | private var channel0: EncryptService.ChannelProxy? = null 61 | 62 | private val channel: EncryptService.ChannelProxy get() = channel0 ?: throw IllegalStateException("need initialize") 63 | 64 | private val token = java.util.concurrent.atomic.AtomicLong(0) 65 | 66 | override fun initialize(context: EncryptServiceContext) { 67 | val device = context.extraArgs[EncryptServiceContext.KEY_DEVICE_INFO] 68 | val qimei36 = context.extraArgs[EncryptServiceContext.KEY_QIMEI36] 69 | val channel = context.extraArgs[EncryptServiceContext.KEY_CHANNEL_PROXY] 70 | 71 | logger.info("Bot(${context.id}) initialize by $server") 72 | 73 | channel0 = channel 74 | 75 | if (token.get() == 0L) { 76 | val uin = context.id 77 | @OptIn(MiraiInternalApi::class) 78 | register( 79 | uin = uin, 80 | androidId = device.androidId.decodeToString(), 81 | guid = device.guid.toUHexString(), 82 | qimei36 = qimei36 83 | ) 84 | coroutineContext.job.invokeOnCompletion { 85 | try { 86 | destroy(uin = uin) 87 | } catch (cause : Throwable) { 88 | logger.warning("Bot(${uin}) destroy", cause) 89 | } finally { 90 | token.compareAndSet(uin, 0) 91 | } 92 | } 93 | } 94 | 95 | logger.info("Bot(${context.id}) initialize complete") 96 | } 97 | 98 | private fun register(uin: Long, androidId: String, guid: String, qimei36: String) { 99 | val response = client.prepareGet("${server}/register") 100 | .addQueryParam("uin", uin.toString()) 101 | .addQueryParam("android_id", androidId) 102 | .addQueryParam("guid", guid) 103 | .addQueryParam("qimei36", qimei36) 104 | .addQueryParam("key", key) 105 | .execute().get() 106 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 107 | body.check(uin = uin) 108 | 109 | logger.info("Bot(${uin}) register, ${body.message}") 110 | } 111 | 112 | private fun destroy(uin: Long) { 113 | val response = client.prepareGet("${server}/destroy") 114 | .addQueryParam("uin", uin.toString()) 115 | .addQueryParam("key", key) 116 | .execute().get() 117 | if (response.statusCode == 404) return 118 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 119 | 120 | logger.info("Bot(${uin}) destroy, ${body.message}") 121 | } 122 | 123 | private fun DataWrapper.check(uin: Long) { 124 | if (code == 0) return 125 | token.compareAndSet(uin, 0) 126 | val cause = IllegalStateException("unidbg-fetch-qsign 服务异常, 请检查其日志, $message") 127 | launch(CoroutineName(name = "Dropped(${uin})")) { 128 | if ("Uin is not registered." != message) return@launch 129 | @OptIn(MiraiInternalApi::class) 130 | BotOfflineEvent.Dropped( 131 | bot = Bot.getInstance(qq = uin), 132 | cause = cause 133 | ).broadcast() 134 | } 135 | throw cause 136 | } 137 | 138 | override fun encryptTlv(context: EncryptServiceContext, tlvType: Int, payload: ByteArray): ByteArray? { 139 | if (tlvType != 0x544) return null 140 | val command = context.extraArgs[EncryptServiceContext.KEY_COMMAND_STR] 141 | 142 | val data = customEnergy(uin = context.id, salt = payload, data = command) 143 | 144 | return data.hexToBytes() 145 | } 146 | 147 | private fun customEnergy(uin: Long, salt: ByteArray, data: String): String { 148 | val response = client.prepareGet("${server}/custom_energy") 149 | .addQueryParam("uin", uin.toString()) 150 | .addQueryParam("salt", salt.toUHexString("")) 151 | .addQueryParam("data", data) 152 | .execute().get() 153 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 154 | body.check(uin = uin) 155 | 156 | logger.debug("Bot(${uin}) custom_energy ${data}, ${body.message}") 157 | 158 | return Json.decodeFromJsonElement(String.serializer(), body.data) 159 | } 160 | 161 | override fun qSecurityGetSign( 162 | context: EncryptServiceContext, 163 | sequenceId: Int, 164 | commandName: String, 165 | payload: ByteArray 166 | ): EncryptService.SignResult? { 167 | if (commandName == "StatSvc.register") { 168 | if (token.compareAndSet(0, context.id)) { 169 | val uin = context.id 170 | launch(CoroutineName(name = "RequestToken")) { 171 | while (isActive) { 172 | val interval = System.getProperty(REQUEST_TOKEN_INTERVAL, "2400000").toLong() 173 | if (interval <= 0L) break 174 | if (interval < 600_000) logger.warning("$REQUEST_TOKEN_INTERVAL=${interval} < 600_000 (ms)") 175 | delay(interval) 176 | val request = try { 177 | requestToken(uin = uin) 178 | } catch (cause: Throwable) { 179 | logger.error(cause) 180 | continue 181 | } 182 | callback(uin = uin, request = request) 183 | } 184 | } 185 | } 186 | } 187 | 188 | if (commandName !in CMD_WHITE_LIST) return null 189 | 190 | val data = sign(uin = context.id, cmd = commandName, seq = sequenceId, buffer = payload) 191 | 192 | callback(uin = context.id, request = data.request) 193 | 194 | return EncryptService.SignResult( 195 | sign = data.sign.hexToBytes(), 196 | token = data.token.hexToBytes(), 197 | extra = data.extra.hexToBytes() 198 | ) 199 | } 200 | 201 | private fun sign(uin: Long, cmd: String, seq: Int, buffer: ByteArray): SignResult { 202 | val response = client.preparePost("${server}/sign") 203 | .addFormParam("uin", uin.toString()) 204 | .addFormParam("cmd", cmd) 205 | .addFormParam("seq", seq.toString()) 206 | .addFormParam("buffer", buffer.toUHexString("")) 207 | .execute().get() 208 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 209 | body.check(uin = uin) 210 | 211 | logger.debug("Bot(${uin}) sign ${cmd}, ${body.message}") 212 | 213 | return Json.decodeFromJsonElement(SignResult.serializer(), body.data) 214 | } 215 | 216 | private fun requestToken(uin: Long): List { 217 | val response = client.prepareGet("${server}/request_token") 218 | .addQueryParam("uin", uin.toString()) 219 | .execute().get() 220 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 221 | body.check(uin = uin) 222 | 223 | logger.info("Bot(${uin}) request_token, ${body.message}") 224 | 225 | return Json.decodeFromJsonElement(ListSerializer(RequestCallback.serializer()), body.data) 226 | } 227 | 228 | private fun submit(uin: Long, cmd: String, callbackId: Long, buffer: ByteArray) { 229 | val response = client.prepareGet("${server}/submit") 230 | .addQueryParam("uin", uin.toString()) 231 | .addQueryParam("cmd", cmd) 232 | .addQueryParam("callback_id", callbackId.toString()) 233 | .addQueryParam("buffer", buffer.toUHexString("")) 234 | .execute().get() 235 | val body = Json.decodeFromString(DataWrapper.serializer(), response.responseBody) 236 | body.check(uin = uin) 237 | 238 | logger.debug("Bot(${uin}) submit ${cmd}, ${body.message}") 239 | } 240 | 241 | private fun callback(uin: Long, request: List) { 242 | launch(CoroutineName(name = "SendMessage")) { 243 | for (callback in request) { 244 | logger.debug("Bot(${uin}) sendMessage ${callback.cmd} ") 245 | val result = try { 246 | channel.sendMessage( 247 | remark = "mobileqq.msf.security", 248 | commandName = callback.cmd, 249 | uin = 0, 250 | data = callback.body.hexToBytes() 251 | ) 252 | } catch (cause: Throwable) { 253 | throw RuntimeException("Bot(${uin}) callback ${callback.cmd}", cause) 254 | } 255 | if (result == null) { 256 | logger.debug("Bot(${uin}) callback ${callback.cmd} ChannelResult is null") 257 | continue 258 | } 259 | 260 | submit(uin = uin, cmd = result.cmd, callbackId = callback.id, buffer = result.data) 261 | } 262 | } 263 | } 264 | 265 | override fun toString(): String { 266 | return "UnidbgFetchQsignService(server=${server}, uin=${token})" 267 | } 268 | 269 | 270 | } 271 | 272 | @Serializable 273 | private data class DataWrapper( 274 | @SerialName("code") 275 | val code: Int = 0, 276 | @SerialName("msg") 277 | val message: String = "", 278 | @SerialName("data") 279 | val data: JsonElement 280 | ) 281 | 282 | @Serializable 283 | private data class SignResult( 284 | @SerialName("token") 285 | val token: String = "", 286 | @SerialName("extra") 287 | val extra: String = "", 288 | @SerialName("sign") 289 | val sign: String = "", 290 | @SerialName("o3did") 291 | val o3did: String = "", 292 | @SerialName("requestCallback") 293 | val request: List = emptyList() 294 | ) 295 | 296 | @Serializable 297 | private data class RequestCallback( 298 | @SerialName("body") 299 | val body: String, 300 | @SerialName("callback_id") 301 | @OptIn(ExperimentalSerializationApi::class) 302 | @JsonNames("callbackId", "callback_id") 303 | val id: Long, 304 | @SerialName("cmd") 305 | val cmd: String 306 | ) 307 | -------------------------------------------------------------------------------- /spring-boot-itbaima-robot/src/main/resources/cmd.txt: -------------------------------------------------------------------------------- 1 | OidbSvcTrpcTcp.0x55f_0 2 | OidbSvcTrpcTcp.0x1100_1 3 | qidianservice.269 4 | OidbSvc.0x4ff_9_IMCore 5 | MsgProxy.SendMsg 6 | SQQzoneSvc.shuoshuo 7 | OidbSvc.0x758_1 8 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.DoReply 9 | trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginUnusualDevice 10 | wtlogin.device_lock 11 | OidbSvc.0x758_0 12 | wtlogin_device.tran_sim_emp 13 | OidbSvc.0x4ff_9 14 | trpc.springfestival.redpacket.LuckyBag.SsoSubmitGrade 15 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoReply 16 | trpc.o3.report.Report.SsoReport 17 | SQQzoneSvc.addReply 18 | OidbSvc.0x8a1_7 19 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.DoComment 20 | OidbSvcTrpcTcp.0xf67_1 21 | friendlist.ModifyGroupInfoReq 22 | OidbSvcTrpcTcp.0xf65_1 23 | OidbSvcTrpcTcp.0xf65_10 24 | OidbSvcTrpcTcp.0xf65_10 25 | OidbSvcTrpcTcp.0xf67_5 26 | OidbSvc.0x56c_6 27 | OidbSvc.0x8ba 28 | SQQzoneSvc.like 29 | OidbSvcTrpcTcp.0xf88_1 30 | OidbSvc.0x8a1_0 31 | wtlogin.name2uin 32 | SQQzoneSvc.addComment 33 | wtlogin.login 34 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Access 35 | OidbSvcTrpcTcp.0x101e_2 36 | qidianservice.135 37 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoComment 38 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoBarrage 39 | OidbSvcTrpcTcp.0x101e_1 40 | OidbSvc.0x89a_0 41 | friendlist.addFriend 42 | ProfileService.GroupMngReq 43 | OidbSvc.oidb_0x758 44 | MessageSvc.PbSendMsg 45 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoLike 46 | OidbSvc.0x758 47 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Establish 48 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoPush 49 | qidianservice.290 50 | trpc.qlive.relationchain_svr.RelationchainSvr.Follow 51 | trpc.o3.ecdh_access.EcdhAccess.SsoSecureAccess 52 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.DoFollow 53 | SQQzoneSvc.forward 54 | ConnAuthSvr.sdk_auth_api 55 | wtlogin.qrlogin 56 | wtlogin.register 57 | OidbSvcTrpcTcp.0x6d9_4 58 | trpc.passwd.manager.PasswdManager.SetPasswd 59 | friendlist.AddFriendReq 60 | qidianservice.207 61 | ProfileService.getGroupInfoReq 62 | OidbSvcTrpcTcp.0x1107_1 63 | OidbSvcTrpcTcp.0x1105_1 64 | SQQzoneSvc.publishmood 65 | wtlogin.exchange_emp 66 | OidbSvc.0x88d_0 67 | wtlogin_device.login 68 | OidbSvcTrpcTcp.0xfa5_1 69 | trpc.qqhb.qqhb_proxy.Handler.sso_handle 70 | OidbSvcTrpcTcp.0xf89_1 71 | OidbSvc.0x9fa 72 | FeedCloudSvr.trpc.feedcloud.commwriter.ComWriter.PublishFeed 73 | QChannelSvr.trpc.qchannel.commwriter.ComWriter.PublishFeed 74 | OidbSvcTrpcTcp.0xf57_106 75 | ConnAuthSvr.sdk_auth_api_emp 76 | OidbSvcTrpcTcp.0xf6e_1 77 | trpc.qlive.word_svr.WordSvr.NewPublicChat 78 | trpc.passwd.manager.PasswdManager.VerifyPasswd 79 | trpc.group_pro.msgproxy.sendmsg 80 | OidbSvc.0x89b_1 81 | OidbSvcTrpcTcp.0xf57_9 82 | FeedCloudSvr.trpc.videocircle.circleprofile.CircleProfile.SetProfile 83 | OidbSvc.0x6d9_4 84 | OidbSvcTrpcTcp.0xf55_1 85 | ConnAuthSvr.fast_qq_login 86 | OidbSvcTrpcTcp.0xf57_1 87 | trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey 88 | wtlogin.trans_emp -------------------------------------------------------------------------------- /spring-boot-starter-itbaima-robot/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | net.itbaima 9 | itbaima-robot-starter 10 | 1.0.2 11 | 12 | spring-boot-starter-itbaima-robot 13 | 14 | 15 | 17 16 | 17 17 | UTF-8 18 | 19 | 20 | 21 | 22 | net.itbaima 23 | spring-boot-itbaima-robot 24 | 1.0.2 25 | 26 | 27 | 28 | net.itbaima 29 | spring-boot-autoconfigure-itbaima-robot 30 | 1.0.2 31 | 32 | 33 | 34 | --------------------------------------------------------------------------------