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

2 | nonebot 3 |

4 | 5 | 6 | 7 |
8 | 9 | **你的star是我的动力** 10 | **↓** 11 | 12 | # 简易群管 (上岸缓更) 13 | ~~[dev分支](https://github.com/yzyyz1387/nonebot_plugin_admin/tree/dev)~~ 14 | _✨ NoneBot2 (有点不)简易群管✨ _ 15 | 16 | 17 | [//]: # ([![wakatime](https://wakatime.com/badge/user/e4795d94-d154-4c3d-a94b-b655c82e57f4/project/d4a8cb5e-ee86-4ad9-99e5-48873f38c3bd.svg)](https://wakatime.com/badge/user/e4795d94-d154-4c3d-a94b-b655c82e57f4/project/d4a8cb5e-ee86-4ad9-99e5-48873f38c3bd)) 18 | 19 | 20 | 踢 改 禁....... 21 | **欢迎 ISSUES PR** 22 |
23 | 24 | 25 | **权限说明:见下方指令↓** 26 | 27 | ## 安装💿(pip) 28 | `pip install nonebot-plugin-admin` 29 | 30 | ### 导入📲 31 | 在**bot.py** 导入,语句: 32 | `nonebot.load_plugin("nonebot_plugin_admin")` 33 | 34 | 请注意与nonebot版本适配,匹配请查看:[更新](#%E6%9B%B4%E6%96%B0-1) 35 | **Python 3.9+** 36 | 37 | ## 安装💿(nb plugin) 38 | `nb plugin install nonebot-plugin-admin` 39 | 40 | 41 | ## 更新 42 | 43 | `pip install --upgrade nonebot-plugin-admin ` 44 | 45 | ## 配置 46 | **鉴黄配置**: 47 | 腾讯云图片安全,开通地址:[https://console.cloud.tencent.com/cms](https://console.cloud.tencent.com/cms) 48 | 文档:[https://cloud.tencent.com/document/product/1125](https://cloud.tencent.com/document/product/1125) 49 | 50 | 需要使用此功能时在 `.env.*` 文件中加入以下内容,并且设置你自己的 `api id` 与 `api key`【不需要此功能可以不配置】: 51 | ``` 52 | # 腾讯云图片安全api 53 | tenid="xxxxxx" 54 | tenkeys="xxxxxx" 55 | # 是否开启禁言等操作的成功提示【不开启的话踢人/禁言等成功没有QQ消息提示】 56 | callback_notice=true # 如果不想开启设置成 false 或者不添加此配置项【默认关闭】 57 | ``` 58 | ✨Pay tribute to A60 [https://github.com/djkcyl/ABot-Graia](https://github.com/djkcyl/ABot-Graia) 59 | 60 | **早晚安配置** 61 | 62 | 额外依赖pip install nonebot_plugin_apscheduler 63 | 定时推送群消息需要在.evn中配置: 64 | ```yaml 65 | send_group_id = ["xxx","xxx"] # 必填 群号 66 | send_switch_morning = False # 选填 True/False 默认开启 早上消息推送是否开启 67 | send_switch_night = False # 选填 True/False 默认开启 晚上消息推送是否开启 68 | send_mode = 1 # 选填 默认模式2 模式1发送自定义句子,模式2随机调用一句 69 | send_sentence_morning = ["句子1","句子2","..."] # 如果是模式1 此项必填,早上随机发送该字段中的一句 70 | send_sentence_night = ["句子1","句子2","..."] # 如果是模式1 此项必填,晚上随机发送该字段中的一句 71 | send_time_moring = "8 0" # 选填 早上发送时间默认为7:00 72 | send_time_night = "23 0" # 选填 晚上发送时间默认为22:00 73 | ``` 74 | 75 | 更多配置项请查看 [config.py](./nonebot_plugin_admin/config.py) 76 | 77 | 78 | 79 | ## 注意 80 | **[dev分支](https://github.com/yzyyz1387/nonebot_plugin_admin/tree/dev)由于API的加入,首次使用本插件时,会终止机器人程序,需要再启动一次** 81 | **控制台会有对应提示** 82 | 83 | ## 指令💻 84 | 85 | **Tips:** 86 | 87 | - 关于命令,对/sp这类`斜杠+英文`的命令做了保留,汉字命令去除了`/`若使用者担心错误触发,可下载源码自行修改`__init__.py` 88 | - 群词云功能所用库 wordcloud 未写入依赖,请自行安装:`pip install wordcloud` 安装失败参考:[WordCloud 第三方库安装失败原因及解决方法](https://www.freesion.com/article/4756295761/) 89 | - 一般情况下可正常使用,可能由于权重出现问题,matcher权重请自行查看代码 90 | - 使用`开关状态`指令查看各功能状态,首次使用可能会下载100Mb+的`Chromium`,请耐心等待 91 | ``` 92 | 【初始化】: 93 | 群管初始化 :初始化插件 94 | 95 | 【群管】: 96 | 权限:permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 97 | 禁言: 98 | 禁 @某人 时间(s)[1,2591999] 99 | 禁 时间(s)@某人 [1,2591999] 100 | 禁 @某人 缺省时间则随机 101 | 禁 @某人 0 可解禁 102 | 解 @某人 103 | 禁言时,该条消息中所有数字都会组合作为禁言时间,如:‘禁@某人 1哈2哈0哈’,则禁言120s 104 | 105 | 全群禁言 若命令前缀不为空,请使用//all,若为空,需用 /all 来触发 106 | /all 107 | /all 解 108 | 109 | 改名片 110 | 改 @某人 名片 111 | 112 | 踢出: 113 | 踢 @某人 114 | 踢出并拉黑: 115 | 黑 @某人 116 | 117 | 撤回: 118 | 撤回 (回复某条消息即可撤回对应消息) 119 | 撤回 @user [(可选,默认n=5)历史消息倍数n] (实际检查的历史数为 n*19) 120 | 121 | 设置精华 122 | 回复某条消息 + 加精 123 | 取消精华 124 | 回复某条消息 + 取消精华 125 | 126 | 【头衔】 127 | 改头衔 128 | 自助领取:头衔 xxx 129 | 自助删头衔:删头衔 130 | 超级用户更改他人头衔:头衔 @某人 头衔 131 | 超级用户删他人头衔:删头衔 @某人 132 | 133 | 【管理员】permission=SUPERUSER | GROUP_OWNER 134 | gl+ @xxx 设置某人为管理员 135 | 管理员+ @xxx 设置某人为管理员 136 | 管理员加 @xxx 设置某人为管理员 137 | 加管理 @xxx 设置某人为管理员 138 | 139 | gl- @xxx 取消某人管理员 140 | 管理员- @xxx 取消某人管理员 141 | 管理员减 @xxx 取消某人管理员 142 | 减管理 @xxx 取消某人管理员 143 | 144 | 145 | 【加群自动审批】: 146 | 群内发送 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 147 | 查看词条 : 查看本群审批词条 或/审批 148 | ct+ [词条] :增加审批词条 或/审批+ 149 | 词条+ [词条] :增加审批词条 或/审批+ 150 | ct- [词条] :删除审批词条 或/审批- 151 | 词条- [词条] :删除审批词条 或/审批- 152 | 按照黑名单自动拒绝:(当验证消息与黑名单内容匹配时,自动拒绝) 153 | 添加示例: 154 | jj+管理员你好,请通过一下 155 | ctjj+管理员你好,请通过一下 156 | 词条拒绝+管理员你好,请通过一下 157 | 拒绝词条+管理员你好,请通过一下 158 | /spx+管理员你好,请通过一下 159 | 删除示例: 160 | jj-管理员你好,请通过一下 161 | ctjj-管理员你好,请通过一下 162 | 词条拒绝-管理员你好,请通过一下 163 | 拒绝词条-管理员你好,请通过一下 164 | /spx-管理员你好,请通过一下 165 | 166 | 【superuser】: 167 | 所有词条 : 查看所有审批词条 或/su审批 168 | zdct+ [词条] :增加审批词条 169 | 指定词条+ [群号] [词条] :增加指定群审批词条 170 | 指定词条加 [群号] [词条] :增加指定群审批词条 或/su审批+ 171 | zdct- [词条] :删除审批词条 172 | 指定词条- [群号] [词条] :删除指定群审批词条 173 | 指定词条减 [群号] [词条] :删除指定群审批词条 或/su审批- 174 | 自动审批处理结果将发送给superuser 175 | 176 | 【分群管理员设置】*分管:可以接受加群处理结果消息的用户 177 | 群内发送 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 178 | fg+ [user] :user可用@或qq 添加分群管理员 179 | 分管+ [user] :user可用@或qq 添加分群管理员 180 | 分管加 [user] :user可用@或qq 添加分群管理员 181 | fg- [user] :删除分群管理员 182 | 分管- [user] :删除分群管理员 183 | 分管减 [user] :删除分群管理员 184 | 查看分管 :查看本群分群管理员 185 | 186 | 群内或私聊 permission=SUPERUSER 187 | 所有分管 :查看所有分群管理员 188 | 群管接收 :打开或关闭超管消息接收(关闭则审批结果不会发送给superusers) 189 | 190 | 【群词云统计】 191 | 该功能所用库 wordcloud 未写入依赖,请自行安装 192 | 群内发送: 193 | 记录本群 : 开始统计聊天记录 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 194 | 停止记录本群 :停止统计聊天记录 195 | 群词云 : 发送词云图片 196 | 更新mask : 更新mask图片 197 | 增加停用词 停用词1 停用词2 ... 198 | 删除停用词 停用词1 停用词2 ... 199 | 停用词列表 : 查看停用词列表 200 | 201 | 群发言排行 202 | - 日: 203 | - 日榜首:今日榜首, aliases={'今天谁话多', '今儿谁话多', '今天谁屁话最多'} 204 | - 日排行:今日发言排行, aliases={'今日排行榜', '今日发言排行榜', '今日排行'} 205 | - 昨日排行 206 | - 总 207 | - 总排行:排行, aliases={'谁话多', '谁屁话最多', '排行', '排行榜'} 208 | - 某人发言数 209 | - 日:今日发言数@xxx, aliases={'今日发言数', '今日发言', '今日发言量'} 210 | - 总:发言数@xxx, aliases={'发言数', '发言', '发言量'} 211 | 212 | 213 | 【被动识别】 214 | 涩图检测: 215 | - 图片检测偏向于涩图检测,90分以上色图禁言,其他基本不处理 216 | - 用户违禁一次等级+1 最高7级 217 | - 禁言时间(s): 218 | - time_scop_map = { 219 | 0: [0, 5*60], 220 | 1: [5*60, 10*60], 221 | 2: [10*60, 30*60], 222 | 3: [30*60, 10*60*60], 223 | 4: [10*60*60, 24*60*60], 224 | 5: [24*60*60, 7*24*60*60], 225 | 6: [7*24*60*60, 14*24*60*60], 226 | 7: [14*24*60*60, 2591999] 227 | } 228 | 229 | 违禁词检测: 230 | (如果要使用正则匹配,暂时需要用户自定编辑【机器人项目/config/违禁词.txt】,后续会优化,有需要请提issue催更) 231 | - 支持正则表达式(使用用制表符分隔) 232 | - 可定义触发违禁词操作(默认为禁言+撤回) 233 | - 可定义生效范围(排除某些群 or 仅限某些群生效) 234 | - 示例: 235 | - 加(群|君\S?羊|羣)\S*\d{6,} $撤回$禁言$仅限123456789,987654321 236 | - 狗群主 $禁言$排除987654321 237 | 238 | 【功能开关】 239 | 群内发送: 240 | 开关xx : 对某功能进行开/关 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 241 | 开关状态 : 查看各功能的状态 242 | xx in : 243 | ['管理', '踢', '禁', '改', '基础群管'] #基础功能 踢、禁、改、管理员+- 244 | ['加群', '审批', '加群审批', '自动审批'] #加群审批 245 | ['词云', '群词云', 'wordcloud'] #群词云 246 | ['违禁词', '违禁词检测'] #违禁词检测 247 | ['图片检测', '图片鉴黄', '涩图检测', '色图检测'] #图片检测 248 | ['消息记录', '群消息记录', '发言记录'], 249 | ['早安晚安', '早安', '晚安'], 250 | ['广播消息', '群广播', '广播'], 251 | ['事件通知', '变动通知', '事件提醒'], 252 | ['防撤回', '防止撤回'] 253 | 图片检测和违禁词检测默认关,其他默认开 254 | 255 | 【广播】permission = SUPERUSER 256 | 本功能默认关闭 257 | "发送【广播】/【广播+[消息]】可广播消息" 258 | "发送【群列表】可查看能广播到的所有群" 259 | "发送【排除列表】可查看已排除的群" 260 | "发送【广播排除+】可添加群到广播排除列表" 261 | "发送【广播排除-】可从广播排除列表删除群" 262 | "发送【广播帮助】可查看广播帮助" 263 | 发送【开关广播】来开启/关闭(意义不大) 264 | 265 | 【特殊事件提醒】 266 | 包括管理员变动,加群退群等... 267 | 待完善 268 | 发送【开关事件通知】来开启/关闭功能 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 269 | 270 | 271 | 【防撤回】 272 | 默认关闭 273 | 发送【开关防撤回】开启或关闭功能 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 274 | 275 | 【群员清理】 276 | 群内发送 permission=SUPERUSER | GROUP_OWNER 277 | 该功能暂不被开关控制 278 | 发送【群员清理】可根据[等级] 或 [发言时间] 清理群员 279 | 在执行此命令时,当前群会对此操作加锁,防止其他人同时操作,如果出现问题,可执行【清理解锁】来手动解锁 280 | ``` 281 | 282 | 283 |
284 |

截图🖼

