├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── io │ └── github │ └── aplini │ └── chat2qq │ ├── Chat2QQ.java │ ├── bot │ ├── onBotOnline.java │ └── onCardChange.java │ ├── listener │ ├── EventFunc.java │ ├── onAutoResponseMessage.java │ ├── onGroupCommandMessage.java │ ├── onGroupMessage.java │ ├── onPlayerJoin.java │ ├── onPlayerMessage.java │ └── onPlayerQuit.java │ └── utils │ ├── Metrics.java │ ├── Util.java │ ├── _Commander.java │ ├── _OfflinePlayer.java │ ├── __spigot.java │ └── renderGroupMessage.java └── resources ├── config.yml └── plugin.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "maven" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: 设置 JDK 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '17' 23 | distribution: 'zulu' 24 | 25 | - name: 构建 26 | run: mvn clean package 27 | 28 | - name: 上传工件 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: "Chat2QQ.jar" 32 | path: /home/runner/work/Chat2QQ/Chat2QQ/target/Chat2QQ.jar 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | 11 | # Compiled class file 12 | *.class 13 | 14 | # Log file 15 | *.log 16 | 17 | # BlueJ files 18 | *.ctxt 19 | 20 | # Package Files # 21 | *.jar 22 | *.war 23 | *.nar 24 | *.ear 25 | *.zip 26 | *.tar.gz 27 | *.rar 28 | 29 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 30 | hs_err_pid* 31 | 32 | *~ 33 | 34 | # temporary files which can be created if a process still has a handle open of a deleted file 35 | .fuse_hidden* 36 | 37 | # KDE directory preferences 38 | .directory 39 | 40 | # Linux trash folder which might appear on any partition or disk 41 | .Trash-* 42 | 43 | # .nfs files are created when an open file is removed but is still being accessed 44 | .nfs* 45 | 46 | # General 47 | .DS_Store 48 | .AppleDouble 49 | .LSOverride 50 | 51 | # Icon must end with two \r 52 | Icon 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | # Windows thumbnail cache files 74 | Thumbs.db 75 | Thumbs.db:encryptable 76 | ehthumbs.db 77 | ehthumbs_vista.db 78 | 79 | # Dump file 80 | *.stackdump 81 | 82 | # Folder config file 83 | [Dd]esktop.ini 84 | 85 | # Recycle Bin used on file shares 86 | $RECYCLE.BIN/ 87 | 88 | # Windows Installer files 89 | *.cab 90 | *.msi 91 | *.msix 92 | *.msm 93 | *.msp 94 | 95 | # Windows shortcuts 96 | *.lnk 97 | 98 | target/ 99 | 100 | pom.xml.tag 101 | pom.xml.releaseBackup 102 | pom.xml.versionsBackup 103 | pom.xml.next 104 | 105 | release.properties 106 | dependency-reduced-pom.xml 107 | buildNumber.properties 108 | .mvn/timing.properties 109 | .mvn/wrapper/maven-wrapper.jar 110 | .flattened-pom.xml 111 | 112 | # Common working directory 113 | run/ 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chat2QQ+ 2 | 3 | 这个插件专注于实现更好的消息显示, 并以模块的方式集成了一些其他的功能 4 | 5 | - **🚧 说明** 6 | - [ApliNi/Chat2QQ](https://github.com/ApliNi/Chat2QQ) 是包含 [DreamVoid/Chat2QQ](https://github.com/DreamVoid/Chat2QQ) 几乎所有功能的插件, 经过重写并添加了许多功能. 7 | - 如果你有任何想法请点击 [`Issues`](https://github.com/ApliNi/Chat2QQ/issues), 打开一个功能请求. 如果我能做到就会去实现出来. 8 | - 如果您使用默认配置, 那么还需要开启 MiraiMC 插件配置中的 `bot.contact-cache.enable-group-member-list-cache`, 才能保证软件正常运行. 9 |

10 | 11 | - **📦 下载** 12 | - 在 [`Releases`](https://github.com/ApliNi/Chat2QQ/releases) 中下载最新的jar文件. 13 | - 如果需要开发版本, 可以点击 [`Actions`](https://github.com/ApliNi/Chat2QQ/actions), 打开第一个, 找到最下面的jar文件. 14 |

15 | 16 | - **📄 其他** 17 | - 订阅更新: 点击右上角 `Watch` 按钮, 选择 `Custom` 中的 `Releases` ! 18 | - 使用量统计: [bStats](https://bstats.org/plugin/bukkit/ApliNi-Chat2QQ/17587) 19 |

20 | 21 | 22 | ![](https://bstats.org/signatures/bukkit/ApliNi-Chat2QQ.svg) 23 | 24 | 25 |
26 | 27 | ### 更新计划 28 | 29 | - [x] 聊天消息转发 30 | - [x] 格式化 31 | - [x] 多群组支持 32 | - [x] 消息预处理功能 33 | - [x] 多行文本和长文本显示优化 34 | - [x] 引用回复显示优化 35 | - [x] 群名片过滤 36 | - [x] 艾特显示优化 37 | - [x] 用户黑名单 38 | 39 | - [x] 事件处理 40 | - [x] 玩家加入退出 41 | 42 | --- 43 | 这些功能在可配置的模块中 44 | 45 | - [x] 运行指令 46 | - [x] 返回指令输出消息 47 | - [x] 更好的等待指令返回 48 | - [x] 权限管理 49 | - [x] 继承 50 | - [x] 指令黑名单 51 | 52 | - [x] 从群名片中匹配游戏名称 53 | 54 | - [x] 消息预处理 55 | - [x] 匹配方式: 前缀 / 包含 / 相等 / 正则 56 | - [x] 支持让匹配到的消息完全不发送 57 | 58 | - [x] 按行预处理指令的返回消息 59 | - [x] 支持(匹配方式) 60 | 61 | - [x] 按合并后的完整消息预处理指令返回消息 62 | - [x] 支持(匹配方式) 63 | - [x] 添加一些特殊的占位符 64 | 65 | - [x] 预设的格式调整功能 66 | - [x] 删除消息中的格式化字符 67 | - [x] 删除消息前后空格和空行 68 | - [x] 删除群名片中的格式化代码 69 | - [x] 将长文本转换为鼠标悬浮显示文本 70 | - [x] 更好的多行消息 71 | - [x] 将聊天消息转发到控制台 72 | 73 | - [x] 引用回复优化 74 | - [x] 格式化 75 | - [x] 删除重复的艾特 76 | 77 | - [x] `/qchat` 78 | - [x] 区分玩家运行指令和控制台运行指令 79 | 80 | - [x] 群成员信息缓存 \[前置\] 81 | - [x] 自动更新数据 82 | - [x] 支持使用外部 Mirai 的缓存文件 83 | 84 | - [x] 艾特显示优化 85 | 86 | - [x] 事件任务 87 | - [x] 群成员加入 88 | - [x] 群成员退出 89 | 90 | - [x] 自动回复 91 | - [x] 支持(匹配方式) 92 | - [x] 支持 PAPI 93 | 94 |
95 | 96 | 97 | ### ✨ 图片 98 | 99 | 运行指令 100 | 101 | ![image](https://user-images.githubusercontent.com/59365724/227127511-a149cc33-9683-4bd9-9640-fd938546859d.png) 102 | 103 | 104 | 群名片过滤 105 | 106 | ![image](https://user-images.githubusercontent.com/59365724/227157788-d9259da8-46e6-438e-b32b-3926a2d3a9a3.png) 107 | ![image](https://user-images.githubusercontent.com/59365724/227157812-a0245843-66e9-44bc-8bc2-e0935ebc2d2b.png) 108 | 109 | 110 | 特殊消息预处理 111 | 112 | ![image](https://user-images.githubusercontent.com/59365724/227128189-8f217293-04e9-472d-a09e-daad7eef79f0.png) 113 | ![image](https://user-images.githubusercontent.com/59365724/227128209-f6be73c2-efd1-4ca2-b85b-b2c2b84dad7d.png) 114 | 115 | 116 | 多行文本和长文本显示优化 117 | 118 | ![image](https://user-images.githubusercontent.com/59365724/227128013-013e0514-771e-4075-8d4d-c28557cfa126.png) 119 | ![image](https://user-images.githubusercontent.com/59365724/227127980-628d662d-cc47-4468-9437-2e5298a6d6c3.png) 120 | 121 | ![image](https://user-images.githubusercontent.com/59365724/227158659-b35ac9b3-113e-4d3d-80d7-f214c8064b02.png) 122 | ![image](https://user-images.githubusercontent.com/59365724/227158697-45a16e51-f4b1-4f97-8296-03d4074d3efb.png) 123 | 124 | 125 | 126 | 引用回复显示优化 127 | 128 | ![image](https://user-images.githubusercontent.com/59365724/227128959-ff5fcdab-f09e-4ed8-bee9-08ade7cf7ef3.png) 129 | ![image](https://user-images.githubusercontent.com/59365724/227128919-092545b6-6d38-4324-b8b8-1903cb522a05.png) 130 | 131 | ![image](https://user-images.githubusercontent.com/59365724/227159155-8a731828-c929-44f8-b0c1-5f380da403cc.png) 132 | ![image](https://user-images.githubusercontent.com/59365724/227159055-02e5cdcd-3a5e-4c20-8fda-85b0df90062a.png) 133 | 134 | ![image](https://user-images.githubusercontent.com/59365724/227159023-7333b385-c687-45a4-965b-e9b066af0e6f.png) 135 | ![image](https://user-images.githubusercontent.com/59365724/227159083-e8d6b7e7-9ba6-41e8-9562-e7a174f87be5.png) 136 | 137 | 138 | 更好的名称显示 139 | 140 | ![image](https://user-images.githubusercontent.com/59365724/227157070-c8fa51ca-b623-4167-a7ef-f02239d84aef.png) 141 | ![image](https://user-images.githubusercontent.com/59365724/227157097-d6b226e4-2158-4b3b-a36b-db21acfeff2e.png) 142 | 143 | 144 | 自动回复 145 | 146 | ![image](https://user-images.githubusercontent.com/59365724/227129074-9a8316e1-8b8f-4abe-9e74-ad73f212f9ec.png) 147 | 148 | 149 | ### 完整配置和功能 150 | ```yaml 151 | 152 | # 游戏内配置 153 | # QQ -> MC 的消息 154 | general: 155 | # 转发哪些QQ群的消息 156 | group-ids: 157 | - 1000000 158 | - 1000001 159 | 160 | # 群聊天前缀 (聊天需要带有指定前缀才能发送到服务器) 161 | requite-special-word-prefix: 162 | enabled: false 163 | prefix: 164 | - '#' 165 | 166 | # 当群名片不存在时是否尝试获取昵称 167 | use-nick-if-namecard-null: true 168 | 169 | # QQ群消息广播到游戏内聊天的格式 格式化代码: § 170 | # %groupname% - 群名称 171 | # %groupid% - 群号 172 | # %nick% - 发送者群名片 173 | # %regex_nick% - 使用正则匹配到的名称, 需要开启 aplini.cleanup-name 模块 174 | # %qq% - 发送者QQ号 175 | # %message% - 消息内容, 支持预处理模块 aplini.pretreatment 176 | # %_reply_% - 如果是回复消息..., 配置在 aplini.reply-message 模块 177 | in-game-chat-format: '§f[§7%nick%§r§f] %_reply_%§7%message%' 178 | # 为每个群使用不同的格式, 如果没有则使用上方的 in-game-chat-format 179 | special: 180 | 1000000: '§f[§7主群 %nick%§r§f] %_reply_%§7%message%' 181 | 1000001: '§7[外群 %nick%] %_reply_%%message%' 182 | 183 | # 启用 MiraiMC 内置的QQ绑定 184 | use-miraimc-bind: false 185 | # 已绑定玩家的广播消息格式 186 | bind-chat-format: '§f[§7%nick%§r§f] %_reply_%§7%message%' 187 | # 为每个群使用不同的格式, 如果没有则使用上方的 bind-chat-format 188 | special-bind: 189 | 1000000: '§f[§7主群 %nick%§r§f] %_reply_%§7%message%' 190 | 191 | 192 | 193 | # 机器人配置 194 | # MC -> QQ 的消息 195 | bot: 196 | # 使用哪些QQ号处理消息 197 | # 只能添加一个 198 | bot-accounts: 199 | - 2000000 200 | 201 | # 将消息转发到那些QQ群 202 | group-ids: 203 | - 1000000 204 | 205 | # 玩家在以下世界中聊天才会被转发 206 | available-worlds: 207 | #- world 208 | # 将以上配置作为黑名单, 玩家不在以上世界中聊天才会被转发 209 | available-worlds-use-as-blacklist: true 210 | 211 | # 游戏聊天前缀 (聊天需要带有指定前缀才能发送到QQ群) 212 | requite-special-word-prefix: 213 | enabled: true 214 | prefix: 215 | - '#' 216 | 217 | # 是否转发被其他插件取消过的聊天消息事件, 用于修复一些兼容性问题 218 | ignoreCancelled: false 219 | 220 | # 服务器消息发送到QQ群的格式 221 | # %player% - 玩家名称 222 | # %message% - 消息内容 223 | group-chat-format: '[%player%] %message%' 224 | 225 | 226 | # 是否发送玩家进出服务器的消息 227 | # %player% - 玩家显示昵称 228 | send-player-join-quit-message: false 229 | # 加入 230 | player-join-message: '%player% 进入服务器' 231 | # 退出 232 | player-quit-message: '%player% 离开服务器' 233 | # 防刷屏, 在此时间内多次进出服务器不会发送消息 234 | player-join-quit-message-interval: 0 235 | 236 | 237 | 238 | # 黑名单, 可用于添加其他QQ机器人 239 | # 优先级大于上方配置 240 | blacklist: 241 | # 不转发以下QQ号的聊天消息 242 | qq: 243 | #- 2000001 244 | 245 | # 不转发以下玩家名的聊天消息 246 | player: 247 | #- playerName 248 | 249 | 250 | # ############### # 251 | # 以下为功能模块配置 # 252 | # ############### # 253 | 254 | aplini: 255 | 256 | ## 1 257 | # 在QQ群中运行指令 [需要单独添加QQ群] 258 | # 此模块不处理黑名单 blacklist 259 | run-command: 260 | enabled: false 261 | # 启用的 QQ群 262 | qq-group: 263 | - 1000001 264 | 265 | # 指令前缀, 可以是多个字符, 比如 "~$" 266 | command-prefix: '/' 267 | # 指令最大长度 (不包括指令前缀) 268 | command-max-length: 255 269 | # 获取指令的正则表达式, 当第一个捕获组的内容与指令白名单中的匹配时则允许运行 (不带斜杠或前缀) 270 | regex-command-main: '^([^ ]+)' 271 | # 判断指令返回为空的正则, 匹配多行文本. (经过 pretreatment-command-message 处理后) 272 | return-isNull: '^\s*$' 273 | # 是否将主命令转换为小写再执行 274 | always-lowercase: false 275 | 276 | # 是否发送指令的输出, 关闭可提高性能或解决一些兼容性问题 277 | return: true 278 | # 等待指令运行多长时间再将结果发送到QQ群 (毫秒), 需要开启 run-command.return 279 | # 如果你遇到了一些提前输出类似 "正在运行...请稍等" 消息的插件, 可以在 pretreatment-command-message 中配置完全删除这条消息. 然后 return-sleep-min 保持不变 :) 280 | return-sleep-min: 14 # 最小等待时间 281 | return-sleep-max: 5346 # 最大等待时间, 如果一些长耗时指令没有输出请增大此值 282 | return-sleep-sampling-interval: 172 # 输出内容检查间隔, 如果经常执行长耗时指令可以增大此值 283 | # 是否将指令的输出打印到控制台和日志 284 | return-log: true 285 | 286 | # 执行不在白名单中的指令时发送返回消息 287 | message-miss: '未命中的指令' 288 | # 运行无返回指令的消息 289 | message-no-out: '运行无返回指令' 290 | 291 | # 设置各组可执行的主命令白名单 (不带斜杠或前缀) 292 | # 权限更高的用户将可以使用更低的用户的指令 293 | # 如果添加一条 ___ALL_COMMAND___ 作为指令, 则表示此组可以使用所有指令, 此功能请勿随意使用 ! 294 | group: 295 | # permission_ 是 MiraiMC 获取到的权限数字, 以后更新了其他权限只需要以此格式添加即可使用 296 | permission_2: # 群主 297 | #- chat2qq 298 | permission_1: # 管理员 299 | #- spark 300 | permission_0: # 成员 301 | #- list 302 | #- tps 303 | 304 | # 特殊指令配置 305 | special: 306 | no-return: # 这些指令始终不输出消息 307 | #- plugins 308 | #- version 309 | 310 | 311 | ## 2 312 | # 从 群名片(%nick%) 中匹配 MC 可用的游戏名称 313 | # 添加变量: %regex_nick% - 使用正则匹配到的名称, 需要开启 cleanup-name 功能 314 | cleanup-name: 315 | enabled: false 316 | # 程序取第一个捕获组的结果 317 | regex: '([a-zA-Z0-9_]{3,16})' 318 | # 如果匹配不到, 则使用以下字符串 319 | # %nick% - 群名片 320 | # %qq% - qq号 321 | not-captured: '%nick%' 322 | 323 | 324 | ## 3 325 | # 预处理 %message% 中的消息 326 | pretreatment: 327 | enabled: true 328 | # **使用方法** 329 | # list: 330 | # - 匹配方式: prefix (前缀匹配), 处理方式: to_all, to_replace 331 | # contain (包含), 处理方式: to_all, to_replace 332 | # equal (完全相等), 处理方式: to_all 333 | # regular (正则匹配), 处理方式: to_all, to_regular 334 | # 335 | # 处理方式: to_all (替换整条消息) 336 | # to_replace (替换匹配到的部分) 337 | # to_regular (使用正则替换, 可使用正则变量) 338 | # 339 | # 是否发送: send (填写 send 配置将取消转发送匹配到的消息, 不需要时请忽略) 340 | 341 | # 示例配置, 默认配置了一些可能有用的功能: 342 | list: 343 | 344 | # 群公告, JSON 345 | - prefix: '{"app":"com.tencent.mannounce"' 346 | to_all: '[群公告]' 347 | 348 | # 视频, 字符串 349 | - prefix: '你的QQ暂不支持查看视频短片' 350 | to_all: '[视频]' 351 | 352 | # 使中括号与文本的前后始终有空格 353 | - regular: '\[([^\]]+)\]([^\s])' 354 | to_regular: '[$1] $2' 355 | - regular: '([^\s])\[([^\]]+)\]' 356 | to_regular: '$1 [$2]' 357 | 358 | # 转发消息使用前缀, 在群中使用 # 前缀将改变消息格式 359 | - regular: '^\s*(?:#|#)' 360 | to_regular: '§7> §f' 361 | 362 | # 示例: 取消发送包含此内容的消息 363 | #- contain: '此内容' 364 | # send: false 365 | 366 | 367 | ## 3.1 368 | # 按行预处理指令返回消息, 用于处理返回到QQ群的消息 369 | pretreatment-command-message: 370 | enabled: true 371 | # 使用方法: 如上 372 | list: 373 | # 删除格式化字符 374 | - regular: '§[a-z0-9]' 375 | to_regular: '' 376 | 377 | # 示例: co插件翻页消息处理 378 | #- regular: '◀? ?第 (.*) 页 ▶? ?\((.*)\)' 379 | # to_regular: '第 $1 页, 使用 /co page <页码> 翻页' 380 | 381 | 382 | ## 3.2 383 | # 按多行文本预处理指令返回消息 384 | # 可使用占位符: 385 | # - %command% :: 用户运行的指令原文(不带斜杠/前缀) 386 | # - %time% :: 指令运行耗时 387 | # - %qq% :: 执行指令的qq号 388 | # - %group% :: 执行指令的群号 389 | pretreatment-command-message-all: 390 | enabled: false 391 | enabled-placeholder: false # 关闭占位符可提高性能 392 | # 使用方法: 如上 393 | list: 394 | # 示例: 显示指令运行时间, 需要开启占位符 395 | #- regular: '([\s\S]+)' 396 | # to_regular: '$1\n - 运行耗时: %time%ms' 397 | 398 | 399 | ## 4 400 | # 预设的格式调整功能 401 | other-format-presets: 402 | # 是否删除 %message% 消息 中的格式化字符 403 | render-message_format-code: false 404 | # 删除 %message% 消息 前后的空格和空行 405 | message-trim: true 406 | # 是否删除 %nick% 群名片 中的格式化字符 407 | render-nick_format-code: true 408 | 409 | # 聊天消息过长时转换为悬浮文本 410 | long-message: 411 | enabled: true 412 | # 以下任意一个条件成立时被判定为长消息, 若需取消一个, 请改为很大的数 413 | # 条件1: 消息长度达到此值 414 | condition-length: 210 415 | # 条件2: 换行数量达到此值, 在 message-trim 之后运行 416 | condition-line_num: 6 417 | # 显示为 418 | message: '§f[§7长消息§f]' 419 | 420 | # 是否启用 "更好的多行消息" 421 | multiline-message: 422 | enabled: true 423 | line-0: '' # [多行消息] 424 | line-prefix: ' ' 425 | 426 | # 是否将聊天消息转发到控制台/日志 427 | message-to-log: true 428 | 429 | 430 | ## 5 431 | # 引用回复 432 | # 添加变量: %_reply_% 433 | # 如果是回复消息, 则为变量赋值并为消息添加悬浮文本框用于显示内容. 可以将鼠标悬停在消息上查看回复的内容 434 | reply-message: 435 | # 可用变量: 436 | # %qq% - 被回复的消息的发送者QQ号 437 | # %c_name% - 群名片 - 需要开启 aplini.format-qq-id 438 | var: '§f[§7回复 @%c_name%§f] ' 439 | 440 | # 可用变量: 441 | # %_/n_% - 换行 442 | # %qq% - 被回复的消息的发送者QQ号 443 | # %c_name% - 群名片 - 需要开启 aplini.format-qq-id 444 | # %message% - 回复内容 445 | # %main_message% - 当前消息内容 446 | message: '§f[§7引用 @%c_name%§f]%_/n_%§7%message%§r%_/n_%%_/n_%§f——%main_message%' 447 | 448 | # 删除重复@ :: 如果引用回复对象等于消息开头的@对象, 则删除消息开头的 @ 449 | del-duplicates-at: true 450 | 451 | 452 | ## 6 453 | # 发送消息的指令 454 | # /qchat <消息> - 使用此指令 455 | qchat: 456 | # 使用上方 general.group-ids 中配置的群 457 | use-general-group-ids: true 458 | # 消息转发到哪些群, 需要 use-general-group-ids: false 459 | group-ids: 460 | - 1000000 461 | # 如果是玩家使用指令 462 | player: 463 | # 转发到QQ群的格式 464 | # %name% - 玩家名称 465 | # %message% - 消息 466 | qq-format: '[%name%] %message%' 467 | # 是否同时将消息广播到MC服务器 468 | mc-broadcast: true 469 | # 广播到MC服务器的 470 | mc-format: '§f[§7%name%§f] §7%message%' 471 | # 如果是控制台或插件使用指令, 同时绕过关键词和玩家黑名单 472 | console: 473 | # %message% - 消息 474 | qq-format: '%message%' 475 | 476 | 477 | ## 7 478 | # [前置] 群成员信息缓存 479 | # ! 需要开启 MiraiMC 配置中的 bot.contact-cache.enable-group-member-list-cache 480 | player-cache: 481 | # 在机器人登录和服务器启动时运行此程序 482 | enabled: true 483 | # 在玩家群名片修改时更新缓存 484 | auto-update: true 485 | # 在玩家发送消息时更新缓存 486 | auto-update-form-msg: true 487 | # 群名片修改时发出日志 488 | auto-update-log: true 489 | # 使用上方 general.group-ids 中配置的群 490 | use-general-group-ids: true 491 | # 缓存哪些群, 需要 use-general-group-ids: false 492 | group-ids: 493 | - 1000000 494 | # MiraiMC 群缓存文件路径, 如果你修改了插件目录相关的配置, 才需要修改它 495 | # %qq% - 机器人账号 496 | # %group% - 群号 497 | mirai-cache-path: "plugins/MiraiMC/MiraiBot/bots/%qq%/cache/contacts/groups/%group%.json" 498 | 499 | 500 | ## 8 501 | # 将 %message% 中的 @qqID 替换为 @名称 502 | # 需要开启前置: aplini.player-cache 503 | format-qq-id: 504 | enabled: true 505 | # 用于匹配 @qqID 的正则 506 | regular: '(@[0-9]{5,11})' 507 | # 格式 508 | # %qq% - qq号 509 | # %name% - 名称 510 | format: '§f[§7@%name%§f]§7' 511 | # 一条消息最多匹配几次, 防止刷屏浪费性能 512 | max-cycles-num: 10 513 | 514 | 515 | ## 9 516 | # 事件任务 517 | event-func: 518 | # enable 修改后需要重启服务器 519 | enable: false 520 | # 使用上方 general.group-ids 中配置的群 521 | use-general-group-ids: true 522 | # 启用在哪些群, 需要 use-general-group-ids: false 523 | group-ids: 524 | - 1000000 525 | 526 | # 每个事件可用的任务不同, 这里列出了所有任务的使用方法: 527 | # - command: 'command' - 发送指令 528 | 529 | # - message-text: '消息' - 向事件来源发送消息, 群 或 好友/私聊 530 | 531 | # - message-group: 1000000 - 向指定群发送消息 532 | # message-text: '消息' 533 | 534 | # - message-friend: 2000003 - 向指定好友发送消息 535 | # message-text: '消息' 536 | 537 | MiraiMemberJoinEvent: # 群成员加入 538 | # 可使用: command, message-text, message-group 539 | # 一个事件中可添加多个相同或不相同的任务, 就像这样: 540 | #- message-text: '欢迎' 541 | #- command: 'tps' 542 | #- command: 'mspt' 543 | #- message-group: 1000000 544 | # message-text: '消息' 545 | 546 | MiraiMemberLeaveEvent: # 成员退出 547 | # 可使用: command, message-text, message-group 548 | 549 | 550 | ## 10 551 | # 自动回复 552 | # 当QQ群中的消息匹配时发送自定义消息 553 | auto-response: 554 | enable: true 555 | # 使用上方 general.group-ids 中配置的群 556 | use-general-group-ids: true 557 | # 回复哪些群的消息, 需要 use-general-group-ids: false 558 | group-ids: 559 | - 1000000 560 | # 为此功能启用PAPI, 需要安装PAPI插件 561 | enable-papi: false 562 | 563 | # **使用方法** 564 | # list: 565 | # - 匹配方式: prefix (前缀匹配) 566 | # contain (包含) 567 | # equal (完全相等) 568 | # regular (正则匹配, send 中可使用正则变量) 569 | # send (发送的消息内容) 570 | # 571 | # > 正则的性能较差, 请尽量避免使用很多正则 572 | # !! 请小心使用正则拼接PAPI变量, 如果正则设计有问题则可能出现注入漏洞 !! 573 | # - 提示: 应指定匹配的字符范围和最小最大次数, 要绝对的防止输入PAPI变量的保留符号: % 574 | # - 比如: - regular: '^\#ping ([a-zA-Z0-9_]{3,16})$' 575 | # send: '$1 的延迟为: %player_ping_$1%ms' 576 | # 示例配置, 默认配置了一些可能有用的功能: 577 | list: 578 | 579 | # 使用PAPI获取在线玩家数量, 需要启用 aplini.auto-response.enable-papi 580 | # PlayerList: /papi ecloud download playerlist 581 | - equal: '#list' 582 | send: '在线玩家: [%playerlist_online,normal,yes,amount%] \n%playerlist_online,normal,yes,list%' 583 | 584 | # 使用PAPI获取服务器TPS, 需要启用 aplini.auto-response.enable-papi 585 | # Server: /papi ecloud download Server 586 | - equal: '#tps' 587 | send: 'TPS [1m, 5m, 15m]: %server_tps_1% / %server_tps_5% / %server_tps_15%' 588 | 589 | # 指令列表 590 | - equal: '#help' 591 | send: '指令列表: 592 | \n - #list - 显示在线玩家列表 593 | \n - #tps - 显示服务器TPS' 594 | 595 | # @一个QQ号时发送消息 596 | - contain: '@2000000' 597 | send: 'OwO' 598 | 599 | 600 | # <- 至此, 您已经完成了所有配置, 部分功能使用 /chat2qq reload 重载插件即可应用 uwu 601 | 602 | ``` 603 | 604 | 605 | ### 指令和权限 606 | 607 | - `qchat <消息>` - 发送消息到群, 详细配置在 `aplini.qchat` 608 | - `chat2qq` - 插件主命令 & 帮助信息 609 | - `chat2qq setgroupcacheall` - 重建群成员缓存数据 610 | - `chat2qq outgroupcacheall` - 打印群成员缓存数据 611 | 612 | plugin.yml 613 | ```yaml 614 | commands: 615 | qchat: 616 | description: 发送聊天消息到QQ群 617 | permission: chat2qq.command.qchat 618 | 619 | chat2qq: 620 | description: Chat2QQ 插件主命令 621 | 622 | permissions: 623 | chat2qq.qq.receive: 624 | description: 允许收到来自QQ群的消息 625 | default: true 626 | 627 | chat2qq.chat.requite: 628 | description: 允许使用前缀符号转发消息到QQ群 629 | default: true 630 | 631 | chat2qq.join.silent: 632 | description: 允许悄悄加入服务器 633 | default: false 634 | 635 | chat2qq.quit.silent: 636 | description: 允许悄悄离开服务器 637 | default: false 638 | 639 | chat2qq.command.qchat: 640 | description: 允许使用 /qchat 641 | default: op 642 | 643 | chat2qq.command.chat2qq: 644 | description: 允许使用 /chat2qq 645 | default: op 646 | 647 | chat2qq:.command.setgroupcacheall: 648 | description: 允许使用 /chat2qq setgroupcacheall 649 | default: op 650 | ``` 651 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.aplini 8 | Chat2QQ 9 | 1.8.11 10 | 11 | Chat2QQ 12 | 13 | 14 | 1.8 15 | UTF-8 16 | 17 | 18 | 19 | ${project.artifactId} 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-compiler-plugin 24 | 3.14.0 25 | 26 | 11 27 | 11 28 | 29 | 30 | 31 | 32 | 33 | src/main/resources 34 | true 35 | 36 | 37 | 38 | 39 | 40 | 41 | spigotmc-repo 42 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 43 | 44 | 45 | sonatype 46 | https://oss.sonatype.org/content/groups/public/ 47 | 48 | 49 | placeholderapi 50 | https://repo.extendedclip.com/content/repositories/placeholderapi/ 51 | 52 | 53 | 54 | 55 | 56 | org.spigotmc 57 | spigot-api 58 | 1.20.4-R0.1-SNAPSHOT 59 | provided 60 | 61 | 62 | io.github.dreamvoid 63 | MiraiMC-Integration 64 | 1.9-pre3 65 | provided 66 | 67 | 68 | org.jetbrains 69 | annotations 70 | 26.0.2 71 | compile 72 | 73 | 74 | 75 | com.google.code.gson 76 | gson 77 | 2.13.1 78 | 79 | 80 | 81 | me.clip 82 | placeholderapi 83 | 2.11.6 84 | provided 85 | 86 | 87 | 88 | 89 | jakarta.websocket 90 | jakarta.websocket-client-api 91 | 2.1.0 92 | provided 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/Chat2QQ.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq; 2 | 3 | import io.github.aplini.chat2qq.bot.onBotOnline; 4 | import io.github.aplini.chat2qq.bot.onCardChange; 5 | import io.github.aplini.chat2qq.listener.*; 6 | import io.github.aplini.chat2qq.utils.Metrics; 7 | import org.bukkit.Bukkit; 8 | import org.bukkit.command.Command; 9 | import org.bukkit.command.CommandExecutor; 10 | import org.bukkit.command.CommandSender; 11 | import org.bukkit.command.TabExecutor; 12 | import org.bukkit.entity.Player; 13 | import org.bukkit.event.Listener; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | import org.jetbrains.annotations.NotNull; 16 | 17 | import java.util.*; 18 | 19 | import static io.github.aplini.chat2qq.utils.Util._setGroupCacheAll; 20 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 21 | 22 | public class Chat2QQ extends JavaPlugin implements Listener, CommandExecutor, TabExecutor { 23 | public static Chat2QQ plugin; 24 | // Map<群号, Map> 25 | public static Map> group_cache_all = new HashMap<>(); 26 | 27 | @Override // 加载插件 28 | public void onLoad() { 29 | plugin = this; 30 | plugin.saveDefaultConfig(); 31 | plugin.reloadConfig(); 32 | } 33 | 34 | @Override // 启用插件 35 | public void onEnable() { 36 | 37 | // 注册事件 38 | 39 | // 机器人上线 40 | Bukkit.getPluginManager().registerEvents(new onBotOnline(this), this); 41 | // 群成员修改名片 :: 群成员缓存 42 | Bukkit.getPluginManager().registerEvents(new onCardChange(), this); 43 | 44 | // 群成员发送消息 45 | Bukkit.getPluginManager().registerEvents(new onGroupMessage(this), this); 46 | // 群成员发送消息 :: 指令 47 | Bukkit.getPluginManager().registerEvents(new onGroupCommandMessage(this), this); 48 | // 群成员发送消息 :: 自动回复 49 | Bukkit.getPluginManager().registerEvents(new onAutoResponseMessage(this), this); 50 | // 玩家发送消息 51 | Bukkit.getPluginManager().registerEvents(new onPlayerMessage(this), this); 52 | // 玩家加入退出 53 | Bukkit.getPluginManager().registerEvents(new onPlayerJoin(this), this); 54 | Bukkit.getPluginManager().registerEvents(new onPlayerQuit(this), this); 55 | 56 | // 事件任务程序 57 | if(getConfig().getBoolean("aplini.event-func.enable", false)){ 58 | Bukkit.getPluginManager().registerEvents(new EventFunc(this), this); 59 | } 60 | 61 | // 注册指令 62 | Objects.requireNonNull(getCommand("qchat")).setExecutor(this); 63 | Objects.requireNonNull(getCommand("chat2qq")).setExecutor(this); 64 | 65 | // PAPI 66 | if(Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null){ 67 | Bukkit.getPluginManager().registerEvents(this, this); 68 | } 69 | 70 | // bStats 71 | new Metrics(this, 17587); 72 | } 73 | 74 | // 指令 75 | @Override 76 | public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { 77 | if(command.getName().equalsIgnoreCase("qchat")){ 78 | if(! sender.hasPermission("chat2qq.command.qchat")){ 79 | sender.sendMessage("§f[§7Chat2QQ§f] §7没有权限"); 80 | return false; 81 | } 82 | 83 | if(args.length == 0){ 84 | sender.sendMessage("/qchat "); 85 | return false; 86 | } 87 | 88 | // 获取消息内容 89 | String message = String.join(" ", args); 90 | 91 | String formatText; 92 | 93 | // 是否为玩家 94 | if(sender instanceof Player){ 95 | String name = ((Player) sender).getDisplayName(); 96 | // 黑名单. 玩家名 97 | if(getConfig().getStringList("blacklist.player").contains(name)){ 98 | return false; 99 | } 100 | // 黑名单. 关键词 101 | if(getConfig().getStringList("blacklist.word").stream().anyMatch(message :: contains)){ 102 | return false; 103 | } 104 | // 广播到服务器 105 | if(getConfig().getBoolean("aplini.qchat.player.mc-broadcast", true)){ 106 | Bukkit.broadcastMessage( 107 | getConfig().getString("aplini.qchat.player.mc-format", "mc-format") 108 | .replace("%name%", name) 109 | .replace("%message%", message) 110 | ); 111 | } 112 | // 发送到群的消息格式 113 | formatText = getConfig().getString("aplini.qchat.player.qq-format", "qq-format") 114 | .replace("%name%", name) 115 | .replace("%message%", message); 116 | }else{ 117 | // 发送到群的消息格式 118 | formatText = getConfig().getString("aplini.qchat.console.qq-format", "qq-format") 119 | .replace("%message%", message); 120 | } 121 | 122 | // 发送到群 123 | getConfig().getLongList( 124 | getConfig().getBoolean("aplini.qchat.use-general-group-ids", true)? "general.group-ids" : "aplini.qchat.group-ids") 125 | .forEach(gid -> sendToGroup(this, gid, formatText)); 126 | } 127 | 128 | else if(command.getName().equalsIgnoreCase("chat2qq")){ 129 | if(args.length == 0){ 130 | sender.sendMessage("§7 | "); 131 | sender.sendMessage("§7 //| |\\ | _| |\\ _|"); 132 | sender.sendMessage("§7 | "); 133 | sender.sendMessage("§f[§7Chat2QQ§f] §7https://github.com/ApliNi/Chat2QQ"); 134 | sender.sendMessage("指令列表: "); 135 | sender.sendMessage(" - /qchat <消息> §7- 发送一条消息到QQ群中"); 136 | sender.sendMessage(" - /chat2qq reload §7- 重载配置"); 137 | sender.sendMessage(" - /chat2qq setgroupcacheall §7- 重建群成员缓存数据"); 138 | sender.sendMessage(" - /chat2qq outgroupcacheall §7- 打印群成员缓存数据"); 139 | return true; 140 | } 141 | else if(args[0].equalsIgnoreCase("reload")){ 142 | if(! sender.hasPermission("chat2qq.command.chat2qq")){ 143 | sender.sendMessage("§f[§7Chat2QQ§f] §7没有权限"); 144 | return false; 145 | } 146 | reloadConfig(); 147 | sender.sendMessage("§f[§7Chat2QQ§f] §7已重载配置"); 148 | return true; 149 | } 150 | else if(args[0].equalsIgnoreCase("setgroupcacheall")){ 151 | if(! sender.hasPermission("chat2qq.command.setgroupcacheall")){ 152 | sender.sendMessage("§f[§7Chat2QQ§f] §7没有权限"); 153 | return false; 154 | } 155 | // 未开启此功能 156 | if(! getConfig().getBoolean("aplini.player-cache.enabled", true)){ 157 | sender.sendMessage("§f[§7Chat2QQ§f] §7此功能未开启: aplini.player-cache.enabled"); 158 | return false; 159 | } 160 | sender.sendMessage("§f[§7Chat2QQ§f] §f群成员缓存程序已启动"); 161 | _setGroupCacheAll(this); 162 | return true; 163 | } 164 | else if(args[0].equalsIgnoreCase("outgroupcacheall")){ 165 | if(! sender.hasPermission("chat2qq.command.chat2qq")){ 166 | sender.sendMessage("§f[§7Chat2QQ§f] §7没有权限"); 167 | return false; 168 | } 169 | sender.sendMessage("§f[§7Chat2QQ§f] §7群成员缓存: \n" + group_cache_all); 170 | return true; 171 | } 172 | } 173 | return false; 174 | } 175 | 176 | // 指令补全 177 | @Override 178 | public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String s, @NotNull String[] args) { 179 | if (command.getName().equalsIgnoreCase("chat2qq")) { 180 | if (args.length == 1) { 181 | List list = new ArrayList<>(); 182 | list.add("reload"); // 重载配置 183 | list.add("setgroupcacheall"); // 启动群成员缓存 184 | list.add("outgroupcacheall"); // 打印群成员缓存数据 185 | return list; 186 | } 187 | } 188 | return null; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/bot/onBotOnline.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.bot; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import me.dreamvoid.miraimc.bukkit.event.bot.MiraiBotOnlineEvent; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.Listener; 7 | import org.bukkit.event.server.ServerLoadEvent; 8 | 9 | import java.util.concurrent.CompletableFuture; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import static io.github.aplini.chat2qq.utils.Util._setGroupCacheAll; 13 | import static org.bukkit.Bukkit.getLogger; 14 | 15 | public class onBotOnline implements Listener { 16 | private final Chat2QQ plugin; 17 | public onBotOnline(Chat2QQ plugin){ 18 | this.plugin = plugin; 19 | } 20 | 21 | static boolean alreadyRunning = false; 22 | 23 | @EventHandler // 机器人登录 24 | public void onMiraiBotOnlineEvent(MiraiBotOnlineEvent e) { 25 | // 启用群缓存功能 26 | if(plugin.getConfig().getBoolean("aplini.player-cache.enabled", true)){ 27 | // 如果这是已配置的机器人 28 | if(plugin.getConfig().getLongList("bot.bot-accounts").contains(e.getBotID())){ 29 | alreadyRunning = true; 30 | getLogger().info("[Chat2QQ] 群成员缓存程序已启动..."); 31 | _setGroupCacheAll(plugin); 32 | } 33 | } 34 | } 35 | 36 | 37 | @EventHandler // 服务器启动完成 38 | public void onServerLoad(ServerLoadEvent event) { 39 | // 异步 40 | CompletableFuture.runAsync(() -> { 41 | // 无法启动的解决方案 42 | try { 43 | TimeUnit.MILLISECONDS.sleep(1000); 44 | 45 | if(!alreadyRunning){ 46 | getLogger().info("[Chat2QQ] 群成员缓存程序已启动..."); 47 | _setGroupCacheAll(plugin); 48 | } 49 | 50 | } catch (InterruptedException ex) { 51 | throw new RuntimeException(ex); 52 | } 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/bot/onCardChange.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.bot; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import me.dreamvoid.miraimc.bukkit.event.group.member.MiraiMemberCardChangeEvent; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.Listener; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | import static io.github.aplini.chat2qq.Chat2QQ.plugin; 14 | import static io.github.aplini.chat2qq.utils.Util.isGroupInConfig; 15 | import static org.bukkit.Bukkit.getLogger; 16 | 17 | public class onCardChange implements Listener { 18 | 19 | @EventHandler // 成员群名片修改 20 | public void onMiraiMemberCardChangeEvent(MiraiMemberCardChangeEvent e) { 21 | if(plugin.getConfig().getBoolean("aplini.player-cache.auto-update", true)){ 22 | updateMemberCardChange(e.getBotID(), e.getGroupID(), e.getMemberID(), e.getNewNick(), e.getMemberNick()); 23 | } 24 | } 25 | 26 | // 更新群成员缓存 27 | public static void updateMemberCardChange(long botID, long groupID, long qq, String name1, String name2){ 28 | CompletableFuture.runAsync(() -> { 29 | // 启用群缓存功能 30 | if (!plugin.getConfig().getBoolean("aplini.player-cache.enabled", true)) { 31 | return; 32 | } 33 | // 如果这是已配置的机器人 34 | if (!plugin.getConfig().getLongList("bot.bot-accounts").contains(botID)) { 35 | return; 36 | } 37 | // 如果这个群在配置中 38 | if (!isGroupInConfig(plugin, "aplini.player-cache", groupID)) { 39 | return; 40 | } 41 | 42 | // 获取散列表 43 | Map group_cache = Chat2QQ.group_cache_all.get(groupID); 44 | if(group_cache == null){ 45 | Chat2QQ.group_cache_all.put(groupID, new HashMap<>()); 46 | group_cache = Chat2QQ.group_cache_all.get(groupID); 47 | } 48 | 49 | String name; 50 | if (Objects.equals(name1, "")) { 51 | if (plugin.getConfig().getBoolean("general.use-nick-if-namecard-null", true)) { 52 | name = name2; 53 | } else { 54 | name = String.valueOf(qq); 55 | } 56 | }else{ 57 | name = name1; 58 | } 59 | 60 | String oldName = group_cache.get(qq); 61 | if(Objects.equals(oldName, name)){ 62 | return; 63 | } 64 | 65 | if(plugin.getConfig().getBoolean("aplini.player-cache.auto-update-log", true)){ 66 | getLogger().info("[Chat2QQ] 群名片修改 "+ groupID +"."+ qq +": "+ oldName +" -> "+ name); 67 | } 68 | 69 | group_cache.put(qq, name); 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/EventFunc.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import me.dreamvoid.miraimc.bukkit.event.group.member.MiraiMemberJoinEvent; 5 | import me.dreamvoid.miraimc.bukkit.event.group.member.MiraiMemberLeaveEvent; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.event.EventHandler; 8 | import org.bukkit.event.Listener; 9 | 10 | import java.util.Map; 11 | 12 | import static io.github.aplini.chat2qq.utils.Util.isGroupInConfig; 13 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 14 | import static org.bukkit.Bukkit.getLogger; 15 | 16 | public class EventFunc implements Listener { 17 | private final Chat2QQ plugin; 18 | public EventFunc(Chat2QQ plugin){ 19 | this.plugin = plugin; 20 | } 21 | 22 | // 群成员 复用 23 | public void FuncMember(String eventNameInConfig, Long groupID){ 24 | // 如果不是配置中的群 25 | if(! isGroupInConfig(plugin, "aplini.event-func", groupID)){ 26 | return; 27 | } 28 | 29 | for(Map funcConfig : plugin.getConfig().getMapList("aplini.event-func."+ eventNameInConfig)){ 30 | // 执行指令 31 | if(funcConfig.get("command") != null){ 32 | getLogger().info("[Chat2QQ] [event-func] 运行指令: /"+ funcConfig.get("command")); 33 | Bukkit.getScheduler().callSyncMethod(plugin, () -> 34 | Bukkit.dispatchCommand(Bukkit.getServer().getConsoleSender(), String.valueOf(funcConfig.get("command")))); 35 | } 36 | // 发送消息 37 | else if(funcConfig.get("message-text") != null){ 38 | Long messageGroupID = funcConfig.get("message-group") != null ? Long.valueOf(String.valueOf(funcConfig.get("message-group"))) : groupID; 39 | sendToGroup(plugin, messageGroupID, String.valueOf(funcConfig.get("message-text"))); 40 | } 41 | } 42 | } 43 | @EventHandler // 成员加入 44 | public void onMiraiMemberJoinEvent(MiraiMemberJoinEvent e) { 45 | FuncMember("MiraiMemberJoinEvent", e.getGroupID()); 46 | } 47 | @EventHandler // 成员退出 48 | public void onMiraiMemberLeaveEvent(MiraiMemberLeaveEvent e) { 49 | FuncMember("MiraiMemberLeaveEvent", e.getGroupID()); 50 | } 51 | // @EventHandler // 收到群消息 52 | // public void onMiraiGroupMessageEvent(MiraiGroupMessageEvent e) { 53 | // FuncMember("MiraiGroupMessageEvent", e.getGroupID()); 54 | // } 55 | 56 | 57 | // 功能不完整? 58 | // https://jd.miraimc.dreamvoid.me/me/dreamvoid/miraimc/bukkit/event/message/passive/package-summary 59 | 60 | // // 好友消息/私聊 复用 61 | // public void FuncFriend(String eventNameInConfig, Long groupID){ 62 | // // 如果不是配置中的群 63 | // if(! isGroupInConfig(plugin, "aplini.event-func", groupID)){ 64 | // return; 65 | // } 66 | // 67 | // for(Map funcConfig : plugin.getConfig().getMapList("aplini.event-func."+ eventNameInConfig)){ 68 | // // 执行指令 69 | // if(funcConfig.get("command") != null){ 70 | // Bukkit.getScheduler().callSyncMethod(plugin, () -> Bukkit.dispatchCommand(new Commander(), (String) funcConfig.get("command"))); 71 | // } 72 | // // 发送消息 73 | // else if(funcConfig.get("message-text") != null){ 74 | // Long messageGroupID = funcConfig.get("message-group") != null ? (Long) funcConfig.get("message-group") : groupID; 75 | // sendToGroup(plugin, messageGroupID, (String) funcConfig.get("message")); 76 | // } 77 | // } 78 | // } 79 | // @EventHandler // 好友消息 80 | // public void onMiraiFriendMessageEvent(MiraiFriendMessageEvent e) { 81 | // FuncFriend("MiraiFriendMessageEvent", e.getFriend().getID()); 82 | // } 83 | // @EventHandler // 群临时会话 84 | // public void onMiraiGroupTempMessageEvent(MiraiGroupTempMessageEvent e) { 85 | // FuncFriend("MiraiFriendMessageEvent", e.); 86 | // } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onAutoResponseMessage.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import me.dreamvoid.miraimc.bukkit.event.message.passive.MiraiGroupMessageEvent; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.Listener; 7 | 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import java.util.concurrent.CompletableFuture; 11 | import java.util.regex.Pattern; 12 | 13 | import static io.github.aplini.chat2qq.utils.Util.*; 14 | 15 | public class onAutoResponseMessage implements Listener { 16 | private final Chat2QQ plugin; 17 | public onAutoResponseMessage(Chat2QQ plugin){ 18 | this.plugin = plugin; 19 | } 20 | 21 | @EventHandler 22 | public void onGroupMessageReceive(MiraiGroupMessageEvent e) { 23 | 24 | // 异步 25 | CompletableFuture.runAsync(() -> { 26 | 27 | // 是否启用 28 | if (!plugin.getConfig().getBoolean("aplini.auto-response.enabled", true)) { 29 | return; 30 | } 31 | 32 | // 是否为设置的QQ群 33 | if (!isGroupInConfig(plugin, "aplini.auto-response", e.getGroupID())) { 34 | return; 35 | } 36 | 37 | // 获取消息内容 38 | String message = e.getMessage(); 39 | // 要发送的消息 40 | String sendMessage = null; 41 | 42 | // 遍历所有配置 43 | for (Map config : plugin.getConfig().getMapList("aplini.auto-response.list")) { 44 | // 前缀匹配 45 | if (config.get("prefix") != null && message.startsWith((String) config.get("prefix"))) { 46 | sendMessage = (String) config.get("send"); 47 | } 48 | 49 | // 包含 50 | else if (config.get("contain") != null && message.contains((String) config.get("contain"))) { 51 | sendMessage = (String) config.get("send"); 52 | } 53 | 54 | // 相等 55 | else if (config.get("equal") != null && Objects.equals(message, config.get("equal"))) { 56 | sendMessage = (String) config.get("send"); 57 | } 58 | 59 | // 正则匹配 60 | else if (config.get("regular") != null && Pattern.compile((String) config.get("regular")).matcher(message).find()) { 61 | sendMessage = message.replaceAll((String) config.get("regular"), (String) config.get("send")); 62 | } 63 | } 64 | 65 | // 是否存在消息 66 | if (sendMessage == null) { 67 | return; 68 | } 69 | 70 | // PAPI 71 | if (plugin.getConfig().getBoolean("aplini.auto-response.enable-papi", false)) { 72 | sendMessage = PAPIString(sendMessage); 73 | } 74 | 75 | // 处理格式化字符 76 | sendMessage = sendMessage.replaceAll("§[a-z0-9]", ""); 77 | 78 | // 发送消息 79 | sendToGroup(plugin, e.getGroupID(), sendMessage); 80 | 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onGroupCommandMessage.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import io.github.aplini.chat2qq.utils._Commander; 5 | import me.dreamvoid.miraimc.bukkit.event.message.passive.MiraiGroupMessageEvent; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.event.EventHandler; 8 | import org.bukkit.event.Listener; 9 | 10 | import java.util.Objects; 11 | import java.util.concurrent.CompletableFuture; 12 | import java.util.concurrent.TimeUnit; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | import static io.github.aplini.chat2qq.utils.Util.pretreatment; 17 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 18 | import static org.bukkit.Bukkit.getLogger; 19 | 20 | // 运行指令的功能 21 | 22 | public class onGroupCommandMessage implements Listener { 23 | private final Chat2QQ plugin; 24 | public onGroupCommandMessage(Chat2QQ plugin){ 25 | this.plugin = plugin; 26 | } 27 | 28 | @EventHandler 29 | public void onGroupMessageReceive(MiraiGroupMessageEvent e) { 30 | 31 | // 异步 32 | CompletableFuture.runAsync(() -> { 33 | 34 | // 是否启用 35 | if (!plugin.getConfig().getBoolean("aplini.run-command.enabled", false)) { 36 | return; 37 | } 38 | 39 | // 是否为设置的QQ群 40 | if (!plugin.getConfig().getLongList("aplini.run-command.qq-group").contains(e.getGroupID())) { 41 | return; 42 | } 43 | 44 | // 消息是否以指令前缀开头 45 | if (!e.getMessage().startsWith(plugin.getConfig().getString("aplini.run-command.command-prefix", "/"))) { 46 | return; 47 | } 48 | 49 | // 获取指令 50 | String command = e.getMessage().substring(plugin.getConfig().getString("aplini.run-command.command-prefix", "/").length()); 51 | 52 | // 长度限制 53 | if (!(command.length() <= plugin.getConfig().getInt("aplini.run-command.command-max-length", 255))) { 54 | return; 55 | } 56 | 57 | // 正则匹配主命令 58 | Matcher matcher = Pattern.compile(plugin.getConfig().getString("aplini.run-command.regex-command-main", "^([^ ]+)")).matcher(command); 59 | 60 | // 是否匹配到主命令 61 | if (!matcher.find()) { 62 | return; 63 | } 64 | 65 | // 主命令 66 | String commandMain = matcher.group(1); 67 | 68 | // 转换为小写 69 | if (plugin.getConfig().getBoolean("aplini.run-command.always-lowercase", false)) { 70 | commandMain = commandMain.toLowerCase(); 71 | } 72 | 73 | // 初始化运行状态 // 指令是否运行成功 74 | boolean runOK = false; 75 | 76 | 77 | // 遍历所有小于等于自己权限数值的组 78 | for (int permission_int = e.getSenderPermission(); permission_int >= 0; permission_int--) { 79 | for (String list1 : plugin.getConfig().getStringList("aplini.run-command.group.permission_" + permission_int)) { 80 | 81 | // ___ALL_COMMAND___ 表示可以运行任何指令 82 | if (Objects.equals(commandMain, list1) || Objects.equals(list1, "___ALL_COMMAND___")) { 83 | 84 | getLogger().info("[Chat2QQ] " + e.getGroupID() + "." + e.getSenderID() + " 运行指令: /" + command); 85 | 86 | // 执行指令 87 | // 是否开启获取指令返回消息 88 | if (plugin.getConfig().getBoolean("aplini.run-command.return", true) 89 | && !plugin.getConfig().getStringList("aplini.run-command.special.no-return").contains(command)) { 90 | 91 | // 判断指令输出为空的正则 92 | Pattern isNull = Pattern.compile(plugin.getConfig().getString("aplini.run-command.return-isNull", "^\\s*$"), Pattern.DOTALL); 93 | // 循环判断指令返回是否为空, 总计等待时间 94 | int sleepTime = plugin.getConfig().getInt("aplini.run-command.return-sleep-min", 14); 95 | // 指令输出 96 | String text; 97 | 98 | _Commander Sender = new _Commander(); 99 | 100 | try { 101 | Bukkit.getScheduler().callSyncMethod(plugin, () -> Bukkit.dispatchCommand(Sender, command)); 102 | 103 | // 等待指令运行 104 | TimeUnit.MILLISECONDS.sleep(plugin.getConfig().getInt("aplini.run-command.return-sleep-min", 14)); 105 | 106 | // 循环判断指令返回是否为空 107 | while (true) { 108 | // 输出不为空或换行 109 | text = mergeCommandMessage(Sender); 110 | if (!isNull.matcher(text).matches()) { 111 | break; 112 | } 113 | // 超时 114 | if (sleepTime >= plugin.getConfig().getInt("aplini.run-command.return-sleep-max", 5346)) { 115 | break; 116 | } 117 | // 等待一个采样间隔时间, 并累计总计等待时间 118 | TimeUnit.MILLISECONDS.sleep(plugin.getConfig().getInt("aplini.run-command.return-sleep-sampling-interval", 172)); 119 | sleepTime = sleepTime + plugin.getConfig().getInt("aplini.run-command.return-sleep-sampling-interval", 172); 120 | } 121 | 122 | } catch (InterruptedException ex) { 123 | getLogger().info("[Chat2QQ] 运行指令 \"/" + command + "\" 时出现异常!"); 124 | throw new RuntimeException(ex); 125 | } 126 | 127 | // 如果指令输出为空 128 | if (isNull.matcher(text).matches()) { 129 | text = plugin.getConfig().getString("aplini.run-command.message-no-out", "message-no-out"); 130 | } 131 | 132 | // 后处理 133 | text = pretreatment(plugin, "aplini.pretreatment-command-message-all", text); 134 | if (plugin.getConfig().getBoolean("aplini.pretreatment-command-message-all.enabled-placeholder", false)) { 135 | text = text 136 | .replace("%command%", command) 137 | .replace("%time%", String.valueOf(sleepTime)) 138 | .replace("%qq%", String.valueOf(e.getSenderID())) 139 | .replace("%group%", String.valueOf(e.getGroupID())); 140 | } 141 | 142 | // 打印日志 143 | if (plugin.getConfig().getBoolean("aplini.run-command.return-log", true)) { 144 | getLogger().info("[Chat2QQ] 指令运行完成, 耗时 " + sleepTime + "ms: \n" + text); 145 | } 146 | 147 | // 指令返回消息 148 | sendToGroup(plugin, e.getGroupID(), text); 149 | 150 | } else { 151 | 152 | Bukkit.getScheduler().callSyncMethod(plugin, () -> Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command)); 153 | 154 | // "运行无返回指令" 155 | if (!plugin.getConfig().getString("aplini.run-command.message-no-out", "").isEmpty()) { 156 | sendToGroup(plugin, e.getGroupID(), plugin.getConfig().getString("aplini.run-command.message-no-out", "message-no-out")); 157 | } 158 | } 159 | 160 | // 运行成功 161 | runOK = true; 162 | break; 163 | } 164 | } 165 | if (runOK) break; 166 | } 167 | 168 | // 指令没有运行成功 && 设置了未命中消息 169 | if (!runOK && !plugin.getConfig().getString("aplini.run-command.message-miss", "").isEmpty()) { 170 | // 发送消息 171 | sendToGroup(plugin, e.getGroupID(), plugin.getConfig().getString("aplini.run-command.message-miss")); 172 | } 173 | }); 174 | } 175 | 176 | 177 | // 合并指令输出为多行字符串 178 | public String mergeCommandMessage(_Commander Sender){ 179 | StringBuilder text = new StringBuilder(); 180 | 181 | if(!Sender.message.isEmpty()){ 182 | for(String line : Sender.message){ 183 | text.append(pretreatment(plugin, "aplini.pretreatment-command-message", line +"\n")); 184 | } 185 | } 186 | 187 | return text.toString(); 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onGroupMessage.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import me.dreamvoid.miraimc.bukkit.event.message.passive.MiraiGroupMessageEvent; 5 | import net.md_5.bungee.api.chat.TextComponent; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.event.EventHandler; 9 | import org.bukkit.event.Listener; 10 | 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | import static io.github.aplini.chat2qq.bot.onCardChange.updateMemberCardChange; 14 | import static io.github.aplini.chat2qq.utils.renderGroupMessage.renderMessage1; 15 | import static io.github.aplini.chat2qq.utils.renderGroupMessage.renderMessage2; 16 | 17 | public class onGroupMessage implements Listener { 18 | private final Chat2QQ plugin; 19 | public onGroupMessage(Chat2QQ plugin){ 20 | this.plugin = plugin; 21 | } 22 | 23 | @EventHandler 24 | public void onGroupMessageReceive(MiraiGroupMessageEvent e) { 25 | 26 | // 异步 27 | CompletableFuture.runAsync(() -> { 28 | // 自动更新玩家信息缓存 29 | if(plugin.getConfig().getBoolean("aplini.player-cache.auto-update", true)){ 30 | if(plugin.getConfig().getBoolean("aplini.player-cache.auto-update-form-msg", true)){ 31 | updateMemberCardChange(e.getBotID(), e.getGroupID(), e.getSenderID(), e.getSenderName(), null); 32 | } 33 | } 34 | 35 | // QQID黑名单 36 | if (plugin.getConfig().getLongList("blacklist.qq").contains(e.getSenderID())) return; 37 | 38 | // 渲染为可见消息 39 | String[] message = renderMessage1(plugin, e); 40 | 41 | 42 | if (!message[2].isEmpty() && 43 | plugin.getConfig().getLongList("bot.bot-accounts").contains(e.getBotID()) && 44 | plugin.getConfig().getLongList("general.group-ids").contains(e.getGroupID())) { 45 | 46 | // 输出到控制台 47 | if (plugin.getConfig().getBoolean("aplini.other-format-presets.message-to-log", true)) { 48 | Bukkit.getConsoleSender().sendMessage("[QQ] "+ message[3]); 49 | } 50 | 51 | // 渲染为JSON消息 52 | TextComponent formatText = renderMessage2(plugin, message, e); 53 | 54 | // 广播给具有 chat2qq.qq.receive 权限的玩家 55 | for(Player player : Bukkit.getOnlinePlayers()){ 56 | if(player.hasPermission("chat2qq.qq.receive")){ 57 | player.spigot().sendMessage(formatText); 58 | } 59 | } 60 | // getServer().spigot().broadcast(formatText); 61 | } 62 | 63 | }); 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onPlayerJoin.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.Listener; 7 | import org.bukkit.event.player.PlayerJoinEvent; 8 | import org.bukkit.scheduler.BukkitRunnable; 9 | 10 | import java.util.ArrayList; 11 | 12 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 13 | 14 | public class onPlayerJoin implements Listener { 15 | private final Chat2QQ plugin; 16 | public onPlayerJoin(Chat2QQ plugin){ 17 | this.plugin = plugin; 18 | } 19 | private static final ArrayList cache = new ArrayList<>(); 20 | 21 | @EventHandler 22 | public void onPlayerJoinEvent(PlayerJoinEvent e){ 23 | if(plugin.getConfig().getBoolean("bot.send-player-join-quit-message",false) && !e.getPlayer().hasPermission("chat2qq.join.silent") && !cache.contains(e.getPlayer())){ 24 | new BukkitRunnable() { 25 | @Override 26 | public void run() { 27 | String message = plugin.getConfig().getString("bot.player-join-message", "player Join").replace("%player%", e.getPlayer().getName()); 28 | plugin.getConfig().getLongList("bot.bot-accounts").forEach(bot -> plugin.getConfig().getLongList("general.group-ids").forEach(group -> { 29 | try { 30 | sendToGroup(plugin, group, message); 31 | } finally { 32 | int interval = plugin.getConfig().getInt("bot.player-join-quit-message-interval"); 33 | if(interval > 0) { 34 | cache.add(e.getPlayer()); 35 | new BukkitRunnable() { 36 | @Override 37 | public void run() { 38 | cache.remove(e.getPlayer()); 39 | } 40 | }.runTaskLaterAsynchronously(plugin,interval * 20L); 41 | } 42 | } 43 | })); 44 | } 45 | }.runTaskAsynchronously(plugin); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onPlayerMessage.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import org.bukkit.ChatColor; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.Listener; 8 | import org.bukkit.event.player.AsyncPlayerChatEvent; 9 | 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 13 | 14 | public class onPlayerMessage implements Listener { 15 | private final Chat2QQ plugin; 16 | public onPlayerMessage(Chat2QQ plugin){ 17 | this.plugin = plugin; 18 | } 19 | 20 | @EventHandler(priority = EventPriority.MONITOR) 21 | public void onPlayerChat(AsyncPlayerChatEvent e){ 22 | 23 | // 异步 24 | CompletableFuture.runAsync(() -> { 25 | 26 | if(e.isCancelled() && !plugin.getConfig().getBoolean("bot.ignoreCancelled", false)){ 27 | return; 28 | } 29 | 30 | // 检查权限 31 | if(!e.getPlayer().hasPermission("chat2qq.chat.requite")){ 32 | return; 33 | } 34 | 35 | if (!(plugin.getConfig().getBoolean("bot.require-command-to-chat", false))) { 36 | boolean allowWorld = false; 37 | boolean allowPrefix = false; 38 | String message = ChatColor.stripColor(e.getMessage()); 39 | 40 | // 判断玩家所处世界 41 | for (String world : plugin.getConfig().getStringList("bot.available-worlds")) { 42 | if (e.getPlayer().getWorld().getName().equalsIgnoreCase(world)) { 43 | allowWorld = true; 44 | break; 45 | } 46 | } 47 | if (plugin.getConfig().getBoolean("bot.available-worlds-use-as-blacklist", true)){ 48 | allowWorld = !allowWorld; 49 | } 50 | 51 | // 判断消息是否带前缀 52 | if (plugin.getConfig().getBoolean("bot.requite-special-word-prefix.enabled", true)) { 53 | for (String prefix : plugin.getConfig().getStringList("bot.requite-special-word-prefix.prefix")) { 54 | if (message.startsWith(prefix)) { 55 | allowPrefix = true; 56 | message = message.substring(prefix.length()); 57 | break; 58 | } 59 | } 60 | } else allowPrefix = true; 61 | 62 | // 服务器消息发送到QQ群的格式 63 | String formatText = plugin.getConfig().getString("bot.group-chat-format", "message") 64 | .replace("%player%", e.getPlayer().getName()) 65 | .replace("%message%", message); 66 | 67 | if (allowWorld && allowPrefix) { 68 | plugin.getConfig().getLongList("bot.group-ids").forEach(group -> sendToGroup(plugin, group, formatText)); 69 | } 70 | 71 | } 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/listener/onPlayerQuit.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.listener; 2 | 3 | import io.github.aplini.chat2qq.Chat2QQ; 4 | import org.bukkit.entity.Player; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.Listener; 7 | import org.bukkit.event.player.PlayerQuitEvent; 8 | import org.bukkit.scheduler.BukkitRunnable; 9 | 10 | import java.util.ArrayList; 11 | 12 | import static io.github.aplini.chat2qq.utils.Util.sendToGroup; 13 | 14 | public class onPlayerQuit implements Listener { 15 | private final Chat2QQ plugin; 16 | public onPlayerQuit(Chat2QQ plugin){ 17 | this.plugin = plugin; 18 | } 19 | private static final ArrayList cache = new ArrayList<>(); 20 | 21 | @EventHandler 22 | public void onPlayerQuitEvent(PlayerQuitEvent e){ 23 | if(plugin.getConfig().getBoolean("bot.send-player-join-quit-message",false) && !e.getPlayer().hasPermission("chat2qq.quit.silent") && !cache.contains(e.getPlayer())){ 24 | new BukkitRunnable() { 25 | @Override 26 | public void run() { 27 | String message = plugin.getConfig().getString("bot.player-quit-message", "player Quit").replace("%player%", e.getPlayer().getName()); 28 | plugin.getConfig().getLongList("bot.bot-accounts").forEach(bot -> plugin.getConfig().getLongList("general.group-ids").forEach(group -> { 29 | try { 30 | sendToGroup(plugin, group, message); 31 | } finally { 32 | int interval = plugin.getConfig().getInt("bot.player-join-quit-message-interval"); 33 | if(interval > 0) { 34 | cache.add(e.getPlayer()); 35 | new BukkitRunnable() { 36 | @Override 37 | public void run() { 38 | cache.remove(e.getPlayer()); 39 | } 40 | }.runTaskLaterAsynchronously(plugin,interval * 20L); 41 | } 42 | } 43 | })); 44 | } 45 | }.runTaskAsynchronously(plugin); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/Metrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Metrics class was auto-generated and can be copied into your project if you are 3 | * not using a build tool like Gradle or Maven for dependency management. 4 | * 5 | * IMPORTANT: You are not allowed to modify this class, except changing the package. 6 | * 7 | * Unallowed modifications include but are not limited to: 8 | * - Remove the option for users to opt-out 9 | * - Change the frequency for data submission 10 | * - Obfuscate the code (every obfucator should allow you to make an exception for specific files) 11 | * - Reformat the code (if you use a linter, add an exception) 12 | * 13 | * Violations will result in a ban of your plugin and account from bStats. 14 | */ 15 | package io.github.aplini.chat2qq.utils; 16 | 17 | import org.bukkit.Bukkit; 18 | import org.bukkit.configuration.file.YamlConfiguration; 19 | import org.bukkit.entity.Player; 20 | import org.bukkit.plugin.Plugin; 21 | import org.bukkit.plugin.java.JavaPlugin; 22 | 23 | import javax.net.ssl.HttpsURLConnection; 24 | import java.io.*; 25 | import java.lang.reflect.Method; 26 | import java.net.URL; 27 | import java.nio.charset.StandardCharsets; 28 | import java.util.*; 29 | import java.util.concurrent.Callable; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.ScheduledExecutorService; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.function.BiConsumer; 34 | import java.util.function.Consumer; 35 | import java.util.function.Supplier; 36 | import java.util.logging.Level; 37 | import java.util.stream.Collectors; 38 | import java.util.zip.GZIPOutputStream; 39 | 40 | public class Metrics { 41 | 42 | private final Plugin plugin; 43 | 44 | private final MetricsBase metricsBase; 45 | 46 | /** 47 | * Creates a new Metrics instance. 48 | * 49 | * @param plugin Your plugin instance. 50 | * @param serviceId The id of the service. It can be found at What is my plugin id? 52 | */ 53 | public Metrics(JavaPlugin plugin, int serviceId) { 54 | this.plugin = plugin; 55 | // Get the config file 56 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 57 | File configFile = new File(bStatsFolder, "config.yml"); 58 | YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); 59 | if (!config.isSet("serverUuid")) { 60 | config.addDefault("enabled", true); 61 | config.addDefault("serverUuid", UUID.randomUUID().toString()); 62 | config.addDefault("logFailedRequests", false); 63 | config.addDefault("logSentData", false); 64 | config.addDefault("logResponseStatusText", false); 65 | // Inform the server owners about bStats 66 | config 67 | .options() 68 | .header( 69 | "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" 70 | + "many people use their plugin and their total player count. It's recommended to keep bStats\n" 71 | + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" 72 | + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" 73 | + "anonymous.") 74 | .copyDefaults(true); 75 | try { 76 | config.save(configFile); 77 | } catch (IOException ignored) { 78 | } 79 | } 80 | // Load the data 81 | boolean enabled = config.getBoolean("enabled", true); 82 | String serverUUID = config.getString("serverUuid"); 83 | boolean logErrors = config.getBoolean("logFailedRequests", false); 84 | boolean logSentData = config.getBoolean("logSentData", false); 85 | boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); 86 | metricsBase = 87 | new MetricsBase( 88 | "bukkit", 89 | serverUUID, 90 | serviceId, 91 | enabled, 92 | this::appendPlatformData, 93 | this::appendServiceData, 94 | submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), 95 | plugin::isEnabled, 96 | (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), 97 | (message) -> this.plugin.getLogger().log(Level.INFO, message), 98 | logErrors, 99 | logSentData, 100 | logResponseStatusText); 101 | } 102 | 103 | /** 104 | * Adds a custom chart. 105 | * 106 | * @param chart The chart to add. 107 | */ 108 | public void addCustomChart(CustomChart chart) { 109 | metricsBase.addCustomChart(chart); 110 | } 111 | 112 | private void appendPlatformData(JsonObjectBuilder builder) { 113 | builder.appendField("playerAmount", getPlayerAmount()); 114 | builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); 115 | builder.appendField("bukkitVersion", Bukkit.getVersion()); 116 | builder.appendField("bukkitName", Bukkit.getName()); 117 | builder.appendField("javaVersion", System.getProperty("java.version")); 118 | builder.appendField("osName", System.getProperty("os.name")); 119 | builder.appendField("osArch", System.getProperty("os.arch")); 120 | builder.appendField("osVersion", System.getProperty("os.version")); 121 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 122 | } 123 | 124 | private void appendServiceData(JsonObjectBuilder builder) { 125 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 126 | } 127 | 128 | private int getPlayerAmount() { 129 | try { 130 | // Around MC 1.8 the return type was changed from an array to a collection, 131 | // This fixes java.lang.NoSuchMethodError: 132 | // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; 133 | Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); 134 | return onlinePlayersMethod.getReturnType().equals(Collection.class) 135 | ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() 136 | : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; 137 | } catch (Exception e) { 138 | // Just use the new method if the reflection failed 139 | return Bukkit.getOnlinePlayers().size(); 140 | } 141 | } 142 | 143 | public static class MetricsBase { 144 | 145 | /** The version of the Metrics class. */ 146 | public static final String METRICS_VERSION = "3.0.0"; 147 | 148 | private static final ScheduledExecutorService scheduler = 149 | Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); 150 | 151 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 152 | 153 | private final String platform; 154 | 155 | private final String serverUuid; 156 | 157 | private final int serviceId; 158 | 159 | private final Consumer appendPlatformDataConsumer; 160 | 161 | private final Consumer appendServiceDataConsumer; 162 | 163 | private final Consumer submitTaskConsumer; 164 | 165 | private final Supplier checkServiceEnabledSupplier; 166 | 167 | private final BiConsumer errorLogger; 168 | 169 | private final Consumer infoLogger; 170 | 171 | private final boolean logErrors; 172 | 173 | private final boolean logSentData; 174 | 175 | private final boolean logResponseStatusText; 176 | 177 | private final Set customCharts = new HashSet<>(); 178 | 179 | private final boolean enabled; 180 | 181 | /** 182 | * Creates a new MetricsBase class instance. 183 | * 184 | * @param platform The platform of the service. 185 | * @param serviceId The id of the service. 186 | * @param serverUuid The server uuid. 187 | * @param enabled Whether or not data sending is enabled. 188 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 189 | * appends all platform-specific data. 190 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 191 | * appends all service-specific data. 192 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 193 | * used to delegate the data collection to a another thread to prevent errors caused by 194 | * concurrency. Can be {@code null}. 195 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 196 | * @param errorLogger A consumer that accepts log message and an error. 197 | * @param infoLogger A consumer that accepts info log messages. 198 | * @param logErrors Whether or not errors should be logged. 199 | * @param logSentData Whether or not the sent data should be logged. 200 | * @param logResponseStatusText Whether or not the response status text should be logged. 201 | */ 202 | public MetricsBase( 203 | String platform, 204 | String serverUuid, 205 | int serviceId, 206 | boolean enabled, 207 | Consumer appendPlatformDataConsumer, 208 | Consumer appendServiceDataConsumer, 209 | Consumer submitTaskConsumer, 210 | Supplier checkServiceEnabledSupplier, 211 | BiConsumer errorLogger, 212 | Consumer infoLogger, 213 | boolean logErrors, 214 | boolean logSentData, 215 | boolean logResponseStatusText) { 216 | this.platform = platform; 217 | this.serverUuid = serverUuid; 218 | this.serviceId = serviceId; 219 | this.enabled = enabled; 220 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 221 | this.appendServiceDataConsumer = appendServiceDataConsumer; 222 | this.submitTaskConsumer = submitTaskConsumer; 223 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 224 | this.errorLogger = errorLogger; 225 | this.infoLogger = infoLogger; 226 | this.logErrors = logErrors; 227 | this.logSentData = logSentData; 228 | this.logResponseStatusText = logResponseStatusText; 229 | checkRelocation(); 230 | if (enabled) { 231 | // WARNING: Removing the option to opt-out will get your plugin banned from bStats 232 | startSubmitting(); 233 | } 234 | } 235 | 236 | public void addCustomChart(CustomChart chart) { 237 | this.customCharts.add(chart); 238 | } 239 | 240 | private void startSubmitting() { 241 | final Runnable submitTask = 242 | () -> { 243 | if (!enabled || !checkServiceEnabledSupplier.get()) { 244 | // Submitting data or service is disabled 245 | scheduler.shutdown(); 246 | return; 247 | } 248 | if (submitTaskConsumer != null) { 249 | submitTaskConsumer.accept(this::submitData); 250 | } else { 251 | this.submitData(); 252 | } 253 | }; 254 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution 255 | // of requests on the 256 | // bStats backend. To circumvent this problem, we introduce some randomness into the initial 257 | // and second delay. 258 | // WARNING: You must not modify and part of this Metrics class, including the submit delay or 259 | // frequency! 260 | // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! 261 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 262 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 263 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 264 | scheduler.scheduleAtFixedRate( 265 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 266 | } 267 | 268 | private void submitData() { 269 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 270 | appendPlatformDataConsumer.accept(baseJsonBuilder); 271 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 272 | appendServiceDataConsumer.accept(serviceJsonBuilder); 273 | JsonObjectBuilder.JsonObject[] chartData = 274 | customCharts.stream() 275 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 276 | .filter(Objects::nonNull) 277 | .toArray(JsonObjectBuilder.JsonObject[]::new); 278 | serviceJsonBuilder.appendField("id", serviceId); 279 | serviceJsonBuilder.appendField("customCharts", chartData); 280 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 281 | baseJsonBuilder.appendField("serverUUID", serverUuid); 282 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 283 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 284 | scheduler.execute( 285 | () -> { 286 | try { 287 | // Send the data 288 | sendData(data); 289 | } catch (Exception e) { 290 | // Something went wrong! :( 291 | if (logErrors) { 292 | errorLogger.accept("Could not submit bStats metrics data", e); 293 | } 294 | } 295 | }); 296 | } 297 | 298 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 299 | if (logSentData) { 300 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 301 | } 302 | String url = String.format(REPORT_URL, platform); 303 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 304 | // Compress the data to save bandwidth 305 | byte[] compressedData = compress(data.toString()); 306 | connection.setRequestMethod("POST"); 307 | connection.addRequestProperty("Accept", "application/json"); 308 | connection.addRequestProperty("Connection", "close"); 309 | connection.addRequestProperty("Content-Encoding", "gzip"); 310 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 311 | connection.setRequestProperty("Content-Type", "application/json"); 312 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 313 | connection.setDoOutput(true); 314 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 315 | outputStream.write(compressedData); 316 | } 317 | StringBuilder builder = new StringBuilder(); 318 | try (BufferedReader bufferedReader = 319 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 320 | String line; 321 | while ((line = bufferedReader.readLine()) != null) { 322 | builder.append(line); 323 | } 324 | } 325 | if (logResponseStatusText) { 326 | infoLogger.accept("Sent data to bStats and received response: " + builder); 327 | } 328 | } 329 | 330 | /** Checks that the class was properly relocated. */ 331 | private void checkRelocation() { 332 | // You can use the property to disable the check in your test environment 333 | if (System.getProperty("bstats.relocatecheck") == null 334 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 335 | // Maven's Relocate is clever and changes strings, too. So we have to use this little 336 | // "trick" ... :D 337 | final String defaultPackage = 338 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 339 | final String examplePackage = 340 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 341 | // We want to make sure no one just copy & pastes the example and uses the wrong package 342 | // names 343 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 344 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 345 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 346 | } 347 | } 348 | } 349 | 350 | /** 351 | * Gzips the given string. 352 | * 353 | * @param str The string to gzip. 354 | * @return The gzipped string. 355 | */ 356 | private static byte[] compress(final String str) throws IOException { 357 | if (str == null) { 358 | return null; 359 | } 360 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 361 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 362 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 363 | } 364 | return outputStream.toByteArray(); 365 | } 366 | } 367 | 368 | public static class DrilldownPie extends CustomChart { 369 | 370 | private final Callable>> callable; 371 | 372 | /** 373 | * Class constructor. 374 | * 375 | * @param chartId The id of the chart. 376 | * @param callable The callable which is used to request the chart data. 377 | */ 378 | public DrilldownPie(String chartId, Callable>> callable) { 379 | super(chartId); 380 | this.callable = callable; 381 | } 382 | 383 | @Override 384 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 385 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 386 | Map> map = callable.call(); 387 | if (map == null || map.isEmpty()) { 388 | // Null = skip the chart 389 | return null; 390 | } 391 | boolean reallyAllSkipped = true; 392 | for (Map.Entry> entryValues : map.entrySet()) { 393 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 394 | boolean allSkipped = true; 395 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 396 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 397 | allSkipped = false; 398 | } 399 | if (!allSkipped) { 400 | reallyAllSkipped = false; 401 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 402 | } 403 | } 404 | if (reallyAllSkipped) { 405 | // Null = skip the chart 406 | return null; 407 | } 408 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 409 | } 410 | } 411 | 412 | public static class AdvancedPie extends CustomChart { 413 | 414 | private final Callable> callable; 415 | 416 | /** 417 | * Class constructor. 418 | * 419 | * @param chartId The id of the chart. 420 | * @param callable The callable which is used to request the chart data. 421 | */ 422 | public AdvancedPie(String chartId, Callable> callable) { 423 | super(chartId); 424 | this.callable = callable; 425 | } 426 | 427 | @Override 428 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 429 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 430 | Map map = callable.call(); 431 | if (map == null || map.isEmpty()) { 432 | // Null = skip the chart 433 | return null; 434 | } 435 | boolean allSkipped = true; 436 | for (Map.Entry entry : map.entrySet()) { 437 | if (entry.getValue() == 0) { 438 | // Skip this invalid 439 | continue; 440 | } 441 | allSkipped = false; 442 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 443 | } 444 | if (allSkipped) { 445 | // Null = skip the chart 446 | return null; 447 | } 448 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 449 | } 450 | } 451 | 452 | public static class MultiLineChart extends CustomChart { 453 | 454 | private final Callable> callable; 455 | 456 | /** 457 | * Class constructor. 458 | * 459 | * @param chartId The id of the chart. 460 | * @param callable The callable which is used to request the chart data. 461 | */ 462 | public MultiLineChart(String chartId, Callable> callable) { 463 | super(chartId); 464 | this.callable = callable; 465 | } 466 | 467 | @Override 468 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 469 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 470 | Map map = callable.call(); 471 | if (map == null || map.isEmpty()) { 472 | // Null = skip the chart 473 | return null; 474 | } 475 | boolean allSkipped = true; 476 | for (Map.Entry entry : map.entrySet()) { 477 | if (entry.getValue() == 0) { 478 | // Skip this invalid 479 | continue; 480 | } 481 | allSkipped = false; 482 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 483 | } 484 | if (allSkipped) { 485 | // Null = skip the chart 486 | return null; 487 | } 488 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 489 | } 490 | } 491 | 492 | public static class SimpleBarChart extends CustomChart { 493 | 494 | private final Callable> callable; 495 | 496 | /** 497 | * Class constructor. 498 | * 499 | * @param chartId The id of the chart. 500 | * @param callable The callable which is used to request the chart data. 501 | */ 502 | public SimpleBarChart(String chartId, Callable> callable) { 503 | super(chartId); 504 | this.callable = callable; 505 | } 506 | 507 | @Override 508 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 509 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 510 | Map map = callable.call(); 511 | if (map == null || map.isEmpty()) { 512 | // Null = skip the chart 513 | return null; 514 | } 515 | for (Map.Entry entry : map.entrySet()) { 516 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 517 | } 518 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 519 | } 520 | } 521 | 522 | public abstract static class CustomChart { 523 | 524 | private final String chartId; 525 | 526 | protected CustomChart(String chartId) { 527 | if (chartId == null) { 528 | throw new IllegalArgumentException("chartId must not be null"); 529 | } 530 | this.chartId = chartId; 531 | } 532 | 533 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 534 | BiConsumer errorLogger, boolean logErrors) { 535 | JsonObjectBuilder builder = new JsonObjectBuilder(); 536 | builder.appendField("chartId", chartId); 537 | try { 538 | JsonObjectBuilder.JsonObject data = getChartData(); 539 | if (data == null) { 540 | // If the data is null we don't send the chart. 541 | return null; 542 | } 543 | builder.appendField("data", data); 544 | } catch (Throwable t) { 545 | if (logErrors) { 546 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 547 | } 548 | return null; 549 | } 550 | return builder.build(); 551 | } 552 | 553 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 554 | } 555 | 556 | public static class SimplePie extends CustomChart { 557 | 558 | private final Callable callable; 559 | 560 | /** 561 | * Class constructor. 562 | * 563 | * @param chartId The id of the chart. 564 | * @param callable The callable which is used to request the chart data. 565 | */ 566 | public SimplePie(String chartId, Callable callable) { 567 | super(chartId); 568 | this.callable = callable; 569 | } 570 | 571 | @Override 572 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 573 | String value = callable.call(); 574 | if (value == null || value.isEmpty()) { 575 | // Null = skip the chart 576 | return null; 577 | } 578 | return new JsonObjectBuilder().appendField("value", value).build(); 579 | } 580 | } 581 | 582 | public static class AdvancedBarChart extends CustomChart { 583 | 584 | private final Callable> callable; 585 | 586 | /** 587 | * Class constructor. 588 | * 589 | * @param chartId The id of the chart. 590 | * @param callable The callable which is used to request the chart data. 591 | */ 592 | public AdvancedBarChart(String chartId, Callable> callable) { 593 | super(chartId); 594 | this.callable = callable; 595 | } 596 | 597 | @Override 598 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 599 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 600 | Map map = callable.call(); 601 | if (map == null || map.isEmpty()) { 602 | // Null = skip the chart 603 | return null; 604 | } 605 | boolean allSkipped = true; 606 | for (Map.Entry entry : map.entrySet()) { 607 | if (entry.getValue().length == 0) { 608 | // Skip this invalid 609 | continue; 610 | } 611 | allSkipped = false; 612 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 613 | } 614 | if (allSkipped) { 615 | // Null = skip the chart 616 | return null; 617 | } 618 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 619 | } 620 | } 621 | 622 | public static class SingleLineChart extends CustomChart { 623 | 624 | private final Callable callable; 625 | 626 | /** 627 | * Class constructor. 628 | * 629 | * @param chartId The id of the chart. 630 | * @param callable The callable which is used to request the chart data. 631 | */ 632 | public SingleLineChart(String chartId, Callable callable) { 633 | super(chartId); 634 | this.callable = callable; 635 | } 636 | 637 | @Override 638 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 639 | int value = callable.call(); 640 | if (value == 0) { 641 | // Null = skip the chart 642 | return null; 643 | } 644 | return new JsonObjectBuilder().appendField("value", value).build(); 645 | } 646 | } 647 | 648 | /** 649 | * An extremely simple JSON builder. 650 | * 651 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 652 | * for its use-case. 653 | */ 654 | public static class JsonObjectBuilder { 655 | 656 | private StringBuilder builder = new StringBuilder(); 657 | 658 | private boolean hasAtLeastOneField = false; 659 | 660 | public JsonObjectBuilder() { 661 | builder.append("{"); 662 | } 663 | 664 | /** 665 | * Appends a null field to the JSON. 666 | * 667 | * @param key The key of the field. 668 | * @return A reference to this object. 669 | */ 670 | public JsonObjectBuilder appendNull(String key) { 671 | appendFieldUnescaped(key, "null"); 672 | return this; 673 | } 674 | 675 | /** 676 | * Appends a string field to the JSON. 677 | * 678 | * @param key The key of the field. 679 | * @param value The value of the field. 680 | * @return A reference to this object. 681 | */ 682 | public JsonObjectBuilder appendField(String key, String value) { 683 | if (value == null) { 684 | throw new IllegalArgumentException("JSON value must not be null"); 685 | } 686 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 687 | return this; 688 | } 689 | 690 | /** 691 | * Appends an integer field to the JSON. 692 | * 693 | * @param key The key of the field. 694 | * @param value The value of the field. 695 | * @return A reference to this object. 696 | */ 697 | public JsonObjectBuilder appendField(String key, int value) { 698 | appendFieldUnescaped(key, String.valueOf(value)); 699 | return this; 700 | } 701 | 702 | /** 703 | * Appends an object to the JSON. 704 | * 705 | * @param key The key of the field. 706 | * @param object The object. 707 | * @return A reference to this object. 708 | */ 709 | public JsonObjectBuilder appendField(String key, JsonObject object) { 710 | if (object == null) { 711 | throw new IllegalArgumentException("JSON object must not be null"); 712 | } 713 | appendFieldUnescaped(key, object.toString()); 714 | return this; 715 | } 716 | 717 | /** 718 | * Appends a string array to the JSON. 719 | * 720 | * @param key The key of the field. 721 | * @param values The string array. 722 | * @return A reference to this object. 723 | */ 724 | public JsonObjectBuilder appendField(String key, String[] values) { 725 | if (values == null) { 726 | throw new IllegalArgumentException("JSON values must not be null"); 727 | } 728 | String escapedValues = 729 | Arrays.stream(values) 730 | .map(value -> "\"" + escape(value) + "\"") 731 | .collect(Collectors.joining(",")); 732 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 733 | return this; 734 | } 735 | 736 | /** 737 | * Appends an integer array to the JSON. 738 | * 739 | * @param key The key of the field. 740 | * @param values The integer array. 741 | * @return A reference to this object. 742 | */ 743 | public JsonObjectBuilder appendField(String key, int[] values) { 744 | if (values == null) { 745 | throw new IllegalArgumentException("JSON values must not be null"); 746 | } 747 | String escapedValues = 748 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 749 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 750 | return this; 751 | } 752 | 753 | /** 754 | * Appends an object array to the JSON. 755 | * 756 | * @param key The key of the field. 757 | * @param values The integer array. 758 | * @return A reference to this object. 759 | */ 760 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 761 | if (values == null) { 762 | throw new IllegalArgumentException("JSON values must not be null"); 763 | } 764 | String escapedValues = 765 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 766 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 767 | return this; 768 | } 769 | 770 | /** 771 | * Appends a field to the object. 772 | * 773 | * @param key The key of the field. 774 | * @param escapedValue The escaped value of the field. 775 | */ 776 | private void appendFieldUnescaped(String key, String escapedValue) { 777 | if (builder == null) { 778 | throw new IllegalStateException("JSON has already been built"); 779 | } 780 | if (key == null) { 781 | throw new IllegalArgumentException("JSON key must not be null"); 782 | } 783 | if (hasAtLeastOneField) { 784 | builder.append(","); 785 | } 786 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 787 | hasAtLeastOneField = true; 788 | } 789 | 790 | /** 791 | * Builds the JSON string and invalidates this builder. 792 | * 793 | * @return The built JSON string. 794 | */ 795 | public JsonObject build() { 796 | if (builder == null) { 797 | throw new IllegalStateException("JSON has already been built"); 798 | } 799 | JsonObject object = new JsonObject(builder.append("}").toString()); 800 | builder = null; 801 | return object; 802 | } 803 | 804 | /** 805 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 806 | * 807 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 808 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 809 | * 810 | * @param value The value to escape. 811 | * @return The escaped value. 812 | */ 813 | private static String escape(String value) { 814 | final StringBuilder builder = new StringBuilder(); 815 | for (int i = 0; i < value.length(); i++) { 816 | char c = value.charAt(i); 817 | if (c == '"') { 818 | builder.append("\\\""); 819 | } else if (c == '\\') { 820 | builder.append("\\\\"); 821 | } else if (c <= '\u000F') { 822 | builder.append("\\u000").append(Integer.toHexString(c)); 823 | } else if (c <= '\u001F') { 824 | builder.append("\\u00").append(Integer.toHexString(c)); 825 | } else { 826 | builder.append(c); 827 | } 828 | } 829 | return builder.toString(); 830 | } 831 | 832 | /** 833 | * A super simple representation of a JSON object. 834 | * 835 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 836 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 837 | * JsonObject)}. 838 | */ 839 | public static class JsonObject { 840 | 841 | private final String value; 842 | 843 | private JsonObject(String value) { 844 | this.value = value; 845 | } 846 | 847 | @Override 848 | public String toString() { 849 | return value; 850 | } 851 | } 852 | } 853 | } -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/Util.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.utils; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonObject; 6 | import io.github.aplini.chat2qq.Chat2QQ; 7 | import me.clip.placeholderapi.PlaceholderAPI; 8 | import me.dreamvoid.miraimc.api.MiraiBot; 9 | import org.bukkit.plugin.Plugin; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Paths; 14 | import java.util.*; 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | import static org.bukkit.Bukkit.getLogger; 21 | 22 | public class Util { 23 | // 发送消息到群 24 | public static void sendToGroup(long botID, long groupID, String message) { 25 | try { 26 | MiraiBot.getBot(botID).getGroup(groupID).sendMessageMirai(message); 27 | } catch (NoSuchElementException e) { 28 | getLogger().warning("[Chat2QQ] 发送消息出现异常: botID="+ botID +" -> groupID="+ groupID +": "+ e); 29 | } 30 | } 31 | // 简化调用 32 | public static void sendToGroup(Plugin plugin, long groupID, String message) { 33 | sendToGroup(plugin.getConfig().getLongList("bot.bot-accounts").get(0), groupID, message); 34 | } 35 | 36 | 37 | // 从群缓存中获取一个群中的玩家的名称 38 | public static String getNameFromCache(Plugin plugin, long groupID, long playerID, String defaultName) { 39 | if(plugin.getConfig().getBoolean("aplini.player-cache.enabled", false)){ 40 | Map group_cache = Chat2QQ.group_cache_all.get(groupID); 41 | if(group_cache != null){ 42 | String name = group_cache.get(playerID); 43 | if(name != null){ 44 | return name; 45 | } 46 | } 47 | return String.valueOf(playerID); 48 | } 49 | return defaultName; 50 | } 51 | 52 | 53 | // 从群名片匹配出MC标准名称 54 | public static String cleanupName(Plugin plugin, String name, Long qqID) { 55 | if(plugin.getConfig().getBoolean("aplini.cleanup-name.enabled",false)){ 56 | Matcher matcher = Pattern.compile(plugin.getConfig().getString("aplini.cleanup-name.regex", "([a-zA-Z0-9_]{3,16})")).matcher(name); 57 | if(matcher.find()){ 58 | return matcher.group(1); 59 | } else { 60 | return plugin.getConfig().getString("aplini.cleanup-name.not-captured", "%nick%") 61 | .replace("%nick%", name) 62 | .replace("%qq%", String.valueOf(qqID)); 63 | } 64 | } 65 | return "[Chat2QQ.未启用此功能: aplini.cleanup-name.enabled]"; 66 | } 67 | 68 | 69 | // 从文本中匹配 @qqID, 并替换为 @名称 70 | public static String formatQQID(Plugin plugin, String message, long groupID) { 71 | if(plugin.getConfig().getBoolean("aplini.format-qq-id.enabled", true)){ 72 | 73 | Matcher matcher = Pattern.compile(plugin.getConfig().getString("aplini.format-qq-id.regular", "(@[0-9]{5,11})")).matcher(message); 74 | 75 | int count = 0; 76 | while (matcher.find()) { 77 | count ++; 78 | 79 | // 获取qqid 80 | long qqID = Long.parseLong(matcher.group().substring(1)); 81 | // 获取群名片 82 | String name = getNameFromCache(plugin, groupID, qqID, matcher.group()); 83 | // 从群名片匹配出MC标准名称 84 | if(plugin.getConfig().getBoolean("aplini.cleanup-name.enabled",false)){ 85 | name = cleanupName(plugin, name, qqID); 86 | } 87 | // 替换 88 | message = message.replace(matcher.group(), 89 | plugin.getConfig().getString("aplini.format-qq-id.format", "format") 90 | .replace("%qq%", String.valueOf(qqID)) 91 | .replace("%name%", name) 92 | ); 93 | 94 | if(count == plugin.getConfig().getInt("aplini.format-qq-id.max-cycles-num", 10)){ 95 | break; 96 | } 97 | } 98 | 99 | } 100 | return message; 101 | } 102 | 103 | 104 | // 初始化群成员缓存 105 | public static void _setGroupCacheAll(Plugin plugin) { 106 | // 新线程 107 | ExecutorService executor = Executors.newSingleThreadExecutor(); 108 | executor.submit(() -> { 109 | try { 110 | Map> _group_cache_all = new HashMap<>(); 111 | 112 | // 获取开启此功能的群 113 | List configGroupList; 114 | if (plugin.getConfig().getBoolean("aplini.player-cache.use-general-group-ids", true)) { 115 | configGroupList = plugin.getConfig().getLongList("general.group-ids"); 116 | } else { 117 | configGroupList = plugin.getConfig().getLongList("aplini.player-cache.group-ids"); 118 | } 119 | 120 | // 获取机器人账号 121 | Long botID = plugin.getConfig().getLongList("bot.bot-accounts").get(0); 122 | 123 | // 遍历需要缓存的群 124 | configGroupList.forEach(gid -> { 125 | getLogger().info("[Chat2QQ] 正在缓存群: " + gid); 126 | // 散列表 127 | Map group_cache = new HashMap<>(); 128 | 129 | // 获取群数据 130 | JsonArray groupArray; 131 | try { 132 | String jsonString = Files.readString(Paths.get( 133 | plugin.getConfig().getString("aplini.player-cache.mirai-cache-path", "plugins/MiraiMC/MiraiBot/bots/%qq%/cache/contacts/groups/%group%.json") 134 | .replace("%qq%", String.valueOf(botID)) 135 | .replace("%group%", String.valueOf(gid)) 136 | )); 137 | JsonObject groupJson = new Gson().fromJson(jsonString, JsonObject.class); 138 | groupArray = groupJson.getAsJsonArray("list"); 139 | } catch (IOException e) { 140 | getLogger().warning("[Chat2QQ] 读取MiraiMC群数据缓存时出错. botID: " + botID + ", groupID: " + gid); 141 | getLogger().warning("[Chat2QQ] 请检查 MiraiMC 的配置 \"bot.contact-cache.enable-group-member-list-cache\" 是否开启"); 142 | 143 | throw new RuntimeException(e); 144 | } 145 | 146 | groupArray.forEach(JsonElement -> { 147 | JsonObject aGroup = (JsonObject) JsonElement; 148 | Long id = aGroup.get("uin").getAsLong(); 149 | String name = aGroup.get("nameCard").getAsString(); 150 | if (Objects.equals(name, "")) { 151 | if (plugin.getConfig().getBoolean("general.use-nick-if-namecard-null", true)) { 152 | name = aGroup.get("nick").getAsString(); 153 | } else { 154 | name = String.valueOf(id); 155 | } 156 | } 157 | group_cache.put(id, name); 158 | }); 159 | 160 | // 添加到all 161 | _group_cache_all.put(gid, group_cache); 162 | }); 163 | 164 | Chat2QQ.group_cache_all = _group_cache_all; 165 | 166 | getLogger().info("[Chat2QQ] 群成员缓存完成!"); 167 | }catch (Exception e){ 168 | getLogger().warning("[Chat2QQ] 无法读取来自 MiraiMC 插件的缓存文件, 仅可使用基于玩家消息的缓存"); 169 | } 170 | }); 171 | executor.shutdown(); 172 | } 173 | 174 | // 判断是否为配置中的群 175 | public static boolean isGroupInConfig(Plugin plugin, String funcConfigString, Long groupID){ 176 | if(plugin.getConfig().getBoolean(funcConfigString +".use-general-group-ids", true)){ 177 | return plugin.getConfig().getLongList("general.group-ids").contains(groupID); 178 | }else{ 179 | return plugin.getConfig().getLongList(funcConfigString + ".group-ids").contains(groupID); 180 | } 181 | } 182 | 183 | // PAPI 184 | public static String PAPIString(String inp){ 185 | // return PlaceholderAPI.setPlaceholders(new _OfflinePlayer(), inp); 186 | return PlaceholderAPI.setPlaceholders(null, inp); 187 | } 188 | 189 | // 预处理模块 :: 消息替换 190 | public static String pretreatment(Plugin plugin, String configPath, String message){ 191 | if(plugin.getConfig().getBoolean(configPath +".enabled",false)){ 192 | for(Map config : plugin.getConfig().getMapList(configPath +".list")){ 193 | // 前缀匹配 194 | if(config.get("prefix") != null && message.startsWith(config.get("prefix").toString())){ 195 | if(config.get("send") != null){ 196 | return ""; 197 | } 198 | else if(config.get("to_all") != null){ 199 | message = config.get("to_all").toString(); 200 | } 201 | else if(config.get("to_replace") != null){ 202 | message = message.replace(config.get("prefix").toString(), config.get("to_replace").toString()); 203 | } 204 | } 205 | 206 | // 包含 207 | else if(config.get("contain") != null && message.contains(config.get("contain").toString())){ 208 | if(config.get("send") != null){ 209 | return ""; 210 | } 211 | else if(config.get("to_replace") != null){ 212 | message = message.replace(config.get("contain").toString(), config.get("to_replace").toString()); 213 | } 214 | else if(config.get("to_all") != null){ 215 | message = config.get("to_all").toString(); 216 | } 217 | } 218 | 219 | // 相等 220 | else if(config.get("equal") != null && Objects.equals(message, config.get("equal"))){ 221 | if(config.get("send") != null){ 222 | return ""; 223 | } 224 | else if(config.get("to_all") != null){ 225 | message = config.get("to_all").toString(); 226 | } 227 | } 228 | 229 | // 正则匹配 230 | else if(config.get("regular") != null && Pattern.compile(config.get("regular").toString()).matcher(message).find()){ 231 | if(config.get("send") != null){ 232 | return ""; 233 | } 234 | else if(config.get("to_regular") != null){ 235 | message = message.replaceAll(config.get("regular").toString(), config.get("to_regular").toString()); 236 | } 237 | else if(config.get("to_all") != null){ 238 | message = config.get("to_all").toString(); 239 | } 240 | } 241 | 242 | // 匹配任何 243 | else if(config.get("any") != null){ 244 | 245 | } 246 | } 247 | } 248 | 249 | return message; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/_Commander.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.utils; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.Server; 5 | import org.bukkit.command.CommandSender; 6 | import org.bukkit.permissions.Permission; 7 | import org.bukkit.permissions.PermissionAttachment; 8 | import org.bukkit.permissions.PermissionAttachmentInfo; 9 | import org.bukkit.plugin.Plugin; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | import java.util.*; 14 | 15 | public class _Commander implements CommandSender { 16 | public List message = new ArrayList<>(); 17 | 18 | @Override 19 | public void sendMessage(@NotNull String message) { 20 | this.message.add(message); 21 | } 22 | 23 | @Override 24 | public void sendMessage(@NotNull String[] messages) { 25 | message.addAll(Arrays.asList(messages)); 26 | } 27 | 28 | @Override 29 | public void sendMessage(@Nullable UUID sender, @NotNull String message) { 30 | this.message.add(message); 31 | } 32 | 33 | @Override 34 | public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { 35 | message.addAll(Arrays.asList(messages)); 36 | } 37 | 38 | @NotNull 39 | @Override 40 | public Server getServer() { 41 | return Bukkit.getConsoleSender().getServer(); 42 | } 43 | 44 | @NotNull 45 | @Override 46 | public String getName() { 47 | return "_Chat2QQ_"; 48 | } 49 | 50 | @NotNull 51 | @Override 52 | public Spigot spigot() { 53 | return new __spigot(message); 54 | // return Bukkit.getConsoleSender().spigot(); 55 | } 56 | 57 | @Override 58 | public boolean isPermissionSet(@NotNull String s) { 59 | return Bukkit.getConsoleSender().isPermissionSet(s); 60 | } 61 | 62 | @Override 63 | public boolean isPermissionSet(@NotNull Permission permission) { 64 | return Bukkit.getConsoleSender().isPermissionSet(permission); 65 | } 66 | 67 | @Override 68 | public boolean hasPermission(@NotNull String s) { 69 | return Bukkit.getConsoleSender().hasPermission(s); 70 | } 71 | 72 | @Override 73 | public boolean hasPermission(@NotNull Permission permission) { 74 | return Bukkit.getConsoleSender().hasPermission(permission); 75 | } 76 | 77 | @NotNull 78 | @Override 79 | public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String s, boolean b) { 80 | return Bukkit.getConsoleSender().addAttachment(plugin, s, b); 81 | } 82 | 83 | @NotNull 84 | @Override 85 | public PermissionAttachment addAttachment(@NotNull Plugin plugin) { 86 | return Bukkit.getConsoleSender().addAttachment(plugin); 87 | } 88 | 89 | @Override 90 | public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String s, boolean b, int i) { 91 | return Bukkit.getConsoleSender().addAttachment(plugin, s, b, i); 92 | } 93 | 94 | @Override 95 | public PermissionAttachment addAttachment(@NotNull Plugin plugin, int i) { 96 | return Bukkit.getConsoleSender().addAttachment(plugin, i); 97 | } 98 | 99 | @Override 100 | public void removeAttachment(@NotNull PermissionAttachment permissionAttachment) { 101 | 102 | } 103 | 104 | @Override 105 | public void recalculatePermissions() { 106 | 107 | } 108 | 109 | @NotNull 110 | @Override 111 | public Set getEffectivePermissions() { 112 | return Bukkit.getConsoleSender().getEffectivePermissions(); 113 | } 114 | 115 | @Override 116 | public boolean isOp() { 117 | return true; 118 | } 119 | 120 | @Override 121 | public void setOp(boolean b) { 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/_OfflinePlayer.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.utils; 2 | 3 | import org.bukkit.*; 4 | import org.bukkit.entity.EntityType; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.profile.PlayerProfile; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import java.time.Duration; 11 | import java.time.Instant; 12 | import java.util.Date; 13 | import java.util.Map; 14 | import java.util.UUID; 15 | 16 | public class _OfflinePlayer implements OfflinePlayer { 17 | @Override 18 | public boolean isOnline() { 19 | return false; 20 | } 21 | 22 | @Nullable 23 | @Override 24 | public String getName() { 25 | return "_Chat2QQ_"; 26 | } 27 | 28 | @NotNull 29 | @Override 30 | public UUID getUniqueId() { 31 | return null; 32 | } 33 | 34 | /** 35 | * Gets a copy of the player's profile. 36 | *

37 | * If the player is online, the returned profile will be complete. 38 | * Otherwise, only the unique id is guaranteed to be present. You can use 39 | * {@link PlayerProfile#update()} to complete the returned profile. 40 | * 41 | * @return the player's profile 42 | */ 43 | @NotNull 44 | @Override 45 | public PlayerProfile getPlayerProfile() { 46 | return null; 47 | } 48 | 49 | @Override 50 | public boolean isBanned() { 51 | return false; 52 | } 53 | 54 | /** 55 | * Adds this user to the {@link ProfileBanList}. If a previous ban exists, this will 56 | * update the entry. 57 | * 58 | * @param reason reason for the ban, null indicates implementation default 59 | * @param expires date for the ban's expiration (unban), or null to imply 60 | * forever 61 | * @param source source of the ban, null indicates implementation default 62 | * @return the entry for the newly created ban, or the entry for the 63 | * (updated) previous ban 64 | */ 65 | @Nullable 66 | @Override 67 | public BanEntry ban(@Nullable String reason, @Nullable Date expires, @Nullable String source) { 68 | return null; 69 | } 70 | 71 | /** 72 | * Adds this user to the {@link ProfileBanList}. If a previous ban exists, this will 73 | * update the entry. 74 | * 75 | * @param reason reason for the ban, null indicates implementation default 76 | * @param expires instant for the ban's expiration (unban), or null to imply 77 | * forever 78 | * @param source source of the ban, null indicates implementation default 79 | * @return the entry for the newly created ban, or the entry for the 80 | * (updated) previous ban 81 | */ 82 | @Nullable 83 | @Override 84 | public BanEntry ban(@Nullable String reason, @Nullable Instant expires, @Nullable String source) { 85 | return null; 86 | } 87 | 88 | /** 89 | * Adds this user to the {@link ProfileBanList}. If a previous ban exists, this will 90 | * update the entry. 91 | * 92 | * @param reason reason for the ban, null indicates implementation default 93 | * @param duration how long the ban last, or null to imply 94 | * forever 95 | * @param source source of the ban, null indicates implementation default 96 | * @return the entry for the newly created ban, or the entry for the 97 | * (updated) previous ban 98 | */ 99 | @Nullable 100 | @Override 101 | public BanEntry ban(@Nullable String reason, @Nullable Duration duration, @Nullable String source) { 102 | return null; 103 | } 104 | 105 | @Override 106 | public boolean isWhitelisted() { 107 | return false; 108 | } 109 | 110 | @Override 111 | public void setWhitelisted(boolean value) { 112 | 113 | } 114 | 115 | @Nullable 116 | @Override 117 | public Player getPlayer() { 118 | return null; 119 | } 120 | 121 | @Override 122 | public long getFirstPlayed() { 123 | return 0; 124 | } 125 | 126 | @Override 127 | public long getLastPlayed() { 128 | return 0; 129 | } 130 | 131 | @Override 132 | public boolean hasPlayedBefore() { 133 | return false; 134 | } 135 | 136 | @Nullable 137 | @Override 138 | public Location getBedSpawnLocation() { 139 | return null; 140 | } 141 | 142 | @Nullable 143 | @Override 144 | public Location getRespawnLocation() { 145 | return null; 146 | } 147 | 148 | @Override 149 | public void incrementStatistic(@NotNull Statistic statistic) throws IllegalArgumentException { 150 | 151 | } 152 | 153 | @Override 154 | public void decrementStatistic(@NotNull Statistic statistic) throws IllegalArgumentException { 155 | 156 | } 157 | 158 | @Override 159 | public void incrementStatistic(@NotNull Statistic statistic, int amount) throws IllegalArgumentException { 160 | 161 | } 162 | 163 | @Override 164 | public void decrementStatistic(@NotNull Statistic statistic, int amount) throws IllegalArgumentException { 165 | 166 | } 167 | 168 | @Override 169 | public void setStatistic(@NotNull Statistic statistic, int newValue) throws IllegalArgumentException { 170 | 171 | } 172 | 173 | @Override 174 | public int getStatistic(@NotNull Statistic statistic) throws IllegalArgumentException { 175 | return 0; 176 | } 177 | 178 | @Override 179 | public void incrementStatistic(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException { 180 | 181 | } 182 | 183 | @Override 184 | public void decrementStatistic(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException { 185 | 186 | } 187 | 188 | @Override 189 | public int getStatistic(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException { 190 | return 0; 191 | } 192 | 193 | @Override 194 | public void incrementStatistic(@NotNull Statistic statistic, @NotNull Material material, int amount) throws IllegalArgumentException { 195 | 196 | } 197 | 198 | @Override 199 | public void decrementStatistic(@NotNull Statistic statistic, @NotNull Material material, int amount) throws IllegalArgumentException { 200 | 201 | } 202 | 203 | @Override 204 | public void setStatistic(@NotNull Statistic statistic, @NotNull Material material, int newValue) throws IllegalArgumentException { 205 | 206 | } 207 | 208 | @Override 209 | public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException { 210 | 211 | } 212 | 213 | @Override 214 | public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException { 215 | 216 | } 217 | 218 | @Override 219 | public int getStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException { 220 | return 0; 221 | } 222 | 223 | @Override 224 | public void incrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount) throws IllegalArgumentException { 225 | 226 | } 227 | 228 | @Override 229 | public void decrementStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int amount) { 230 | 231 | } 232 | 233 | @Override 234 | public void setStatistic(@NotNull Statistic statistic, @NotNull EntityType entityType, int newValue) { 235 | 236 | } 237 | 238 | /** 239 | * Gets the player's last death location. 240 | * 241 | * @return the last death location if it exists, otherwise null. 242 | */ 243 | @Nullable 244 | @Override 245 | public Location getLastDeathLocation() { 246 | return null; 247 | } 248 | 249 | @Nullable 250 | @Override 251 | public Location getLocation() { 252 | return null; 253 | } 254 | 255 | @NotNull 256 | @Override 257 | public Map serialize() { 258 | return null; 259 | } 260 | 261 | @Override 262 | public boolean isOp() { 263 | return true; 264 | } 265 | 266 | @Override 267 | public void setOp(boolean value) { 268 | 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/__spigot.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.utils; 2 | 3 | import org.bukkit.command.CommandSender; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.util.List; 8 | import java.util.UUID; 9 | 10 | public class __spigot extends CommandSender.Spigot { 11 | List message; 12 | public __spigot(List message) { 13 | this.message = message; 14 | } 15 | 16 | @Deprecated 17 | public void sendMessage(@NotNull net.md_5.bungee.api.chat.BaseComponent component) { 18 | this.message.add(component.toLegacyText()); 19 | } 20 | 21 | @Deprecated 22 | public void sendMessage(@NotNull net.md_5.bungee.api.chat.BaseComponent... components) { 23 | for(net.md_5.bungee.api.chat.BaseComponent component : components){ 24 | this.message.add(component.toLegacyText()); 25 | } 26 | } 27 | 28 | @Deprecated 29 | public void sendMessage(@Nullable UUID sender, @NotNull net.md_5.bungee.api.chat.BaseComponent component) { 30 | this.message.add(component.toLegacyText()); 31 | } 32 | 33 | @Deprecated 34 | public void sendMessage(@Nullable UUID sender, @NotNull net.md_5.bungee.api.chat.BaseComponent... components) { 35 | for(net.md_5.bungee.api.chat.BaseComponent component : components){ 36 | this.message.add(component.toLegacyText()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/aplini/chat2qq/utils/renderGroupMessage.java: -------------------------------------------------------------------------------- 1 | package io.github.aplini.chat2qq.utils; 2 | 3 | import me.dreamvoid.miraimc.api.MiraiMC; 4 | import me.dreamvoid.miraimc.bukkit.event.message.passive.MiraiGroupMessageEvent; 5 | import net.md_5.bungee.api.chat.HoverEvent; 6 | import net.md_5.bungee.api.chat.TextComponent; 7 | import net.md_5.bungee.api.chat.hover.content.Text; 8 | import org.bukkit.plugin.Plugin; 9 | 10 | import static io.github.aplini.chat2qq.utils.Util.*; 11 | 12 | public class renderGroupMessage { 13 | 14 | // 渲染 message 15 | public static String _renderMessage(Plugin plugin, String message) { 16 | // 预处理模块 17 | message = pretreatment(plugin, "aplini.pretreatment", message); 18 | 19 | // 预设的格式调整功能. 是否删除 %message% 消息 中的格式化字符 20 | if(plugin.getConfig().getBoolean("aplini.other-format-presets.render-message_format-code",false)){ 21 | message = message.replaceAll("§[a-z0-9]", ""); 22 | } 23 | 24 | // 预设的格式调整功能. 删除 %message% 前后的空格和换行 25 | if(plugin.getConfig().getBoolean("aplini.other-format-presets.message-trim",true)){ 26 | message = message.trim(); 27 | } 28 | 29 | return message; 30 | } 31 | 32 | 33 | 34 | // 回复消息 35 | public static String getReplyVar(Plugin plugin, MiraiGroupMessageEvent e) { 36 | if(e.getQuoteReplyMessage() != null){ 37 | return plugin.getConfig().getString("aplini.reply-message.var", "[reply] ") 38 | .replace("%c_name%", cleanupName( 39 | plugin, 40 | getNameFromCache(plugin, e.getGroupID(), e.getQuoteReplySenderID(), String.valueOf(e.getQuoteReplySenderID())), 41 | e.getQuoteReplySenderID())) 42 | .replace("%qq%", String.valueOf(e.getQuoteReplySenderID())) 43 | .replace("%_/n_%", "\n"); 44 | } 45 | return ""; 46 | } 47 | 48 | 49 | // 可读的消息 50 | public static String [] renderMessage1(Plugin plugin, MiraiGroupMessageEvent e) { 51 | 52 | String [] message = new String [4]; 53 | message[0] = ""; // 消息类型 54 | message[1] = e.getMessage(); // 源消息 55 | message[2] = ""; // 经过格式化后, 内容可见的消息 56 | message[3] = ""; // 可能不包含可见消息的显示消息, 用于控制台以及配合JSON消息 57 | 58 | // no = 不发送 59 | 60 | // 群聊天前缀 (聊天需要带有指定前缀才能发送到服务器) 61 | if(plugin.getConfig().getBoolean("general.requite-special-word-prefix.enabled",false)){ 62 | boolean allowPrefix = false; 63 | // - "#" 64 | for(String prefix : plugin.getConfig().getStringList("general.requite-special-word-prefix.prefix")){ 65 | if(message[1].startsWith(prefix)){ 66 | allowPrefix = true; 67 | // 移除消息中的前缀 68 | message[1] = message[1].substring(prefix.length()); 69 | break; 70 | } 71 | } 72 | if(! allowPrefix){ 73 | message[0] = "no"; 74 | return message; 75 | } 76 | } 77 | 78 | // 当群名片不存在时是否尝试获取昵称 79 | String name = e.getSenderNameCard(); 80 | if(name.equalsIgnoreCase("") && plugin.getConfig().getBoolean("general.use-nick-if-namecard-null",true)){ 81 | name = e.getSenderName(); 82 | } 83 | // 预设的格式调整功能. 是否删除 %nick% 群名片 中的格式化字符 84 | if(plugin.getConfig().getBoolean("aplini.other-format-presets.render-nick_format-code",true)){ 85 | name = name.replaceAll("§[a-z0-9]", ""); 86 | } 87 | 88 | 89 | // 经过格式化后, 内容可见的消息 90 | message[2] = message[1]; 91 | 92 | 93 | // 引用回复 94 | // 如果是回复消息, 则删除消息开头重复的 @qqID 95 | if(e.getQuoteReplyMessage() != null && plugin.getConfig().getBoolean("aplini.reply-message.del-duplicates-at",true)){ 96 | String atField = "@"+ e.getQuoteReplySenderID(); 97 | if(message[2].startsWith(atField)){ 98 | message[2] = message[2].substring(atField.length()); 99 | } 100 | } 101 | 102 | // 预处理 103 | message[2] = _renderMessage(plugin, message[2]); 104 | if(message[2].equals("")){ 105 | message[0] = "no"; 106 | return message; 107 | } 108 | 109 | 110 | // 可能不包含可见消息的显示消息 111 | message[3] = message[2]; 112 | 113 | 114 | // 预设的格式调整功能. 更好的多行消息 115 | if(plugin.getConfig().getBoolean("aplini.other-format-presets.multiline-message.enabled",true) && message[3].contains("\n")){ 116 | String _l0 = plugin.getConfig().getString("aplini.other-format-presets.multiline-message.line-0", "line-0"); 117 | String _l1 = plugin.getConfig().getString("aplini.other-format-presets.multiline-message.line-prefix", "line-prefix"); 118 | message[3] = _l0 + "\n" + _l1 + message[3].replace("\n", "\n" + _l1); 119 | } 120 | 121 | // 预设的格式调整功能. 聊天消息过长时转换为悬浮文本 122 | if(message[2].length() > plugin.getConfig().getInt("aplini.other-format-presets.long-message.condition-length", 210) || 123 | message[2].contains("\n") && 124 | message[2].length() - message[2].replace("\n","").length() > 125 | plugin.getConfig().getInt("aplini.other-format-presets.long-message.condition-line_num", 6)){ 126 | message[0] = "lm"; 127 | message[3] = plugin.getConfig().getString("aplini.other-format-presets.long-message.message"); 128 | 129 | // 删除消息中多余的换行, 用于解决 "预设的格式调整功能. 更好的多行消息" 造成的消息开头多一个空行 130 | message[2] = message[2].trim(); 131 | } 132 | 133 | 134 | // 消息模板路径 135 | String message2_config_path; 136 | // Mirai 内置QQ绑定 137 | if(plugin.getConfig().getBoolean("general.use-miraimc-bind",false) && MiraiMC.getBind(e.getSenderID()) != null){ 138 | message2_config_path = plugin.getConfig().getString("general.special-bind."+ e.getGroupID(), 139 | plugin.getConfig().getString("general.bind-chat-format", "message")); 140 | }else{ 141 | message2_config_path = plugin.getConfig().getString("general.special."+ e.getGroupID(), 142 | plugin.getConfig().getString("general.in-game-chat-format", "message")); 143 | } 144 | 145 | String message3 = message[3]; 146 | message[3] = message2_config_path 147 | .replace("%groupname%",e.getGroupName()) 148 | .replace("%groupid%",String.valueOf(e.getGroupID())) 149 | .replace("%qq%",String.valueOf(e.getSenderID())) 150 | .replace("%nick%",name) 151 | .replace("%regex_nick%", cleanupName(plugin, name, e.getSenderID())) // aplini.cleanup-name 152 | .replace("%_reply_%", getReplyVar(plugin, e)) 153 | .replace("%message%", formatQQID(plugin, message3, e.getGroupID())); 154 | 155 | return message; 156 | } 157 | 158 | // JSON消息 159 | public static TextComponent renderMessage2(Plugin plugin, String [] message, MiraiGroupMessageEvent e) { 160 | 161 | // 转换为JSON文本 162 | TextComponent formatText = new TextComponent(message[3]); 163 | 164 | // 预设的格式调整功能. 聊天消息过长时转换为悬浮文本 165 | if(message[0].equals("lm")){ 166 | // 设置悬浮文本 167 | formatText.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(message[2]))); 168 | } 169 | 170 | // 如果是回复消息 171 | if(e.getQuoteReplyMessage() != null){ 172 | // 创建回复消息的悬浮文本 173 | String replyMessage = plugin.getConfig().getString("aplini.reply-message.message", "[引用回复]") 174 | .replace("%c_name%", cleanupName(plugin, 175 | getNameFromCache(plugin, e.getGroupID(), e.getQuoteReplySenderID(), String.valueOf(e.getQuoteReplySenderID())), 176 | e.getQuoteReplySenderID())) 177 | .replace("%qq%", String.valueOf(e.getQuoteReplySenderID())) 178 | .replace("%_/n_%", "\n") 179 | .replace("%message%", formatQQID(plugin, _renderMessage(plugin, e.getQuoteReplyMessage()), e.getGroupID())) 180 | .replace("%main_message%", message[2]); // 跳过消息过长部分 181 | // 设置悬浮文本 182 | formatText.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(replyMessage))); 183 | } 184 | 185 | return formatText; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | 2 | # 游戏内配置 3 | # QQ -> MC 的消息 4 | general: 5 | # 转发哪些QQ群的消息 6 | group-ids: 7 | - 1000000 8 | - 1000001 9 | 10 | # 群聊天前缀 (聊天需要带有指定前缀才能发送到服务器) 11 | requite-special-word-prefix: 12 | enabled: false 13 | prefix: 14 | - '#' 15 | 16 | # 当群名片不存在时是否尝试获取昵称 17 | use-nick-if-namecard-null: true 18 | 19 | # QQ群消息广播到游戏内聊天的格式 格式化代码: § 20 | # %groupname% - 群名称 21 | # %groupid% - 群号 22 | # %nick% - 发送者群名片 23 | # %regex_nick% - 使用正则匹配到的名称, 需要开启 aplini.cleanup-name 模块 24 | # %qq% - 发送者QQ号 25 | # %message% - 消息内容, 支持预处理模块 aplini.pretreatment 26 | # %_reply_% - 如果是回复消息..., 配置在 aplini.reply-message 模块 27 | in-game-chat-format: '§f[§7%nick%§r§f] %_reply_%§7%message%' 28 | # 为每个群使用不同的格式, 如果没有则使用上方的 in-game-chat-format 29 | special: 30 | 1000000: '§f[§7主群 %nick%§r§f] %_reply_%§7%message%' 31 | 1000001: '§7[外群 %nick%] %_reply_%%message%' 32 | 33 | # 启用 MiraiMC 内置的QQ绑定 34 | use-miraimc-bind: false 35 | # 已绑定玩家的广播消息格式 36 | bind-chat-format: '§f[§7%nick%§r§f] %_reply_%§7%message%' 37 | # 为每个群使用不同的格式, 如果没有则使用上方的 bind-chat-format 38 | special-bind: 39 | 1000000: '§f[§7主群 %nick%§r§f] %_reply_%§7%message%' 40 | 41 | 42 | 43 | # 机器人配置 44 | # MC -> QQ 的消息 45 | bot: 46 | # 使用哪些QQ号处理消息 47 | # 只能添加一个 48 | bot-accounts: 49 | - 2000000 50 | 51 | # 将消息转发到那些QQ群 52 | group-ids: 53 | - 1000000 54 | 55 | # 玩家在以下世界中聊天才会被转发 56 | available-worlds: 57 | #- world 58 | # 将以上配置作为黑名单, 玩家不在以上世界中聊天才会被转发 59 | available-worlds-use-as-blacklist: true 60 | 61 | # 游戏聊天前缀 (聊天需要带有指定前缀才能发送到QQ群) 62 | requite-special-word-prefix: 63 | enabled: true 64 | prefix: 65 | - '#' 66 | 67 | # 是否转发被其他插件取消过的聊天消息事件, 用于修复一些兼容性问题 68 | ignoreCancelled: false 69 | 70 | # 服务器消息发送到QQ群的格式 71 | # %player% - 玩家名称 72 | # %message% - 消息内容 73 | group-chat-format: '[%player%] %message%' 74 | 75 | 76 | # 是否发送玩家进出服务器的消息 77 | # %player% - 玩家显示昵称 78 | send-player-join-quit-message: false 79 | # 加入 80 | player-join-message: '%player% 进入服务器' 81 | # 退出 82 | player-quit-message: '%player% 离开服务器' 83 | # 防刷屏, 在此时间内多次进出服务器不会发送消息 84 | player-join-quit-message-interval: 0 85 | 86 | 87 | 88 | # 黑名单, 可用于添加其他QQ机器人 89 | # 优先级大于上方配置 90 | blacklist: 91 | # 不转发以下QQ号的聊天消息 92 | qq: 93 | #- 2000001 94 | 95 | # 不转发以下玩家名的聊天消息 96 | player: 97 | #- playerName 98 | 99 | 100 | # ############### # 101 | # 以下为功能模块配置 # 102 | # ############### # 103 | 104 | aplini: 105 | 106 | ## 1 107 | # 在QQ群中运行指令 [需要单独添加QQ群] 108 | # 此模块不处理黑名单 blacklist 109 | run-command: 110 | enabled: false 111 | # 启用的 QQ群 112 | qq-group: 113 | - 1000001 114 | 115 | # 指令前缀, 可以是多个字符, 比如 "~$" 116 | command-prefix: '/' 117 | # 指令最大长度 (不包括指令前缀) 118 | command-max-length: 255 119 | # 获取指令的正则表达式, 当第一个捕获组的内容与指令白名单中的匹配时则允许运行 (不带斜杠或前缀) 120 | regex-command-main: '^([^ ]+)' 121 | # 判断指令返回为空的正则, 匹配多行文本. (经过 pretreatment-command-message 处理后) 122 | return-isNull: '^\s*$' 123 | # 是否将主命令转换为小写再执行 124 | always-lowercase: false 125 | 126 | # 是否发送指令的输出, 关闭可提高性能或解决一些兼容性问题 127 | return: true 128 | # 等待指令运行多长时间再将结果发送到QQ群 (毫秒), 需要开启 run-command.return 129 | # 如果你遇到了一些提前输出类似 "正在运行...请稍等" 消息的插件, 可以在 pretreatment-command-message 中配置完全删除这条消息. 然后 return-sleep-min 保持不变 :) 130 | return-sleep-min: 14 # 最小等待时间 131 | return-sleep-max: 5346 # 最大等待时间, 如果一些长耗时指令没有输出请增大此值 132 | return-sleep-sampling-interval: 172 # 输出内容检查间隔, 如果经常执行长耗时指令可以增大此值 133 | # 是否将指令的输出打印到控制台和日志 134 | return-log: true 135 | 136 | # 执行不在白名单中的指令时发送返回消息 137 | message-miss: '未命中的指令' 138 | # 运行无返回指令的消息 139 | message-no-out: '运行无返回指令' 140 | 141 | # 设置各组可执行的主命令白名单 (不带斜杠或前缀) 142 | # 权限更高的用户将可以使用更低的用户的指令 143 | # 如果添加一条 ___ALL_COMMAND___ 作为指令, 则表示此组可以使用所有指令, 此功能请勿随意使用 ! 144 | group: 145 | # permission_ 是 MiraiMC 获取到的权限数字, 以后更新了其他权限只需要以此格式添加即可使用 146 | permission_2: # 群主 147 | #- chat2qq 148 | permission_1: # 管理员 149 | #- spark 150 | permission_0: # 成员 151 | #- list 152 | #- tps 153 | 154 | # 特殊指令配置 155 | special: 156 | no-return: # 这些指令始终不输出消息 157 | #- plugins 158 | #- version 159 | 160 | 161 | ## 2 162 | # 从 群名片(%nick%) 中匹配 MC 可用的游戏名称 163 | # 添加变量: %regex_nick% - 使用正则匹配到的名称, 需要开启 cleanup-name 功能 164 | cleanup-name: 165 | enabled: false 166 | # 程序取第一个捕获组的结果 167 | regex: '([a-zA-Z0-9_]{3,16})' 168 | # 如果匹配不到, 则使用以下字符串 169 | # %nick% - 群名片 170 | # %qq% - qq号 171 | not-captured: '%nick%' 172 | 173 | 174 | ## 3 175 | # 预处理 %message% 中的消息 176 | pretreatment: 177 | enabled: true 178 | # **使用方法** 179 | # list: 180 | # - 匹配方式: prefix (前缀匹配), 处理方式: to_all, to_replace 181 | # contain (包含), 处理方式: to_all, to_replace 182 | # equal (完全相等), 处理方式: to_all 183 | # regular (正则匹配), 处理方式: to_all, to_regular 184 | # 185 | # 处理方式: to_all (替换整条消息) 186 | # to_replace (替换匹配到的部分) 187 | # to_regular (使用正则替换, 可使用正则变量) 188 | # 189 | # 是否发送: send (填写 send 配置将取消转发送匹配到的消息, 不需要时请忽略) 190 | 191 | # 示例配置, 默认配置了一些可能有用的功能: 192 | list: 193 | 194 | # 群公告, JSON 195 | - prefix: '{"app":"com.tencent.mannounce"' 196 | to_all: '[群公告]' 197 | 198 | # 视频, 字符串 199 | - prefix: '你的QQ暂不支持查看视频短片' 200 | to_all: '[视频]' 201 | 202 | # 使中括号与文本的前后始终有空格 203 | - regular: '\[([^\]]+)\]([^\s])' 204 | to_regular: '[$1] $2' 205 | - regular: '([^\s])\[([^\]]+)\]' 206 | to_regular: '$1 [$2]' 207 | 208 | # 转发消息使用前缀, 在群中使用 # 前缀将改变消息格式 209 | - regular: '^\s*(?:#|#)' 210 | to_regular: '§7> §f' 211 | 212 | # 示例: 取消发送包含此内容的消息 213 | #- contain: '此内容' 214 | # send: false 215 | 216 | 217 | ## 3.1 218 | # 按行预处理指令返回消息, 用于处理返回到QQ群的消息 219 | pretreatment-command-message: 220 | enabled: true 221 | # 使用方法: 如上 222 | list: 223 | # 删除格式化字符 224 | - regular: '§[a-z0-9]' 225 | to_regular: '' 226 | 227 | # 示例: co插件翻页消息处理 228 | #- regular: '◀? ?第 (.*) 页 ▶? ?\((.*)\)' 229 | # to_regular: '第 $1 页, 使用 /co page <页码> 翻页' 230 | 231 | 232 | ## 3.2 233 | # 按多行文本预处理指令返回消息 234 | # 可使用占位符: 235 | # - %command% :: 用户运行的指令原文(不带斜杠/前缀) 236 | # - %time% :: 指令运行耗时 237 | # - %qq% :: 执行指令的qq号 238 | # - %group% :: 执行指令的群号 239 | pretreatment-command-message-all: 240 | enabled: false 241 | enabled-placeholder: false # 关闭占位符可提高性能 242 | # 使用方法: 如上 243 | list: 244 | # 示例: 显示指令运行时间, 需要开启占位符 245 | #- regular: '([\s\S]+)' 246 | # to_regular: '$1\n - 运行耗时: %time%ms' 247 | 248 | 249 | ## 4 250 | # 预设的格式调整功能 251 | other-format-presets: 252 | # 是否删除 %message% 消息 中的格式化字符 253 | render-message_format-code: false 254 | # 删除 %message% 消息 前后的空格和空行 255 | message-trim: true 256 | # 是否删除 %nick% 群名片 中的格式化字符 257 | render-nick_format-code: true 258 | 259 | # 聊天消息过长时转换为悬浮文本 260 | long-message: 261 | enabled: true 262 | # 以下任意一个条件成立时被判定为长消息, 若需取消一个, 请改为很大的数 263 | # 条件1: 消息长度达到此值 264 | condition-length: 210 265 | # 条件2: 换行数量达到此值, 在 message-trim 之后运行 266 | condition-line_num: 6 267 | # 显示为 268 | message: '§f[§7长消息§f]' 269 | 270 | # 是否启用 "更好的多行消息" 271 | multiline-message: 272 | enabled: true 273 | line-0: '' # [多行消息] 274 | line-prefix: ' ' 275 | 276 | # 是否将聊天消息转发到控制台/日志 277 | message-to-log: true 278 | 279 | 280 | ## 5 281 | # 引用回复 282 | # 添加变量: %_reply_% 283 | # 如果是回复消息, 则为变量赋值并为消息添加悬浮文本框用于显示内容. 可以将鼠标悬停在消息上查看回复的内容 284 | reply-message: 285 | # 可用变量: 286 | # %qq% - 被回复的消息的发送者QQ号 287 | # %c_name% - 群名片 - 需要开启 aplini.format-qq-id 288 | var: '§f[§7回复 @%c_name%§f] ' 289 | 290 | # 可用变量: 291 | # %_/n_% - 换行 292 | # %qq% - 被回复的消息的发送者QQ号 293 | # %c_name% - 群名片 - 需要开启 aplini.format-qq-id 294 | # %message% - 回复内容 295 | # %main_message% - 当前消息内容 296 | message: '§f[§7引用 @%c_name%§f]%_/n_%§7%message%§r%_/n_%%_/n_%§f——%main_message%' 297 | 298 | # 删除重复@ :: 如果引用回复对象等于消息开头的@对象, 则删除消息开头的 @ 299 | del-duplicates-at: true 300 | 301 | 302 | ## 6 303 | # 发送消息的指令 304 | # /qchat <消息> - 使用此指令 305 | qchat: 306 | # 使用上方 general.group-ids 中配置的群 307 | use-general-group-ids: true 308 | # 消息转发到哪些群, 需要 use-general-group-ids: false 309 | group-ids: 310 | - 1000000 311 | # 如果是玩家使用指令 312 | player: 313 | # 转发到QQ群的格式 314 | # %name% - 玩家名称 315 | # %message% - 消息 316 | qq-format: '[%name%] %message%' 317 | # 是否同时将消息广播到MC服务器 318 | mc-broadcast: true 319 | # 广播到MC服务器的 320 | mc-format: '§f[§7%name%§f] §7%message%' 321 | # 如果是控制台或插件使用指令, 同时绕过关键词和玩家黑名单 322 | console: 323 | # %message% - 消息 324 | qq-format: '%message%' 325 | 326 | 327 | ## 7 328 | # [前置] 群成员信息缓存 329 | # ! 需要开启 MiraiMC 配置中的 bot.contact-cache.enable-group-member-list-cache 330 | player-cache: 331 | # 在机器人登录和服务器启动时运行此程序 332 | enabled: true 333 | # 在玩家群名片修改时更新缓存 334 | auto-update: true 335 | # 在玩家发送消息时更新缓存 336 | auto-update-form-msg: true 337 | # 群名片修改时发出日志 338 | auto-update-log: true 339 | # 使用上方 general.group-ids 中配置的群 340 | use-general-group-ids: true 341 | # 缓存哪些群, 需要 use-general-group-ids: false 342 | group-ids: 343 | - 1000000 344 | # MiraiMC 群缓存文件路径, 如果你修改了插件目录相关的配置, 才需要修改它 345 | # %qq% - 机器人账号 346 | # %group% - 群号 347 | mirai-cache-path: "plugins/MiraiMC/MiraiBot/bots/%qq%/cache/contacts/groups/%group%.json" 348 | 349 | 350 | ## 8 351 | # 将 %message% 中的 @qqID 替换为 @名称 352 | # 需要开启前置: aplini.player-cache 353 | format-qq-id: 354 | enabled: true 355 | # 用于匹配 @qqID 的正则 356 | regular: '(@[0-9]{5,11})' 357 | # 格式 358 | # %qq% - qq号 359 | # %name% - 名称 360 | format: '§f[§7@%name%§f]§7' 361 | # 一条消息最多匹配几次, 防止刷屏浪费性能 362 | max-cycles-num: 10 363 | 364 | 365 | ## 9 366 | # 事件任务 367 | event-func: 368 | # enable 修改后需要重启服务器 369 | enable: false 370 | # 使用上方 general.group-ids 中配置的群 371 | use-general-group-ids: true 372 | # 启用在哪些群, 需要 use-general-group-ids: false 373 | group-ids: 374 | - 1000000 375 | 376 | # 每个事件可用的任务不同, 这里列出了所有任务的使用方法: 377 | # - command: 'command' - 发送指令 378 | 379 | # - message-text: '消息' - 向事件来源发送消息, 群 或 好友/私聊 380 | 381 | # - message-group: 1000000 - 向指定群发送消息 382 | # message-text: '消息' 383 | 384 | # - message-friend: 2000003 - 向指定好友发送消息 385 | # message-text: '消息' 386 | 387 | MiraiMemberJoinEvent: # 群成员加入 388 | # 可使用: command, message-text, message-group 389 | # 一个事件中可添加多个相同或不相同的任务, 就像这样: 390 | #- message-text: '欢迎' 391 | #- command: 'tps' 392 | #- command: 'mspt' 393 | #- message-group: 1000000 394 | # message-text: '消息' 395 | 396 | MiraiMemberLeaveEvent: # 成员退出 397 | # 可使用: command, message-text, message-group 398 | 399 | 400 | ## 10 401 | # 自动回复 402 | # 当QQ群中的消息匹配时发送自定义消息 403 | auto-response: 404 | enable: true 405 | # 使用上方 general.group-ids 中配置的群 406 | use-general-group-ids: true 407 | # 回复哪些群的消息, 需要 use-general-group-ids: false 408 | group-ids: 409 | - 1000000 410 | # 为此功能启用PAPI, 需要安装PAPI插件 411 | enable-papi: false 412 | 413 | # **使用方法** 414 | # list: 415 | # - 匹配方式: prefix (前缀匹配) 416 | # contain (包含) 417 | # equal (完全相等) 418 | # regular (正则匹配, send 中可使用正则变量) 419 | # send (发送的消息内容) 420 | # 421 | # > 正则的性能较差, 请尽量避免使用很多正则 422 | # !! 请小心使用正则拼接PAPI变量, 如果正则设计有问题则可能出现注入漏洞 !! 423 | # - 提示: 应指定匹配的字符范围和最小最大次数, 要绝对的防止输入PAPI变量的保留符号: % 424 | # - 比如: - regular: '^\#ping ([a-zA-Z0-9_]{3,16})$' 425 | # send: '$1 的延迟为: %player_ping_$1%ms' 426 | # 示例配置, 默认配置了一些可能有用的功能: 427 | list: 428 | 429 | # 使用PAPI获取在线玩家数量, 需要启用 aplini.auto-response.enable-papi 430 | # PlayerList: /papi ecloud download playerlist 431 | - equal: '#list' 432 | send: '在线玩家: [%playerlist_online,normal,yes,amount%] \n%playerlist_online,normal,yes,list%' 433 | 434 | # 使用PAPI获取服务器TPS, 需要启用 aplini.auto-response.enable-papi 435 | # Server: /papi ecloud download Server 436 | - equal: '#tps' 437 | send: 'TPS [1m, 5m, 15m]: %server_tps_1% / %server_tps_5% / %server_tps_15%' 438 | 439 | # 指令列表 440 | - equal: '#help' 441 | send: '指令列表: 442 | \n - #list - 显示在线玩家列表 443 | \n - #tps - 显示服务器TPS' 444 | 445 | # @一个QQ号时发送消息 446 | - contain: '@2000000' 447 | send: 'OwO' 448 | 449 | 450 | # <- 至此, 您已经完成了所有配置, 部分功能使用 /chat2qq reload 重载插件即可应用 uwu 451 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Chat2QQ 2 | main: io.github.aplini.chat2qq.Chat2QQ 3 | version: '${project.version}' 4 | api-version: 1.18 5 | folia-supported: true 6 | prefix: MiraiMC 7 | author: ApliNi 8 | 9 | softdepend: 10 | - PlaceholderAPI 11 | 12 | depend: 13 | - MiraiMC 14 | 15 | commands: 16 | qchat: 17 | description: 发送聊天消息到QQ群 18 | permission: chat2qq.command.qchat 19 | 20 | chat2qq: 21 | description: Chat2QQ 插件主命令 22 | 23 | permissions: 24 | chat2qq.qq.receive: 25 | description: 允许收到来自QQ群的消息 26 | default: true 27 | 28 | chat2qq.chat.requite: 29 | description: 允许使用前缀符号转发消息到QQ群 30 | default: true 31 | 32 | chat2qq.join.silent: 33 | description: 允许悄悄加入服务器 34 | default: false 35 | 36 | chat2qq.quit.silent: 37 | description: 允许悄悄离开服务器 38 | default: false 39 | 40 | chat2qq.command.qchat: 41 | description: 允许使用 /qchat 42 | default: op 43 | 44 | chat2qq.command.chat2qq: 45 | description: 允许使用 /chat2qq 46 | default: op 47 | 48 | chat2qq:.command.setgroupcacheall: 49 | description: 允许使用 /chat2qq setgroupcacheall 50 | default: op 51 | --------------------------------------------------------------------------------