285 | 286 | **禁 改 踢** 287 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_kick.gif) 288 | 289 | **管理员+ -** 290 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_admin.gif) 291 | 292 | **群词云** 293 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_cloud.gif) 294 | 295 | **违禁词检测** 296 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_autoban.gif) 297 | 298 | **图片检测** 299 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_r18ban.gif) 300 | 301 | **功能开关** 302 | ![](https://cdn.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/ad_switcher.gif) 303 | 304 |
305 | 306 | ## TODO 307 | - [x] 加群自动审批[#issues1](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/1) 308 | - [x] /sp在未配置群聊中的提示 309 | - [x] /删头衔bug修复 310 | - [x] 加群处理状态分群分用户发送[#issues2](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/2) 311 | - [x] 关键词禁言,图片鉴黄(简单实现),[#issues3](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/3) 312 | - [ ] 恶意检测, [#issues3](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/3) 313 | - [ ] ~~鉴黄置信度呈现~~ 314 | - [x] 头衔命令所有人可用,删头衔命令加权限 315 | - [x] 修复加群审批默认处理规则 316 | - [x] 词云停用词优化 317 | - [x] 分群群词云自定义停用词 318 | - [x] 违禁词优化 319 | - [ ] 全局开关 320 | - [ ] 潜水查询 321 | - [ ] 群聊内容分析 322 | - [ ] 写一个文档 323 | - [ ] 一些大事 324 | 325 | ## 感谢贡献者们 326 | 327 | 328 | 329 | 330 | 331 | 338 | 345 | 352 | 359 | 366 | 373 | 374 | 375 | 382 | 383 | 384 |
332 | 333 | balconyjh 334 |
335 | BalconyJH 336 |
337 |
339 | 340 | lakwsh 341 |
342 | Lakwsh 343 |
344 |
346 | 347 | yzyyz1387 348 |
349 | 幼稚园园长 350 |
351 |
353 | 354 | tom-snow 355 |
356 | A Lucky Guy 357 |
358 |
360 | 361 | DarkDRG 362 |
363 | NekoPunch! 364 |
365 |
367 | 368 | Redmomn 369 |
370 | 脑袋里进花生了 371 |
372 |
376 | 377 | deepsourcebot 378 |
379 | DeepSource Bot 380 |
381 |
385 | 386 | 387 |
388 |

更新日志

389 | 390 | - 0.3.21 391 | - 优化默认配置;同时增加一个配置项:设置禁言等基础操作是否在 qq 返回操作结果 [#18](https://github.com/yzyyz1387/nonebot_plugin_admin/pull/18) 392 | - 修复`禁@xxx`的buggi 393 | - 0.3.19 394 | - 修复`__init__.py`中的bug🐛 [PULL#17](https://github.com/yzyyz1387/nonebot_plugin_admin/pull/17) [@tom-snow](https://github.com/tom-snow) 395 | - 优化`禁@xxx`,改善灵活性 [#15](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/15) 396 | - `switcher.py`网页截图错误捕捉 397 | - 修改cdn地址 398 | - 修聊天记录编码问题 399 | - 改善违禁词检测功能的灵活性[@lakwsh](https://github.com/yzyyz1387/nonebot_plugin_admin/commits?author=lakwsh) 400 | - 违禁词检测:已支持正则表达式,可定义触发违禁词操作(默认为禁言+撤回) 401 | - 定义操作方法:用制表符分隔,左边为触发条件,右边为操作定义($禁言、$撤回) 402 | - 修复触发违禁词不会阻止事件传播的问题[@lakwsh](https://github.com/yzyyz1387/nonebot_plugin_admin/commits?author=lakwsh) 403 | - 修复可能会导致其他插件无法捕获消息的问题[@lakwsh](https://github.com/yzyyz1387/nonebot_plugin_admin/commits?author=lakwsh) 404 | - 修复部分文件编码错误,开关状态图片乱码及SIGINT信号被劫持的问题[@lakwsh](https://github.com/yzyyz1387/nonebot_plugin_admin/commits?author=lakwsh) 405 | 406 | 407 | - 0.3.18(beta) 408 | - update LICENSE to AGPL-3.0 409 | - 🐛修复`管理员-`无效的bug 410 | - 🐛修复`简单违禁词`、`严格违禁词`无效的bug 411 | - 🐛修复`禁 解 改`等指令有无空格的问题 412 | - 禁言命令新增不禁言superuser 413 | - 鉴黄api改为腾讯云,请自行开通配置 414 | - 违禁词词库每周一自动更新,手动更新:`更新违禁词库` 415 | - 分群功能开关 416 | - 使用`开关状态`指令查看各功能状态,首次使用可能会下载109Mb的`Chromium` 417 | - 0.3.16(b1) 418 | - 修复启动时`word_analyze`报错 419 | - 修复词云路径错误 420 | - 分词优化 421 | - 图片鉴黄 422 | - 违禁词检测 违禁词词库整理上传于:[f_words](https://github.com/yzyyz1387/nwafu/tree/main/f_words) 423 | - 词库有赘余,欢迎大神pr精简 424 | - 0.3.15(a16) 425 | - 同 0.3.16 426 | - 0.3.6(b1) 427 | - 修复适配错误 428 | - 补充依赖 429 | - 0.3.5(a16) 430 | - 补充依赖(谁教教我项目管理.. 431 | - 0.3.4 (b1) 432 | - 0.3.3(a16) 433 | - 修复导入错误 434 | - 修复路径错误 435 | - 0.2.8 (nonebot b1适配) 436 | - b1适配,功能同0.2.7 437 | - 0.2.7 (nonebot a16适配) 438 | - 对应adapter加入依赖 439 | - 优化代码结构 440 | - 增加群词云功能 441 | - 更新后请执行`群管初始化`(不影响已保存的配置) 442 | - 机器人提示`成功`后开始记录本群所有文本内容 443 | - 发送`群词云`使用此功能 444 | - 修复`禁@xxx 60 `这类命令失效的bug 445 | - 0.2.6 (nonebot a16适配) 446 | - 0.2.5 (nonebot b1适配) 447 | - 代码优化 448 | - 踢禁改等命令增加权限:机器人主人,群主,群管理员 `permission=SUPERUSER|GROUP_ADMIN | GROUP_OWNER` 449 | - 增加添加/删除管理员操作,命令:`管理员+@xxx` `管理员-@xxx` 450 | - 修复 `禁言多人而不带具体时间时只禁言第一位`的bug🐛 451 | - 0.2.4 (nonebot b1适配) 452 | - 同0.2.3 453 | - 0.2.3 (nonebot a16适配) 454 | - 代码优化 455 | - 命令去除 `/` 456 | - 摒弃英文命令,改为汉字命令 457 | - 0.2.2 (适配 nonebot b1) [issue#2](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/2) 458 | 459 | - **更新后请初始化**:`/spinit` 460 | 461 | - 修复未配置时`/sp`,命令出现错误 462 | - 修复`/删头衔`的bug 463 | - 增加分群管理,加群请求处理结果将发送给分群管理 464 | - 加群处理结果消息对 superuser 可开启或关闭: `/sumsg` 465 | - **0.2.1** 466 | - 修复requiers 467 | - **0.1.9** 468 | - 修复初始化功能 469 | - **0.1.0** [issue#1](https://github.com/yzyyz1387/nonebot_plugin_admin/issues/1) 470 | - 支持入群自动审批 471 | - 支持在线对不同群的关键词进行增减操作 472 | - **0.0.1-4** 473 | - 支持 踢 、禁 、改 、头衔 474 |
475 | 476 | ## 其他插件 477 | [简易群管](https://github.com/yzyyz1387/nonebot_plugin_admin) 478 | [在线运行代码](https://github.com/yzyyz1387/nonebot_plugin_code) 479 | [it咨讯(垃圾插件)](https://github.com/yzyyz1387/nonebot_plugin_itnews "it资讯") 480 | [工作性价比(还没更新beta不能用)](https://github.com/yzyyz1387/nonebot_plugin_workscore) 481 | [黑丝插件(jsdelivr问题国内服务器不能用)](https://github.com/yzyyz1387/nonebot_plugin_heisi) 482 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/__init__.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/23 0:52 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | 9 | import nonebot 10 | 11 | from . import ( 12 | admin, approve, auto_ban, broadcast, func_hook, group_msg, group_request_verify, group_recall, img_check, 13 | kick_member_by_rule, notice, particular_e_notice, requests, request_manual, word_analyze, wordcloud, switcher, utils, 14 | util 15 | ) 16 | from .config import global_config, Config 17 | from .path import * 18 | 19 | su = global_config.superusers 20 | driver = nonebot.get_driver() 21 | 22 | @driver.on_bot_connect 23 | async def _(bot: nonebot.adapters.Bot): 24 | await utils.init() 25 | await switcher.switcher_integrity_check(bot) 26 | 27 | __plugin_meta__ = nonebot.plugin.PluginMetadata( 28 | name="不简易群管", 29 | description="Nonebot2 群管插件 插件", 30 | usage="包含踢改禁,头衔,精华操作,图片安全,违禁词识别,发言记录等功能等你探索", 31 | type="application", 32 | homepage="https://github.com/yzyyz1387/nonebot_plugin_admin", 33 | config=Config, 34 | supported_adapters=None 35 | ) 36 | 37 | """ 38 | ! 消息防撤回模块,默认不开启,有需要的自行开启,想对部分群生效也需自行实现(可以并入本插件的开关系统内,也可控制 go-cqhttp 的事件过滤器) 39 | 40 | 如果在 go-cqhttp 开启了事件过滤器,请确保允许 post_type = notice 通行 41 | 【至少也得允许 notice_type = group_recall 通行】 42 | """ 43 | 44 | __usage__ = """ 45 | 【初始化】: 46 | 群管初始化 :初始化插件 47 | 48 | 【群管】: 49 | 权限:permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 50 | 禁言: 51 | 禁 @某人 时间(s)[1,2591999] 52 | 禁 时间(s)@某人 [1,2591999] 53 | 禁 @某人 缺省时间则随机 54 | 禁 @某人 0 可解禁 55 | 解 @某人 56 | 禁言时,该条消息中所有数字都会组合作为禁言时间,如:‘禁@某人 1哈2哈0哈’,则禁言120s 57 | 58 | 全群禁言 若命令前缀不为空,请使用//all,若为空,需用 /all 来触发 59 | /all 60 | /all 解 61 | 62 | 改名片 63 | 改 @某人 名片 64 | 65 | 踢出: 66 | 踢 @某人 67 | 踢出并拉黑: 68 | 黑 @某人 69 | 70 | 撤回: 71 | 撤回 (回复某条消息即可撤回对应消息) 72 | 撤回 @user [(可选,默认n=5)历史消息倍数n] (实际检查的历史数为 n*19) 73 | 74 | 设置精华 75 | 回复某条消息 + 加精 76 | 取消精华 77 | 回复某条消息 + 取消精华 78 | 79 | 【头衔】 80 | 改头衔 81 | 自助领取:头衔 xxx 82 | 自助删头衔:删头衔 83 | 超级用户更改他人头衔:头衔 @某人 头衔 84 | 超级用户删他人头衔:删头衔 @某人 85 | 86 | 【管理员】permission=SUPERUSER | GROUP_OWNER 87 | gl+ @xxx 设置某人为管理员 88 | 管理员+ @xxx 设置某人为管理员 89 | 管理员加 @xxx 设置某人为管理员 90 | 加管理 @xxx 设置某人为管理员 91 | 92 | gl- @xxx 取消某人管理员 93 | 管理员- @xxx 取消某人管理员 94 | 管理员减 @xxx 取消某人管理员 95 | 减管理 @xxx 取消某人管理员 96 | 97 | 98 | 【加群自动审批】: 99 | 群内发送 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 100 | 查看词条 : 查看本群审批词条 或/审批 101 | ct+ [词条] :增加审批词条 或/审批+ 102 | 词条+ [词条] :增加审批词条 或/审批+ 103 | ct- [词条] :删除审批词条 或/审批- 104 | 词条- [词条] :删除审批词条 或/审批- 105 | 按照黑名单自动拒绝:(当验证消息与黑名单内容匹配时,自动拒绝) 106 | 添加示例: 107 | jj+管理员你好,请通过一下 108 | ctjj+管理员你好,请通过一下 109 | 词条拒绝+管理员你好,请通过一下 110 | 拒绝词条+管理员你好,请通过一下 111 | /spx+管理员你好,请通过一下 112 | 删除示例: 113 | jj-管理员你好,请通过一下 114 | ctjj-管理员你好,请通过一下 115 | 词条拒绝-管理员你好,请通过一下 116 | 拒绝词条-管理员你好,请通过一下 117 | /spx-管理员你好,请通过一下 118 | 119 | 【superuser】: 120 | 所有词条 : 查看所有审批词条 或/su审批 121 | zdct+ [词条] :增加审批词条 122 | 指定词条+ [群号] [词条] :增加指定群审批词条 123 | 指定词条加 [群号] [词条] :增加指定群审批词条 或/su审批+ 124 | zdct- [词条] :删除审批词条 125 | 指定词条- [群号] [词条] :删除指定群审批词条 126 | 指定词条减 [群号] [词条] :删除指定群审批词条 或/su审批- 127 | 自动审批处理结果将发送给superuser 128 | 129 | 【分群管理员设置】*分管:可以接受加群处理结果消息的用户 130 | 群内发送 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 131 | fg+ [user] :user可用@或qq 添加分群管理员 132 | 分管+ [user] :user可用@或qq 添加分群管理员 133 | 分管加 [user] :user可用@或qq 添加分群管理员 134 | fg- [user] :删除分群管理员 135 | 分管- [user] :删除分群管理员 136 | 分管减 [user] :删除分群管理员 137 | 查看分管 :查看本群分群管理员 138 | 139 | 群内或私聊 permission=SUPERUSER 140 | 所有分管 :查看所有分群管理员 141 | 群管接收 :打开或关闭超管消息接收(关闭则审批结果不会发送给superusers) 142 | 143 | 【群词云统计】 144 | 该功能所用库 wordcloud 未写入依赖,请自行安装 145 | 群内发送: 146 | 记录本群 : 开始统计聊天记录 permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER 147 | 停止记录本群 :停止统计聊天记录 148 | 群词云 : 发送词云图片 149 | 更新mask : 更新mask图片 150 | 增加停用词 停用词1 停用词2 ... 151 | 删除停用词 停用词1 停用词2 ... 152 | 停用词列表 : 查看停用词列表 153 | 154 | 群发言排行 155 | - 日: 156 | - 日榜首:今日榜首, aliases={'今天谁话多', '今儿谁话多', '今天谁屁话最多'} 157 | - 日排行:今日发言排行, aliases={'今日排行榜', '今日发言排行榜', '今日排行'} 158 | - 昨日排行 159 | - 总 160 | - 总排行:排行, aliases={'谁话多', '谁屁话最多', '排行', '排行榜'} 161 | - 某人发言数 162 | - 日:今日发言数@xxx, aliases={'今日发言数', '今日发言', '今日发言量'} 163 | - 总:发言数@xxx, aliases={'发言数', '发言', '发言量'} 164 | 165 | 166 | 【被动识别】 167 | 涩图检测: 168 | - 图片检测偏向于涩图检测,90分以上色图禁言,其他基本不处理 169 | - 用户违禁一次等级+1 最高7级 170 | - 禁言时间(s): 171 | - time_scop_map = { 172 | 0: [0, 5*60], 173 | 1: [5*60, 10*60], 174 | 2: [10*60, 30*60], 175 | 3: [30*60, 10*60*60], 176 | 4: [10*60*60, 24*60*60], 177 | 5: [24*60*60, 7*24*60*60], 178 | 6: [7*24*60*60, 14*24*60*60], 179 | 7: [14*24*60*60, 2591999] 180 | } 181 | 182 | 违禁词检测: 183 | (如果要使用正则匹配,暂时需要用户自定编辑【机器人项目/config/违禁词.txt】,后续会优化,有需要请提issue催更) 184 | - 支持正则表达式(使用用制表符分隔) 185 | - 可定义触发违禁词操作(默认为禁言+撤回) 186 | - 可定义生效范围(排除某些群 or 仅限某些群生效) 187 | - 示例: 188 | - 加(群|君\S?羊|羣)\S*\d{6,} $撤回$禁言$仅限123456789,987654321 189 | - 狗群主 $禁言$排除987654321 190 | 191 | 【功能开关】 192 | 群内发送: 193 | 开关xx : 对某功能进行开/关 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 194 | 开关状态 : 查看各功能的状态 195 | xx in : 196 | ['管理', '踢', '禁', '改', '基础群管'] #基础功能 踢、禁、改、管理员+- 197 | ['加群', '审批', '加群审批', '自动审批'] #加群审批 198 | ['词云', '群词云', 'wordcloud'] #群词云 199 | ['违禁词', '违禁词检测'] #违禁词检测 200 | ['图片检测', '图片鉴黄', '涩图检测', '色图检测'] #图片检测 201 | ['消息记录', '群消息记录', '发言记录'], 202 | ['早安晚安', '早安', '晚安'], 203 | ['广播消息', '群广播', '广播'], 204 | ['事件通知', '变动通知', '事件提醒'], 205 | ['防撤回', '防止撤回'] 206 | 图片检测和违禁词检测默认关,其他默认开 207 | 208 | 【广播】permission = SUPERUSER 209 | 本功能默认关闭 210 | "发送【广播】/【广播+[消息]】可广播消息" 211 | "发送【群列表】可查看能广播到的所有群" 212 | "发送【排除列表】可查看已排除的群" 213 | "发送【广播排除+】可添加群到广播排除列表" 214 | "发送【广播排除-】可从广播排除列表删除群" 215 | "发送【广播帮助】可查看广播帮助" 216 | 发送【开关广播】来开启/关闭(意义不大) 217 | 218 | 【特殊事件提醒】 219 | 包括管理员变动,加群退群等... 220 | 待完善 221 | 发送【开关事件通知】来开启/关闭功能 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 222 | 223 | 224 | 【防撤回】 225 | 默认关闭 226 | 发送【开关防撤回】开启或关闭功能 permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER 227 | 228 | 【群员清理】 229 | 群内发送 permission=SUPERUSER | GROUP_OWNER 230 | 该功能暂不被开关控制 231 | 发送【群员清理】可根据[等级] 或 [发言时间] 清理群员 232 | 在执行此命令时,当前群会对此操作加锁,防止其他人同时操作,如果出现问题,可执行【清理解锁】来手动解锁 233 | 234 | 235 | """ 236 | __help_plugin_name__ = '简易群管' 237 | 238 | __permission__ = 1 239 | __help__version__ = '0.2.0' 240 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/admin.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/12/19 3:01 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : admin.py 7 | # @Software: PyCharm 8 | import asyncio 9 | from random import randint 10 | from traceback import print_exc 11 | 12 | from nonebot import on_command 13 | from nonebot.adapters.onebot.v11.exception import ActionFailed 14 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 15 | from nonebot.matcher import Matcher 16 | from nonebot.params import Depends 17 | from nonebot.permission import SUPERUSER 18 | 19 | from .admin_role import DEPUTY_ADMIN 20 | from .config import global_config 21 | from .message import * 22 | from .utils import mute_sb, change_s_title, fi, log_fi, sd, log_sd 23 | 24 | su = global_config.superusers 25 | 26 | ban = on_command('禁', priority=2, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 27 | @ban.handle() 28 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, msg: str = Depends(msg_text), 29 | sb: list = Depends(msg_at)): 30 | """ 31 | /禁 @user 禁言 32 | """ 33 | try: 34 | msg = msg.replace(' ', '').replace('禁', '') 35 | # 提取消息中所有数字作为禁言时间 36 | time = int(''.join(map(str, list(map(lambda x: int(x), filter(lambda x: x.isdigit(), msg)))))) 37 | except ValueError: 38 | time = None 39 | if sb: 40 | baning = mute_sb(bot, event.group_id, lst=sb, time=time) 41 | try: 42 | async for baned in baning: 43 | if baned: 44 | await baned 45 | await log_fi(matcher, '禁言操作成功' if time is not None else '用户已被禁言随机时长') 46 | except ActionFailed: 47 | await fi(matcher, '权限不足') 48 | 49 | unban = on_command('解', priority=2, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 50 | @unban.handle() 51 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 52 | """ 53 | /解 @user 解禁 54 | """ 55 | if sb: 56 | baning = mute_sb(bot, event.group_id, lst=sb, time=0) 57 | try: 58 | async for baned in baning: 59 | if baned: 60 | await baned 61 | await log_fi(matcher, '解禁操作成功') 62 | except ActionFailed: 63 | await fi(matcher, '权限不足') 64 | 65 | ban_all = on_command('/all', priority=2, aliases={'/全员'}, block=True, 66 | permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 67 | @ban_all.handle() 68 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, msg: str = Depends(msg_text)): 69 | """ 70 | # note: 如果在 .env.* 文件内设置了 COMMAND_START ,且不包含 "" (即所有指令都有前缀,假设 '/' 是其中一个前缀),则应该发 //all 触发 71 | /all 全员禁言 72 | /all 解 关闭全员禁言 73 | """ 74 | if msg and '解' in str(msg): 75 | enable = False 76 | else: 77 | enable = True 78 | try: 79 | await bot.set_group_whole_ban(group_id=event.group_id, enable=enable) 80 | await log_fi(matcher, f"全体操作成功: {'禁言' if enable else '解禁'}") 81 | except ActionFailed: 82 | await fi(matcher, '权限不足') 83 | 84 | change = on_command('改', priority=2, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 85 | @change.handle() 86 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, msg: str = Depends(msg_text), 87 | sb: list = Depends(msg_at)): 88 | """ 89 | /改 @user xxx 改群昵称 90 | """ 91 | if sb: 92 | try: 93 | for user_ in sb: 94 | await bot.set_group_card(group_id=event.group_id, user_id=int(user_), card=msg.split()[-1:][0]) 95 | await log_fi(matcher, '改名片操作成功') 96 | except ActionFailed: 97 | await fi(matcher, '权限不足') 98 | 99 | title = on_command('头衔', priority=2, block=True) 100 | @title.handle() 101 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, msg: str = Depends(msg_text), 102 | sb: list = Depends(msg_at)): 103 | """ 104 | /头衔 @user xxx 给某人头衔 105 | """ 106 | s_title = msg.replace(' ', '').replace('头衔', '', 1) 107 | gid = event.group_id 108 | uid = event.user_id 109 | if not sb or (len(sb) == 1 and sb[0] == uid): 110 | await change_s_title(bot, matcher, gid, uid, s_title) 111 | elif sb: 112 | if 'all' not in sb: 113 | if uid in su or (str(uid) in su): 114 | for qq in sb: 115 | await change_s_title(bot, matcher, gid, int(qq), s_title) 116 | else: 117 | await fi(matcher, '超级用户才可以更改他人头衔,更改自己头衔请直接使用【头衔 xxx】') 118 | else: 119 | await fi(matcher, '不能含有@全体成员') 120 | 121 | title_ = on_command('删头衔', priority=2, block=True) 122 | @title_.handle() 123 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 124 | """ 125 | /删头衔 @user 删除头衔 126 | """ 127 | s_title = '' 128 | gid = event.group_id 129 | uid = event.user_id 130 | if not sb or (len(sb) == 1 and sb[0] == uid): 131 | await change_s_title(bot, matcher, gid, uid, s_title) 132 | elif sb: 133 | if 'all' not in sb: 134 | if uid in su or (str(uid) in su): 135 | for qq in sb: 136 | await change_s_title(bot, matcher, gid, int(qq), s_title) 137 | else: 138 | await fi(matcher, '超级用户才可以删他人头衔,删除自己头衔请直接使用【删头衔】') 139 | else: 140 | await fi(matcher, '不能含有@全体成员') 141 | 142 | kick = on_command('踢', priority=2, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 143 | @kick.handle() 144 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 145 | """ 146 | /踢 @user 踢出某人 147 | """ 148 | if not sb or 'all' in sb: 149 | await fi(matcher, '指令不正确 或 不能含有@全体成员') 150 | try: 151 | for qq in sb: 152 | if qq == event.user_id: 153 | await sd(matcher, '你在玩一种很新的东西,不能踢自己!') 154 | continue 155 | if qq in su or (str(qq) in su): 156 | await sd(matcher, '超级用户不能被踢') 157 | continue 158 | await bot.set_group_kick(group_id=event.group_id, user_id=int(qq), reject_add_request=False) 159 | await log_fi(matcher, '踢人操作执行完毕') 160 | except ActionFailed: 161 | await fi(matcher, '权限不足') 162 | 163 | kick_ = on_command('黑', priority=2, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 164 | @kick_.handle() 165 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 166 | """ 167 | 黑 @user 踢出并拉黑某人 168 | """ 169 | if not sb or 'all' in sb: 170 | await fi(matcher, '指令不正确 或 不能含有@全体成员') 171 | try: 172 | for qq in sb: 173 | if qq == event.user_id: 174 | await sd(matcher, '你在玩一种很新的东西,不能踢自己!') 175 | continue 176 | if qq in su or (str(qq) in su): 177 | await sd(matcher, '超级用户不能被踢') 178 | continue 179 | await bot.set_group_kick(group_id=event.group_id, user_id=int(qq), reject_add_request=True) 180 | await log_fi(matcher, '踢人并拉黑操作执行完毕') 181 | except ActionFailed: 182 | await fi(matcher, '权限不足') 183 | 184 | set_g_admin = on_command('管理员+', aliases={'加管理', '管理加', '加管理员', '管理员加', 'gl+', 'gly+'}, priority=2, block=True, permission=SUPERUSER | GROUP_OWNER) 185 | @set_g_admin.handle() 186 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 187 | """ 188 | 管理员+ @user 添加群管理员 189 | """ 190 | if not sb or 'all' in sb: 191 | await fi(matcher, '指令不正确 或 不能含有@全体成员') 192 | try: 193 | for qq in sb: 194 | await bot.set_group_admin(group_id=event.group_id, user_id=int(qq), enable=True) 195 | await log_fi(matcher, '设置管理员操作成功') 196 | except ActionFailed: 197 | await fi(matcher, '权限不足') 198 | 199 | unset_g_admin = on_command('管理员-', aliases={'减管理', '管理减', '减管理员', '管理员减', 'gl-', 'gly-'}, priority=2, block=True, permission=SUPERUSER | GROUP_OWNER) 200 | @unset_g_admin.handle() 201 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, sb: list = Depends(msg_at)): 202 | """ 203 | 管理员+ @user 添加群管理员 204 | """ 205 | if not sb or 'all' in sb: 206 | await fi(matcher, '指令不正确 或 不能含有@全体成员') 207 | try: 208 | for qq in sb: 209 | await bot.set_group_admin(group_id=event.group_id, user_id=int(qq), enable=False) 210 | await log_fi(matcher, '取消管理员操作成功') 211 | except ActionFailed: 212 | await fi(matcher, '权限不足') 213 | 214 | set_essence = on_command("加精", priority=2, aliases={'加精', 'set_essence'}, block=True, 215 | permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 216 | @set_essence.handle() 217 | async def _(bot: Bot, rp = Depends(msg_reply)): 218 | if rp: await bot.call_api(api='set_essence_msg', message_id=rp) 219 | 220 | del_essence = on_command("取消精华", priority=2, aliases={'取消加精', 'del_essence'}, block=True, 221 | permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 222 | @del_essence.handle() 223 | async def _(bot: Bot, rp = Depends(msg_reply)): 224 | if rp: await bot.call_api(api='delete_essence_msg', message_id=rp) 225 | 226 | msg_recall = on_command('撤回', priority=2, aliases={'recall'}, block=True, 227 | permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER | DEPUTY_ADMIN) 228 | @msg_recall.handle() 229 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, msg: str = Depends(msg_text), 230 | sb: list = Depends(msg_at), rp = Depends(msg_reply)): 231 | # by: @tom-snow 232 | """ 233 | 指令格式: 234 | /撤回 @user n 235 | 回复指定消息时撤回该条消息;使用艾特时撤回被艾特的人在本群 n*19 历史消息内的所有消息。 236 | 不输入 n 则默认 n = 5 237 | """ 238 | recall_msg_id = [] 239 | if rp: 240 | recall_msg_id.append(rp) 241 | elif sb: 242 | seq = None 243 | if len(msg.split(' ')) > 1: 244 | try: # counts = n 245 | counts = int(msg.split(' ')[-1]) 246 | except ValueError: 247 | counts = 5 # 出现错误就默认为 5 【理论上除非是 /撤回 @user n 且 n 不是数值时才有可能触发】 248 | else: 249 | counts = 5 250 | 251 | try: 252 | for _ in range(counts): # 获取 n 次 253 | await asyncio.sleep(randint(0, 5)) # 睡眠随机时间,避免黑号 254 | res = await bot.call_api('get_group_msg_history', group_id=event.group_id, message_seq=seq) # 获取历史消息 255 | flag = True 256 | for message in res['messages']: # 历史消息列表 257 | if flag: 258 | seq = int(message['message_seq']) - 1 259 | flag = False 260 | if int(message['user_id']) in sb: # 将消息id加入列表 261 | recall_msg_id.append(int(message['message_id'])) 262 | except ActionFailed as e: 263 | await log_sd(matcher, '获取群历史消息时发生错误', f"获取群历史消息时发生错误:{e}, seq: {seq}", err=True) 264 | print_exc() 265 | else: 266 | await fi(matcher, 267 | '指令格式:\n/撤回 @user n\n回复指定消息时撤回该条消息;使用艾特时撤回被艾特的人在本群 n*19 历史消息内的所有消息。\n不输入 n 则默认 n = 5') 268 | 269 | # 实际进行撤回的部分 270 | if recall_msg_id: 271 | try: 272 | for msg_id in recall_msg_id: 273 | await asyncio.sleep(randint(0, 2)) # 睡眠随机时间,避免黑号 274 | await bot.delete_msg(message_id=msg_id) 275 | await log_fi(matcher, f"操作成功,一共撤回了 {len(recall_msg_id)} 条消息") 276 | except ActionFailed as e: 277 | await log_fi(matcher, '撤回失败', f"撤回失败 {e}") 278 | else: 279 | pass 280 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/admin_role.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2023/1/19 3:34 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : admin_role.py 7 | # @Software: PyCharm 8 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 9 | from nonebot.permission import Permission 10 | 11 | from .approve import g_admin 12 | 13 | 14 | async def _deputy_admin(event: GroupMessageEvent) -> bool: 15 | admins = g_admin() 16 | gid = str(event.group_id) 17 | if admins.get(gid): 18 | return event.user_id in admins[gid] 19 | else: 20 | return False 21 | 22 | 23 | DEPUTY_ADMIN: Permission = Permission(_deputy_admin) 24 | """匹配分管事件""" 25 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/approve.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/26 5:29 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : approve.py 7 | # @Software: PyCharm 8 | import json 9 | from typing import Optional 10 | 11 | from nonebot import logger 12 | 13 | from .path import * 14 | from .utils import json_load 15 | 16 | 17 | def g_admin() -> dict: 18 | """ 19 | { 20 | "su":True, //是否将加群审批结果通知到分管 21 | "123456":[2222,33333] //群12345(str) 的 分管列表 List[int] 22 | } 23 | :return : 分群管理json对象 24 | """ 25 | with open(config_group_admin, mode='r') as f: 26 | admins = json.load(f) 27 | return admins 28 | 29 | 30 | async def g_admin_add(gid: str, qq: int) -> Optional[bool]: 31 | """ 32 | 添加分群管理(处理加群请求时接收处理结果) 33 | :param gid: 群号 34 | :param qq: qq 35 | :return: bool 36 | """ 37 | admins = g_admin() 38 | if gid in admins: 39 | if qq in admins[gid]: 40 | logger.info(f"{qq}已经是群{gid}的分群管理了") 41 | return False 42 | else: 43 | gadmins = admins[gid] 44 | gadmins.append(qq) 45 | admins[gid] = gadmins 46 | with open(config_group_admin, mode='w') as c: 47 | c.write(str(json.dumps(admins))) 48 | logger.info(f"群{gid}添加分群管理:{qq}") 49 | return True 50 | else: 51 | logger.info(f"群{gid}首次加入分群管理") 52 | admins.update({gid: [qq]}) 53 | with open(config_group_admin, mode='w') as c: 54 | c.write(str(json.dumps(admins))) 55 | return True 56 | 57 | 58 | async def g_admin_del(gid: str, qq: int) -> Optional[bool]: 59 | """ 60 | 删除分群管理 61 | :param gid: 群号 62 | :param qq: qq 63 | :return: bool 64 | """ 65 | admins = g_admin() 66 | if gid in admins: 67 | if qq in admins[gid]: 68 | logger.info(f"已删除群{gid}的分群管理{qq}") 69 | data = admins[gid] 70 | data.remove(qq) 71 | if data: 72 | admins[gid] = data 73 | else: 74 | del (admins[gid]) 75 | with open(config_group_admin, mode='w') as c: 76 | c.write(str(json.dumps(admins))) 77 | return True 78 | else: 79 | logger.info(f"删除失败:群{gid}中{qq}还不是分群管理") 80 | return False 81 | else: 82 | logger.info(f"群{gid}还未添加过分群管理") 83 | return None 84 | 85 | 86 | async def su_on_off() -> Optional[bool]: 87 | admins = g_admin() 88 | if admins['su'] == 'False': 89 | admins['su'] = 'True' 90 | logger.info('打开审批消息接收') 91 | with open(config_group_admin, mode='w') as c: 92 | c.write(str(json.dumps(admins))) 93 | return True 94 | else: 95 | admins['su'] = 'False' 96 | logger.info('关闭审批消息接收') 97 | with open(config_group_admin, mode='w') as c: 98 | c.write(str(json.dumps(admins))) 99 | return False 100 | 101 | 102 | async def write(gid: str, answer: str) -> Optional[bool]: 103 | """ 104 | 写入词条 105 | :param gid: 群号 106 | :param answer: 词条 107 | :return: bool 108 | """ 109 | contents = json_load(config_admin) 110 | if gid in contents: 111 | data = contents[gid] 112 | if answer in data: 113 | logger.info(f"{answer} 已存在于群{gid}的词条中") 114 | return False 115 | else: 116 | data.append(answer) 117 | contents[gid] = data 118 | with open(config_admin, mode='w') as c: 119 | c.write(str(json.dumps(contents))) 120 | logger.info(f"群{gid}添加入群审批词条:{answer}") 121 | return True 122 | 123 | else: 124 | logger.info(f"群{gid}第一次配置此词条:{answer}") 125 | contents.update({gid: [answer]}) 126 | with open(config_admin, mode='w') as c: 127 | c.write(str(json.dumps(contents))) 128 | return True 129 | 130 | 131 | async def delete(gid: str, answer: str) -> Optional[bool]: 132 | """ 133 | 删除词条 134 | :param gid: 群号 135 | :param answer: 词条 136 | :return: bool 137 | """ 138 | contents = json_load(config_admin) 139 | if gid in contents: 140 | if answer in contents[gid]: 141 | data = contents[gid] 142 | data.remove(answer) 143 | if data: 144 | contents[gid] = data 145 | else: 146 | del (contents[gid]) 147 | with open(config_admin, mode='w') as c: 148 | c.write(str(json.dumps(contents))) 149 | logger.info(f'群{gid}删除词条:{answer}') 150 | return True 151 | 152 | else: 153 | logger.info(f"删除失败,群{gid}不存在词条:{answer}") 154 | return False 155 | else: 156 | logger.info(f"群{gid}从未配置过词条") 157 | return None -------------------------------------------------------------------------------- /nonebot_plugin_admin/auto_ban.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/1/29 0:43 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : auto_ban.py 7 | # @Software: PyCharm 8 | from nonebot import logger, on_message, on_command 9 | from nonebot.adapters.onebot.v11.exception import ActionFailed 10 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 11 | from nonebot.permission import SUPERUSER 12 | from nonebot.matcher import Matcher 13 | from nonebot.params import Depends 14 | from nonebot.adapters import Message 15 | from nonebot.params import CommandArg 16 | 17 | from .config import plugin_config 18 | from .message import * 19 | from .path import * 20 | from .utils import mute_sb, get_user_violation, sd, del_txt_line, add_txt_line, get_txt_line 21 | 22 | cb_notice = plugin_config.callback_notice 23 | 24 | del_custom_limit_words = on_command('删除违禁词', priority=2, aliases={'移除违禁词', '去除违禁词'}, block=True, 25 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 26 | 27 | 28 | @del_custom_limit_words.handle() 29 | async def _(matcher: Matcher, args: Message = CommandArg()): 30 | await del_txt_line(limit_word_path, matcher, args, '违禁词') 31 | 32 | 33 | # TODO: 支持配置是否撤回&禁言 34 | add_custom_limit_words = on_command('添加违禁词', priority=2, aliases={'增加违禁词', '新增违禁词'}, block=True, 35 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 36 | 37 | 38 | @add_custom_limit_words.handle() 39 | async def _(matcher: Matcher, args: Message = CommandArg()): 40 | await add_txt_line(limit_word_path, matcher, args, '违禁词') 41 | 42 | 43 | get_custom_limit_words = on_command('查看违禁词', priority=2, aliases={'查看违禁词', '查询违禁词', '违禁词列表'}, 44 | block=True, permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 45 | 46 | 47 | @get_custom_limit_words.handle() 48 | async def _(matcher: Matcher): 49 | if cb_notice: 50 | await get_txt_line(limit_word_path, matcher, '违禁词') 51 | 52 | 53 | def check_msg(text: str, gid: int) -> [bool, bool, str]: 54 | rules = [re.sub(r'\t+', '\t', i).split('\t') for i in limit_word_path.read_text(encoding='utf-8').split('\n')] 55 | for rule in rules: 56 | if not rule[0]: continue 57 | delete, ban = True, True # 默认禁言&撤回 58 | if len(rule) > 1: 59 | delete, ban = rule[1].find('$撤回') != -1, rule[1].find('$禁言') != -1 60 | rf = re.search(r'\$(仅限|排除)(([0-9]{6,},?)+)', rule[1]) 61 | if rf: 62 | chk = rf.groups() 63 | lst = chk[1].split(',') 64 | if chk[0] == '仅限': 65 | if str(gid) not in lst: continue 66 | else: 67 | if str(gid) in lst: continue 68 | try: 69 | if not re.search(rule[0], text): continue 70 | except Exception: 71 | if text.find(rule[0]) == -1: continue 72 | return delete, ban, rule[0] 73 | return False, False, None 74 | 75 | 76 | # priority等级要比del_custom_limit_words低 否则在删除违禁词时会触发一次无效的检测 77 | f_word = on_message(priority=3, block=False) 78 | 79 | 80 | @f_word.handle() 81 | async def _(bot: Bot, event: GroupMessageEvent, matcher: Matcher, msg: str = Depends(msg_raw)) -> None: 82 | """ 83 | 违禁词禁言 84 | :param bot: 85 | :param event: 86 | :return: 87 | """ 88 | gid = event.group_id 89 | uid = event.user_id 90 | logger.info(f"{gid}收到{uid}的消息: \"{msg}\"") 91 | delete, ban, rule = check_msg(msg, gid) 92 | if not rule: 93 | return 94 | 95 | matcher.stop_propagation() # block 96 | logger.info(f"敏感词触发: \"{rule}\"") 97 | if delete: 98 | try: 99 | await bot.delete_msg(message_id=event.message_id) 100 | logger.info('消息已撤回') 101 | except ActionFailed: 102 | logger.info('消息撤回失败') 103 | if ban: 104 | level = await get_user_violation(gid, uid, 'Porn', event.raw_message) 105 | mute_lst = mute_sb(bot, gid, lst=[uid], scope=time_scop_map[level]) 106 | async for mute in mute_lst: 107 | if not mute: 108 | continue 109 | 110 | try: 111 | await mute 112 | await sd(matcher, f"触发违禁词,当前处罚级别为{level}级", at=True) 113 | logger.info(f"禁言成功,用户: {uid}") 114 | except ActionFailed: 115 | logger.info('禁言失败,权限不足') 116 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/broadcast.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/12/17 18:07 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : broadcast.py 7 | # @Software: PyCharm 8 | import asyncio 9 | 10 | from nonebot import logger, on_command 11 | from nonebot.adapters import Message 12 | from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, ActionFailed 13 | from nonebot.matcher import Matcher 14 | from nonebot.params import CommandArg, ArgStr, Arg 15 | from nonebot.permission import SUPERUSER 16 | from nonebot.typing import T_State 17 | 18 | from .config import global_config 19 | from .path import broadcast_avoid_path, config_path 20 | from .utils import json_load, json_upload, sd, fi 21 | 22 | try: 23 | su = global_config.superusers 24 | except AttributeError: 25 | su = [] 26 | logger.error("请配置超级用户") 27 | if not config_path.exists(): 28 | config_path.mkdir() 29 | if broadcast_avoid_path.exists(): 30 | broadcast_config = json_load(broadcast_avoid_path) 31 | else: 32 | broadcast_config = {} 33 | if su: 34 | for su_ in su: 35 | broadcast_config.update({str(su_): []}) 36 | json_upload(broadcast_avoid_path, broadcast_config) 37 | 38 | on_broadcast = on_command('广播', priority=2, aliases={'告诉所有人', '告诉大家', '告诉全世界'}, block=True, 39 | permission=SUPERUSER) 40 | 41 | 42 | @on_broadcast.handle() 43 | async def _(bot: Bot, matcher: Matcher, event: MessageEvent, state: T_State, args: Message = CommandArg()): 44 | if args: 45 | state['b_args'] = args 46 | await broadcast(str(event.user_id), args, bot, matcher) 47 | 48 | 49 | @on_broadcast.got('b_args', prompt='请输入要广播的内容,发送【取消】取消') 50 | async def _( 51 | bot: Bot, 52 | matcher: Matcher, 53 | event: MessageEvent, 54 | b_args: Message = Arg("b_args")): 55 | uid = str(event.user_id) 56 | for i in ["取消", "算了", "退出"]: 57 | if i in str(b_args): 58 | await fi(matcher, "已取消广播") 59 | else: 60 | await broadcast(uid, b_args, bot, matcher) 61 | 62 | 63 | add_broadcast_avoid = on_command('广播排除', priority=2, block=True, permission=SUPERUSER) 64 | 65 | 66 | @add_broadcast_avoid.handle() 67 | async def _(bot: Bot, matcher: Matcher, event: MessageEvent, args: Message = CommandArg()): 68 | """ 69 | 添加广播排除群 70 | """ 71 | if args: 72 | if "+" in str(args): 73 | await add_avoid_group(bot, event, args, matcher) 74 | elif "-" in str(args): 75 | await del_avoid_group(event, args, matcher) 76 | else: 77 | await fi(matcher, "请发送【广播排除+12345 / 广播排除-123456】\n多个群用空格分隔,查看所有群号请发送【群列表】") 78 | 79 | 80 | all_group_list = on_command('群列表', priority=2, block=True, permission=SUPERUSER) 81 | 82 | 83 | @all_group_list.handle() 84 | async def _(bot: Bot, matcher: Matcher, state: T_State): 85 | """ 86 | 获取所有群号 87 | """ 88 | groups = await bot.get_group_list() 89 | r = "" 90 | g_list = [] 91 | for g in groups: 92 | g_list.append(str(g["group_id"])) 93 | r += f"{g['group_name']}: {g['group_id']} \n" 94 | state['g_list'] = g_list 95 | await sd(matcher, f"共【{len(g_list)}】个群\n{r}") 96 | 97 | 98 | @all_group_list.got("gid", 99 | prompt="你现在可以直接告诉我群号,我可以帮你添加到广播排除列表\n多个用【空格】分隔\n只保留本群其他全部排除请回复【0】\n发送【取消】取消") 100 | async def _( 101 | bot: Bot, 102 | matcher: Matcher, 103 | event: MessageEvent, 104 | state: T_State, 105 | gid: str = ArgStr("gid")): 106 | """ 107 | 添加群号到广播排除列表 108 | """ 109 | g_list = state['g_list'] 110 | for i in ["取消", "算了", "退出"]: 111 | if i in gid: 112 | await fi(matcher, "已取消添加") 113 | if gid == "0": 114 | if isinstance(event, GroupMessageEvent): 115 | g_list.remove(str(event.group_id)) 116 | g_avoid = " ".join(g_list) 117 | await add_avoid_group(bot, event, g_avoid, matcher) 118 | else: 119 | await fi(matcher, "当前不在群聊中") 120 | else: 121 | await add_avoid_group(bot, event, gid, matcher) 122 | 123 | 124 | async def add_avoid_group(bot: Bot, event: MessageEvent, args, matcher: Matcher): 125 | uid = str(event.user_id) 126 | args = str(args).replace("+", "").strip() 127 | groups = str(args).split(" ") 128 | groups_bot = await bot.get_group_list() 129 | g_list = [] 130 | for g in groups_bot: 131 | g_list.append(str(g["group_id"])) 132 | r = "" 133 | try: 134 | history_group = broadcast_config[uid] 135 | except KeyError: 136 | history_group = [] 137 | for g in groups: 138 | if g in g_list: 139 | if g in history_group: 140 | r += f"{g} 已存在,跳过\n" 141 | continue 142 | else: 143 | r += f"{g} 添加到广播排除列表成功\n" 144 | history_group.append(str(g)) 145 | else: 146 | r += f"我不在群 {g} 捏,跳过\n" 147 | continue 148 | broadcast_config.update({uid: history_group}) 149 | json_upload(broadcast_avoid_path, broadcast_config) 150 | await sd(matcher, f"{r}\n 发送【排除列表】可查看已排除的群") 151 | 152 | 153 | avoided_group_list = on_command('排除列表', priority=2, block=True, permission=SUPERUSER) 154 | 155 | 156 | @avoided_group_list.handle() 157 | async def _(bot: Bot, matcher: Matcher, event: MessageEvent): 158 | """ 159 | 获取广播排除列表 160 | """ 161 | uid = str(event.user_id) 162 | _config = json_load(broadcast_avoid_path) 163 | try: 164 | history_group = _config[uid] 165 | except KeyError: 166 | history_group = [] 167 | r = "【排除列表】" 168 | for g in history_group: 169 | g_name = (await bot.get_group_info(group_id=int(g)))['group_name'] 170 | r += f"{g_name}: {g} \n" 171 | await sd(matcher, f"共【{len(history_group)}】个群\n{r}") 172 | 173 | 174 | async def del_avoid_group(event: MessageEvent, args, matcher: Matcher): 175 | args = str(args).replace("-", "").strip() 176 | uid = str(event.user_id) 177 | groups = str(args).split(" ") 178 | try: 179 | history_group = broadcast_config[uid] 180 | for g in groups: 181 | if g in history_group: 182 | history_group.remove(g) 183 | else: 184 | groups.remove(g) 185 | broadcast_config.update({uid: history_group}) 186 | json_upload(broadcast_avoid_path, broadcast_config) 187 | r = '\n'.join(groups) 188 | await sd(matcher, f"已从广播排除列表中删除\n{r}") 189 | except KeyError: 190 | await fi(matcher, "广播排除列表为空") 191 | 192 | 193 | async def broadcast(uid: str, args: Message, bot: Bot, matcher: Matcher): 194 | b_config = json_load(broadcast_avoid_path) 195 | if uid in b_config: 196 | groups = await bot.get_group_list() 197 | group_list = [g["group_id"] for g in groups] 198 | success = 0 199 | failed = 0 200 | excluded = 0 201 | for g in group_list: 202 | if str(g) not in b_config[uid]: 203 | try: 204 | success += 1 205 | logger.debug(f"正在向群{g}广播{args}") 206 | await bot.send_group_msg(group_id=g, message="广播:") 207 | await bot.send_group_msg(group_id=g, message=args) 208 | except ActionFailed as e: 209 | failed += 1 210 | logger.error(f"广播时发生错误{e}") 211 | else: 212 | excluded += 1 213 | logger.debug(f"群{g}已在广播排除列表中") 214 | await asyncio.sleep(1) 215 | await fi(matcher, f"广播完成\n成功:{success}\n失败:{failed}\n排除:{excluded}") 216 | 217 | 218 | broad_cast_help = on_command('广播帮助', priority=2, block=True) 219 | 220 | 221 | @broad_cast_help.handle() 222 | async def _(matcher: Matcher): 223 | """ 224 | 广播帮助 225 | """ 226 | r = "【广播帮助】\n" \ 227 | "发送【广播】/【广播+[消息]】可广播消息\n" \ 228 | "发送【群列表】可查看能广播到的所有群\n" \ 229 | "发送【排除列表】可查看已排除的群\n" \ 230 | "发送【广播排除+】可添加群到广播排除列表\n" \ 231 | "发送【广播排除-】可从广播排除列表删除群\n" \ 232 | "发送【广播帮助】可查看广播帮助" 233 | await sd(matcher, r) 234 | # FIXME 适用于 su 在复习考研时发癫向所有群广播消息 暂时未写入README 235 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/config.py: -------------------------------------------------------------------------------- 1 | # from typing import Optional 2 | from nonebot import get_driver 3 | from nonebot import get_plugin_config 4 | from pydantic import BaseModel, Extra 5 | 6 | class Config(BaseModel, extra=Extra.ignore): 7 | tenid: str = 'xxxxxx' # 腾讯云图片安全,开通地址: https://console.cloud.tencent.com/cms 8 | tenkeys: str = 'xxxxxx' # 文档: https://cloud.tencent.com/document/product/1125 9 | callback_notice: bool = True # 是否在操作完成后在 QQ 返回提示 10 | ban_rand_time_min: int = 60 # 随机禁言最短时间(s) default: 1分钟 11 | ban_rand_time_max: int = 2591999 # 随机禁言最长时间(s) default: 30天: 60*60*24*30 12 | 13 | driver = get_driver() 14 | global_config = driver.config 15 | # plugin_config = Config.parse_obj(global_config) 16 | plugin_config = get_plugin_config(Config) 17 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/func_hook.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/6/25 17:52 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : func_hook.py 7 | # @Software: PyCharm 8 | import nonebot 9 | from nonebot import logger 10 | from nonebot.adapters.onebot.v11 import ( 11 | Bot, ActionFailed, GroupMessageEvent, GroupRequestEvent, Event, HonorNotifyEvent, GroupUploadNoticeEvent, 12 | GroupDecreaseNoticeEvent, GroupIncreaseNoticeEvent, GroupAdminNoticeEvent, LuckyKingNotifyEvent, 13 | GroupRecallNoticeEvent 14 | ) 15 | from nonebot.matcher import Matcher 16 | from nonebot.message import run_preprocessor, IgnoredException 17 | 18 | from .config import plugin_config, global_config 19 | from .path import * 20 | from .switcher import switcher_integrity_check 21 | from .utils import json_load 22 | 23 | cb_notice = plugin_config.callback_notice 24 | su = global_config.superusers 25 | admin_path = Path(__file__).parts[-2] 26 | 27 | @run_preprocessor 28 | async def _(matcher: Matcher, bot: Bot, event: Event): 29 | module = str(matcher.module_name).split('.') 30 | if len(module) < 2 or module[-2] != admin_path: return # 位置与文件路径有关 31 | which_module = module[-1] 32 | # logger.info(f"{which_module}插件开始hook处理") 33 | if isinstance(event, (GroupMessageEvent, HonorNotifyEvent, GroupUploadNoticeEvent, GroupDecreaseNoticeEvent, 34 | GroupIncreaseNoticeEvent, GroupAdminNoticeEvent, LuckyKingNotifyEvent, 35 | GroupRecallNoticeEvent)): 36 | gid = event.group_id 37 | try: 38 | if which_module in admin_funcs: 39 | status = await check_func_status(which_module, str(gid)) 40 | if not status and which_module not in ['auto_ban', 41 | 'img_check', 42 | 'particular_e_notice', 43 | 'word_analyze', 44 | 'group_recall']: # 违禁词检测和图片检测日志太多了,不用logger记录或者发消息记录 45 | if cb_notice: 46 | await bot.send_group_msg(group_id=gid, 47 | message=f"功能处于关闭状态,发送【开关{admin_funcs[which_module][0]}】开启") 48 | raise IgnoredException('未开启此功能...') 49 | elif not status and which_module in ['auto_ban', 50 | 'img_check', 51 | 'particular_e_notice', 52 | 'word_analyze', 53 | 'group_recall']: 54 | raise IgnoredException('未开启此功能...') 55 | except ActionFailed: 56 | pass 57 | except FileNotFoundError: 58 | pass 59 | elif isinstance(event, GroupRequestEvent): 60 | gid = event.group_id 61 | try: 62 | if which_module == 'requests': 63 | logger.info(event.flag) 64 | if event.sub_type == 'add': 65 | status = await check_func_status(which_module, str(gid)) 66 | if status is False: 67 | re_msg = f"群{gid}收到{event.user_id}的加群请求,flag为:{event.flag},但审批处于关闭状态\n发送【请求同意/拒绝 " \ 68 | f"flag】来处理次请求,例:\n请求同意{event.flag}\n发送【开关{admin_funcs[which_module][0]}】开启,或人工审批 " 69 | logger.info(re_msg) 70 | if cb_notice: 71 | try: 72 | for qq in su: 73 | await bot.send_msg(user_id=qq, message=re_msg) 74 | except ActionFailed: 75 | logger.info('发送消息失败,可能superuser之一不是好友') 76 | raise IgnoredException('未开启此功能...') 77 | else: 78 | pass 79 | except ActionFailed: 80 | pass 81 | except FileNotFoundError: 82 | pass 83 | 84 | async def check_func_status(func_name: str, gid: str) -> bool: 85 | """ 86 | 检查某个群的某个功能是否开启 87 | :param func_name: 功能名 88 | :param gid: 群号 89 | :return: bool 90 | """ 91 | funcs_status = json_load(switcher_path) 92 | if funcs_status is None: 93 | raise FileNotFoundError(switcher_path) 94 | try: 95 | return bool(funcs_status[gid][func_name]) 96 | except KeyError: # 新加入的群 97 | logger.info(f"本群({gid})尚未初始化!自动初始化中...") 98 | bots = nonebot.get_bots() 99 | for bot in bots.values(): 100 | await switcher_integrity_check(bot) 101 | return False 102 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/group_msg.py: -------------------------------------------------------------------------------- 1 | """ 2 | 额外依赖pip install nonebot_plugin_apscheduler 3 | 定时推送群消息需要在.evn中配置: 4 | send_group_id = ["xxx", "xxx"] # 必填 群号 5 | send_switch_morning = False # 选填 True/False 默认开启 早上消息推送是否开启 6 | send_switch_night = False # 选填 True/False 默认开启 晚上消息推送是否开启 7 | send_mode = 1 # 选填 默认模式2 模式1发送自定义句子,模式2随机调用一句 8 | send_sentence_morning = ["句子1", "句子2", "..."] # 如果是模式1 此项必填,早上随机发送该字段中的一句 9 | send_sentence_night = ["句子1", "句子2", "..."] # 如果是模式1 此项必填,晚上随机发送该字段中的一句 10 | send_time_morning = "8 0" # 选填 早上发送时间默认为7:00 11 | send_time_night = "23 0" # 选填 晚上发送时间默认为22:00 12 | """ 13 | # FIXME 此功能为用户PR,目前先用配置形式,后续修改为动态配置 14 | import asyncio 15 | import json 16 | import random 17 | 18 | import requests 19 | from nonebot import require, get_bots, get_driver 20 | from nonebot.log import logger 21 | from nonebot.plugin import get_available_plugin_names 22 | 23 | from .func_hook import check_func_status 24 | 25 | # 获取QQ群号 26 | try: 27 | send_group_id = get_driver().config.send_group_id # <-填写需要收发的QQ群号,利用for循环遍历发送 28 | except Exception as e: 29 | logger.error("ValueError:{}", e) 30 | logger.error('请配置send_group_id') 31 | 32 | # 开关 默认全开 33 | try: 34 | send_switch_morning = get_driver().config.send_switch_morning 35 | except(AttributeError, AssertionError): 36 | send_switch_morning = True 37 | try: 38 | send_switch_night = get_driver().config.send_switch_night 39 | except(AttributeError, AssertionError): 40 | send_switch_night = True 41 | # print(send_switch_morning) 42 | # print(not send_switch_morning) 43 | # print(type(send_switch_morning)) 44 | # evn读进来是str类型,吐了啊,这个bug找了好久一直以为是逻辑有错。str转bool 45 | send_switch_morning = bool(send_switch_morning) 46 | send_switch_night = bool(send_switch_night) 47 | 48 | # 获取模式 默认模式2 如果是模式1就读取自定义句子,模式2使用API 49 | try: 50 | send_mode = get_driver().config.send_mode 51 | except(AttributeError, AssertionError): 52 | send_mode = 2 53 | if send_mode == 1: 54 | send_sentence_morning = get_driver().config.send_sentence_morning 55 | send_sentence_night = get_driver().config.send_sentence_night 56 | 57 | # 获取自定义时间,默认早上七点,晚上十点 58 | try: 59 | send_time_morning = get_driver().config.send_time_morning 60 | send_time_night = get_driver().config.send_time_night 61 | assert send_time_morning is not None 62 | except(AttributeError, AssertionError): 63 | send_time_morning = '7 0' 64 | send_time_night = '22 0' 65 | m_hour, m_minute = send_time_morning.split(' ') 66 | n_hour, n_minute = send_time_night.split(' ') 67 | 68 | # 随机一言API 69 | def hitokoto(): 70 | url = "https://v1.hitokoto.cn?c=a&c=b&c=c&c=d&c=h" 71 | txt = requests.get(url) 72 | data = json.loads(txt.text) 73 | msg = data['hitokoto'] 74 | add = "" 75 | if works := data['from']: 76 | add += f"《{works}》" 77 | if from_who := data['from_who']: 78 | add += f"{from_who}" 79 | if add: 80 | msg += f"\n——{add}" 81 | return msg 82 | 83 | async def send_morning(): 84 | # 如果False直接退出函数 85 | if not send_switch_morning: 86 | logger.info('send_morning()关闭,跳出函数') 87 | return 88 | sendSuccess = False 89 | while not sendSuccess: 90 | try: 91 | await asyncio.sleep(random.randint(1, 10)) 92 | # await get_bot().send_private_msg(user_id = fire_user_id, message = "🌞早,又是元气满满的一天") # 93 | # 当未连接到onebot.v11协议端时会抛出异常 94 | bots = get_bots() 95 | for bot in bots.values(): 96 | for gid in send_group_id: 97 | if await check_func_status('group_msg', gid): 98 | if send_mode == 1: 99 | try: 100 | await bot.send_group_msg(group_id=gid, 101 | message=f"{random.choice(send_sentence_morning)}") 102 | except Exception: 103 | # 这个机器人没有加这个群 104 | pass 105 | if send_mode == 2: 106 | try: 107 | await bot.send_group_msg(group_id=gid, message=hitokoto()) 108 | except Exception: 109 | # 这个机器人没有加这个群 110 | pass 111 | logger.info('群聊推送消息') 112 | sendSuccess = True 113 | except ValueError as E: 114 | logger.error("ValueError:{}", E) 115 | logger.error('群聊推送消息插件获取bot失败,1s后重试') 116 | await asyncio.sleep(1) # 重试前时延,防止阻塞 117 | 118 | async def send_night(): 119 | # 如果False直接退出函数 120 | if not send_switch_night: 121 | logger.info('send_night()关闭,跳出函数') 122 | return 123 | sendSuccess = False 124 | while not sendSuccess: 125 | try: 126 | await asyncio.sleep(random.randint(1, 10)) 127 | # await get_bot().send_private_msg(user_id = fire_user_id, message = "🌛今天续火花了么,晚安啦") # 128 | # 当未连接到onebot.v11协议端时会抛出异常 129 | bots = get_bots() 130 | for bot in bots.values(): 131 | for gid in send_group_id: 132 | if await check_func_status('group_msg', gid): 133 | if send_mode == 1: 134 | try: 135 | await bot.send_group_msg(group_id=gid, message=f"{random.choice(send_sentence_night)}") 136 | except Exception: 137 | # 这个机器人没有加这个群 138 | pass 139 | if send_mode == 2: 140 | try: 141 | await bot.send_group_msg(group_id=gid, message=hitokoto()) 142 | except Exception: 143 | # 这个机器人没有加这个群 144 | pass 145 | logger.info('群聊推送消息') 146 | sendSuccess = True 147 | except ValueError as E: 148 | logger.error("ValueError:{}", E) 149 | logger.error('群聊推送消息插件获取bot失败,1s后重试') 150 | await asyncio.sleep(1) # 重试前时延,防止阻塞 151 | 152 | try: 153 | assert 'nonebot_plugin_apscheduler' in get_available_plugin_names() 154 | require('nonebot_plugin_apscheduler') 155 | from nonebot_plugin_apscheduler import scheduler 156 | logger.info('已检测到软依赖nonebot_plugin_apscheduler,开启定时任务功能') 157 | scheduler.add_job(send_morning, 'cron', hour=m_hour, minute=m_minute, id='send_morning') # 早上推送 158 | scheduler.add_job(send_night, 'cron', hour=n_hour, minute=n_minute, id='send_night') # 晚上推送 159 | except: 160 | logger.error('未检测到软依赖nonebot_plugin_apscheduler,禁用定时任务功能') 161 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/group_recall.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/12/19 3:57 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : group_recall.py 7 | # @Software: PyCharm 8 | from nonebot import on_notice 9 | from nonebot.adapters.onebot.v11 import GroupRecallNoticeEvent, Bot, Message, MessageSegment 10 | from pydantic import parse_obj_as 11 | 12 | from .config import global_config 13 | 14 | su = global_config.superusers 15 | 16 | group_recall = on_notice(priority=5) 17 | @group_recall.handle() 18 | async def _(bot: Bot, event: GroupRecallNoticeEvent): 19 | user_id = event.user_id # 消息发送者 20 | operator_id = event.operator_id # 撤回消息的人 21 | group_id = event.group_id # 群号 22 | message_id = event.message_id # 消息 id 23 | 24 | if int(user_id) != int(operator_id): return # 撤回人不是发消息人,是管理员撤回成员消息,不处理 25 | if int(operator_id) in su or str(operator_id) in su: return # 发起撤回的人是超管,不处理 26 | # 管理员撤回自己的也不处理 27 | operator_info = await bot.get_group_member_info(group_id=group_id, user_id=operator_id, no_cache=True) 28 | if operator_info['role'] != 'member': return 29 | # 防撤回 30 | recalled_message = await bot.get_msg(message_id=message_id) 31 | recall_notice = f"检测到{operator_info['card'] if operator_info['card'] else operator_info['nickname']}({operator_info['user_id']})撤回了一条消息:\n\n" 32 | if not isinstance(recalled_message['message'], str): 33 | _message = Message([ 34 | MessageSegment.text(recall_notice), 35 | parse_obj_as(MessageSegment, *recalled_message['message']) 36 | ]) 37 | else: 38 | _message = recall_notice + recalled_message['message'] 39 | await bot.send_group_msg(group_id=group_id, message=_message) 40 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/group_request_verify.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2021/12/26 1:58 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : group_request_verify.py 7 | # @Software: PyCharm 8 | import json 9 | from typing import Optional 10 | 11 | from fuzzyfinder import fuzzyfinder 12 | from nonebot import logger 13 | 14 | from .path import * 15 | 16 | 17 | async def verify(word: str, group_id: str) -> Optional[bool]: 18 | """ 19 | 验证答案,验证消息必须大于等于答案长度的1/2 20 | :param word: 用户答案 21 | :param group_id: 群号 22 | :return: bool 23 | """ 24 | with open(config_admin, mode='r') as f: 25 | answers = json.load(f) 26 | if group_id in answers: 27 | answer = answers[group_id] 28 | suggestions = fuzzyfinder(word, answer) 29 | result = list(suggestions) 30 | return result and len(word) >= len(result[0]) / 2 31 | logger.info(f"群{group_id}从未配置审批词条,不进行操作") 32 | return None 33 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/img_check.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/2/5 16:25 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : img_check.py 7 | # @Software: PyCharm 8 | from nonebot import logger, on_message 9 | from nonebot.adapters.onebot.v11.exception import ActionFailed 10 | from nonebot.matcher import Matcher 11 | from nonebot.params import Depends 12 | 13 | from .message import * 14 | from .path import * 15 | from .utils import mute_sb, image_moderation_async, get_user_violation, sd, fi 16 | 17 | find_pic = on_message(priority=2, block=False) 18 | @find_pic.handle() 19 | async def check_pic(bot: Bot, matcher: Matcher, event: GroupMessageEvent, img_lst: list = Depends(msg_img)): 20 | uid = [event.get_user_id()] 21 | gid = event.group_id 22 | for img in img_lst: 23 | # result = await pic_ban_cof(url = img) 24 | result = await image_moderation_async(img) 25 | try: 26 | if result and result['Suggestion'] != 'Pass': 27 | if result['Score'] >= 90: 28 | if result['Label'] == 'Porn': 29 | level = await get_user_violation(gid, event.user_id, 'Porn', event.raw_message) 30 | await sd(matcher, f"你的违规等级为{level},色色不规范,群主两行泪,请群友小心驾驶") 31 | await send_pics_ban(bot, event, time_scop_map[level]) 32 | else: 33 | level = (await get_user_violation(gid, event.user_id, 'Porn', event.raw_message, add_=False)) 34 | logger.info(f"{uid}发送的内容涉及{result['Label']}, 分值{result['Score']}, 违规等级{level}级") 35 | # await sd(find_pic, f"你发送的内容涉及{result['Label']}\n你的违规等级为{level}级,网络并非法外之地,请谨言慎行!", True) 36 | # await send_pics_ban(bot, event, scope = time_scop_map[level]) 37 | # FIXME 上面的发送出来有点烦,下面:90分以上除了色图在此处理 38 | elif result['Score'] <= 90 and result['Label'] == 'Porn': 39 | # 地低于90分的色色内容 40 | await fi(matcher, '色色不规范,群主两行泪,请群友小心驾驶') 41 | else: 42 | # 低于90的其他内容 43 | pass 44 | except TypeError: 45 | logger.error("请求图片安全接口失败") 46 | 47 | async def send_pics_ban(bot: Bot, event: GroupMessageEvent, scope: list = None): 48 | """ 49 | 发送违规图片,禁言用户 50 | :param bot: 51 | :param event: 52 | :param scope: 时间范围 53 | """ 54 | gid = event.group_id 55 | uid = [event.user_id] 56 | eid = event.message_id 57 | try: 58 | await bot.delete_msg(message_id=eid) 59 | logger.info('检测到违规图片,撤回成功') 60 | except ActionFailed: 61 | logger.info('检测到违规图片,但权限不足,撤回失败') 62 | baning = mute_sb(bot, gid, lst=uid, scope=scope) 63 | async for baned in baning: 64 | if baned: 65 | try: 66 | await baned 67 | await bot.send(event=event, message='发送了违规图片,现对你进行处罚,有异议请联系管理员', at_sender=True) 68 | logger.info(f"检测到违规图片,禁言操作成功,用户: {uid[0]}") 69 | except ActionFailed: 70 | logger.info('检测到违规图片,但权限不足,禁言失败') 71 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/kick_member_by_rule.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2023-09-13 0:05 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : kick_member_by_rule.py 7 | # @Software: PyCharm 8 | import asyncio 9 | import datetime 10 | from random import randint 11 | 12 | from nonebot import on_command, logger 13 | from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent 14 | from nonebot.adapters.onebot.v11.exception import ActionFailed 15 | from nonebot.adapters.onebot.v11.permission import GROUP_OWNER 16 | from nonebot.matcher import Matcher 17 | from nonebot.params import ArgStr 18 | from nonebot.permission import SUPERUSER 19 | from nonebot.typing import T_State 20 | 21 | from .config import global_config 22 | from .path import * 23 | 24 | su = global_config.superusers 25 | global level_dic, last_send_dict 26 | 27 | 28 | def make_send(kick_list: list, category: str, data_dict: dict): 29 | """ 30 | :param kick_list: 踢出列表 31 | :param category: 踢出条件 32 | :param data_dict: 踢出列表对应的数据字典 33 | :return: 生成的发送文本 34 | 根据踢出列表和踢出条件生成发送文本 35 | """ 36 | kick_c_index = int(category) - 1 37 | prompt = [["将踢出等级 ≤", " 的成员:\n", "等级:"], ["将踢出在 ", "之后未发言的成员:\n", "最后:"]] 38 | send_text = prompt[kick_c_index][0] + category + prompt[kick_c_index][1] 39 | if kick_list: 40 | for qq in kick_list: 41 | send_text += f"{qq}: {prompt[kick_c_index][2]}{data_dict[qq] if category == '1' else datetime.datetime.fromtimestamp(data_dict[qq])}\n" 42 | else: 43 | send_text += "没有满足条件的成员, 已取消操作..." 44 | return send_text 45 | 46 | 47 | async def get_qq_lever(bot: Bot, qq: int): 48 | """ 49 | :param bot: bot 50 | :param qq: qq号 51 | :return: qq等级 52 | 获取qq等级 53 | """ 54 | return (await bot.get_stranger_info(user_id=qq, no_cache=True))['level'] 55 | 56 | 57 | async def finish_Matcher(matcher: Matcher, state: T_State, arg: str): 58 | """ 59 | :param matcher: Matcher 60 | :param state: T_State 61 | :param arg: 输入词 62 | :return: None 63 | 结束Matcher 64 | """ 65 | if arg in ["取消", "算了", "退出", "结束"]: 66 | state['this_lock'].unlink() 67 | await matcher.finish("已取消操作...") 68 | 69 | 70 | kick_by_rule = on_command('成员清理', priority=2, block=True, permission=SUPERUSER | GROUP_OWNER) 71 | 72 | 73 | @kick_by_rule.got('k_category', prompt='请输入要清理方式(数字):\n1、等级 \n2、最后发言时间 \n输入“取消”取消操作') 74 | async def _( 75 | event: GroupMessageEvent, 76 | matcher: Matcher, 77 | state: T_State, 78 | k_category=ArgStr(), 79 | ): 80 | this_lock: Path = kick_lock_path / f"{event.group_id}.lock" 81 | state['this_lock'] = this_lock 82 | if this_lock.exists(): 83 | await kick_by_rule.finish("当前群组已有成员清理任务正在执行,如需手动解锁,请输入【清理解锁】") 84 | else: 85 | this_lock.touch() 86 | k_category = str(k_category) 87 | 88 | await finish_Matcher(matcher, state, k_category) 89 | 90 | k_prompt = ['等级(数字):\n例如:2 则踢出等级 ≤ 2 的成员 \n★=1 ☾=4 ☀=16\n输入“取消”取消操作\n 请等待...', 91 | '最后发言时间(8位日期):\n例如:20230912 则踢出2023-09-12后未发言的成员 \n输入“取消”取消操作\n 请等待...'] 92 | if k_category in ["1", "2"]: 93 | await kick_by_rule.send(k_prompt[int(k_category) - 1]) 94 | else: 95 | await kick_by_rule.reject("输入错误, 请重新输入:") 96 | 97 | 98 | @kick_by_rule.got('kick_condition', prompt='请输入:') 99 | async def _( 100 | bot: Bot, 101 | event: GroupMessageEvent, 102 | matcher: Matcher, 103 | state: T_State, 104 | kick_condition=ArgStr() 105 | ): 106 | global level_dic, last_send_dict 107 | await kick_by_rule.send("请坐和放宽,轮询成员信息中,这可能会花费几分钟...") 108 | kick_condition = str(kick_condition) 109 | kick_list = [] 110 | 111 | await finish_Matcher(matcher, state, kick_condition) 112 | 113 | member_list = await bot.get_group_member_list(group_id=event.group_id) 114 | category = str(state['k_category']) 115 | qq_list = [member['user_id'] for member in member_list] 116 | if category == "1": 117 | # 获取所有成员等级 118 | level_dic = {} 119 | for qq in qq_list: 120 | level_dic[qq] = (await get_qq_lever(bot, qq)) 121 | 122 | kick_list = [qq for qq, level in level_dic.items() if 0 < level <= int(kick_condition)] 123 | zero_level_list = [qq for qq, level in level_dic.items() if level == 0] 124 | state['zero_level_list'] = zero_level_list 125 | send_0_tips = f"0级成员:\n" 126 | if zero_level_list: 127 | for qq in zero_level_list: 128 | send_0_tips += f"{qq} " 129 | send_0_tips += "\n0级成员可能是未获取到等级信息,不做处理\n" 130 | await kick_by_rule.send(send_0_tips) 131 | 132 | elif category == "2": 133 | last_send_dict = {} 134 | kick_condition = str(kick_condition) 135 | for member in member_list: 136 | last_send_dict[member['user_id']] = member['last_sent_time'] 137 | logger.debug(f"last_send_list: {last_send_dict}") 138 | try: 139 | if len(kick_condition) != 8: 140 | raise ValueError 141 | input_time = datetime.datetime.strptime(kick_condition, "%Y%m%d") 142 | now_time = datetime.datetime.now() 143 | if input_time > now_time: 144 | await kick_by_rule.reject("日期不能大于当前日期, 请重新输入:") 145 | for qq, last in last_send_dict.items(): 146 | try: 147 | logger.debug(f"{qq}:{datetime.datetime.fromtimestamp(last)}") 148 | logger.debug(f"{datetime.datetime.fromtimestamp(last) <= input_time}") 149 | if datetime.datetime.fromtimestamp(last) <= input_time: 150 | kick_list.append(qq) 151 | except ValueError as e: 152 | logger.error(f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]{qq}:{e}") 153 | pass 154 | except ValueError: 155 | await kick_by_rule.reject("日期格式错误, 请重新输入:") 156 | else: 157 | kick_by_rule.reject("输入错误, 请重新输入:") 158 | if kick_list: 159 | # 保存踢出列表 160 | state['kick_list'] = kick_list 161 | logger.debug(f"kick_list: {kick_list}") 162 | if len(member_list) - len(kick_list) <= 3: 163 | state['this_lock'].unlink() 164 | await kick_by_rule.finish("踢出后群人数将少于3人, 已取消操作...") 165 | 166 | else: 167 | await kick_by_rule.send( 168 | make_send( 169 | kick_list, 170 | category, 171 | level_dic if category == "1" else last_send_dict 172 | )) 173 | else: 174 | state['this_lock'].unlink() 175 | await kick_by_rule.finish("没有满足条件的成员, 已取消操作...") 176 | 177 | 178 | @kick_by_rule.got('confirm', prompt='确定执行吗:\n1:确定\n2: 取消') 179 | async def _(matcher: Matcher, state: T_State): 180 | confirm = str(state['confirm']) 181 | if confirm == "1": 182 | await kick_by_rule.send("正准备执行...") 183 | else: 184 | await finish_Matcher(matcher, state, '取消') 185 | 186 | 187 | @kick_by_rule.handle() 188 | async def _(bot: Bot, event: GroupMessageEvent, state: T_State): 189 | kick_list = state['kick_list'] 190 | if kick_list: 191 | await kick_by_rule.send("尝试采用随机时间间隔清理,执行完毕会发出提示...") 192 | success = [] 193 | fail = [] 194 | for qq in kick_list: 195 | try: 196 | await asyncio.sleep(randint(0, 5)) # 睡眠随机时间,避免黑号 197 | await bot.set_group_kick(group_id=event.group_id, user_id=qq) 198 | logger.debug(f"踢出{qq},操作者:{event.user_id},群:{event.group_id}") 199 | # await bot.send_group_msg(group_id=event.group_id, message=f"T{qq}") # 测试用 200 | success.append(qq) 201 | except ActionFailed as e: 202 | logger.error(f"{qq}-踢出失败:{e}") 203 | fail.append(qq) 204 | if success: 205 | await kick_by_rule.send(f"踢出成功:{success}") 206 | if fail: 207 | await kick_by_rule.send(f"踢出失败:{fail}") 208 | 209 | else: 210 | await kick_by_rule.send("没有需要踢出的成员, 已取消操作...") 211 | 212 | 213 | delete_lock_manually = on_command('清理解锁', priority=2, block=True, permission=SUPERUSER | GROUP_OWNER) 214 | 215 | 216 | @delete_lock_manually.handle() 217 | async def _(event: GroupMessageEvent): 218 | this_lock: Path = kick_lock_path / f"{event.group_id}.lock" 219 | if this_lock.exists(): 220 | this_lock.unlink() 221 | await delete_lock_manually.finish("已解锁") 222 | else: 223 | await delete_lock_manually.finish("当前群组没有成员清理任务正在执行") 224 | 225 | # 测试 226 | # get_by_qq = on_command("/get", priority=2, permission=GROUP_OWNER | SUPERUSER) 227 | # @get_by_qq.handle() 228 | # async def _(bot: Bot, event: MessageEvent, args: Message = CommandArg()): 229 | # if qq := args.extract_plain_text(): 230 | # info = await bot.get_stranger_info(user_id=int(qq)) 231 | # await get_by_qq.finish(str(info)) 232 | # else: 233 | # await get_by_qq.finish("PSE INPUT ARG") 234 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/message.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Union 3 | 4 | from nonebot.adapters import Bot 5 | from nonebot.adapters.onebot.v11 import GroupMessageEvent 6 | 7 | async def msg_text(event: GroupMessageEvent) -> str: 8 | return event.get_plaintext() 9 | 10 | async def msg_text_no_url(event: GroupMessageEvent) -> str: 11 | msg = event.get_plaintext() 12 | no_url = re.sub(r'https?://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]', '', msg) 13 | return re.sub(r'\s+', '', no_url) 14 | 15 | async def msg_img(event: GroupMessageEvent) -> list: 16 | img = [] 17 | for msg in event.message: 18 | if msg.type in ['image', 'mface']: 19 | img.append(msg.data['url']) 20 | return img 21 | 22 | async def msg_raw(bot: Bot, event: GroupMessageEvent) -> str: 23 | raw = event.raw_message 24 | for msg in event.message: 25 | if msg.type in ['image', 'mface']: 26 | if msg.type == 'image': 27 | try: 28 | res = await bot.call_api(api='ocr_image', image=msg.data['url']) 29 | raw = raw.replace(str(msg), ' ' + ' '.join([i['text'] for i in res['texts']]) + ' ', 1) 30 | continue 31 | except Exception: 32 | pass 33 | raw = raw.replace(str(msg), ' ' + msg.data['url'] + ' ', 1) 34 | elif msg.type == 'forward': # 合并转发不可能与其他消息结合 35 | try: 36 | forward = await bot.get_forward_msg(id=msg.data['id']) 37 | return ' '.join([i['raw_message'] for i in forward['messages']]) 38 | except Exception: 39 | break 40 | return raw 41 | 42 | async def msg_at(event: GroupMessageEvent) -> list: 43 | qq = [] 44 | for msg in event.message: 45 | if msg.type == 'at': 46 | qq.append(msg.data['qq']) 47 | return qq 48 | 49 | async def msg_reply(event: GroupMessageEvent) -> Union[int, None]: 50 | return event.reply.message_id if event.reply else None 51 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/notice.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/1/16 22:02 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : notice.py 7 | # @Software: PyCharm 8 | from nonebot import on_command 9 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 10 | from nonebot.matcher import Matcher 11 | from nonebot.params import Depends 12 | from nonebot.permission import SUPERUSER 13 | from nonebot.typing import T_State 14 | 15 | from . import approve 16 | from .func_hook import check_func_status 17 | from .message import * 18 | from .utils import fi 19 | 20 | # 查看当前群分管 21 | gad = on_command('分管', priority=2, aliases={'/gad', '/分群管理'}, block=True, 22 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 23 | @gad.handle() 24 | async def _(matcher: Matcher, event: GroupMessageEvent): 25 | gid = str(event.group_id) 26 | admins = approve.g_admin() 27 | try: 28 | rely = str(admins[gid]) 29 | await matcher.finish(f"本群分管:{rely}") 30 | except KeyError: 31 | await matcher.finish('查询不到呢,使用 分管+@xx 来添加分管') 32 | 33 | # 查看所有分管 34 | su_g_admin = on_command('所有分管', priority=2, aliases={'/sugad', '/su分群管理'}, block=True, permission=SUPERUSER) 35 | @su_g_admin.handle() 36 | async def _(matcher: Matcher): 37 | admins = approve.g_admin() 38 | await matcher.finish(str(admins)) 39 | 40 | # 添加分群管理员 41 | g_admin = on_command('分管+', priority=2, aliases={'/gad+', '分群管理+', '分管加', '分群管理加', 'fg+'}, block=True, 42 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 43 | @g_admin.handle() 44 | async def _(matcher: Matcher, event: GroupMessageEvent, state: T_State, sb: list = Depends(msg_at)): 45 | gid = str(event.group_id) 46 | if sb and 'all' not in sb: 47 | for qq in sb: 48 | g_admin_handle = await approve.g_admin_add(gid, int(qq)) 49 | if g_admin_handle: 50 | await matcher.send(f"{qq}已成为本群分群管理:将接收加群处理结果,同时具有群管权限,但分管不能任命超管") 51 | else: 52 | await matcher.send(f"用户{qq}已存在") 53 | else: 54 | sb = str(state['_prefix']['command_arg']).split(' ') 55 | for qq in sb: 56 | g_admin_handle = await approve.g_admin_add(gid, int(qq)) 57 | if g_admin_handle: 58 | await matcher.send(f"{qq}已成为本群分群管理:将接收加群处理结果,同时具有群管权限,但分管不能任命超管") 59 | else: 60 | await matcher.send(f"用户{qq}已存在") 61 | 62 | # 开启superuser接收处理结果 63 | su_gad = on_command('接收', priority=2, block=True, permission=SUPERUSER) 64 | @su_gad.handle() 65 | async def _(matcher: Matcher): 66 | status = await approve.su_on_off() 67 | await matcher.finish('已开启审批消息接收' if status else '已关闭审批消息接收') 68 | 69 | # 删除分群管理 70 | g_admin_ = on_command('分管-', priority=2, aliases={'/gad-', '分群管理-', '分管减', '分群管理减', 'fg-'}, block=True, 71 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 72 | @g_admin_.handle() 73 | async def _(matcher: Matcher, event: GroupMessageEvent, state: T_State, sb: list = Depends(msg_at)): 74 | gid = str(event.group_id) 75 | status = await check_func_status('requests', str(gid)) 76 | if not status: 77 | await fi(matcher, '请先发送【开关加群审批】开启加群处理') 78 | else: 79 | if sb and 'all' not in sb: 80 | for qq in sb: 81 | g_admin_del_handle = await approve.g_admin_del(gid, int(qq)) 82 | if g_admin_del_handle: 83 | await matcher.send(f"{qq}删除成功") 84 | elif not g_admin_del_handle: 85 | await matcher.send(f"{qq}还不是分群管理") 86 | elif g_admin_del_handle is None: 87 | await matcher.send(f"群{gid}未添加过分群管理\n使用/gadmin+ [用户(可@ 可qq)]来添加分群管理") 88 | else: 89 | sb = str(state['_prefix']['command_arg']).split(' ') 90 | for qq in sb: 91 | g_admin_del_handle = await approve.g_admin_del(gid, int(qq)) 92 | if g_admin_del_handle: 93 | await matcher.send(f"{qq}删除成功") 94 | elif not g_admin_del_handle: 95 | await matcher.send(f"{qq}还不是分群管理") 96 | elif g_admin_del_handle is None: 97 | await matcher.send(f"群{gid}未添加过分群管理\n使用/gadmin+ [用户(可@ 可qq)]来添加分群管理") 98 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/particular_e_notice.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/12/19 0:23 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : particular_e_notice.py 7 | # @Software: PyCharm 8 | import asyncio 9 | from datetime import datetime 10 | 11 | from nonebot.adapters.onebot.v11 import ( 12 | Bot, Event, PokeNotifyEvent, HonorNotifyEvent, GroupUploadNoticeEvent, GroupDecreaseNoticeEvent, 13 | GroupIncreaseNoticeEvent, GroupAdminNoticeEvent, LuckyKingNotifyEvent, MessageSegment 14 | ) 15 | from nonebot.matcher import Matcher 16 | from nonebot.plugin import on_notice 17 | from nonebot.typing import T_State 18 | 19 | from .utils import fi 20 | 21 | # 获取戳一戳状态 22 | async def _is_poke(bot: Bot, event: Event, state: T_State) -> bool: 23 | return isinstance(event, PokeNotifyEvent) and event.is_tome() 24 | 25 | 26 | # 获取群荣誉变更 27 | async def _is_honor(bot: Bot, event: Event, state: T_State) -> bool: 28 | return isinstance(event, HonorNotifyEvent) 29 | 30 | 31 | # 获取文件上传 32 | async def _is_checker(bot: Bot, event: Event, state: T_State) -> bool: 33 | return isinstance(event, GroupUploadNoticeEvent) 34 | 35 | 36 | # 群成员减少 37 | async def _is_user_decrease(bot: Bot, event: Event, state: T_State) -> bool: 38 | return isinstance(event, GroupDecreaseNoticeEvent) 39 | 40 | 41 | # 群成员增加 42 | async def _is_user_increase(bot: Bot, event: Event, state: T_State) -> bool: 43 | return isinstance(event, GroupIncreaseNoticeEvent) 44 | 45 | 46 | # 管理员变动 47 | async def _is_admin_change(bot: Bot, event: Event, state: T_State) -> bool: 48 | return isinstance(event, GroupAdminNoticeEvent) 49 | 50 | 51 | # 红包运气王 52 | async def _is_red_packet(bot: Bot, event: Event, state: T_State) -> bool: 53 | return isinstance(event, LuckyKingNotifyEvent) 54 | 55 | 56 | poke = on_notice(_is_poke, priority=50) 57 | honor = on_notice(_is_honor, priority=50) 58 | upload_files = on_notice(_is_checker, priority=50) 59 | user_decrease = on_notice(_is_user_decrease, priority=50) 60 | user_increase = on_notice(_is_user_increase, priority=50) 61 | admin_change = on_notice(_is_admin_change, priority=50) 62 | red_packet = on_notice(_is_red_packet, priority=50) 63 | 64 | 65 | @poke.handle() 66 | async def _(bot: Bot, event: Event, state: T_State): 67 | # TODO 在本地做记录 不太想写文本,因为dev分支已经用了数据库,后再在写 68 | ... 69 | 70 | 71 | @honor.handle() 72 | async def _(bot: Bot, event: HonorNotifyEvent, state: T_State, matcher: Matcher): 73 | honor_type = event.honor_type 74 | uid = event.user_id 75 | reply = "" 76 | honor_map = {"performer": ["🔥", "群聊之火"], "emotion": ["🤣", "快乐源泉"]} 77 | # 龙王 78 | u_info = await bot.get_group_member_info(user_id=event.user_id) 79 | u_name = u_info["card"] if u_info["card"] else u_info["nickname"] 80 | if honor_type == "talkative": 81 | if uid == bot.self_id: 82 | reply = "💦 新龙王诞生,原来是我自己~" 83 | else: 84 | reply = f"💦 恭喜 {u_name} 荣获龙王标识~" 85 | for key, value in honor_map.items(): 86 | if honor_type == key: 87 | reply = f"{value[0]} 恭喜{u_name}荣获【{value[1]}】标识~" 88 | await fi(matcher, reply) 89 | 90 | 91 | @upload_files.handle() 92 | async def _(bot: Bot, event: GroupUploadNoticeEvent, state: T_State, matcher: Matcher): 93 | # TODO 在本地做记录 94 | ... 95 | 96 | 97 | @user_decrease.handle() 98 | async def _(bot: Bot, event: GroupDecreaseNoticeEvent, state: T_State, matcher: Matcher): 99 | op = await bot.get_group_member_info(group_id=event.group_id, user_id=event.operator_id) 100 | casualty_name = (await bot.get_stranger_info(user_id=event.user_id)).get("nickname") 101 | op_name = op['card'] if op.get('card') else op['nickname'] 102 | e_time = datetime.fromtimestamp(event.time).strftime("%Y-%m-%d %H:%M:%S") 103 | avatar = get_avatar(event.user_id) 104 | farewell_words = "感谢/o给/n送上的飞机,谢谢/o" 105 | farewell_self_words = "/n离群出走/n" 106 | # TODO 为以后自定义欢送词做准备 107 | if event.operator_id != event.user_id: 108 | reply = f"🛫 成员变动\n {farewell_words.replace('/o', f' {op_name} ').replace('/n', f' {casualty_name} ')}" 109 | reply += MessageSegment.image(avatar) + f" \n {e_time}\n" 110 | else: 111 | reply = f"🛫 成员变动\n {farewell_self_words.replace('/n', f' {casualty_name} ')}" 112 | await fi(matcher, reply) 113 | 114 | 115 | @user_increase.handle() 116 | async def _(bot: Bot, event: GroupIncreaseNoticeEvent, state: T_State, matcher: Matcher): 117 | await asyncio.sleep(1) 118 | avatar = get_avatar(event.user_id) 119 | new_be = (await bot.get_group_member_info(group_id=event.group_id, user_id=event.user_id))['nickname'] 120 | wel_words = "欢迎/n加入" 121 | # TODO 为以后自定义欢迎词做准备 122 | reply = "✨ 成员变动\n"+MessageSegment.image(avatar) + MessageSegment.at(event.user_id) + f"\n {wel_words.replace('/n', f' {new_be} ')}\n " 123 | await fi(matcher, reply) 124 | 125 | 126 | @admin_change.handle() 127 | async def _(bot: Bot, event: GroupAdminNoticeEvent, state: T_State, matcher: Matcher): 128 | reply = "" 129 | sub_type = event.sub_type 130 | uid = event.user_id 131 | user = await bot.get_group_member_info(group_id=event.group_id, user_id=uid) 132 | u_name = user['card'] if user.get('card') else user['nickname'] 133 | cong_words = "恭喜/n成为管理" 134 | re_words = "Ops! /n不再是本群管理" 135 | if uid == bot.self_id: 136 | if sub_type == "set": 137 | reply = f"🚔 管理员变动\n{cong_words.replace('/n', '我')}" 138 | if sub_type == "unset": 139 | reply = f"🚔 管理员变动\n{re_words.replace('/n', '我')}" 140 | else: 141 | if sub_type == "set": 142 | reply = f"🚔 管理员变动\n{cong_words.replace('/n', f' {u_name} ')}" 143 | if sub_type == "unset": 144 | reply = f"🚔 管理员变动\n{re_words.replace('/n', f' {u_name} ')}" 145 | await fi(matcher, reply) 146 | 147 | 148 | @red_packet.handle() 149 | async def _(bot: Bot, event: LuckyKingNotifyEvent, state: T_State, matcher: Matcher): 150 | # TODO 也许做点本记录(运气王) 151 | ... 152 | 153 | 154 | def get_avatar(uid): 155 | return f"https://q4.qlogo.cn/headimg_dl?dst_uin={uid}&spec=640" 156 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/path.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/2/24 22:23 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : path.py 7 | # @Software: PyCharm 8 | from pathlib import Path 9 | from nonebot import get_driver 10 | from .util.time_util import * 11 | 12 | # FIXME 群配置文件目前都以配置文件的类型分文件夹,而不是以群分文件夹,后者是不是会更好,但是目前懒得改了 13 | config_path = Path() / 'config' 14 | config_admin = config_path / 'admin.json' 15 | config_group_admin = config_path / 'group_admin.json' 16 | word_path = config_path / 'word_config.txt' 17 | words_contents_path = Path() / 'config' / 'words' 18 | res_path = Path() / 'resource' 19 | re_img_path = Path() / 'resource' / 'imgs' 20 | ttf_name = Path() / 'resource' / 'msyhblod.ttf' 21 | limit_word_path = config_path / '违禁词.txt' 22 | switcher_path = config_path / '开关.json' 23 | template_path = config_path / 'template' 24 | stop_words_path = config_path / 'stop_words' 25 | wordcloud_bg_path = config_path / 'wordcloud_bg' 26 | user_violation_info_path = config_path / '群内用户违规信息' 27 | group_message_data_path = config_path / '群消息数据' 28 | error_path = config_path / 'admin插件错误数据' 29 | broadcast_avoid_path = config_path / '广播排除群聊.json' 30 | ttf_path = Path() / 'resource' / 'msyhblod.ttf' 31 | summary_path = config_path / 'summary' 32 | kick_lock_path = config_path / 'kick_lock' 33 | appr_bk = config_path / '加群验证信息黑名单.json' 34 | 35 | admin_funcs = { 36 | 'admin': ['管理', '踢', '禁', '改', '基础群管'], 37 | 'requests': ['审批', '加群审批', '加群', '自动审批'], 38 | 'wordcloud': ['群词云', '词云', 'wordcloud'], 39 | 'auto_ban': ['违禁词', '违禁词检测'], 40 | 'img_check': ['图片检测', '图片鉴黄', '涩图检测', '色图检测'], 41 | 'word_analyze': ['消息记录', '群消息记录', '发言记录'], 42 | 'group_msg': ['早安晚安', '早安', '晚安'], 43 | 'broadcast': ['广播消息', '群广播', '广播'], 44 | 'particular_e_notice': ['事件通知', '变动通知', '事件提醒'], 45 | 'group_recall': ['防撤回', '防止撤回'] 46 | } 47 | # TODO 后续在这里对功能加 {‘default': True} 以便于初始化时自动设置开关状态 48 | funcs_name_cn = ['基础群管', '加群审批', '群词云', '违禁词检测', '图片检测'] 49 | 50 | 51 | GROUP_MUTE_MAX_TIME = 30 * TIME_DAY - 1 52 | # 交给Copilot 53 | # 0到5分钟、5到10分钟、10分钟到30分钟、30分钟到10小时、10到24小时、24小时到7天、7天到14天、14天到2591999秒 54 | time_scop_map = { 55 | 0: [0, 5 * TIME_MINUS], 56 | 1: [5 * TIME_MINUS, 10 * TIME_MINUS], 57 | 2: [10 * TIME_MINUS, 30 * TIME_MINUS], 58 | 3: [30 * TIME_MINUS, 10 * TIME_HOUR], 59 | 4: [10 * TIME_HOUR, 1 * TIME_DAY], 60 | 5: [1 * TIME_DAY, 7 * TIME_DAY], 61 | 6: [7 * TIME_DAY, 14 * TIME_DAY], 62 | 7: [14 * TIME_DAY, GROUP_MUTE_MAX_TIME] 63 | } 64 | 65 | 66 | localhost = "http://" + str(get_driver().config.host) + ":" + str(get_driver().config.port) 67 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/request_manual.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/6/25 21:37 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : request_manual.py 7 | # @Software: PyCharm 8 | import re 9 | 10 | from nonebot import on_command 11 | from nonebot.adapters.onebot.v11 import ( 12 | Bot, MessageEvent, ActionFailed 13 | ) 14 | 15 | request_m = on_command('请求', priority=2, block=True) 16 | @request_m.handle() 17 | async def _(bot: Bot, event: MessageEvent): 18 | msg = str(event.message) 19 | flag = re.findall(re.compile(r'\d+'), msg)[0] 20 | if '同意' in msg and '拒绝' in msg: 21 | await bot.send(event, '请使用同意或拒绝') 22 | elif '同意' in msg: 23 | try: 24 | await bot.set_group_add_request(flag=flag, sub_type='add', approve=True, reason=' ') 25 | await bot.send(event, '已同意') 26 | except ActionFailed: 27 | try: 28 | await bot.set_group_add_request(flag=flag, sub_type='invite', approve=True, reason=' ') 29 | await bot.send(event, '已同意') 30 | except ActionFailed: 31 | await bot.send(event, '错误:请求不存在') 32 | elif '拒绝' in msg: 33 | try: 34 | await bot.set_group_add_request(flag=flag, sub_type='add', approve=False, reason='管理员拒绝') 35 | await bot.send(event, '已拒绝') 36 | except ActionFailed: 37 | try: 38 | await bot.set_group_add_request(flag=flag, sub_type='invite', approve=False, reason='管理员拒绝') 39 | await bot.send(event, '已拒绝') 40 | except ActionFailed: 41 | await bot.send(event, '错误:请求不存在') 42 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/requests.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/1/16 22:03 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : group_request.py 7 | # @Software: PyCharm 8 | import re 9 | 10 | from nonebot import on_command, on_request, logger 11 | from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, GroupRequestEvent, MessageEvent 12 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 13 | from nonebot.matcher import Matcher 14 | from nonebot.permission import SUPERUSER 15 | from nonebot.typing import T_State 16 | from fuzzyfinder import fuzzyfinder 17 | 18 | from . import approve 19 | from .approve import g_admin 20 | from .config import global_config 21 | from .group_request_verify import verify 22 | from .path import * 23 | from .utils import json_load, json_upload, mk 24 | 25 | su = global_config.superusers 26 | 27 | # 查看所有审批词条 28 | super_sp = on_command('所有词条', priority=2, aliases={'/susp', '/su审批'}, block=True, permission=SUPERUSER) 29 | 30 | 31 | @super_sp.handle() 32 | async def _(matcher: Matcher): 33 | answers = json_load(config_admin) 34 | rely = '' 35 | for i in answers: 36 | rely += i + ' : ' + str(answers[i]) + '\n' 37 | await matcher.finish(rely) 38 | 39 | 40 | # 按群号添加词条 41 | super_sp_add = on_command('指定词条+', priority=2, aliases={'/susp+', '/su审批+', "指定词条加", 'zdct+'}, block=True, permission=SUPERUSER) 42 | 43 | 44 | @super_sp_add.handle() 45 | async def _(matcher: Matcher, event: MessageEvent): 46 | msg = str(event.get_message()).split() 47 | logger.info(str(len(msg)), msg) 48 | if len(msg) == 3: 49 | gid = msg[1] 50 | answer = msg[2] 51 | sp_write = await approve.write(gid, answer) 52 | if gid.isdigit(): 53 | if sp_write: 54 | 55 | await matcher.finish(f"群{gid}添加入群审批词条:{answer}") 56 | else: 57 | await matcher.finish(f"{answer} 已存在于群{gid}的词条中") 58 | else: 59 | await matcher.finish('输入有误 /susp+ [群号] [词条]') 60 | else: 61 | await matcher.finish('输入有误 /susp+ [群号] [词条]') 62 | 63 | 64 | # 按群号删除词条 65 | super_sp_de = on_command('指定词条-', priority=2, aliases={'/susp-', '/su审批-', "指定词条减", 'zdct-'}, block=True, permission=SUPERUSER) 66 | 67 | 68 | @super_sp_de.handle() 69 | async def _(matcher: Matcher, event: MessageEvent): 70 | msg = str(event.get_message()).split() 71 | if len(msg) == 3: 72 | gid = msg[1] 73 | answer = msg[2] 74 | if gid.isdigit(): 75 | sp_delete = await approve.delete(gid, answer) 76 | if sp_delete: 77 | await matcher.finish(f"群{gid}删除入群审批词条:{answer}") 78 | elif not sp_delete: 79 | await matcher.finish(f"群{gid}不存在此词条") 80 | elif sp_delete is None: 81 | await matcher.finish(f"群{gid}从未配置过词条") 82 | else: 83 | await matcher.finish('输入有误 /susp- [群号] [词条]') 84 | else: 85 | await matcher.finish('输入有误 /susp- [群号] [词条]') 86 | 87 | 88 | check = on_command('查看词条', priority=2, aliases={'/sp', '/审批'}, block=True, 89 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 90 | 91 | 92 | @check.handle() 93 | async def _(matcher: Matcher, event: GroupMessageEvent): 94 | """ 95 | /sp 查看本群词条 96 | """ 97 | a_config = json_load(config_admin) 98 | gid = str(event.group_id) 99 | if gid in a_config: 100 | this_config = a_config[gid] 101 | await matcher.finish(f"当前群审批词条:{this_config}") 102 | await matcher.finish('当前群从未配置过审批词条') 103 | 104 | 105 | add_appr_term = on_command('词条+', priority=2, aliases={'/sp+', '/审批+', '审批词条加', "词条加", 'ct+', 'sp+'}, block=True, 106 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 107 | 108 | 109 | @add_appr_term.handle() 110 | async def _(matcher: Matcher, event: GroupMessageEvent, state: T_State): 111 | """ 112 | /sp+ 增加本群词条 113 | """ 114 | msg = str(state['_prefix']['command_arg']) 115 | sp_write = await approve.write(str(event.group_id), msg) 116 | gid = str(event.group_id) 117 | if sp_write: 118 | await matcher.finish(f"群{gid}添加词条:{msg}") 119 | await matcher.finish(f"{msg} 已存在于群{gid}的词条中") 120 | 121 | 122 | del_appr_term = on_command('词条-', priority=2, aliases={'/sp-', '/审批-', '审批词条减', "词条减", 'ct-', 'sp-'}, block=True, 123 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 124 | 125 | 126 | @del_appr_term.handle() 127 | async def _(matcher: Matcher, event: GroupMessageEvent, state: T_State): 128 | """ 129 | /sp- 删除本群某词条 130 | """ 131 | msg = str(state['_prefix']['command_arg']) 132 | gid = str(event.group_id) 133 | sp_delete = await approve.delete(gid, msg) 134 | if sp_delete: 135 | await matcher.finish(f"群{gid}删除入群审批词条:{msg}") 136 | elif not sp_delete: 137 | await matcher.finish("当前群不存在此词条") 138 | elif sp_delete is None: 139 | await matcher.finish("当前群从未配置过词条") 140 | 141 | 142 | edit_appr_bk = on_command('词条拒绝', priority=2, aliases={'/spx', '/审批拒绝', '拒绝词条', 'ctjj', 'jj'}, block=True, 143 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 144 | 145 | 146 | @edit_appr_bk.handle() 147 | async def _(matcher: Matcher, event: GroupMessageEvent, state: T_State): 148 | """ 149 | /spx 词条拒绝 150 | """ 151 | msg = str(state['_prefix']['command_arg']) 152 | cmd = 'add' if msg[0] == '+' else 'unknown' 153 | gid = str(event.group_id) 154 | word = (msg[1:]).replace(' ', '') 155 | if not appr_bk.exists() and cmd == 'unknown': 156 | await matcher.send("当前机器人从未为任何群配置过加群验证信息黑名单...") 157 | content = f"{{\"{gid}\":[\"This_is\",\"an_example\"]}}" 158 | await mk('file', appr_bk, 'w', content=content) 159 | # bk_example: {"123456":["This_is","an_example"]} 160 | # appr_bk = config_path / '加群验证信息黑名单.json' is a json file 161 | appr_obj = json_load(appr_bk) 162 | if cmd == 'add': 163 | if gid not in appr_obj: 164 | appr_obj[gid] = [word] 165 | else: 166 | if word not in appr_obj[gid]: 167 | appr_obj[gid].append(word) 168 | else: 169 | await matcher.finish(f"群{gid}已存在此词条,无需重复添加") 170 | json_upload(appr_bk, appr_obj) 171 | await matcher.finish(f"群{gid}添加自动拒绝词条:{word}") 172 | elif cmd == 'unknown': 173 | if msg[0] == '-': 174 | if gid in appr_obj: 175 | if word in appr_obj[gid]: 176 | appr_obj[gid].remove(word) 177 | json_upload(appr_bk, appr_obj) 178 | await matcher.finish(f"群{gid}删除自动拒绝词条:{word}") 179 | else: 180 | await matcher.finish(f"群{gid}不存在此词条") 181 | else: 182 | await matcher.finish(f"群{gid}从未配置过词条") 183 | else: 184 | await matcher.finish("输入有误:\n 拒绝词条 [+/-] [词条]") 185 | 186 | 187 | 188 | 189 | 190 | # 加群审批 191 | group_req = on_request(priority=2, block=True) 192 | 193 | 194 | @group_req.handle() 195 | async def gr_(bot: Bot, matcher: Matcher, event: GroupRequestEvent): 196 | gid = str(event.group_id) 197 | flag = event.flag 198 | logger.info('flag:', str(flag)) 199 | sub_type = event.sub_type 200 | if sub_type == 'add': 201 | comment = event.comment 202 | word = re.findall(re.compile('答案:(.*)'), comment) 203 | word = word[0] if word else comment 204 | compared = await verify(word, gid) 205 | uid = event.user_id 206 | # 加载验证消息黑名单 207 | if appr_bk.exists(): 208 | appr_obj = json_load(appr_bk) 209 | if gid in appr_obj: 210 | suggestions = fuzzyfinder(word, appr_obj[gid]) 211 | result = list(suggestions) 212 | if result and len(word) >= len(result[0]) / 3: 213 | logger.info(f"验证消息【{word}】符合:黑名单词条:【{result}】") 214 | await bot.set_group_add_request(flag=flag, sub_type=sub_type, approve=False, 215 | reason='答案未通过群管验证,可修改答案后再次申请') 216 | logger.info(f"拒绝{uid}加入群 {gid},验证消息为 “{word}”") 217 | logger.info(f"拒绝原因:加群验证消息在黑名单中") 218 | if compared: 219 | logger.info(f"同意{uid}加入群 {gid},验证消息为 “{word}”") 220 | await bot.set_group_add_request(flag=flag, sub_type=sub_type, approve=True, reason=' ') 221 | admins = g_admin() 222 | if admins['su'] == 'True': 223 | for q in su: 224 | await bot.send_msg(user_id=int(q), message=f"同意{uid}加入群 {gid},验证消息为 “{word}”") 225 | if gid in admins: 226 | for q in admins[gid]: 227 | await bot.send_msg(message_type='private', user_id=q, group_id=int(gid), 228 | message=f"同意{uid}加入群 {gid},验证消息为 “{word}”") 229 | 230 | elif compared is False: 231 | logger.info(f"拒绝{uid}加入群 {gid},验证消息为 “{word}”") 232 | await bot.set_group_add_request(flag=flag, sub_type=sub_type, approve=False, 233 | reason='答案未通过群管验证,可修改答案后再次申请') 234 | admins = json_load(config_group_admin) 235 | if admins['su'] == 'True': 236 | for q in su: 237 | await bot.send_msg(user_id=int(q), message=f"拒绝{uid}加入群 {gid},验证消息为 “{word}”") 238 | if gid in admins: 239 | for q in admins[gid]: 240 | await bot.send_msg(message_type='private', user_id=q, group_id=int(gid), 241 | message=f"拒绝{uid}加入群 {gid},验证消息为 “{word}”") 242 | elif compared is None: 243 | await matcher.finish() 244 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/switcher.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ gid }}开关状态 6 | 7 | 8 | 9 |
10 | {% for key, value in funcs_status.items() %} 11 |
12 |
13 | {{ funcs_name[key][0] }} 14 |
15 | {% if value == True %} 16 |
17 | 20 |
21 | {% else %} 22 |
23 | 26 |
27 | {% endif %} 28 |
29 | {% endfor %} 30 |
31 | 32 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/switcher.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/2/24 17:33 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : switcher.py 7 | # @Software: PyCharm 8 | from nonebot import on_command 9 | from nonebot.adapters import Message 10 | from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageSegment, ActionFailed 11 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 12 | from nonebot.exception import FinishedException 13 | from nonebot.matcher import Matcher 14 | from nonebot.params import CommandArg 15 | from nonebot.permission import SUPERUSER 16 | from pyppeteer import launch 17 | 18 | from .path import * 19 | from .utils import json_load, json_upload, fi, log_fi 20 | 21 | switcher = on_command('开关', priority=1, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER) 22 | @switcher.handle() 23 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, args: Message = CommandArg()): 24 | gid = str(event.group_id) 25 | user_input_func_name = str(args) 26 | try: 27 | await switcher_handle(gid, matcher, user_input_func_name) 28 | except KeyError: 29 | await switcher_integrity_check(bot) 30 | await switcher_handle(gid, matcher, user_input_func_name) 31 | 32 | switcher_html = on_command('开关状态', priority=1, block=True, permission=SUPERUSER | GROUP_ADMIN | GROUP_OWNER) 33 | @switcher_html.handle() 34 | async def _(matcher: Matcher, event: GroupMessageEvent): 35 | gid = str(event.group_id) 36 | funcs_status = json_load(switcher_path) 37 | try: 38 | from os.path import dirname 39 | from jinja2 import Environment, FileSystemLoader 40 | env = Environment(loader=FileSystemLoader(str(dirname(__file__)))) 41 | template = env.get_template('switcher.html') 42 | html = template.render(funcs_status=funcs_status[gid], funcs_name=admin_funcs, gid=gid) 43 | with open((template_path / f"{gid}.html").resolve(), 'w', encoding='utf-8') as f: 44 | f.write(html) 45 | f.close() 46 | await save_image(f"file:///{(template_path / f'{gid}.html').resolve()}", 47 | img_path=(re_img_path / f"{gid}.png").resolve()) 48 | with open((re_img_path / f"{gid}.png").resolve(), 'rb') as f: 49 | img_bytes = f.read() 50 | await fi(matcher, MessageSegment.image(img_bytes)) 51 | except ActionFailed: 52 | await log_fi(matcher, '当前群组开关状态:\n' + '\n'.join( 53 | [f"{admin_funcs[func][0]}:{'开启' if funcs_status[gid][func] else '关闭'}" for func in admin_funcs] 54 | ), '可能被风控,已使用文字发送', err=True) 55 | except FinishedException: 56 | pass 57 | except Exception as e: 58 | await log_fi(matcher, '当前群组开关状态:\n' + '\n'.join( 59 | [f"{admin_funcs[func][0]}:{'开启' if funcs_status[gid][func] else '关闭'}" for func in admin_funcs] 60 | ), f'开关渲染网页并截图失败,已使用文字发送,错误信息:\n{"-" * 30}{type(e)}: {e}{"-" * 30}', err=True) 61 | 62 | async def save_image(url, img_path): 63 | """ 64 | 导出图片 65 | :param url: 在线网页的url 66 | :param img_path: 图片存放位置 67 | :return: 68 | """ 69 | browser = await launch(options={'args': ['--no-sandbox']}, handleSIGINT=False) 70 | page = await browser.newPage() 71 | # 加载指定的网页url 72 | await page.goto(url) 73 | # 设置网页显示尺寸 74 | await page.setViewport({'width': 1920, 'height': 1080}) 75 | """ 76 | path: 图片存放位置 77 | clip: 位置与图片尺寸信息 78 | x: 网页截图的x坐标 79 | y: 网页截图的y坐标 80 | width: 图片宽度 81 | height: 图片高度 82 | """ 83 | await page.screenshot({'path': img_path, 'clip': {'x': 0, 'y': 0, 'width': 320, 'height': 800}}) 84 | await browser.close() 85 | 86 | async def switcher_integrity_check(bot: Bot): 87 | g_list = await bot.get_group_list() 88 | switcher_dict = json_load(switcher_path) 89 | for group in g_list: 90 | gid = str(group['group_id']) 91 | if not switcher_dict.get(gid): 92 | switcher_dict[gid] = {} 93 | for func in admin_funcs: 94 | if func in ['img_check', 'auto_ban', 'group_msg', 'particular_e_notice', 'group_recall']: 95 | switcher_dict[gid][func] = False 96 | else: 97 | switcher_dict[gid][func] = True 98 | else: 99 | this_group_switcher = switcher_dict[gid] 100 | for func in admin_funcs: 101 | if this_group_switcher.get(func) is None: 102 | if func in ['img_check', 'auto_ban', 'group_msg', 'particular_e_notice', 'group_recall']: 103 | this_group_switcher[func] = False 104 | else: 105 | this_group_switcher[func] = True 106 | json_upload(switcher_path, switcher_dict) 107 | 108 | async def switcher_handle(gid, matcher, user_input_func_name): 109 | for func in admin_funcs: 110 | if user_input_func_name in admin_funcs[func]: 111 | funcs_status = json_load(switcher_path) 112 | if funcs_status[gid][func]: 113 | funcs_status[gid][func] = False 114 | json_upload(switcher_path, funcs_status) 115 | await fi(matcher, '已关闭' + user_input_func_name) 116 | else: 117 | funcs_status[gid][func] = True 118 | json_upload(switcher_path, funcs_status) 119 | await fi(matcher, '已开启' + user_input_func_name) 120 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/util/__init__.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/12/24 17:12 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : __init__.py.py 7 | # @Software: PyCharm 8 | 9 | from .file_util import * 10 | from .time_util import * 11 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/util/file_util.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from pathlib import Path 3 | 4 | 5 | def read_all_text(path: Path) -> str: 6 | if not os.path.exists(path): 7 | return '' 8 | with open(path, mode='r', encoding='utf-8') as c: 9 | return c.read() 10 | 11 | 12 | def read_all_lines(path: Path, split: str = '\n') -> list[str]: 13 | t = read_all_text(path) 14 | if t is None: 15 | return list[str]() 16 | a = t.split(split) 17 | return a 18 | 19 | 20 | def write_all_txt(path: Path, value: str, append: bool): 21 | if append: 22 | mode = 'a' 23 | else: 24 | mode = 'w' 25 | 26 | with open(path, mode=mode, encoding='utf-8') as c: 27 | c.write(value) 28 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/util/time_util.py: -------------------------------------------------------------------------------- 1 | TIME_SECOND = 1 2 | TIME_MINUS = 60 3 | TIME_HOUR = 60 * 60 4 | TIME_DAY = 24 * TIME_HOUR 5 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/utils.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/1/16 10:15 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : utils.py 7 | # @Software: PyCharm 8 | import asyncio 9 | import base64 10 | import datetime 11 | import json 12 | import os 13 | import random 14 | from typing import Union, Optional 15 | 16 | import httpx 17 | import nonebot 18 | from nonebot import logger 19 | from nonebot.adapters import Message 20 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, ActionFailed, Bot 21 | from nonebot.matcher import Matcher 22 | 23 | from .util.file_util import * 24 | from .config import plugin_config, global_config 25 | from .path import * 26 | 27 | TencentID = plugin_config.tenid 28 | TencentKeys = plugin_config.tenkeys 29 | su = global_config.superusers 30 | cb_notice = plugin_config.callback_notice 31 | 32 | dirs = [config_path, template_path, words_contents_path, res_path, re_img_path, stop_words_path, wordcloud_bg_path, 33 | user_violation_info_path, group_message_data_path, error_path, kick_lock_path] 34 | 35 | 36 | async def init(): 37 | """ 38 | 初始化配置文件 39 | :return: 40 | """ 41 | for d in dirs: 42 | if not d.exists(): 43 | await mk('dir', d, mode=None) 44 | if not config_admin.exists(): 45 | await mk('file', config_admin, 'w', content='{"1008611": ["This_is_an_example"]}') 46 | if not config_group_admin.exists(): 47 | await mk('file', config_group_admin, 'w', content='{"su": "True"}') 48 | if not word_path.exists(): 49 | await mk('file', word_path, 'w', content='123456789\n') 50 | if not appr_bk.exists(): 51 | await mk('file', appr_bk, 'w', content='{"1008611":["This_is","an_example"]}') 52 | if not switcher_path.exists(): 53 | bots = nonebot.get_bots() 54 | for bot in bots.values(): 55 | logger.info('创建开关配置文件,分群设置, 图片检测和违禁词检测,防撤回,广播,早晚安默认关,其他默认开') 56 | g_list = await bot.get_group_list() 57 | switcher_dict = {} 58 | for group in g_list: 59 | switchers = {} 60 | for fn_name in admin_funcs: 61 | switchers.update({fn_name: True}) 62 | if fn_name in ['img_check', 'auto_ban', 'group_msg', 'particular_e_notice', 'group_recall']: 63 | switchers.update({fn_name: False}) 64 | switcher_dict.update({str(group['group_id']): switchers}) 65 | with open(switcher_path, 'w', encoding='utf-8') as swp: 66 | swp.write(f"{json.dumps(switcher_dict)}") 67 | if not limit_word_path.exists(): # 要联网的都丢最后面去 68 | if (config_path / '违禁词_简单.txt').exists(): 69 | with open(config_path / '违禁词_简单.txt', 'r', encoding='utf-8') as f: 70 | content = f.read() 71 | with open(limit_word_path, 'w', encoding='utf-8') as f: 72 | f.write(content) 73 | (config_path / '违禁词_简单.txt').unlink() 74 | else: 75 | await mk('file', limit_word_path, 'w', 76 | url='https://fastly.jsdelivr.net/gh/yzyyz1387/nwafu/f_words/f_word_easy', dec='简单违禁词词库') 77 | if not ttf_name.exists(): 78 | await mk('file', ttf_name, 'wb', url='https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/msyhblod.ttf', 79 | dec='资源字体') 80 | # 删除成员清理锁文件 81 | for lock in kick_lock_path.iterdir(): 82 | lock.unlink() 83 | logger.info(f"删除成员清理锁文件{lock}") 84 | logger.info('Admin 插件 初始化检测完成') 85 | 86 | 87 | async def mk(type_, path_, *mode, **kwargs): 88 | """ 89 | 创建文件夹 下载文件 90 | :param type_: ['dir', 'file'] 91 | :param path_: Path 92 | :param mode: ['wb', 'w'] 93 | :param kwargs: ['url', 'content', 'dec', 'info'] 文件地址 写入内容 描述信息 和 额外信息 94 | :return: None 95 | """ 96 | if 'info' in kwargs: 97 | logger.info(kwargs['info']) 98 | if type_ == 'dir': 99 | os.mkdir(path_) 100 | logger.info(f"创建文件夹{path_}") 101 | elif type_ == 'file': 102 | if 'url' in kwargs: 103 | if kwargs['dec']: 104 | logger.info(f"开始下载文件{kwargs['dec']}") 105 | async with httpx.AsyncClient() as client: 106 | try: 107 | r = await client.get(kwargs['url']) 108 | if mode[0] == 'w': 109 | with open(path_, 'w', encoding='utf-8') as f: 110 | f.write(r.text) 111 | elif mode[0] == 'wb': 112 | with open(path_, 'wb') as f: 113 | f.write(r.content) 114 | logger.info(f"下载文件 {kwargs['dec']} 到 {path_}") 115 | except: 116 | logger.error('文件下载失败!!!') 117 | else: 118 | if mode: 119 | with open(path_, mode[0]) as f: 120 | f.write(kwargs['content']) 121 | logger.info(f"创建文件{path_}") 122 | else: 123 | raise Exception('mode 不能为空') 124 | else: 125 | raise Exception('type_参数错误') 126 | 127 | 128 | async def mute_sb(bot: Bot, gid: int, lst: list, time: int = None, scope: list = None): 129 | """ 130 | 构造禁言 131 | :param gid: 群号 132 | :param time: 时间(s) 133 | :param lst: at列表 134 | :param scope: 用于被动检测禁言的时间范围 135 | :return:禁言操作 136 | """ 137 | if 'all' in lst: 138 | yield bot.set_group_whole_ban(group_id=gid, enable=True) 139 | else: 140 | if time is None: 141 | if scope is None: 142 | time = random.randint(plugin_config.ban_rand_time_min, plugin_config.ban_rand_time_max) 143 | else: 144 | time = random.randint(scope[0], scope[1]) 145 | for qq in lst: 146 | if int(qq) in su or str(qq) in su: 147 | logger.info(f"SUPERUSER无法被禁言, {qq}") 148 | else: 149 | yield bot.set_group_ban(group_id=gid, user_id=qq, duration=time) 150 | 151 | 152 | def participle_simple_handle() -> list[str]: 153 | """ 154 | wordcloud停用词 155 | """ 156 | prep_ = ['么', '了', '与', '不', '且', '之', '为', '兮', '其', '到', '云', '阿', '却', '个', '以', '们', '价', '似', 157 | '讫', '诸', '取', '若', '得', '逝', '将', '夫', '头', '只', '吗', '向', '吧', '呗', '呃', '呀', '员', '呵', 158 | '呢', '哇', '咦', '哟', '哉', '啊', '哩', '啵', '唻', '啰', '唯', '嘛', '噬', '嚜', '家', '如', '掉', '给', 159 | '维', '圪', '在', '尔', '惟', '子', '赊', '焉', '然', '旃', '所', '见', '斯', '者', '来', '欤', '是', '毋', 160 | '曰', '的', '每', '看', '着', '矣', '罢', '而', '耶', '粤', '聿', '等', '言', '越', '馨', '趴', '从', 161 | '自从', '自', '打', '到', '往', '在', '由', '向', '于', '至', '趁', '当', '当着', '沿着', '顺着', '按', 162 | '按照', '遵照', '依照', '靠', '本着', '用', '通过', '根据', '据', '拿', '比', '因', '因为', '由于', '为', 163 | '为了', '为着', '被', '给', '让', '叫', '归', '由', '把', '将', '管', '对', '对于', '关于', '跟', '和', 164 | '给', '替', '向', '同', '除了'] 165 | 166 | pron_ = ['各个', '本人', '这个', '各自', '哪些', '怎的', '我', '大家', '她们', '多少', '怎么', '那么', '那样', 167 | '怎样', '几时', '哪儿', '我们', '自我', '什么', '哪个', '那个', '另外', '自己', '哪样', '这儿', '那些', 168 | '这样', '那儿', '它们', '它', '他', '你', '谁', '今', '吗', '是', '乌', '如何', '彼此', '其次', '列位', 169 | '该', '各', '然', '安', '之', '怎', '夫', '其', '每', '您', '伊', '此', '者', '咱们', '某', '诸位', '这些', 170 | '予', '任何', '若', '彼', '恁', '焉', '兹', '俺', '汝', '几许', '多咱', '谁谁', '有些', '干吗', '何如', 171 | '怎么样', '好多', '哪门子', '这程子', '他人', '奈何', '人家', '若干', '本身', '旁人', '其他', '其余', 172 | '一切', '如此', '谁人', '怎么着', '那会儿', '自家', '哪会儿', '谁边', '这会儿', '几儿', '这么些', '那阵儿', 173 | '那么点儿', '这么点儿', '这么样', '这阵儿', '一应', '多会儿', '何许', '若何', '大伙儿', '几多', '恁地', 174 | '谁个', '乃尔', '那程子', '多早晚', '如许', '倷', '孰', '侬', '怹', '朕', '他们', '这么着', '那么些', 175 | '咱家', '你们', '那么着'] 176 | 177 | others_ = ['就', '这', '那', '都', '也', '还', '又', '有', '没', '好', '我', '我的', '说', '去', '点', '不是', 178 | '就是', '要', '一个', '现在', '啥'] 179 | 180 | sum_ = prep_ + pron_ + others_ 181 | return sum_ 182 | 183 | 184 | # async def pic_cof(data: str, **kwargs) -> Optional[dict]: 185 | # try: 186 | # if kwargs['mode'] == 'url': 187 | # async with httpx.AsyncClient() as client: 188 | # data_ = str(base64.b64encode((await client.get(url = data)).content), encoding = 'utf-8') 189 | # json_ = {'data': [f"data:image/png;base64,{data_}"]} 190 | # else: 191 | # json_ = {'data': [f"data:image/png;base64,{data}"]} 192 | # except Exception as err: 193 | # json_ = {'data': ['data:image/png;base64,']} 194 | # print(err) 195 | # try: 196 | # async with httpx.AsyncClient() as client: 197 | # r = (await client.post( 198 | # url = 'https://hf.space/gradioiframe/mayhug/rainchan-image-porn-detection/+/api/predict/', 199 | # json = json_)).json() 200 | # if 'error' in r: 201 | # return None 202 | # else: 203 | # return r 204 | # except Exception as err: 205 | # logger.debug(f"于\"utils.py\"中的 pic_cof 发生错误:{err}") 206 | # return None 207 | # 208 | # async def pic_ban_cof(**data) -> Optional[bool]: 209 | # global result 210 | # if data: 211 | # if 'url' in data: 212 | # result = await pic_cof(data = data['url'], mode = 'url') 213 | # if 'base64' in data: 214 | # result = await pic_cof(data = data['data'], mode = 'default') 215 | # if result: 216 | # if result['data'][0]['label'] != 'safe': 217 | # return True 218 | # else: 219 | # return False 220 | # else: 221 | # return None 222 | 223 | # TENCENT 图片检测 @A60 https://github.com/djkcyl/ABot-Graia 224 | def image_moderation(img): 225 | try: 226 | from tencentcloud.common import credential 227 | from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException 228 | from tencentcloud.common.profile.client_profile import ClientProfile 229 | from tencentcloud.common.profile.http_profile import HttpProfile 230 | from tencentcloud.ims.v20201229 import ims_client, models 231 | try: 232 | cred = credential.Credential(TencentID, TencentKeys) 233 | httpProfile = HttpProfile() 234 | httpProfile.endpoint = 'ims.tencentcloudapi.com' 235 | 236 | clientProfile = ClientProfile() 237 | clientProfile.httpProfile = httpProfile 238 | client = ims_client.ImsClient(cred, 'ap-shanghai', clientProfile) 239 | 240 | req = models.ImageModerationRequest() 241 | params = ( 242 | {'BizType': 'group_recall', 'FileUrl': img} 243 | if type(img) is str else 244 | {'BizType': 'group_recall', 'FileContent': bytes_to_base64(img)} 245 | ) 246 | req.from_json_string(json.dumps(params)) 247 | 248 | resp = client.ImageModeration(req) 249 | return json.loads(resp.to_json_string()) 250 | 251 | except TencentCloudSDKException as err: 252 | return err 253 | except KeyError as kerr: 254 | return kerr 255 | except Exception: 256 | return None 257 | 258 | 259 | async def image_moderation_async(img: Union[str, bytes]) -> Optional[dict]: 260 | try: 261 | return await asyncio.to_thread(image_moderation, img) 262 | except Exception: 263 | return None 264 | 265 | 266 | def bytes_to_base64(data): 267 | return base64.b64encode(data).decode('utf-8') 268 | 269 | 270 | def json_load(path) -> Optional[dict]: 271 | """ 272 | 加载json文件 273 | :return: Optional[dict] 274 | """ 275 | try: 276 | with open(path, mode='r', encoding='utf-8') as f: 277 | contents = json.load(f) 278 | return contents 279 | except FileNotFoundError: 280 | return None 281 | 282 | 283 | def json_upload(path, dict_content) -> None: 284 | """ 285 | 更新json文件 286 | :param path: 路径 287 | :param dict_content: python对象,字典 288 | """ 289 | with open(path, mode='w', encoding='utf-8') as c: 290 | c.write(json.dumps(dict_content, ensure_ascii=False, indent=2)) 291 | 292 | 293 | def get_group_path(event: GroupMessageEvent, path: Path) -> Path: 294 | return path / f"{str(event.group_id)}.txt" 295 | 296 | 297 | async def del_txt_line(path: Path, matcher: Matcher, args: Message, dec: str) -> None: 298 | """ 299 | 分群、按行删除txt内容 300 | :param path: 文件路径 301 | :param matcher: matcher 302 | :param args: 文本 303 | :param dec: 描述 304 | """ 305 | logger.info(args) 306 | if not args: 307 | await matcher.finish(f"请输入删除内容,多个以空格分隔,例:\n删除{dec} 内容1 内容2") 308 | else: 309 | msg = str(args).split(' ') 310 | logger.info(msg) 311 | saved_words = read_all_lines(path) 312 | success_del = [] 313 | already_del = [] 314 | 315 | for word in msg: 316 | # FIXME: word一般为'群主是猪',需要加参的话\t手机比较难打出来,用代码将word中的\\t替换为\t? 317 | if word in saved_words: 318 | saved_words.remove(word) 319 | success_del.append(word) 320 | logger.info(f"删除'{dec}' '{word}'成功") 321 | else: 322 | already_del.append(word) 323 | 324 | # 回写 325 | if saved_words: 326 | r = '\n'.join(saved_words) 327 | write_all_txt(path, r, False) 328 | if success_del: 329 | await matcher.send(f"{str(success_del)}删除成功") 330 | if already_del: 331 | await matcher.send(f"{str(already_del)}还不是{dec}") 332 | await matcher.finish() 333 | 334 | 335 | async def add_txt_line(path: Path, matcher: Matcher, args: Message, dec: str) -> None: 336 | """ 337 | 分群、按行添加txt内容 338 | :param path: 文件父级路径(文件以群号命名) 339 | :param matcher: matcher 340 | :param args: 文本 341 | :param dec: 描述 342 | """ 343 | logger.info(args) 344 | if not args: 345 | await matcher.finish(f"请输入添加内容,多个以空格分隔,例:\n添加{dec} 内容1 内容2") 346 | else: 347 | msg = str(args).split(' ') 348 | logger.info(msg) 349 | saved_words = read_all_lines(path) 350 | already_add = [] 351 | success_add = [] 352 | write_append = [] 353 | for words in msg: 354 | if words in saved_words: 355 | logger.info(f"{words}已存在") 356 | already_add.append(words) 357 | else: 358 | write_append.append(words + '\n') 359 | logger.info(f"添加\"{words}\"为{dec}成功") 360 | success_add.append(words) 361 | if write_append: 362 | r = '\n'.join(write_append) 363 | write_all_txt(path, r, True) 364 | if already_add: 365 | await matcher.send(f"{str(already_add)}已存在") 366 | if success_add: 367 | await matcher.send(f"{str(success_add)}添加成功") 368 | 369 | 370 | async def get_txt_line(path: Path, matcher: Matcher, dec: str) -> None: 371 | """ 372 | 分群、按行获取txt内容 373 | :param path: 文件父级路径(文件以群号命名) 374 | :param matcher: matcher 375 | :param dec: 描述 376 | """ 377 | try: 378 | saved_words = read_all_text(path).rstrip() 379 | await matcher.finish(saved_words) 380 | except ActionFailed: 381 | await matcher.finish('内容太长,无法发送') 382 | except FileNotFoundError: 383 | await matcher.finish(f"该群没有{dec}") 384 | 385 | 386 | async def change_s_title(bot: Bot, matcher: Matcher, gid: int, uid: int, s_title: Optional[str]): 387 | """ 388 | 改头衔 389 | :param bot: bot 390 | :param matcher: matcher 391 | :param gid: 群号 392 | :param uid: 用户号 393 | :param s_title: 头衔 394 | """ 395 | try: 396 | await bot.set_group_special_title(group_id=gid, user_id=uid, special_title=s_title, duration=-1) 397 | await log_fi(matcher, f"头衔操作成功:{s_title}") 398 | except ActionFailed: 399 | logger.info('权限不足') 400 | 401 | 402 | async def get_user_violation(gid: int, uid: int, label: str, content: str, add_: bool = True) -> int: 403 | """ 404 | 获取用户违规情况 405 | :param gid: 群号 406 | :param uid: 用户号 407 | :param label: 违规标签 408 | :param content: 内容 409 | :param add_: 等级是否+1 410 | :return: 违规等级 411 | """ 412 | path_grop = user_violation_info_path / f"{str(gid)}" 413 | path_user = path_grop / f"{str(uid)}.json" 414 | this_time = str(datetime.datetime.now()).replace(' ', '-') 415 | uid = str(uid) 416 | if not os.path.exists(user_violation_info_path): 417 | await mk('dir', user_violation_info_path, mode=None) 418 | if not os.path.exists(path_grop): 419 | await mk('dir', path_grop, mode=None) 420 | await vio_level_init(path_user, uid, this_time, label, content) 421 | return 0 422 | try: 423 | info = json_load(path_user) 424 | level = info[uid]['level'] 425 | if add_: 426 | info[uid]['level'] += 1 427 | info[uid]['info'][this_time] = [label, content] 428 | json_upload(path_user, info) 429 | if level >= 7: 430 | return 7 431 | else: 432 | return level 433 | except FileNotFoundError: 434 | await vio_level_init(path_user, uid, this_time, label, content) 435 | return 0 436 | except Exception as e: 437 | logger.error(f"获取用户违禁等级出错:{e},尝试初始化此用户违禁等级") 438 | await vio_level_init(path_user, uid, this_time, label, content) 439 | return 0 440 | 441 | 442 | async def vio_level_init(path_user, uid, this_time, label, content) -> None: 443 | with open(path_user, mode='w', encoding='utf-8') as c: 444 | c.write(json.dumps({uid: {'level': 0, 'info': {this_time: [label, content]}}}, ensure_ascii=False)) 445 | 446 | 447 | async def error_log(gid: int, time: str, matcher: Matcher, err: str) -> None: 448 | module = str(matcher.module_name) 449 | if not os.path.exists(error_path): 450 | await mk('dir', error_path, mode=None) 451 | if not os.path.exists(error_path / f"{str(gid)}.json"): 452 | json_upload(error_path / f"{str(gid)}.json", {str(gid): {time: [module, err]}}) 453 | else: 454 | try: 455 | info = json_load(error_path / f"{str(gid)}.json") 456 | info[str(gid)][time] = [module, err] 457 | json_upload(error_path / f"{str(gid)}.json", info) 458 | except Exception as e: 459 | logger.error(f"写入错误日志出错:{e}") 460 | 461 | 462 | async def sd(cmd: Matcher, msg: str, at=False) -> None: 463 | if cb_notice: 464 | await cmd.send(msg, at_sender=at) 465 | 466 | 467 | async def log_sd(cmd: Matcher, msg, log: str = None, at=False, err=False) -> None: 468 | (logger.error if err else logger.info)(log if log else msg) 469 | await sd(cmd, msg, at) 470 | 471 | 472 | async def fi(cmd: Matcher, msg) -> None: 473 | await cmd.finish(msg if cb_notice else None) 474 | 475 | 476 | async def log_fi(cmd: Matcher, msg, log: str = None, err=False) -> None: 477 | (logger.error if err else logger.info)(log if log else msg) 478 | await fi(cmd, msg) 479 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/word_analyze.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/1/16 22:21 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : word_analyze.py 7 | # @Software: PyCharm 8 | import datetime 9 | import os 10 | 11 | import httpx 12 | from nonebot import on_command, logger, on_message 13 | from nonebot.adapters import Message 14 | from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER 15 | from nonebot.matcher import Matcher 16 | from nonebot.params import CommandArg 17 | from nonebot.params import Depends 18 | from nonebot.permission import SUPERUSER 19 | 20 | from .message import * 21 | from .path import * 22 | from .utils import del_txt_line, add_txt_line, get_txt_line, json_upload, json_load, get_group_path 23 | 24 | word_start = on_command('记录本群', priority=2, block=True, permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 25 | 26 | 27 | @word_start.handle() 28 | async def _(matcher: Matcher, event: GroupMessageEvent): 29 | gid = str(event.group_id) 30 | with open(word_path, 'r+', encoding='utf-8') as c: 31 | txt = c.read().split('\n') 32 | if gid not in txt: 33 | c.write(gid + '\n') 34 | logger.info(f"开始记录{gid}") 35 | await matcher.finish('成功') 36 | logger.info(f"{gid}已存在") 37 | await matcher.finish(f"{gid}已存在") 38 | 39 | 40 | word_stop = on_command('停止记录本群', priority=2, block=True, permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 41 | 42 | 43 | @word_stop.handle() 44 | async def _(matcher: Matcher, event: GroupMessageEvent): 45 | gid = str(event.group_id) 46 | txt = word_path.read_text(encoding='utf-8') 47 | if gid in txt: 48 | with open(word_path, 'w', encoding='utf-8') as c: 49 | c.write(txt.replace(gid, '')) 50 | logger.info(f"停止记录{gid}") 51 | await matcher.finish('成功,曾经的记录不会被删除') 52 | else: 53 | logger.info(f"停用失败:{gid}不存在") 54 | await matcher.finish(f"停用失败:{gid}不存在") 55 | 56 | 57 | word = on_message(priority=3, block=False) 58 | 59 | 60 | @word.handle() 61 | async def _(event: GroupMessageEvent, msg: str = Depends(msg_text_no_url)): 62 | """ 63 | 记录聊天内容 64 | :param event: 65 | :return: 66 | """ 67 | gid = str(event.group_id) 68 | uid = str(event.user_id) 69 | path_temp = words_contents_path / f"{str(gid)}.txt" 70 | message_path_group = group_message_data_path / f"{gid}" 71 | # datetime获取今日日期 72 | today = datetime.datetime.now().strftime('%Y-%m-%d') 73 | if not os.path.exists(message_path_group): 74 | os.mkdir(message_path_group) 75 | if not os.path.exists(message_path_group / f"{today}.json"): # 日消息条数记录 {uid:消息数} 76 | json_upload(message_path_group / f"{today}.json", {uid: 1}) 77 | else: 78 | dic_ = json_load(message_path_group / f"{today}.json") 79 | if uid not in dic_: 80 | dic_[uid] = 1 81 | else: 82 | dic_[uid] += 1 83 | json_upload(message_path_group / f"{today}.json", dic_) 84 | if not os.path.exists(message_path_group / 'history.json'): # 历史发言条数记录 {uid:消息数} 85 | json_upload(message_path_group / 'history.json', {uid: 1}) 86 | else: 87 | dic_ = json_load(message_path_group / 'history.json') 88 | if uid not in dic_: 89 | dic_[uid] = 1 90 | else: 91 | dic_[uid] += 1 92 | json_upload(message_path_group / 'history.json', dic_) 93 | txt = word_path.read_text(encoding='utf-8').split('\n') 94 | if gid in txt: 95 | with open(path_temp, 'a+', encoding='utf-8') as c: 96 | c.write(msg + '\n') 97 | 98 | stop_words_add = on_command('添加停用词', priority=2, aliases={'增加停用词', '新增停用词'}, block=True, 99 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 100 | 101 | 102 | @stop_words_add.handle() 103 | async def _(event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()): 104 | """ 105 | 添加停用词 106 | """ 107 | await add_txt_line(get_group_path(event, stop_words_path), matcher, args, '停用词') 108 | 109 | 110 | stop_words_del = on_command('删除停用词', priority=2, aliases={'移除停用词', '去除停用词'}, block=True, 111 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 112 | 113 | 114 | @stop_words_del.handle() 115 | async def _(event: GroupMessageEvent, matcher: Matcher, args: Message = CommandArg()): 116 | """ 117 | 删除停用词 118 | """ 119 | await del_txt_line(get_group_path(event, stop_words_path), matcher, args, '停用词') 120 | 121 | 122 | stop_words_list = on_command('停用词列表', priority=2, aliases={'查看停用词', '查询停用词'}, block=True, 123 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 124 | 125 | 126 | @stop_words_list.handle() 127 | async def _(event: GroupMessageEvent, matcher: Matcher): 128 | """ 129 | 停用词列表 130 | """ 131 | await get_txt_line(get_group_path(event, stop_words_path), matcher, '停用词') 132 | 133 | 134 | update_mask = on_command('更新mask', priority=2, aliases={'下载mask'}, block=True, 135 | permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 136 | 137 | 138 | @update_mask.handle() 139 | async def _(matcher: Matcher): 140 | """ 141 | 更新mask 142 | """ 143 | already_have = len(os.listdir(wordcloud_bg_path)) 144 | try: 145 | async with httpx.AsyncClient() as client: 146 | num_in_cloud = int((await client.get( 147 | 'https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/wordcloud/num.txt')).read()) 148 | if num_in_cloud > already_have: 149 | await matcher.send('正zhai更新中...') 150 | for i in range(already_have, num_in_cloud): 151 | img_content = (await client.get( 152 | f"https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/wordcloud/bg{i}.png")).content 153 | with open(wordcloud_bg_path / f"{i}.png", 'wb') as f: 154 | f.write(img_content) 155 | await matcher.send('更新完成(好耶)') 156 | elif num_in_cloud == already_have: 157 | await matcher.send('蚌!已经是最新了耶') 158 | except Exception as e: 159 | logger.info(e) 160 | await matcher.send(f"QAQ,更新mask失败:\n{e}") 161 | 162 | 163 | # FIXME: 这一块重复代码有点多了 164 | who_speak_most_today = on_command('今日榜首', priority=2, aliases={'今天谁话多', '今儿谁话多', '今天谁屁话最多'}, 165 | block=True) 166 | 167 | 168 | @who_speak_most_today.handle() 169 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent): 170 | gid = event.group_id 171 | today = datetime.date.today().strftime('%Y-%m-%d') 172 | dic_ = json_load(group_message_data_path / f"{gid}" / f"{today}.json") 173 | top = sorted(dic_.items(), key=lambda x: x[1], reverse=True) 174 | top = (await member_in_group(bot, gid, top)) 175 | if len(top) == 0: 176 | await matcher.finish('没有任何人说话') 177 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=top[0][0]))['card'] 178 | if nickname == '': 179 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[0][0])))['nickname'] 180 | await matcher.finish(f"太强了!今日榜首:\n{nickname},发了{top[0][1]}条消息") 181 | 182 | 183 | speak_top = on_command('今日发言排行', priority=2, aliases={'今日排行榜', '今日发言排行榜', '今日排行'}, block=True) 184 | 185 | 186 | @speak_top.handle() 187 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent): 188 | gid = event.group_id 189 | today = datetime.date.today().strftime('%Y-%m-%d') 190 | dic_ = json_load(group_message_data_path / f"{gid}" / f"{today}.json") 191 | top = sorted(dic_.items(), key=lambda x: x[1], reverse=True) 192 | top = (await member_in_group(bot, gid, top)) 193 | if len(top) == 0: 194 | await matcher.finish('没有任何人说话') 195 | top_list = [] 196 | for i in range(min(len(top), 10)): 197 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['card'] 198 | if nickname == '': 199 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['nickname'] 200 | top_list.append(f"{i + 1}. {nickname},发了{top[i][1]}条消息") 201 | await matcher.finish('\n'.join(top_list)) 202 | 203 | 204 | speak_top_yesterday = on_command('昨日发言排行', priority=2, aliases={'昨日排行榜', '昨日发言排行榜', '昨日排行'}, 205 | block=True) 206 | 207 | 208 | @speak_top_yesterday.handle() 209 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent): 210 | gid = event.group_id 211 | yesterday = (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') 212 | if os.path.exists(group_message_data_path / f"{gid}" / f"{yesterday}.json"): 213 | dic_ = json_load(group_message_data_path / f"{gid}" / f"{yesterday}.json") 214 | top = sorted(dic_.items(), key=lambda x: x[1], reverse=True) 215 | top = (await member_in_group(bot, gid, top)) 216 | 217 | if len(top) == 0: 218 | await matcher.finish('没有任何人说话') 219 | top_list = [] 220 | for i in range(min(len(top), 10)): 221 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['card'] 222 | if nickname == '': 223 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['nickname'] 224 | top_list.append(f"{i + 1}. {nickname},发了{top[i][1]}条消息") 225 | 226 | await matcher.finish('\n'.join(top_list)) 227 | else: 228 | await matcher.finish('昨日没有记录') 229 | 230 | 231 | who_speak_most = on_command('排行', priority=2, aliases={'谁话多', '谁屁话最多', '排行', '排行榜'}, block=True) 232 | 233 | 234 | @who_speak_most.handle() 235 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent): 236 | gid = event.group_id 237 | dic_ = json_load(group_message_data_path / f"{gid}" / 'history.json') 238 | if not dic_: 239 | await matcher.finish('没有任何人说话') 240 | top = sorted(dic_.items(), key=lambda x: x[1], reverse=True) 241 | top = (await member_in_group(bot, gid, top)) 242 | if len(top) == 0: 243 | await matcher.finish('没有任何人说话') 244 | top_list = [] 245 | for i in range(min(len(top), 10)): 246 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['card'] 247 | if nickname == '': 248 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(top[i][0])))['nickname'] 249 | top_list.append(f"{i + 1}. {nickname},发了{top[i][1]}条消息") 250 | await matcher.finish('\n'.join(top_list)) 251 | 252 | 253 | get_speak_num = on_command('发言数', priority=2, aliases={'发言数', '发言', '发言量'}, block=True) 254 | 255 | 256 | @get_speak_num.handle() 257 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, at_list: list = Depends(msg_at)): 258 | gid = event.group_id 259 | dic_ = json_load(group_message_data_path / f"{gid}" / 'history.json') 260 | if at_list: 261 | for qq in at_list: 262 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(qq)))['card'] 263 | if nickname == '': 264 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(qq)))['nickname'] 265 | qq = str(qq) 266 | if qq in dic_: 267 | await matcher.send(f"有记录以来{nickname}在本群发了{dic_[qq]}条消息") 268 | else: 269 | await matcher.send(f"{nickname}没有发消息") 270 | 271 | 272 | get_speak_num_today = on_command('今日发言数', priority=2, aliases={'今日发言数', '今日发言', '今日发言量'}, block=True) 273 | 274 | 275 | @get_speak_num_today.handle() 276 | async def _(bot: Bot, matcher: Matcher, event: GroupMessageEvent, at_list: list = Depends(msg_at)): 277 | gid = event.group_id 278 | today = datetime.date.today().strftime('%Y-%m-%d') 279 | dic_ = json_load(group_message_data_path / f"{gid}" / f"{today}.json") 280 | if at_list: 281 | for qq in at_list: 282 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(qq)))['card'] 283 | if nickname == '': 284 | nickname = (await bot.get_group_member_info(group_id=gid, user_id=int(qq)))['nickname'] 285 | qq = str(qq) 286 | if qq in dic_: 287 | await matcher.send(f"今天{nickname}发了{dic_[qq]}条消息") 288 | else: 289 | await matcher.send(f"今天{nickname}没有发消息") 290 | 291 | 292 | async def member_in_group(bot: Bot, gid: int, top: list): 293 | """成员不在本群则不仅从排行(不在本群查询信息时会报错)""" 294 | member_dict = (await bot.get_group_member_list(group_id=gid)) 295 | member_list = [] 296 | for i in member_dict: 297 | member_list.append(i['user_id']) 298 | for j in top: 299 | if int(j[0]) not in member_list: 300 | top.remove(j) 301 | return top 302 | -------------------------------------------------------------------------------- /nonebot_plugin_admin/wordcloud.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2022/6/25 18:26 4 | # @Author : yzyyz 5 | # @Email : youzyyz1384@qq.com 6 | # @File : wordcloud.py 7 | # @Software: PyCharm 8 | import os 9 | import random 10 | 11 | import httpx 12 | from imageio import imread 13 | from nonebot import on_command, logger 14 | from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment 15 | 16 | from .path import * 17 | from .utils import participle_simple_handle 18 | 19 | cloud = on_command('群词云', priority=2, block=True) 20 | 21 | 22 | @cloud.handle() 23 | async def _(event: GroupMessageEvent): 24 | try: 25 | from wordcloud import WordCloud, ImageColorGenerator 26 | import jieba 27 | gid = str(event.group_id) 28 | path_temp = words_contents_path / f"{gid}.txt" 29 | dir_list = os.listdir(words_contents_path) 30 | background_img = os.listdir(wordcloud_bg_path) 31 | if background_img: 32 | try: 33 | async with httpx.AsyncClient() as client: 34 | num = int((await client.get( 35 | 'https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/wordcloud/num.txt')).read()) 36 | if num > len(background_img): 37 | await cloud.send( 38 | f"开发者新提供了{num - len(background_img)}张图片,您可以发送【更新mask】下载新的图片") 39 | except: 40 | pass 41 | 42 | else: 43 | try: 44 | async with httpx.AsyncClient() as client: 45 | range_ = int((await client.get( 46 | 'https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/wordcloud/num.txt')).read()) 47 | logger.info(f"获取到{range_}张mask图片") 48 | for i in range(range_): 49 | wordcloud_bg = await client.get( 50 | f"https://fastly.jsdelivr.net/gh/yzyyz1387/blogimages/nonebot/wordcloud/bg{i}.png") 51 | logger.info(f"正下载{i}张mask图片") 52 | with open(wordcloud_bg_path / f"{i}.png", 'wb') as f: 53 | f.write(wordcloud_bg.content) 54 | f.close() 55 | except: 56 | logger.error('下载词云mask图片出现错误') 57 | return 58 | if gid + '.txt' in dir_list: 59 | text = path_temp.read_text(encoding='utf-8') 60 | txt = jieba.lcut(text) 61 | this_stop_ = stop_words_path / f"{gid}.txt" 62 | if this_stop_.exists(): 63 | stop_ = set(this_stop_.read_text(encoding='utf-8').split('\n') + (participle_simple_handle())) 64 | else: 65 | stop_ = set(participle_simple_handle()) 66 | string = ' '.join(txt) 67 | img_path = Path(re_img_path / f"wordcloud_{gid}.png") 68 | out = await cloud_generator(string, img_path, stop_) 69 | if out[0]: 70 | await cloud.send(MessageSegment.image(out[1])) 71 | else: 72 | await cloud.send(out[1]) 73 | else: 74 | await cloud.finish("当前群未被记录,请先在群内发送,【记录本群】") 75 | except ModuleNotFoundError: 76 | await cloud.finish('未安装wordcloud库') 77 | 78 | 79 | async def cloud_generator(string: str, img_path, stop_=None): 80 | if stop_ is None: 81 | stop_ = set(" ") 82 | wordcloud_bg = random.choice(os.listdir(wordcloud_bg_path)) 83 | background_image = imread(wordcloud_bg_path / wordcloud_bg) 84 | try: 85 | from wordcloud import WordCloud, ImageColorGenerator 86 | except: 87 | logger.info('未安装wordcloud库') 88 | return False, '未安装wordcloud库' 89 | try: 90 | wc = WordCloud(font_path=str(ttf_path.resolve()), 91 | width=1920, height=1080, mode='RGBA', 92 | background_color='#ffffff', 93 | mask=background_image, 94 | stopwords=stop_).generate(string) 95 | img_colors = ImageColorGenerator(background_image, default_color=(255, 255, 255)) 96 | wc.recolor(color_func=img_colors) 97 | wc.to_file(img_path) 98 | return True, img_path.read_bytes() 99 | except Exception as err: 100 | logger.info(f"出现错误{type(err)}:{err}") 101 | return False, f"出现错误{type(err)}:{err}" 102 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fuzzyfinder 2 | httpx 3 | jieba 4 | nonebot-adapter-onebot>=2.0.0 5 | nonebot2>=2.2.0 6 | tencentcloud-sdk-python>=3.0.580 7 | setuptools 8 | jinja2 9 | pyppeteer 10 | imageio 11 | numpy 12 | nonebot_plugin_apscheduler 13 | nb-cli 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | 4 | import setuptools 5 | 6 | with open("README.md", "r", encoding="utf-8", errors="ignore") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup( 10 | name="nonebot-plugin-admin", 11 | version="0.4.7", 12 | author="yzyyz1387", 13 | author_email="youzyyz1384@qq.com", 14 | keywords=("pip", "nonebot2", "nonebot", "admin", "nonebot_plugin"), 15 | description="""nonebot2 plugin for group administration""", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | url="https://github.com/yzyyz1387/nonebot_plugin_admin", 19 | packages=setuptools.find_packages(), 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", 23 | "Operating System :: OS Independent", 24 | ], 25 | include_package_data=True, 26 | platforms="any", 27 | install_requires=["fuzzyfinder", 'nonebot-adapter-onebot>=2.0.0-beta.1', 'nonebot2>=2.0.0-beta' 28 | '.4', "jieba", 29 | "httpx", "tencentcloud-sdk-python>=3.0.580", "jinja2", "pyppeteer", "imageio", "numpy", 30 | "nonebot_plugin_apscheduler"] 31 | ) 32 | --------------------------------------------------------------------------------