From 131200a28e3ac07fcf62d920fdd2554f81e89e6f Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Feb 2024 04:18:54 +0800 Subject: [PATCH 001/132] =?UTF-8?q?init=F0=9F=8E=89:=20=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 34 +- .vscode/settings.json | 15 + README.md | 2 +- basic_plugins/__init__.py | 0 basic_plugins/admin_bot_manage/__init__.py | 35 - .../admin_bot_manage/_data_source.py | 391 - .../custom_welcome_message.py | 64 - basic_plugins/admin_bot_manage/rule.py | 55 - basic_plugins/admin_bot_manage/switch_rule.py | 144 - basic_plugins/admin_bot_manage/timing_task.py | 48 - .../update_group_member_info.py | 51 - basic_plugins/admin_help/__init__.py | 27 - basic_plugins/admin_help/data_source.py | 81 - basic_plugins/apscheduler/__init__.py | 250 - basic_plugins/ban/__init__.py | 179 - basic_plugins/ban/data_source.py | 77 - basic_plugins/broadcast/__init__.py | 73 - basic_plugins/chat_history/_rule.py | 9 - basic_plugins/chat_history/chat_message.py | 72 - .../chat_history/chat_message_handle.py | 113 - basic_plugins/group_handle/__init__.py | 236 - basic_plugins/help/__init__.py | 64 - basic_plugins/help/_config.py | 14 - basic_plugins/help/_data_source.py | 56 - basic_plugins/help/_utils.py | 259 - basic_plugins/hooks/__init__.py | 33 - basic_plugins/hooks/_utils.py | 580 -- basic_plugins/hooks/auth_hook.py | 36 - basic_plugins/hooks/ban_hook.py | 83 - basic_plugins/hooks/chkdsk_hook.py | 72 - basic_plugins/hooks/other_hook.py | 43 - basic_plugins/hooks/task_hook.py | 46 - basic_plugins/hooks/withdraw_message_hook.py | 32 - basic_plugins/init_plugin_config/__init__.py | 55 - .../init_plugin_config/check_plugin_status.py | 18 - basic_plugins/init_plugin_config/init.py | 14 - .../init_none_plugin_count_manager.py | 62 - .../init_plugin_config/init_plugin_info.py | 149 - .../init_plugin_config/init_plugins_config.py | 173 - .../init_plugin_config/init_plugins_data.py | 57 - .../init_plugin_config/init_plugins_limit.py | 64 - .../init_plugins_resources.py | 25 - .../init_plugins_settings.py | 56 - basic_plugins/invite_manager/utils.py | 87 - basic_plugins/nickname.py | 207 - basic_plugins/plugin_shop/__init__.py | 75 - basic_plugins/plugin_shop/data_source.py | 200 - basic_plugins/scripts.py | 108 - basic_plugins/shop/__init__.py | 27 - basic_plugins/shop/buy.py | 109 - basic_plugins/shop/gold.py | 62 - basic_plugins/shop/my_props/__init__.py | 41 - basic_plugins/shop/my_props/_data_source.py | 121 - basic_plugins/shop/shop_handle/__init__.py | 162 - basic_plugins/shop/shop_handle/data_source.py | 416 - basic_plugins/shop/use/__init__.py | 115 - basic_plugins/shop/use/data_source.py | 235 - basic_plugins/super_cmd/bot_friend_group.py | 158 - basic_plugins/super_cmd/clear_data.py | 75 - basic_plugins/super_cmd/exec_sql.py | 92 - basic_plugins/super_cmd/manager_group.py | 235 - basic_plugins/super_cmd/reload_setting.py | 64 - .../super_cmd/set_admin_permissions.py | 114 - .../super_cmd/update_friend_group_info.py | 77 - basic_plugins/super_help/__init__.py | 24 - basic_plugins/super_help/data_source.py | 81 - basic_plugins/update_info.py | 32 - bot.py | 27 +- configs/path_config.py | 47 - models/bag_user.py | 173 - models/ban_user.py | 131 - models/chat_history.py | 126 - models/goods_info.py | 205 - models/level_user.py | 108 - models/sign_group_user.py | 80 - models/statistics.py | 31 - models/user_shop_gold_log.py | 38 - plugins/__init__.py | 0 plugins/about.py | 43 - plugins/aconfig/__init__.py | 60 - plugins/ai/__init__.py | 87 - plugins/ai/data_source.py | 222 - plugins/ai/utils.py | 140 - plugins/alapi/__init__.py | 11 - plugins/alapi/_data_source.py | 26 - plugins/alapi/comments_163.py | 45 - plugins/alapi/cover.py | 47 - plugins/alapi/jitang.py | 45 - plugins/alapi/poetry.py | 41 - plugins/bilibili_sub/__init__.py | 307 - plugins/bilibili_sub/data_source.py | 451 - plugins/bilibili_sub/model.py | 207 - plugins/bilibili_sub/utils.py | 150 - plugins/black_word/__init__.py | 278 - plugins/black_word/data_source.py | 121 - plugins/black_word/model.py | 149 - plugins/black_word/utils.py | 343 - plugins/bt/__init__.py | 91 - plugins/bt/data_source.py | 42 - plugins/check/__init__.py | 31 - plugins/check/data_source.py | 77 - plugins/check_zhenxun_update/__init__.py | 131 - plugins/check_zhenxun_update/data_source.py | 221 - plugins/coser/__init__.py | 70 - plugins/dialogue/__init__.py | 167 - plugins/draw_card/__init__.py | 305 - plugins/draw_card/config.py | 204 - plugins/draw_card/count_manager.py | 149 - plugins/draw_card/handles/azur_handle.py | 304 - plugins/draw_card/handles/ba_handle.py | 156 - plugins/draw_card/handles/base_handle.py | 294 - plugins/draw_card/handles/fgo_handle.py | 221 - plugins/draw_card/handles/genshin_handle.py | 448 - plugins/draw_card/handles/guardian_handle.py | 400 - plugins/draw_card/handles/onmyoji_handle.py | 179 - plugins/draw_card/handles/pcr_handle.py | 147 - plugins/draw_card/handles/pretty_handle.py | 424 - plugins/draw_card/handles/prts_handle.py | 325 - plugins/draw_card/rule.py | 11 - plugins/draw_card/util.py | 60 - plugins/epic/__init__.py | 80 - plugins/epic/data_source.py | 196 - plugins/fake_msg.py | 57 - plugins/fudu.py | 138 - plugins/genshin/almanac/__init__.py | 70 - plugins/genshin/almanac/_data_source.py | 129 - plugins/genshin/material_remind/__init__.py | 106 - .../genshin/query_resource_points/__init__.py | 131 - plugins/genshin/query_resource_points/map.py | 265 - .../query_resource_points/query_resource.py | 272 - plugins/genshin/query_user/__init__.py | 35 - .../genshin/query_user/_models/__init__.py | 108 - plugins/genshin/query_user/_utils/__init__.py | 52 - plugins/genshin/query_user/bind/__init__.py | 153 - .../query_user/genshin_sign/__init__.py | 121 - .../query_user/genshin_sign/data_source.py | 153 - .../query_user/genshin_sign/init_task.py | 124 - .../query_user/mihoyobbs_sign/__init__.py | 90 - .../query_user/mihoyobbs_sign/error.py | 6 - .../query_user/mihoyobbs_sign/mihoyobbs.py | 193 - .../query_user/mihoyobbs_sign/setting.py | 124 - .../query_user/mihoyobbs_sign/tools.py | 65 - .../genshin/query_user/query_memo/__init__.py | 54 - .../query_user/query_memo/data_source.py | 280 - .../genshin/query_user/query_role/__init__.py | 67 - .../query_user/query_role/data_source.py | 254 - .../query_user/query_role/draw_image.py | 595 -- .../reset_today_query_user_data/__init__.py | 18 - .../query_user/resin_remind/__init__.py | 85 - .../query_user/resin_remind/init_task.py | 217 - plugins/gold_redbag/__init__.py | 373 - plugins/gold_redbag/config.py | 311 - plugins/gold_redbag/data_source.py | 154 - plugins/gold_redbag/model.py | 66 - plugins/group_welcome_msg.py | 53 - plugins/image_management/__init__.py | 69 - .../image_management/delete_image/__init__.py | 100 - .../image_management/move_image/__init__.py | 114 - .../image_management/send_image/__init__.py | 120 - plugins/image_management/send_image/anti.py | 26 - plugins/image_management/send_image/rule.py | 18 - .../image_management/upload_image/__init__.py | 134 - .../upload_image/data_source.py | 48 - plugins/luxun/__init__.py | 66 - plugins/music/__init__.py | 54 - plugins/music/music_163.py | 41 - plugins/mute.py | 188 - plugins/my_info/__init__.py | 48 - plugins/nbnhhsh.py | 62 - plugins/one_friend/__init__.py | 70 - plugins/open_cases/__init__.py | 326 - plugins/open_cases/build_image.py | 156 - plugins/open_cases/config.py | 255 - plugins/open_cases/models/__init__.py | 0 plugins/open_cases/models/buff_prices.py | 23 - plugins/open_cases/models/buff_skin.py | 104 - plugins/open_cases/models/buff_skin_log.py | 51 - plugins/open_cases/models/open_cases_log.py | 46 - plugins/open_cases/models/open_cases_user.py | 62 - plugins/open_cases/open_cases_c.py | 461 - plugins/open_cases/utils.py | 643 -- plugins/parse_bilibili_json.py | 174 - plugins/pid_search.py | 117 - plugins/pix_gallery/__init__.py | 57 - plugins/pix_gallery/_data_source.py | 404 - plugins/pix_gallery/_model/__init__.py | 1 - .../pix_gallery/_model/omega_pixiv_illusts.py | 92 - plugins/pix_gallery/_model/pixiv.py | 95 - .../pix_gallery/_model/pixiv_keyword_user.py | 60 - plugins/pix_gallery/pix.py | 217 - plugins/pix_gallery/pix_add_keyword.py | 152 - plugins/pix_gallery/pix_pass_del_keyword.py | 205 - plugins/pix_gallery/pix_show_info.py | 85 - plugins/pix_gallery/pix_update.py | 207 - plugins/pixiv_rank_search/__init__.py | 230 - plugins/pixiv_rank_search/data_source.py | 162 - plugins/poke/__init__.py | 86 - plugins/quotations.py | 40 - plugins/roll.py | 65 - plugins/russian/__init__.py | 536 -- plugins/russian/data_source.py | 33 - plugins/russian/model.py | 108 - plugins/search_anime/__init__.py | 73 - plugins/search_anime/data_source.py | 52 - plugins/search_buff_skin_price/__init__.py | 96 - plugins/search_buff_skin_price/data_source.py | 58 - plugins/search_image/__init__.py | 96 - plugins/search_image/saucenao.py | 57 - plugins/self_message/__init__.py | 90 - plugins/self_message/_rule.py | 9 - plugins/send_dinggong_voice/__init__.py | 49 - plugins/send_setu_/__init__.py | 4 - plugins/send_setu_/_model.py | 84 - plugins/send_setu_/send_setu/__init__.py | 426 - plugins/send_setu_/send_setu/data_source.py | 267 - plugins/send_setu_/update_setu/__init__.py | 46 - plugins/send_setu_/update_setu/data_source.py | 178 - plugins/sign_in/__init__.py | 174 - plugins/sign_in/config.py | 64 - plugins/sign_in/goods_register.py | 59 - plugins/sign_in/group_user_checkin.py | 205 - plugins/sign_in/random_event.py | 31 - plugins/sign_in/utils.py | 361 - plugins/statistics/__init__.py | 127 - plugins/statistics/_config.py | 26 - plugins/statistics/statistics_handle.py | 281 - plugins/statistics/statistics_hook.py | 217 - plugins/statistics/utils.py | 16 - plugins/translate/__init__.py | 78 - plugins/translate/data_source.py | 119 - plugins/update_gocqhttp/__init__.py | 78 - plugins/update_gocqhttp/data_source.py | 73 - plugins/update_picture.py | 303 - plugins/wbtop/__init__.py | 80 - plugins/wbtop/data_source.py | 62 - plugins/weather/__init__.py | 58 - plugins/weather/data_source.py | 79 - plugins/web_ui/__init__.py | 74 - plugins/web_ui/api/__init__.py | 1 - plugins/web_ui/api/logs/__init__.py | 1 - plugins/web_ui/api/logs/log_manager.py | 39 - plugins/web_ui/api/logs/logs.py | 42 - plugins/web_ui/api/tabs/__init__.py | 5 - plugins/web_ui/api/tabs/database/__init__.py | 104 - .../web_ui/api/tabs/database/models/model.py | 26 - .../api/tabs/database/models/sql_log.py | 40 - plugins/web_ui/api/tabs/main/__init__.py | 266 - plugins/web_ui/api/tabs/main/data_source.py | 36 - plugins/web_ui/api/tabs/main/model.py | 107 - plugins/web_ui/api/tabs/manage/__init__.py | 462 - plugins/web_ui/api/tabs/manage/model.py | 270 - .../web_ui/api/tabs/plugin_manage/__init__.py | 198 - .../web_ui/api/tabs/plugin_manage/model.py | 148 - plugins/web_ui/api/tabs/system/__init__.py | 121 - plugins/web_ui/api/tabs/system/model.py | 64 - plugins/web_ui/auth/__init__.py | 43 - plugins/web_ui/base_model.py | 117 - plugins/web_ui/config.py | 86 - plugins/web_ui/utils.py | 170 - plugins/what_anime/__init__.py | 62 - plugins/what_anime/data_source.py | 47 - plugins/white2black_image.py | 143 - plugins/withdraw.py | 29 - plugins/word_bank/__init__.py | 20 - plugins/word_bank/_config.py | 23 - plugins/word_bank/_data_source.py | 270 - plugins/word_bank/_model.py | 528 -- plugins/word_bank/_rule.py | 53 - plugins/word_bank/message_handle.py | 29 - plugins/word_bank/word_handle.py | 359 - plugins/word_clouds/__init__.py | 207 - plugins/word_clouds/data_source.py | 129 - plugins/yiqing/__init__.py | 64 - plugins/yiqing/data_source.py | 110 - plugins/yiqing/other_than.py | 93 - poetry.lock | 3800 +++----- pyproject.toml | 54 +- resources/image/_icon/discode.png | Bin 0 -> 285478 bytes resources/image/_icon/dodo.png | Bin 0 -> 229161 bytes resources/image/_icon/kook.png | Bin 0 -> 342751 bytes resources/image/_icon/qq.png | Bin 0 -> 1058 bytes services/log.py | 144 - update_info.json | 15 - utils/__init__.py | 0 utils/data_utils.py | 73 - utils/decorator/__init__.py | 13 - utils/decorator/shop.py | 204 - utils/depends/__init__.py | 215 - utils/game_utils.py | 192 - utils/http_utils.py | 379 - utils/image_template.py | 35 - utils/image_utils.py | 1778 ---- utils/langconv.py | 274 - utils/manager/__init__.py | 71 - utils/manager/admin_manager.py | 83 - utils/manager/configs_manager.py | 65 - utils/manager/data_class.py | 110 - utils/manager/group_manager.py | 443 - utils/manager/models.py | 150 - utils/manager/none_plugin_count_manager.py | 47 - utils/manager/plugin_data_manager.py | 34 - utils/manager/plugins2block_manager.py | 192 - utils/manager/plugins2cd_manager.py | 199 - utils/manager/plugins2count_manager.py | 191 - utils/manager/plugins2settings_manager.py | 171 - utils/manager/plugins_manager.py | 169 - utils/manager/requests_manager.py | 347 - utils/manager/resources_manager.py | 113 - utils/manager/withdraw_message_manager.py | 52 - utils/message_builder.py | 212 - utils/models/__init__.py | 37 - utils/text_utils.py | 12 - utils/typing.py | 4 - utils/user_agent.py | 50 - utils/utils.py | 629 -- utils/zh_wiki.py | 8275 ----------------- zhenxun/builtin_plugins/__init__.py | 10 + .../builtin_plugins/admin}/__init__.py | 0 .../builtin_plugins/admin/admin_watch.py | 104 +- .../builtin_plugins/admin/welcome_message.py | 116 + .../builtin_plugins/init}/__init__.py | 3 +- zhenxun/builtin_plugins/init/init_config.py | 123 + zhenxun/builtin_plugins/init/init_plugin.py | 126 + .../builtin_plugins/record_request.py | 352 +- .../builtin_plugins/superuser}/__init__.py | 10 +- .../builtin_plugins/superuser/clear_data.py | 86 + zhenxun/builtin_plugins/superuser/exec_sql.py | 78 + .../builtin_plugins/superuser/fg_manage.py | 114 + .../superuser/reload_setting.py | 68 + .../superuser/request_manage.py | 266 + .../builtin_plugins/superuser/set_admin.py | 105 + .../superuser/update_fg_info.py | 119 + {configs => zhenxun/configs}/config.py | 3 +- zhenxun/configs/path_config.py | 33 + .../configs}/utils/__init__.py | 306 +- zhenxun/models/fg_request.py | 111 + {models => zhenxun/models}/friend_user.py | 23 +- .../models/group_info copy.py | 16 +- zhenxun/models/group_info.py | 30 + .../models}/group_member_info.py | 23 +- zhenxun/models/level_user.py | 134 + zhenxun/models/plugin_info.py | 51 + zhenxun/models/plugin_limit.py | 44 + {services => zhenxun/services}/__init__.py | 4 +- {services => zhenxun/services}/db_context.py | 173 +- zhenxun/services/log.py | 307 + zhenxun/utils/_build_image.py | 656 ++ zhenxun/utils/_build_mat.py | 3 + {utils => zhenxun/utils}/browser.py | 95 +- zhenxun/utils/enum.py | 73 + zhenxun/utils/exception.py | 2 + zhenxun/utils/image_utils.py | 2 + zhenxun/utils/rules.py | 46 + .../data_source.py => zhenxun/utils/typing.py | 3 - zhenxun/utils/utils.py | 77 + 355 files changed, 4826 insertions(+), 53229 deletions(-) create mode 100644 .vscode/settings.json delete mode 100755 basic_plugins/__init__.py delete mode 100755 basic_plugins/admin_bot_manage/__init__.py delete mode 100644 basic_plugins/admin_bot_manage/_data_source.py delete mode 100755 basic_plugins/admin_bot_manage/custom_welcome_message.py delete mode 100755 basic_plugins/admin_bot_manage/rule.py delete mode 100755 basic_plugins/admin_bot_manage/switch_rule.py delete mode 100755 basic_plugins/admin_bot_manage/timing_task.py delete mode 100755 basic_plugins/admin_bot_manage/update_group_member_info.py delete mode 100755 basic_plugins/admin_help/__init__.py delete mode 100755 basic_plugins/admin_help/data_source.py delete mode 100755 basic_plugins/apscheduler/__init__.py delete mode 100755 basic_plugins/ban/__init__.py delete mode 100644 basic_plugins/ban/data_source.py delete mode 100755 basic_plugins/broadcast/__init__.py delete mode 100644 basic_plugins/chat_history/_rule.py delete mode 100644 basic_plugins/chat_history/chat_message.py delete mode 100644 basic_plugins/chat_history/chat_message_handle.py delete mode 100755 basic_plugins/group_handle/__init__.py delete mode 100755 basic_plugins/help/__init__.py delete mode 100644 basic_plugins/help/_config.py delete mode 100644 basic_plugins/help/_data_source.py delete mode 100644 basic_plugins/help/_utils.py delete mode 100755 basic_plugins/hooks/__init__.py delete mode 100644 basic_plugins/hooks/_utils.py delete mode 100755 basic_plugins/hooks/auth_hook.py delete mode 100755 basic_plugins/hooks/ban_hook.py delete mode 100755 basic_plugins/hooks/chkdsk_hook.py delete mode 100755 basic_plugins/hooks/other_hook.py delete mode 100644 basic_plugins/hooks/task_hook.py delete mode 100755 basic_plugins/hooks/withdraw_message_hook.py delete mode 100755 basic_plugins/init_plugin_config/__init__.py delete mode 100755 basic_plugins/init_plugin_config/check_plugin_status.py delete mode 100644 basic_plugins/init_plugin_config/init.py delete mode 100755 basic_plugins/init_plugin_config/init_none_plugin_count_manager.py delete mode 100644 basic_plugins/init_plugin_config/init_plugin_info.py delete mode 100755 basic_plugins/init_plugin_config/init_plugins_config.py delete mode 100755 basic_plugins/init_plugin_config/init_plugins_data.py delete mode 100755 basic_plugins/init_plugin_config/init_plugins_limit.py delete mode 100755 basic_plugins/init_plugin_config/init_plugins_resources.py delete mode 100755 basic_plugins/init_plugin_config/init_plugins_settings.py delete mode 100644 basic_plugins/invite_manager/utils.py delete mode 100755 basic_plugins/nickname.py delete mode 100644 basic_plugins/plugin_shop/__init__.py delete mode 100644 basic_plugins/plugin_shop/data_source.py delete mode 100755 basic_plugins/scripts.py delete mode 100644 basic_plugins/shop/__init__.py delete mode 100644 basic_plugins/shop/buy.py delete mode 100644 basic_plugins/shop/gold.py delete mode 100644 basic_plugins/shop/my_props/__init__.py delete mode 100644 basic_plugins/shop/my_props/_data_source.py delete mode 100644 basic_plugins/shop/shop_handle/__init__.py delete mode 100644 basic_plugins/shop/shop_handle/data_source.py delete mode 100644 basic_plugins/shop/use/__init__.py delete mode 100644 basic_plugins/shop/use/data_source.py delete mode 100755 basic_plugins/super_cmd/bot_friend_group.py delete mode 100755 basic_plugins/super_cmd/clear_data.py delete mode 100644 basic_plugins/super_cmd/exec_sql.py delete mode 100755 basic_plugins/super_cmd/manager_group.py delete mode 100755 basic_plugins/super_cmd/reload_setting.py delete mode 100755 basic_plugins/super_cmd/set_admin_permissions.py delete mode 100755 basic_plugins/super_cmd/update_friend_group_info.py delete mode 100755 basic_plugins/super_help/__init__.py delete mode 100755 basic_plugins/super_help/data_source.py delete mode 100755 basic_plugins/update_info.py delete mode 100644 configs/path_config.py delete mode 100755 models/bag_user.py delete mode 100755 models/ban_user.py delete mode 100644 models/chat_history.py delete mode 100644 models/goods_info.py delete mode 100755 models/level_user.py delete mode 100755 models/sign_group_user.py delete mode 100644 models/statistics.py delete mode 100644 models/user_shop_gold_log.py delete mode 100755 plugins/__init__.py delete mode 100644 plugins/about.py delete mode 100755 plugins/aconfig/__init__.py delete mode 100755 plugins/ai/__init__.py delete mode 100755 plugins/ai/data_source.py delete mode 100755 plugins/ai/utils.py delete mode 100755 plugins/alapi/__init__.py delete mode 100644 plugins/alapi/_data_source.py delete mode 100755 plugins/alapi/comments_163.py delete mode 100755 plugins/alapi/cover.py delete mode 100755 plugins/alapi/jitang.py delete mode 100755 plugins/alapi/poetry.py delete mode 100755 plugins/bilibili_sub/__init__.py delete mode 100755 plugins/bilibili_sub/data_source.py delete mode 100755 plugins/bilibili_sub/model.py delete mode 100755 plugins/bilibili_sub/utils.py delete mode 100644 plugins/black_word/__init__.py delete mode 100644 plugins/black_word/data_source.py delete mode 100644 plugins/black_word/model.py delete mode 100644 plugins/black_word/utils.py delete mode 100755 plugins/bt/__init__.py delete mode 100755 plugins/bt/data_source.py delete mode 100755 plugins/check/__init__.py delete mode 100755 plugins/check/data_source.py delete mode 100755 plugins/check_zhenxun_update/__init__.py delete mode 100755 plugins/check_zhenxun_update/data_source.py delete mode 100755 plugins/coser/__init__.py delete mode 100755 plugins/dialogue/__init__.py delete mode 100644 plugins/draw_card/__init__.py delete mode 100644 plugins/draw_card/config.py delete mode 100644 plugins/draw_card/count_manager.py delete mode 100644 plugins/draw_card/handles/azur_handle.py delete mode 100644 plugins/draw_card/handles/ba_handle.py delete mode 100644 plugins/draw_card/handles/base_handle.py delete mode 100644 plugins/draw_card/handles/fgo_handle.py delete mode 100644 plugins/draw_card/handles/genshin_handle.py delete mode 100644 plugins/draw_card/handles/guardian_handle.py delete mode 100644 plugins/draw_card/handles/onmyoji_handle.py delete mode 100644 plugins/draw_card/handles/pcr_handle.py delete mode 100644 plugins/draw_card/handles/pretty_handle.py delete mode 100644 plugins/draw_card/handles/prts_handle.py delete mode 100644 plugins/draw_card/rule.py delete mode 100644 plugins/draw_card/util.py delete mode 100755 plugins/epic/__init__.py delete mode 100755 plugins/epic/data_source.py delete mode 100755 plugins/fake_msg.py delete mode 100755 plugins/fudu.py delete mode 100755 plugins/genshin/almanac/__init__.py delete mode 100644 plugins/genshin/almanac/_data_source.py delete mode 100755 plugins/genshin/material_remind/__init__.py delete mode 100755 plugins/genshin/query_resource_points/__init__.py delete mode 100755 plugins/genshin/query_resource_points/map.py delete mode 100755 plugins/genshin/query_resource_points/query_resource.py delete mode 100644 plugins/genshin/query_user/__init__.py delete mode 100644 plugins/genshin/query_user/_models/__init__.py delete mode 100644 plugins/genshin/query_user/_utils/__init__.py delete mode 100644 plugins/genshin/query_user/bind/__init__.py delete mode 100644 plugins/genshin/query_user/genshin_sign/__init__.py delete mode 100644 plugins/genshin/query_user/genshin_sign/data_source.py delete mode 100644 plugins/genshin/query_user/genshin_sign/init_task.py delete mode 100644 plugins/genshin/query_user/mihoyobbs_sign/__init__.py delete mode 100644 plugins/genshin/query_user/mihoyobbs_sign/error.py delete mode 100644 plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py delete mode 100644 plugins/genshin/query_user/mihoyobbs_sign/setting.py delete mode 100644 plugins/genshin/query_user/mihoyobbs_sign/tools.py delete mode 100644 plugins/genshin/query_user/query_memo/__init__.py delete mode 100644 plugins/genshin/query_user/query_memo/data_source.py delete mode 100644 plugins/genshin/query_user/query_role/__init__.py delete mode 100644 plugins/genshin/query_user/query_role/data_source.py delete mode 100644 plugins/genshin/query_user/query_role/draw_image.py delete mode 100644 plugins/genshin/query_user/reset_today_query_user_data/__init__.py delete mode 100644 plugins/genshin/query_user/resin_remind/__init__.py delete mode 100644 plugins/genshin/query_user/resin_remind/init_task.py delete mode 100755 plugins/gold_redbag/__init__.py delete mode 100644 plugins/gold_redbag/config.py delete mode 100755 plugins/gold_redbag/data_source.py delete mode 100755 plugins/gold_redbag/model.py delete mode 100755 plugins/group_welcome_msg.py delete mode 100755 plugins/image_management/__init__.py delete mode 100755 plugins/image_management/delete_image/__init__.py delete mode 100755 plugins/image_management/move_image/__init__.py delete mode 100755 plugins/image_management/send_image/__init__.py delete mode 100644 plugins/image_management/send_image/anti.py delete mode 100644 plugins/image_management/send_image/rule.py delete mode 100755 plugins/image_management/upload_image/__init__.py delete mode 100755 plugins/image_management/upload_image/data_source.py delete mode 100755 plugins/luxun/__init__.py delete mode 100644 plugins/music/__init__.py delete mode 100644 plugins/music/music_163.py delete mode 100755 plugins/mute.py delete mode 100644 plugins/my_info/__init__.py delete mode 100755 plugins/nbnhhsh.py delete mode 100755 plugins/one_friend/__init__.py delete mode 100755 plugins/open_cases/__init__.py delete mode 100644 plugins/open_cases/build_image.py delete mode 100755 plugins/open_cases/config.py delete mode 100755 plugins/open_cases/models/__init__.py delete mode 100755 plugins/open_cases/models/buff_prices.py delete mode 100644 plugins/open_cases/models/buff_skin.py delete mode 100644 plugins/open_cases/models/buff_skin_log.py delete mode 100644 plugins/open_cases/models/open_cases_log.py delete mode 100755 plugins/open_cases/models/open_cases_user.py delete mode 100755 plugins/open_cases/open_cases_c.py delete mode 100755 plugins/open_cases/utils.py delete mode 100755 plugins/parse_bilibili_json.py delete mode 100755 plugins/pid_search.py delete mode 100755 plugins/pix_gallery/__init__.py delete mode 100644 plugins/pix_gallery/_data_source.py delete mode 100644 plugins/pix_gallery/_model/__init__.py delete mode 100644 plugins/pix_gallery/_model/omega_pixiv_illusts.py delete mode 100644 plugins/pix_gallery/_model/pixiv.py delete mode 100644 plugins/pix_gallery/_model/pixiv_keyword_user.py delete mode 100755 plugins/pix_gallery/pix.py delete mode 100755 plugins/pix_gallery/pix_add_keyword.py delete mode 100755 plugins/pix_gallery/pix_pass_del_keyword.py delete mode 100755 plugins/pix_gallery/pix_show_info.py delete mode 100755 plugins/pix_gallery/pix_update.py delete mode 100755 plugins/pixiv_rank_search/__init__.py delete mode 100755 plugins/pixiv_rank_search/data_source.py delete mode 100755 plugins/poke/__init__.py delete mode 100755 plugins/quotations.py delete mode 100755 plugins/roll.py delete mode 100755 plugins/russian/__init__.py delete mode 100755 plugins/russian/data_source.py delete mode 100755 plugins/russian/model.py delete mode 100755 plugins/search_anime/__init__.py delete mode 100755 plugins/search_anime/data_source.py delete mode 100755 plugins/search_buff_skin_price/__init__.py delete mode 100755 plugins/search_buff_skin_price/data_source.py delete mode 100644 plugins/search_image/__init__.py delete mode 100644 plugins/search_image/saucenao.py delete mode 100644 plugins/self_message/__init__.py delete mode 100644 plugins/self_message/_rule.py delete mode 100755 plugins/send_dinggong_voice/__init__.py delete mode 100755 plugins/send_setu_/__init__.py delete mode 100644 plugins/send_setu_/_model.py delete mode 100755 plugins/send_setu_/send_setu/__init__.py delete mode 100755 plugins/send_setu_/send_setu/data_source.py delete mode 100755 plugins/send_setu_/update_setu/__init__.py delete mode 100755 plugins/send_setu_/update_setu/data_source.py delete mode 100755 plugins/sign_in/__init__.py delete mode 100755 plugins/sign_in/config.py delete mode 100644 plugins/sign_in/goods_register.py delete mode 100755 plugins/sign_in/group_user_checkin.py delete mode 100755 plugins/sign_in/random_event.py delete mode 100755 plugins/sign_in/utils.py delete mode 100755 plugins/statistics/__init__.py delete mode 100644 plugins/statistics/_config.py delete mode 100755 plugins/statistics/statistics_handle.py delete mode 100755 plugins/statistics/statistics_hook.py delete mode 100644 plugins/statistics/utils.py delete mode 100755 plugins/translate/__init__.py delete mode 100755 plugins/translate/data_source.py delete mode 100755 plugins/update_gocqhttp/__init__.py delete mode 100755 plugins/update_gocqhttp/data_source.py delete mode 100755 plugins/update_picture.py delete mode 100644 plugins/wbtop/__init__.py delete mode 100644 plugins/wbtop/data_source.py delete mode 100755 plugins/weather/__init__.py delete mode 100755 plugins/weather/data_source.py delete mode 100644 plugins/web_ui/__init__.py delete mode 100644 plugins/web_ui/api/__init__.py delete mode 100644 plugins/web_ui/api/logs/__init__.py delete mode 100644 plugins/web_ui/api/logs/log_manager.py delete mode 100644 plugins/web_ui/api/logs/logs.py delete mode 100644 plugins/web_ui/api/tabs/__init__.py delete mode 100644 plugins/web_ui/api/tabs/database/__init__.py delete mode 100644 plugins/web_ui/api/tabs/database/models/model.py delete mode 100644 plugins/web_ui/api/tabs/database/models/sql_log.py delete mode 100644 plugins/web_ui/api/tabs/main/__init__.py delete mode 100644 plugins/web_ui/api/tabs/main/data_source.py delete mode 100644 plugins/web_ui/api/tabs/main/model.py delete mode 100644 plugins/web_ui/api/tabs/manage/__init__.py delete mode 100644 plugins/web_ui/api/tabs/manage/model.py delete mode 100644 plugins/web_ui/api/tabs/plugin_manage/__init__.py delete mode 100644 plugins/web_ui/api/tabs/plugin_manage/model.py delete mode 100644 plugins/web_ui/api/tabs/system/__init__.py delete mode 100644 plugins/web_ui/api/tabs/system/model.py delete mode 100644 plugins/web_ui/auth/__init__.py delete mode 100644 plugins/web_ui/base_model.py delete mode 100644 plugins/web_ui/config.py delete mode 100644 plugins/web_ui/utils.py delete mode 100755 plugins/what_anime/__init__.py delete mode 100755 plugins/what_anime/data_source.py delete mode 100755 plugins/white2black_image.py delete mode 100755 plugins/withdraw.py delete mode 100644 plugins/word_bank/__init__.py delete mode 100644 plugins/word_bank/_config.py delete mode 100644 plugins/word_bank/_data_source.py delete mode 100644 plugins/word_bank/_model.py delete mode 100644 plugins/word_bank/_rule.py delete mode 100644 plugins/word_bank/message_handle.py delete mode 100644 plugins/word_bank/word_handle.py delete mode 100644 plugins/word_clouds/__init__.py delete mode 100644 plugins/word_clouds/data_source.py delete mode 100755 plugins/yiqing/__init__.py delete mode 100755 plugins/yiqing/data_source.py delete mode 100644 plugins/yiqing/other_than.py create mode 100644 resources/image/_icon/discode.png create mode 100644 resources/image/_icon/dodo.png create mode 100644 resources/image/_icon/kook.png create mode 100644 resources/image/_icon/qq.png delete mode 100755 services/log.py delete mode 100644 update_info.json delete mode 100755 utils/__init__.py delete mode 100755 utils/data_utils.py delete mode 100644 utils/decorator/__init__.py delete mode 100644 utils/decorator/shop.py delete mode 100644 utils/depends/__init__.py delete mode 100644 utils/game_utils.py delete mode 100644 utils/http_utils.py delete mode 100644 utils/image_template.py delete mode 100755 utils/image_utils.py delete mode 100755 utils/langconv.py delete mode 100755 utils/manager/__init__.py delete mode 100644 utils/manager/admin_manager.py delete mode 100644 utils/manager/configs_manager.py delete mode 100755 utils/manager/data_class.py delete mode 100644 utils/manager/group_manager.py delete mode 100644 utils/manager/models.py delete mode 100644 utils/manager/none_plugin_count_manager.py delete mode 100644 utils/manager/plugin_data_manager.py delete mode 100644 utils/manager/plugins2block_manager.py delete mode 100644 utils/manager/plugins2cd_manager.py delete mode 100644 utils/manager/plugins2count_manager.py delete mode 100644 utils/manager/plugins2settings_manager.py delete mode 100644 utils/manager/plugins_manager.py delete mode 100644 utils/manager/requests_manager.py delete mode 100644 utils/manager/resources_manager.py delete mode 100644 utils/manager/withdraw_message_manager.py delete mode 100755 utils/message_builder.py delete mode 100644 utils/models/__init__.py delete mode 100644 utils/text_utils.py delete mode 100644 utils/typing.py delete mode 100755 utils/user_agent.py delete mode 100755 utils/utils.py delete mode 100755 utils/zh_wiki.py create mode 100644 zhenxun/builtin_plugins/__init__.py rename {plugins/genshin => zhenxun/builtin_plugins/admin}/__init__.py (100%) mode change 100755 => 100644 rename basic_plugins/admin_bot_manage/admin_config.py => zhenxun/builtin_plugins/admin/admin_watch.py (55%) mode change 100755 => 100644 create mode 100644 zhenxun/builtin_plugins/admin/welcome_message.py rename {basic_plugins/chat_history => zhenxun/builtin_plugins/init}/__init__.py (99%) create mode 100644 zhenxun/builtin_plugins/init/init_config.py create mode 100644 zhenxun/builtin_plugins/init/init_plugin.py rename basic_plugins/invite_manager/__init__.py => zhenxun/builtin_plugins/record_request.py (50%) mode change 100755 => 100644 rename {basic_plugins/super_cmd => zhenxun/builtin_plugins/superuser}/__init__.py (95%) mode change 100755 => 100644 create mode 100644 zhenxun/builtin_plugins/superuser/clear_data.py create mode 100644 zhenxun/builtin_plugins/superuser/exec_sql.py create mode 100644 zhenxun/builtin_plugins/superuser/fg_manage.py create mode 100644 zhenxun/builtin_plugins/superuser/reload_setting.py create mode 100644 zhenxun/builtin_plugins/superuser/request_manage.py create mode 100644 zhenxun/builtin_plugins/superuser/set_admin.py create mode 100644 zhenxun/builtin_plugins/superuser/update_fg_info.py rename {configs => zhenxun/configs}/config.py (92%) create mode 100644 zhenxun/configs/path_config.py rename {configs => zhenxun/configs}/utils/__init__.py (56%) create mode 100644 zhenxun/models/fg_request.py rename {models => zhenxun/models}/friend_user.py (73%) mode change 100755 => 100644 rename models/group_info.py => zhenxun/models/group_info copy.py (55%) mode change 100755 => 100644 create mode 100644 zhenxun/models/group_info.py rename {models => zhenxun/models}/group_member_info.py (87%) mode change 100755 => 100644 create mode 100644 zhenxun/models/level_user.py create mode 100644 zhenxun/models/plugin_info.py create mode 100644 zhenxun/models/plugin_limit.py rename {services => zhenxun/services}/__init__.py (95%) mode change 100755 => 100644 rename {services => zhenxun/services}/db_context.py (74%) mode change 100755 => 100644 create mode 100644 zhenxun/services/log.py create mode 100644 zhenxun/utils/_build_image.py create mode 100644 zhenxun/utils/_build_mat.py rename {utils => zhenxun/utils}/browser.py (94%) mode change 100755 => 100644 create mode 100644 zhenxun/utils/enum.py create mode 100644 zhenxun/utils/exception.py create mode 100644 zhenxun/utils/image_utils.py create mode 100644 zhenxun/utils/rules.py rename plugins/my_info/data_source.py => zhenxun/utils/typing.py (50%) create mode 100644 zhenxun/utils/utils.py diff --git a/.env.dev b/.env.dev index 00d51e29..e225f8b8 100644 --- a/.env.dev +++ b/.env.dev @@ -10,7 +10,39 @@ NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"] SESSION_EXPIRE_TIMEOUT=30 -DEBUG=False +# DRIVER=~fastapi +DRIVER=~fastapi+~httpx+~websockets + +# kook adapter toekn +kaiheila_bots =[{""}] + +# discode adapter +DISCORD_BOTS=' +[ + { + "token": "", + "intent": { + "guild_messages": true, + "direct_messages": true + }, + "application_commands": {"*": ["*"]} + } +] +' +DISCORD_PROXY='' + +# dodo adapter +DODO_BOTS=' +[ + { + "client_id": "", + "token": "" + } +] +' + + +LOG_LEVEL=DEBUG # 服务器和端口 HOST = 127.0.0.1 PORT = 8080 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..784fa6a0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "C_Cpp.errorSquiggles": "enabled", + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" + }, + "cSpell.words": [ + "Alconna", + "arclet", + "Arparma", + "getbbox", + "httpx", + "nonebot", + "zhenxun" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 476b3780..562b5db7 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ 后将gocq的配置文件config.yml中的universal改为universal: ws://127.0.0.1:8080/onebot/v11/ws # 获取代码 -git clone https://github.com/HibiKier/zhenxun_bot.git +git clone https://github.com/HibiKier/zhenxun.git # 进入目录 cd zhenxun_bot diff --git a/basic_plugins/__init__.py b/basic_plugins/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/basic_plugins/admin_bot_manage/__init__.py b/basic_plugins/admin_bot_manage/__init__.py deleted file mode 100755 index 79aa5bca..00000000 --- a/basic_plugins/admin_bot_manage/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "admin_bot_manage:custom_welcome_message", - "SET_GROUP_WELCOME_MESSAGE_LEVEL [LEVEL]", - 2, - name="群管理员操作", - help_="设置群欢迎消息权限", - default_value=2, - type=int, -) - -Config.add_plugin_config( - "admin_bot_manage:switch_rule", - "CHANGE_GROUP_SWITCH_LEVEL [LEVEL]", - 2, - help_="开关群功能权限", - default_value=2, - type=int, -) - -Config.add_plugin_config( - "admin_bot_manage", - "ADMIN_DEFAULT_AUTH", - 5, - help_="默认群管理员权限", - default_value=5, - type=int, -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/basic_plugins/admin_bot_manage/_data_source.py b/basic_plugins/admin_bot_manage/_data_source.py deleted file mode 100644 index b9f3a7de..00000000 --- a/basic_plugins/admin_bot_manage/_data_source.py +++ /dev/null @@ -1,391 +0,0 @@ -import asyncio -import os -import time -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import List, Union - -import ujson as json -from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment - -from configs.config import Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.manager import group_manager, plugins2settings_manager, plugins_manager -from utils.message_builder import image -from utils.typing import BLOCK_TYPE -from utils.utils import get_matchers - -CUSTOM_WELCOME_FILE = Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" -CUSTOM_WELCOME_FILE.parent.mkdir(parents=True, exist_ok=True) - -ICON_PATH = IMAGE_PATH / "other" - -GROUP_HELP_PATH = DATA_PATH / "group_help" - - -async def group_current_status(group_id: str) -> str: - """ - 说明: - 获取当前群聊所有通知的开关 - 参数: - :param group_id: 群号 - """ - _data = group_manager.get_task_data() - image_list = [] - for i, task in enumerate(_data): - name = _data[task] - name_image = BuildImage(0, 0, plain_text=f"{i+1}.{name}", font_size=20) - bk = BuildImage( - name_image.w + 200, name_image.h + 20, color=(103, 177, 109), font_size=15 - ) - await bk.apaste(name_image, (10, 0), True, "by_height") - a_icon = BuildImage(40, 40, background=ICON_PATH / "btn_false.png") - if group_manager.check_group_task_status(group_id, task): - a_icon = BuildImage(40, 40, background=ICON_PATH / "btn_true.png") - b_icon = BuildImage(40, 40, background=ICON_PATH / "btn_false.png") - if group_manager.check_task_super_status(task): - b_icon = BuildImage(40, 40, background=ICON_PATH / "btn_true.png") - await bk.atext((name_image.w + 20, 10), "状态") - await bk.apaste(a_icon, (name_image.w + 50, 0), True) - await bk.atext((name_image.w + 100, 10), "全局") - await bk.apaste(b_icon, (name_image.w + 130, 0), True) - image_list.append(bk) - w = max([x.w for x in image_list]) - h = sum([x.h + 10 for x in image_list]) - A = BuildImage(w + 20, h + 70, font_size=30, color=(119, 97, 177)) - await A.atext((15, 20), "群被动状态") - curr_h = 75 - for img in image_list: - # await img.acircle_corner() - await A.apaste(img, (0, curr_h), True) - curr_h += img.h + 10 - return A.pic2bs4() - - -async def custom_group_welcome( - msg: str, img_list: List[str], user_id: str, group_id: str -) -> Union[str, Message]: - """ - 说明: - 替换群欢迎消息 - 参数: - :param msg: 欢迎消息文本 - :param img_list: 欢迎消息图片 - :param user_id: 用户id,用于log记录 - :param group_id: 群号 - """ - img_result = "" - result = "" - img = img_list[0] if img_list else "" - msg_image = DATA_PATH / "custom_welcome_msg" / f"{group_id}.jpg" - if msg_image.exists(): - msg_image.unlink() - data = {} - if CUSTOM_WELCOME_FILE.exists(): - data = json.load(CUSTOM_WELCOME_FILE.open("r", encoding="utf8")) - try: - if msg: - data[group_id] = msg - json.dump( - data, - CUSTOM_WELCOME_FILE.open("w", encoding="utf8"), - indent=4, - ensure_ascii=False, - ) - logger.info(f"更换群欢迎消息 {msg}", "更换群欢迎信息", user_id, group_id) - result += msg - if img: - await AsyncHttpx.download_file(img, msg_image) - img_result = image(msg_image) - logger.info(f"更换群欢迎消息图片", "更换群欢迎信息", user_id, group_id) - except Exception as e: - logger.error(f"替换群消息失败", "更换群欢迎信息", user_id, group_id, e=e) - return "替换群消息失败..." - return f"替换群欢迎消息成功:\n{result}" + img_result - - -task_data = None - - -def change_global_task_status(cmd: str) -> str: - """ - 说明: - 修改全局被动任务状态 - 参数: - :param cmd: 功能名称 - """ - global task_data - if not task_data: - task_data = group_manager.get_task_data() - status = cmd[:2] - _cmd = cmd[4:] - if "全部被动" in cmd: - for task in task_data: - if status == "开启": - group_manager.open_global_task(task) - else: - group_manager.close_global_task(task) - group_manager.save() - return f"已 {status} 全局全部被动技能!" - else: - modules = [x for x in task_data if task_data[x].lower() == _cmd.lower()] - if not modules: - return "未查询到该被动任务" - if status == "开启": - group_manager.open_global_task(modules[0]) - else: - group_manager.close_global_task(modules[0]) - group_manager.save() - return f"已 {status} 全局{_cmd}" - - -async def change_group_switch(cmd: str, group_id: str, is_super: bool = False) -> str: - """ - 说明: - 修改群功能状态 - 参数: - :param cmd: 功能名称 - :param group_id: 群号 - :param is_super: 是否为超级用户,超级用户用于私聊开关功能状态 - """ - global task_data - if not task_data: - task_data = group_manager.get_task_data() - help_path = GROUP_HELP_PATH / f"{group_id}.png" - status = cmd[:2] - cmd = cmd[2:] - type_ = "plugin" - modules = plugins2settings_manager.get_plugin_module(cmd, True) - if cmd == "全部被动": - for task in task_data: - if status == "开启": - if not group_manager.check_group_task_status(group_id, task): - group_manager.open_group_task(group_id, task) - else: - if group_manager.check_group_task_status(group_id, task): - group_manager.close_group_task(group_id, task) - if help_path.exists(): - help_path.unlink() - return f"已 {status} 全部被动技能!" - if cmd == "全部功能": - for f in plugins2settings_manager.get_data(): - if status == "开启": - group_manager.unblock_plugin(f, group_id, False) - else: - group_manager.block_plugin(f, group_id, False) - group_manager.save() - if help_path.exists(): - help_path.unlink() - return f"已 {status} 全部功能!" - if cmd.lower() in [task_data[x].lower() for x in task_data.keys()]: - type_ = "task" - modules = [x for x in task_data.keys() if task_data[x].lower() == cmd.lower()] - for module in modules: - if is_super: - module = f"{module}:super" - if status == "开启": - if type_ == "task": - if group_manager.check_group_task_status(group_id, module): - return f"被动 {task_data[module]} 正处于开启状态!不要重复开启." - group_manager.open_group_task(group_id, module) - else: - if group_manager.get_plugin_status(module, group_id): - return f"功能 {cmd} 正处于开启状态!不要重复开启." - group_manager.unblock_plugin(module, group_id) - else: - if type_ == "task": - if not group_manager.check_group_task_status(group_id, module): - return f"被动 {task_data[module]} 正处于关闭状态!不要重复关闭." - group_manager.close_group_task(group_id, module) - else: - if not group_manager.get_plugin_status(module, group_id): - return f"功能 {cmd} 正处于关闭状态!不要重复关闭." - group_manager.block_plugin(module, group_id) - if help_path.exists(): - help_path.unlink() - if is_super: - for file in os.listdir(GROUP_HELP_PATH): - file = GROUP_HELP_PATH / file - file.unlink() - else: - if help_path.exists(): - help_path.unlink() - return f"{status} {cmd} 功能!" - - -def set_plugin_status(cmd: str, block_type: BLOCK_TYPE = "all"): - """ - 说明: - 设置插件功能状态(超级用户使用) - 参数: - :param cmd: 功能名称 - :param block_type: 限制类型, 'all': 全局, 'private': 私聊, 'group': 群聊 - """ - if block_type not in ["all", "private", "group"]: - raise TypeError("block_type类型错误, 可选值: ['all', 'private', 'group']") - status = cmd[:2] - cmd = cmd[2:] - module = plugins2settings_manager.get_plugin_module(cmd) - if status == "开启": - plugins_manager.unblock_plugin(module) - else: - plugins_manager.block_plugin(module, block_type=block_type) - for file in os.listdir(GROUP_HELP_PATH): - file = GROUP_HELP_PATH / file - file.unlink() - - -async def get_plugin_status(): - """ - 说明: - 获取功能状态 - """ - return await asyncio.get_event_loop().run_in_executor(None, _get_plugin_status) - - -def _get_plugin_status() -> MessageSegment: - """ - 说明: - 合成功能状态图片 - """ - rst = "\t功能\n" - flag_str = "状态".rjust(4) + "\n" - for matcher in get_matchers(True): - if module := matcher.plugin_name: - flag = plugins_manager.get_plugin_block_type(module) - flag = flag.upper() + " CLOSE" if flag else "OPEN" - try: - plugin_name = plugins_manager.get(module).plugin_name - if ( - "[Hidden]" in plugin_name - or "[Admin]" in plugin_name - or "[Superuser]" in plugin_name - ): - continue - rst += f"{plugin_name}" - except KeyError: - rst += f"{module}" - if plugins_manager.get(module).error: - rst += "[ERROR]" - rst += "\n" - flag_str += f"{flag}\n" - height = len(rst.split("\n")) * 24 - a = BuildImage(250, height, font_size=20) - a.text((10, 10), rst) - b = BuildImage(200, height, font_size=20) - b.text((10, 10), flag_str) - A = BuildImage(500, height) - A.paste(a) - A.paste(b, (270, 0)) - return image(b64=A.pic2bs4()) - - -async def update_member_info( - bot: Bot, group_id: int, remind_superuser: bool = False -) -> bool: - """ - 说明: - 更新群成员信息 - 参数: - :param group_id: 群号 - :param remind_superuser: 失败信息提醒超级用户 - """ - _group_user_list = await bot.get_group_member_list(group_id=group_id) - _error_member_list = [] - _exist_member_list = [] - # try: - admin_default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") - if admin_default_auth is not None: - for user_info in _group_user_list: - nickname = user_info["card"] or user_info["nickname"] - # 更新权限 - if user_info["role"] in [ - "owner", - "admin", - ] and not await LevelUser.is_group_flag( - user_info["user_id"], str(group_id) - ): - await LevelUser.set_level( - user_info["user_id"], - user_info["group_id"], - admin_default_auth, - ) - if str(user_info["user_id"]) in bot.config.superusers: - await LevelUser.set_level( - user_info["user_id"], user_info["group_id"], 9 - ) - user = await GroupInfoUser.get_or_none( - user_id=str(user_info["user_id"]), group_id=str(user_info["group_id"]) - ) - if user: - if user.user_name != nickname: - user.user_name = nickname - await user.save(update_fields=["user_name"]) - logger.debug( - f"更新群昵称成功", - "更新群组成员信息", - user_info["user_id"], - user_info["group_id"], - ) - _exist_member_list.append(str(user_info["user_id"])) - continue - join_time = datetime.strptime( - time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"]) - ), - "%Y-%m-%d %H:%M:%S", - ) - await GroupInfoUser.update_or_create( - user_id=str(user_info["user_id"]), - group_id=str(user_info["group_id"]), - defaults={ - "user_name": nickname, - "user_join_time": join_time.replace( - tzinfo=timezone(timedelta(hours=8)) - ), - }, - ) - _exist_member_list.append(str(user_info["user_id"])) - logger.debug("更新成功", "更新成员信息", user_info["user_id"], user_info["group_id"]) - _del_member_list = list( - set(_exist_member_list).difference( - set(await GroupInfoUser.get_group_member_id_list(group_id)) - ) - ) - if _del_member_list: - for del_user in _del_member_list: - await GroupInfoUser.filter( - user_id=str(del_user), group_id=str(group_id) - ).delete() - logger.info(f"删除已退群用户", "更新群组成员信息", del_user, group_id) - if _error_member_list and remind_superuser: - result = "" - for error_user in _error_member_list: - result += error_user - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=result[:-1] - ) - return True - - -def set_group_bot_status(group_id: str, status: bool) -> str: - """ - 说明: - 设置群聊bot开关状态 - 参数: - :param group_id: 群号 - :param status: 状态 - """ - if status: - if group_manager.check_group_bot_status(group_id): - return "我还醒着呢!" - group_manager.turn_on_group_bot_status(group_id) - return "呜..醒来了..." - else: - group_manager.shutdown_group_bot_status(group_id) - return "那我先睡觉了..." diff --git a/basic_plugins/admin_bot_manage/custom_welcome_message.py b/basic_plugins/admin_bot_manage/custom_welcome_message.py deleted file mode 100755 index 7f890bb4..00000000 --- a/basic_plugins/admin_bot_manage/custom_welcome_message.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from configs.config import Config -from services.log import logger -from utils.depends import ImageList, OneCommand - -from ._data_source import custom_group_welcome - -__zx_plugin_name__ = "自定义进群欢迎消息 [Admin]" -__plugin_usage__ = """ -usage: - 指令: - 自定义进群欢迎消息 ?[文本] ?[图片] - Note:可以通过[at]来确认是否艾特新成员 - 示例:自定义进群欢迎消息 欢迎新人![图片] - 示例:自定义进群欢迎消息 欢迎你[at] -""".strip() -__plugin_des__ = "简易的自定义群欢迎消息" -__plugin_cmd__ = ["自定义群欢迎消息 ?[文本] ?[图片]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config( - "admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL" - ), -} - -custom_welcome = on_command( - "自定义进群欢迎消息", - aliases={"自定义欢迎消息", "自定义群欢迎消息", "设置群欢迎消息"}, - permission=GROUP, - priority=5, - block=True, -) - - -@custom_welcome.handle() -async def _( - event: GroupMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - img: List[str] = ImageList(), -): - msg = arg.extract_plain_text().strip() - if not msg and not img: - await custom_welcome.finish(__plugin_usage__) - try: - await custom_welcome.send( - await custom_group_welcome( - msg, img, str(event.user_id), str(event.group_id) - ), - at_sender=True, - ) - logger.info(f"自定义群欢迎消息:{msg}", cmd, event.user_id, event.group_id) - except Exception as e: - logger.error( - f"自定义进群欢迎消息发生错误", cmd, event.user_id, getattr(event, "group_id", None), e=e - ) - await custom_welcome.send("发生了一些未知错误...") diff --git a/basic_plugins/admin_bot_manage/rule.py b/basic_plugins/admin_bot_manage/rule.py deleted file mode 100755 index a0e54efe..00000000 --- a/basic_plugins/admin_bot_manage/rule.py +++ /dev/null @@ -1,55 +0,0 @@ -import time - -from nonebot.adapters.onebot.v11 import Event - -from services.log import logger -from utils.manager import group_manager, plugins2settings_manager -from utils.utils import get_message_text - -cmd = [] - -v = time.time() - - -def switch_rule(event: Event) -> bool: - """ - 说明: - 检测文本是否是关闭功能命令 - 参数: - :param event: pass - """ - global cmd, v - try: - if not cmd or time.time() - v > 60 * 60: - cmd = ["关闭全部被动", "开启全部被动", "开启全部功能", "关闭全部功能"] - _data = group_manager.get_task_data() - for key in _data: - cmd.append(f"开启{_data[key]}") - cmd.append(f"关闭{_data[key]}") - cmd.append(f"开启被动{_data[key]}") - cmd.append(f"关闭被动{_data[key]}") - cmd.append(f"开启 {_data[key]}") - cmd.append(f"关闭 {_data[key]}") - _data = plugins2settings_manager.get_data() - for key in _data.keys(): - try: - if isinstance(_data[key].cmd, list): - for x in _data[key].cmd: - cmd.append(f"开启{x}") - cmd.append(f"关闭{x}") - cmd.append(f"开启 {x}") - cmd.append(f"关闭 {x}") - else: - cmd.append(f"开启{key}") - cmd.append(f"关闭{key}") - cmd.append(f"开启 {key}") - cmd.append(f"关闭 {key}") - except KeyError: - pass - v = time.time() - msg = get_message_text(event.json()).split() - msg = msg[0] if msg else "" - return msg in cmd - except Exception as e: - logger.error(f"检测是否为功能开关命令发生错误", e=e) - return False diff --git a/basic_plugins/admin_bot_manage/switch_rule.py b/basic_plugins/admin_bot_manage/switch_rule.py deleted file mode 100755 index c4ae3c3c..00000000 --- a/basic_plugins/admin_bot_manage/switch_rule.py +++ /dev/null @@ -1,144 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_command, on_message, on_regex -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from services.log import logger -from utils.message_builder import image -from utils.utils import get_message_text, is_number - -from ._data_source import ( - change_global_task_status, - change_group_switch, - get_plugin_status, - group_current_status, - set_group_bot_status, - set_plugin_status, -) -from .rule import switch_rule - -__zx_plugin_name__ = "群功能开关 [Admin]" - -__plugin_usage__ = """ -usage: - 群内功能与被动技能开关 - 指令: - 开启/关闭[功能] - 群被动状态 - 开启全部被动 - 关闭全部被动 - 醒来/休息吧 - 示例:开启/关闭色图 -""".strip() -__plugin_superuser_usage__ = """ -usage: - (私聊)功能总开关与指定群禁用 - 指令: - 功能状态 - 开启/关闭[功能] [group] - 开启/关闭[功能] ['private'/'group'] - 开启被动/关闭被动[被动名称] # 全局被动控制 -""".strip() -__plugin_des__ = "群内功能开关" -__plugin_cmd__ = [ - "开启/关闭[功能]", - "群被动状态", - "开启全部被动", - "关闭全部被动", - "醒来/休息吧", - "功能状态 [_superuser]", - "开启/关闭[功能] [group] [_superuser]", - "开启/关闭[功能] ['private'/'group'] [_superuser]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), - "cmd": ["开启功能", "关闭功能", "开关"], -} - -switch_rule_matcher = on_message(rule=switch_rule, priority=4, block=True) - -plugins_status = on_command("功能状态", permission=SUPERUSER, priority=5, block=True) - -group_task_status = on_command("群被动状态", permission=GROUP, priority=5, block=True) - -group_status = on_regex("^(休息吧|醒来)$", permission=GROUP, priority=5, block=True) - - -@switch_rule_matcher.handle() -async def _( - bot: Bot, - event: MessageEvent, -): - msg = get_message_text(event.message).strip() - msg_split = msg.split() - _cmd = msg_split[0] - if isinstance(event, GroupMessageEvent): - await switch_rule_matcher.send(await change_group_switch(_cmd, event.group_id)) - logger.info(f"使用群功能管理命令 {_cmd}", "功能管理", event.user_id, event.group_id) - else: - if str(event.user_id) in bot.config.superusers: - block_type = " ".join(msg_split[1:]) - block_type = block_type if block_type else "a" - if ("关闭被动" in _cmd or "开启被动" in _cmd) and isinstance( - event, PrivateMessageEvent - ): - await switch_rule_matcher.send(change_global_task_status(_cmd)) - elif is_number(block_type): - if not int(block_type) in [ - g["group_id"] for g in await bot.get_group_list() - ]: - await switch_rule_matcher.finish(f"{NICKNAME}未加入群聊:{block_type}") - await change_group_switch(_cmd, int(block_type), True) - group_name = (await bot.get_group_info(group_id=int(block_type)))[ - "group_name" - ] - await switch_rule_matcher.send( - f"已{_cmd[:2]}群聊 {group_name}({block_type}) 的 {_cmd[2:]} 功能" - ) - elif block_type in ["all", "private", "group", "a", "p", "g"]: - block_type = "all" if block_type == "a" else block_type - block_type = "private" if block_type == "p" else block_type - block_type = "group" if block_type == "g" else block_type - set_plugin_status(_cmd, block_type) # type: ignore - if block_type == "all": - await switch_rule_matcher.send(f"已{_cmd[:2]}功能:{_cmd[2:]}") - elif block_type == "private": - await switch_rule_matcher.send(f"已在私聊中{_cmd[:2]}功能:{_cmd[2:]}") - else: - await switch_rule_matcher.send(f"已在群聊中{_cmd[:2]}功能:{_cmd[2:]}") - else: - await switch_rule_matcher.finish("格式错误:关闭[功能] [group]/[p/g]") - logger.info(f"使用功能管理命令 {_cmd} | {block_type}", f"{_cmd}", event.user_id) - - -@plugins_status.handle() -async def _(): - await plugins_status.send(await get_plugin_status()) - - -@group_task_status.handle() -async def _(event: GroupMessageEvent): - await group_task_status.send(image(b64=await group_current_status(str(event.group_id)))) - - -@group_status.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - cmd = reg_group[0] - if cmd == "休息吧": - msg = set_group_bot_status(str(event.group_id), False) - else: - msg = set_group_bot_status(str(event.group_id), True) - await group_status.send(msg) - logger.info(f"使用总开关命令: {cmd}", cmd, event.user_id, event.group_id) diff --git a/basic_plugins/admin_bot_manage/timing_task.py b/basic_plugins/admin_bot_manage/timing_task.py deleted file mode 100755 index fcae1e74..00000000 --- a/basic_plugins/admin_bot_manage/timing_task.py +++ /dev/null @@ -1,48 +0,0 @@ -from nonebot import get_bots - -from services.log import logger -from utils.utils import scheduler - -from ._data_source import update_member_info - -__zx_plugin_name__ = "管理方面定时任务 [Hidden]" -__plugin_usage__ = "无" -__plugin_des__ = "成员信息和管理权限的定时更新" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -async def update(): - bot_list = get_bots() - if bot_list: - used_group = [] - for key in bot_list: - bot = bot_list[key] - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl if g["group_id"] not in used_group] - for g in gl: - used_group.append(g) - try: - await update_member_info(bot, g) # type: ignore - logger.debug(f"更新群组成员信息成功", "自动更新群组成员信息", group_id=g) - except Exception as e: - logger.error(f"更新群组成员信息错误", "自动更新群组成员信息", group_id=g, e=e) - - -# 自动更新群员信息 -@scheduler.scheduled_job( - "cron", - hour=2, - minute=1, -) -async def _(): - await update() - - -# 快速更新群员信息以及管理员权限 -@scheduler.scheduled_job( - "interval", - minutes=5, -) -async def _(): - await update() diff --git a/basic_plugins/admin_bot_manage/update_group_member_info.py b/basic_plugins/admin_bot_manage/update_group_member_info.py deleted file mode 100755 index 6a59f418..00000000 --- a/basic_plugins/admin_bot_manage/update_group_member_info.py +++ /dev/null @@ -1,51 +0,0 @@ -from nonebot import on_command, on_notice -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupIncreaseNoticeEvent, - GroupMessageEvent, -) - -from services.log import logger - -from ._data_source import update_member_info - -__zx_plugin_name__ = "更新群组成员列表 [Admin]" -__plugin_usage__ = """ -usage: - 更新群组成员的基本信息 - 指令: - 更新群组成员列表/更新群组成员信息 -""".strip() -__plugin_des__ = "更新群组成员列表" -__plugin_cmd__ = ["更新群组成员列表"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": 1, -} - - -refresh_member_group = on_command( - "更新群组成员列表", aliases={"更新群组成员信息"}, permission=GROUP, priority=5, block=True -) - - -@refresh_member_group.handle() -async def _(bot: Bot, event: GroupMessageEvent): - if await update_member_info(bot, event.group_id): - await refresh_member_group.send("更新群员信息成功!", at_sender=True) - logger.info("更新群员信息成功!", "更新群组成员列表", event.user_id, event.group_id) - else: - await refresh_member_group.send("更新群员信息失败!", at_sender=True) - logger.info("更新群员信息失败!", "更新群组成员列表", event.user_id, event.group_id) - - -group_increase_handle = on_notice(priority=1, block=False) - - -@group_increase_handle.handle() -async def _(bot: Bot, event: GroupIncreaseNoticeEvent): - if str(event.user_id) == bot.self_id: - await update_member_info(bot, event.group_id) - logger.info("{NICKNAME}加入群聊更新群组信息", "更新群组成员列表", event.user_id, event.group_id) diff --git a/basic_plugins/admin_help/__init__.py b/basic_plugins/admin_help/__init__.py deleted file mode 100755 index 938aaab2..00000000 --- a/basic_plugins/admin_help/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from nonebot import on_command -from nonebot.typing import T_State -from nonebot.adapters import Bot -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from utils.message_builder import image -from .data_source import create_help_image, ADMIN_HELP_IMAGE - - -__zx_plugin_name__ = '管理帮助 [Admin]' -__plugin_usage__ = '管理员帮助,在群内回复“管理员帮助”' -__plugin_version__ = 0.1 -__plugin_author__ = 'HibiKier' -__plugin_settings__ = { - "admin_level": 1, -} - -admin_help = on_command("管理员帮助", aliases={"管理帮助"}, priority=5, block=True) - -if ADMIN_HELP_IMAGE.exists(): - ADMIN_HELP_IMAGE.unlink() - - -@admin_help.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - if not ADMIN_HELP_IMAGE.exists(): - await create_help_image() - await admin_help.send(image(ADMIN_HELP_IMAGE)) diff --git a/basic_plugins/admin_help/data_source.py b/basic_plugins/admin_help/data_source.py deleted file mode 100755 index 7474f44d..00000000 --- a/basic_plugins/admin_help/data_source.py +++ /dev/null @@ -1,81 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_template import help_template -from utils.image_utils import BuildImage, build_sort_image, group_image, text2image -from utils.manager import group_manager, plugin_data_manager -from utils.manager.models import PluginType - -driver: Driver = nonebot.get_driver() - - -ADMIN_HELP_IMAGE = IMAGE_PATH / "admin_help_img.png" - - -@driver.on_bot_connect -async def init_task(): - if not group_manager.get_task_data(): - group_manager.load_task() - logger.info(f"已成功加载 {len(group_manager.get_task_data())} 个被动技能.") - - -async def create_help_image(): - """ - 创建管理员帮助图片 - """ - if ADMIN_HELP_IMAGE.exists(): - return - plugin_data_ = plugin_data_manager.get_data() - image_list = [] - task_list = [] - for plugin_data in [plugin_data_[x] for x in plugin_data_]: - try: - usage = None - if plugin_data.plugin_type == PluginType.ADMIN and plugin_data.usage: - usage = await text2image( - plugin_data.usage, padding=5, color=(204, 196, 151) - ) - if usage: - await usage.acircle_corner() - level = 5 - if plugin_data.plugin_setting: - level = plugin_data.plugin_setting.level or level - image = await help_template(plugin_data.name + f"[{level}]", usage) - image_list.append(image) - if plugin_data.task: - for x in plugin_data.task.keys(): - task_list.append(plugin_data.task[x]) - except Exception as e: - logger.warning( - f"获取群管理员插件 {plugin_data.model}: {plugin_data.name} 设置失败...", - "管理员帮助", - e=e, - ) - task_str = "\n".join(task_list) - task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str - task_image = await text2image(task_str, padding=5, color=(204, 196, 151)) - task_image = await help_template("被动任务", task_image) - image_list.append(task_image) - image_group, _ = group_image(image_list) - A = await build_sort_image(image_group, color="#f9f6f2", padding_top=180) - await A.apaste( - BuildImage(0, 0, font="CJGaoDeGuo.otf", plain_text="群管理员帮助", font_size=50), - (50, 30), - True, - ) - await A.apaste( - BuildImage( - 0, - 0, - font="CJGaoDeGuo.otf", - plain_text="注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", - font_size=30, - font_color="red", - ), - (50, 90), - True, - ) - await A.asave(ADMIN_HELP_IMAGE) - logger.info(f"已成功加载 {len(image_list)} 条管理员命令") diff --git a/basic_plugins/apscheduler/__init__.py b/basic_plugins/apscheduler/__init__.py deleted file mode 100755 index de0a1962..00000000 --- a/basic_plugins/apscheduler/__init__.py +++ /dev/null @@ -1,250 +0,0 @@ -import shutil -from pathlib import Path -from typing import List - -import nonebot -from nonebot import get_bots, on_message - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger -from utils.message_builder import image -from utils.utils import broadcast_group, scheduler - -__zx_plugin_name__ = "定时任务相关 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"zwa": "早晚安"} - - -Config.add_plugin_config( - "_task", "DEFAULT_ZWA", True, help_="被动 早晚安 进群默认开关状态", default_value=True, type=bool -) - -Config.add_plugin_config( - "_backup", "BACKUP_FLAG", True, help_="是否开启文件备份", default_value=True, type=bool -) - -Config.add_plugin_config( - "_backup", - "BACKUP_DIR_OR_FILE", - [ - "data/black_word", - "data/configs", - "data/statistics", - "data/word_bank", - "data/manager", - "configs", - ], - name="文件备份", - help_="备份的文件夹或文件", - default_value=[], - type=List[str], -) - - -cx = on_message(priority=9999, block=False, rule=lambda: False) - - -# 早上好 -@scheduler.scheduled_job( - "cron", - hour=6, - minute=1, -) -async def _(): - img = image(IMAGE_PATH / "zhenxun" / "zao.jpg") - await broadcast_group("[[_task|zwa]]早上好" + img, log_cmd="被动早晚安") - logger.info("每日早安发送...") - - -# 睡觉了 -@scheduler.scheduled_job( - "cron", - hour=23, - minute=59, -) -async def _(): - img = image(IMAGE_PATH / "zhenxun" / "sleep.jpg") - await broadcast_group( - f"[[_task|zwa]]{NICKNAME}要睡觉了,你们也要早点睡呀" + img, log_cmd="被动早晚安" - ) - logger.info("每日晚安发送...") - - -# 自动更新群组信息 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - bots = nonebot.get_bots() - _used_group = [] - for bot in bots.values(): - try: - group_list = await bot.get_group_list() - gl = [g["group_id"] for g in group_list if g["group_id"] not in _used_group] - for g in gl: - _used_group.append(g) - group_info = await bot.get_group_info(group_id=g) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - logger.debug("自动更新群组信息成功", "自动更新群组", group_id=g) - except Exception as e: - logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) - logger.info("自动更新群组成员信息成功...") - - -# 自动更新好友信息 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - bots = nonebot.get_bots() - for key in bots: - try: - bot = bots[key] - fl = await bot.get_friend_list() - for f in fl: - if FriendUser.exists(user_id=str(f["user_id"])): - await FriendUser.create( - user_id=str(f["user_id"]), user_name=f["nickname"] - ) - logger.debug(f"更新好友信息成功", "自动更新好友", f["user_id"]) - else: - logger.debug(f"好友信息已存在", "自动更新好友", f["user_id"]) - except Exception as e: - logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) - logger.info("自动更新好友信息成功...") - - -# 自动备份 -@scheduler.scheduled_job( - "cron", - hour=3, - minute=25, -) -async def _(): - if Config.get_config("_backup", "BACKUP_FLAG"): - _backup_path = Path() / "backup" - _backup_path.mkdir(exist_ok=True, parents=True) - if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"): - for path_file in backup_dir_or_file: - try: - path = Path(path_file) - _p = _backup_path / path_file - if path.exists(): - if path.is_dir(): - if _p.exists(): - shutil.rmtree(_p, ignore_errors=True) - shutil.copytree(path_file, _p) - else: - if _p.exists(): - _p.unlink() - shutil.copy(path_file, _p) - logger.debug(f"已完成自动备份:{path_file}", "自动备份") - except Exception as e: - logger.error(f"自动备份文件 {path_file} 发生错误", "自动备份", e=e) - logger.info("自动备份成功...", "自动备份") - - # 一次性任务 - - -# 固定时间触发,仅触发一次: -# -# from datetime import datetime -# -# @nonebot.scheduler.scheduled_job( -# 'date', -# run_date=datetime(2021, 1, 1, 0, 0), -# # timezone=None, -# ) -# async def _(): -# await bot.send_group_msg(group_id=123456, -# message="2021,新年快乐!") - -# 定期任务 -# 从 start_date 开始到 end_date 结束,根据类似 Cron -# -# 的规则触发任务: -# -# @nonebot.scheduler.scheduled_job( -# 'cron', -# # year=None, -# # month=None, -# # day=None, -# # week=None, -# day_of_week="mon,tue,wed,thu,fri", -# hour=7, -# # minute=None, -# # second=None, -# # start_date=None, -# # end_date=None, -# # timezone=None, -# ) -# async def _(): -# await bot.send_group_msg(group_id=123456, -# message="起床啦!") - -# 间隔任务 -# -# interval 触发器 -# -# 从 start_date 开始,每间隔一段时间触发,到 end_date 结束: -# -# @nonebot.scheduler.scheduled_job( -# 'interval', -# # weeks=0, -# # days=0, -# # hours=0, -# minutes=5, -# # seconds=0, -# # start_date=time.now(), -# # end_date=None, -# ) -# async def _(): -# has_new_item = check_new_item() -# if has_new_item: -# await bot.send_group_msg(group_id=123456, -# message="XX有更新啦!") - - -# 动态的计划任务 -# import datetime -# -# from apscheduler.triggers.date import DateTrigger # 一次性触发器 -# # from apscheduler.triggers.cron import CronTrigger # 定期触发器 -# # from apscheduler.triggers.interval import IntervalTrigger # 间隔触发器 -# from nonebot import on_command, scheduler -# -# @on_command('赖床') -# async def _(session: CommandSession): -# await session.send('我会在5分钟后再喊你') -# -# # 制作一个“5分钟后”触发器 -# delta = datetime.timedelta(minutes=5) -# trigger = DateTrigger( -# run_date=datetime.datetime.now() + delta -# ) -# -# # 添加任务 -# scheduler.add_job( -# func=session.send, # 要添加任务的函数,不要带参数 -# trigger=trigger, # 触发器 -# args=('不要再赖床啦!',), # 函数的参数列表,注意:只有一个值时,不能省略末尾的逗号 -# # kwargs=None, -# misfire_grace_time=60, # 允许的误差时间,建议不要省略 -# # jobstore='default', # 任务储存库,在下一小节中说明 -# ) diff --git a/basic_plugins/ban/__init__.py b/basic_plugins/ban/__init__.py deleted file mode 100755 index ead19ac2..00000000 --- a/basic_plugins/ban/__init__.py +++ /dev/null @@ -1,179 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from models.ban_user import BanUser -from models.level_user import LevelUser -from services.log import logger -from utils.depends import AtList, OneCommand -from utils.utils import is_number - -from .data_source import a_ban, parse_ban_time - -__zx_plugin_name__ = "封禁Ban用户 [Admin]" -__plugin_usage__ = """ -usage: - 将用户拉入或拉出黑名单 - 指令: - .ban [at] ?[小时] ?[分钟] - .unban - 示例:.ban @user - 示例:.ban @user 6 - 示例:.ban @user 3 10 - 示例:.unban @user -""".strip() -__plugin_superuser_usage__ = """ -usage: - b了=屏蔽用户消息,相当于最上级.ban - 跨群ban以及跨群b了 - 指令: - b了 [at/qq] - .ban [user_id] ?[小时] ?[分钟] - 示例:b了 @user - 示例:b了 1234567 - 示例:.ban 12345567 -""".strip() -__plugin_des__ = "你被逮捕了!丢进小黑屋!" -__plugin_cmd__ = [".ban [at] ?[小时] ?[分钟]", ".unban [at]", "b了 [at] [_superuser]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("ban", "BAN_LEVEL"), - "cmd": [".ban", ".unban", "ban", "unban"], -} -__plugin_configs__ = { - "BAN_LEVEL [LEVEL]": { - "value": 5, - "help": "ban/unban所需要的管理员权限等级", - "default_value": 5, - "type": int, - } -} - - -ban = on_command( - ".ban", - aliases={".unban", "/ban", "/unban"}, - priority=5, - block=True, -) - -super_ban = on_command("b了", permission=SUPERUSER, priority=5, block=True) - - -@ban.handle() -async def _( - bot: Bot, - event: GroupMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - result = "" - if at_list: - qq = at_list[0] - user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = user["card"] or user["nickname"] - msg = arg.extract_plain_text().strip() - time = parse_ban_time(msg) - if isinstance(time, str): - await ban.finish(time, at_sender=True) - user_level = await LevelUser.get_user_level(event.user_id, event.group_id) - is_not_superuser = str(event.user_id) not in bot.config.superusers - if cmd in [".ban", "/ban"]: - at_user_level = await LevelUser.get_user_level(qq, event.group_id) - if user_level <= at_user_level and is_not_superuser: - await ban.finish( - f"您的权限等级比对方低或相等, {NICKNAME}不能为您使用此功能!", - at_sender=True, - ) - logger.info(f"用户封禁 时长: {time}", cmd, event.user_id, event.group_id, qq) - result = await a_ban(qq, time, user_name, event) - else: - if await BanUser.check_ban_level(qq, user_level) and is_not_superuser: - await ban.finish( - f"ban掉 {user_name} 的管理员权限比您高,无法进行unban", at_sender=True - ) - if await BanUser.unban(qq): - logger.info(f"解除用户封禁", cmd, event.user_id, event.group_id, qq) - result = f"已经将 {user_name} 从黑名单中删除了!" - else: - result = f"{user_name} 不在黑名单!" - else: - await ban.finish("艾特人了吗??", at_sender=True) - await ban.send(result, at_sender=True) - - -@ban.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - if msg and str(event.user_id) in bot.config.superusers: - msg_split = msg.split() - if msg_split and is_number(msg_split[0]): - qq = int(msg_split[0]) - param = msg_split[1:] - if cmd in [".ban", "/ban"]: - time = parse_ban_time(" ".join(param)) - if isinstance(time, str): - logger.info(time, cmd, event.user_id, target=qq) - await ban.finish(time) - result = await a_ban(qq, time, str(qq), event, 9) - else: - if await BanUser.unban(qq): - result = f"已经把 {qq} 从黑名单中删除了!" - else: - result = f"{qq} 不在黑名单!" - await ban.send(result) - logger.info(result, cmd, event.user_id, target=qq) - else: - await ban.send("参数不正确!\n格式:.ban [qq] [hour]? [minute]?", at_sender=True) - - -@super_ban.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - user_name = "" - qq = None - if isinstance(event, GroupMessageEvent): - if at_list: - qq = at_list[0] - user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = user["card"] or user["nickname"] - else: - msg = arg.extract_plain_text().strip() - if not is_number(msg): - await super_ban.finish("对象qq必须为纯数字...") - qq = int(msg) - user_name = msg - if qq: - await BanUser.ban(qq, 10, 99999999) - await ban.send(f"已将 {user_name} 拉入黑名单!") - logger.info( - f"已将 {user_name} 拉入黑名单!", - cmd, - event.user_id, - event.group_id if isinstance(event, GroupMessageEvent) else None, - qq, - ) - else: - await super_ban.send("需要提供被super ban的对象,可以使用at或者指定qq...") diff --git a/basic_plugins/ban/data_source.py b/basic_plugins/ban/data_source.py deleted file mode 100644 index b25a29c4..00000000 --- a/basic_plugins/ban/data_source.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Optional, Union - -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.level_user import LevelUser -from services.log import logger -from utils.utils import is_number - - -def parse_ban_time(msg: str) -> Union[int, str]: - """ - 解析ban时长 - :param msg: 文本消息 - """ - try: - if not msg: - return -1 - msg_split = msg.split() - if len(msg_split) == 1: - if not is_number(msg_split[0].strip()): - return "参数必须是数字!" - return int(msg_split[0]) * 60 * 60 - else: - if not is_number(msg_split[0].strip()) or not is_number( - msg_split[1].strip() - ): - return "参数必须是数字!" - return int(msg_split[0]) * 60 * 60 + int(msg_split[1]) * 60 - except ValueError as e: - logger.error("解析ban时长错误", ".ban", e=e) - return "时间解析错误!" - - -async def a_ban( - qq: int, - time: int, - user_name: str, - event: MessageEvent, - ban_level: Optional[int] = None, -) -> str: - """ - ban - :param qq: qq - :param time: ban时长 - :param user_name: ban用户昵称 - :param event: event - :param ban_level: ban级别 - """ - group_id = None - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - ban_level = await LevelUser.get_user_level(event.user_id, event.group_id) - if not ban_level: - return "未查询到ban级用户权限" - if await BanUser.ban(qq, ban_level, time): - logger.info( - f"封禁 时长 {time / 60} 分钟", ".ban", event.user_id, group_id, qq - ) - result = f"已经将 {user_name} 加入{NICKNAME}的黑名单了!" - if time != -1: - result += f"将在 {time / 60} 分钟后解封" - else: - result += f"将在 ∞ 分钟后解封" - else: - ban_time = await BanUser.check_ban_time(qq) - if isinstance(ban_time, int): - ban_time = abs(float(ban_time)) - if ban_time < 60: - ban_time = str(ban_time) + " 秒" - else: - ban_time = str(int(ban_time / 60)) + " 分钟" - else: - ban_time += " 分钟" - result = f"{user_name} 已在黑名单!预计 {ban_time}后解封" - return result diff --git a/basic_plugins/broadcast/__init__.py b/basic_plugins/broadcast/__init__.py deleted file mode 100755 index 4f8cb9cb..00000000 --- a/basic_plugins/broadcast/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.config import Config -from services.log import logger -from utils.depends import ImageList -from utils.manager import group_manager -from utils.message_builder import image - -__zx_plugin_name__ = "广播 [Superuser]" -__plugin_usage__ = """ -usage: - 指令: - 广播- ?[消息] ?[图片] - 示例:广播- 你们好! -""".strip() -__plugin_des__ = "昭告天下!" -__plugin_cmd__ = ["广播-"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"broadcast": "广播"} -Config.add_plugin_config( - "_task", - "DEFAULT_BROADCAST", - True, - help_="被动 广播 进群默认开关状态", - default_value=True, - type=bool, -) - -broadcast = on_command("广播-", priority=1, permission=SUPERUSER, block=True) - - -@broadcast.handle() -async def _( - bot: Bot, - event: MessageEvent, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - msg = arg.extract_plain_text().strip() - rst = "" - for img in img_list: - rst += image(img) - gl = [ - g["group_id"] - for g in await bot.get_group_list() - if group_manager.check_group_task_status(str(g["group_id"]), "broadcast") - ] - g_cnt = len(gl) - cnt = 0 - error = "" - x = 0.25 - for g in gl: - cnt += 1 - if cnt / g_cnt > x: - await broadcast.send(f"已播报至 {int(cnt / g_cnt * 100)}% 的群聊") - x += 0.25 - try: - await bot.send_group_msg(group_id=g, message=msg + rst) - logger.info(f"投递广播成功", "广播", group_id=g) - except Exception as e: - logger.error(f"投递广播失败", "广播", group_id=g, e=e) - error += f"GROUP {g} 投递广播失败:{type(e)}\n" - await asyncio.sleep(0.5) - await broadcast.send(f"已播报至 100% 的群聊") - if error: - await broadcast.send(f"播报时错误:{error}") diff --git a/basic_plugins/chat_history/_rule.py b/basic_plugins/chat_history/_rule.py deleted file mode 100644 index f794a177..00000000 --- a/basic_plugins/chat_history/_rule.py +++ /dev/null @@ -1,9 +0,0 @@ -from nonebot.adapters.onebot.v11 import Event, MessageEvent - -from configs.config import Config - - -def rule(event: Event) -> bool: - return bool( - Config.get_config("chat_history", "FLAG") and isinstance(event, MessageEvent) - ) diff --git a/basic_plugins/chat_history/chat_message.py b/basic_plugins/chat_history/chat_message.py deleted file mode 100644 index 2cefb2d7..00000000 --- a/basic_plugins/chat_history/chat_message.py +++ /dev/null @@ -1,72 +0,0 @@ -from nonebot import on_message -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent - -from configs.config import Config -from models.chat_history import ChatHistory -from services.log import logger -from utils.depends import PlaintText -from utils.utils import scheduler - -from ._rule import rule - -__zx_plugin_name__ = "消息存储 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -Config.add_plugin_config( - "chat_history", - "FLAG", - True, - help_="是否开启消息自从存储", - name="消息存储", - default_value=True, - type=bool, -) - - -chat_history = on_message(rule=rule, priority=1, block=False) - - -TEMP_LIST = [] - - -@chat_history.handle() -async def _(bot: Bot, event: MessageEvent, msg: str = PlaintText()): - group_id = None - if isinstance(event, GroupMessageEvent): - group_id = str(event.group_id) - TEMP_LIST.append( - ChatHistory( - user_id=str(event.user_id), - group_id=group_id, - text=str(event.get_message()), - plain_text=msg, - bot_id=str(bot.self_id), - ) - ) - - -@scheduler.scheduled_job( - "interval", - minutes=1, -) -async def _(): - try: - message_list = TEMP_LIST.copy() - TEMP_LIST.clear() - if message_list: - await ChatHistory.bulk_create(message_list) - logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务") - except Exception as e: - logger.error(f"定时批量添加聊天记录", "定时任务", e=e) - - -# @test.handle() -# async def _(event: MessageEvent): -# print(await ChatHistory.get_user_msg(event.user_id, "private")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) -# print(await ChatHistory.get_user_msg(event.user_id, "group")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) -# print(await ChatHistory.get_group_msg(event.group_id)) -# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/chat_history/chat_message_handle.py b/basic_plugins/chat_history/chat_message_handle.py deleted file mode 100644 index 99d2f40a..00000000 --- a/basic_plugins/chat_history/chat_message_handle.py +++ /dev/null @@ -1,113 +0,0 @@ -from datetime import datetime, timedelta -from typing import Any, Tuple - -import pytz -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.params import RegexGroup - -from models.chat_history import ChatHistory -from models.group_member_info import GroupInfoUser -from utils.image_utils import BuildImage, text2image -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "消息统计" -__plugin_usage__ = """ -usage: - 发言记录统计 - regex:(周|月|日)?消息排行(des|DES)?(n=[0-9]{1,2})? - 指令: - 消息统计?(des)?(n=?) - 周消息统计?(des)?(n=?) - 月消息统计?(des)?(n=?) - 日消息统计?(des)?(n=?) - 示例: - 消息统计 - 消息统计des - 消息统计DESn=15 - 消息统计n=15 -""".strip() -__plugin_des__ = "发言消息排行" -__plugin_cmd__ = ["消息统计", "周消息统计", "月消息统计", "日消息统计"] -__plugin_type__ = ("数据统计", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "cmd": ["消息统计"], -} - - -msg_handler = on_regex( - r"^(周|月|日)?消息统计(des|DES)?(n=[0-9]{1,2})?$", priority=5, block=True -) - - -@msg_handler.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - gid = event.group_id - date_scope = None - date, order, num = reg_group - num = num.split("=")[-1] if num else 10 - if num and is_number(num) and 10 < int(num) < 50: - num = int(num) - time_now = datetime.now() - zero_today = time_now - timedelta( - hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second - ) - if date in ["日"]: - date_scope = (zero_today, time_now) - elif date in ["周"]: - date_scope = (time_now - timedelta(days=7), time_now) - elif date in ["月"]: - date_scope = (time_now - timedelta(days=30), time_now) - if rank_data := await ChatHistory.get_group_msg_rank( - gid, num, order or "DESC", date_scope - ): - name = "昵称:\n\n" - num_str = "发言次数:\n\n" - idx = 1 - for uid, num in rank_data: - if user := await GroupInfoUser.filter(user_id=uid, group_id=gid).first(): - user_name = user.user_name - else: - user_name = uid - name += f"\t{idx}.{user_name} \n\n" - num_str += f"\t{num}\n\n" - idx += 1 - name_img = await text2image(name.strip(), padding=10, color="#f9f6f2") - num_img = await text2image(num_str.strip(), padding=10, color="#f9f6f2") - if not date_scope: - if date_scope := await ChatHistory.get_group_first_msg_datetime(gid): - date_scope = date_scope.astimezone( - pytz.timezone("Asia/Shanghai") - ).replace(microsecond=0) - else: - date_scope = time_now.replace(microsecond=0) - date_str = f"日期:{date_scope} - 至今" - else: - date_str = f"日期:{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" - date_w = BuildImage(0, 0, font_size=15).getsize(date_str)[0] - img_w = date_w if date_w > name_img.w + num_img.w else name_img.w + num_img.w - A = BuildImage( - img_w + 15, - num_img.h + 30, - color="#f9f6f2", - font="CJGaoDeGuo.otf", - font_size=15, - ) - await A.atext((10, 10), date_str) - await A.apaste(name_img, (0, 30)) - await A.apaste(num_img, (name_img.w, 30)) - await msg_handler.send(image(b64=A.pic2bs4())) - - -# @test.handle() -# async def _(event: MessageEvent): -# print(await ChatHistory.get_user_msg(event.user_id, "private")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) -# print(await ChatHistory.get_user_msg(event.user_id, "group")) -# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) -# print(await ChatHistory.get_group_msg(event.group_id)) -# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/basic_plugins/group_handle/__init__.py b/basic_plugins/group_handle/__init__.py deleted file mode 100755 index fbc912f8..00000000 --- a/basic_plugins/group_handle/__init__.py +++ /dev/null @@ -1,236 +0,0 @@ -import os -import random -from datetime import datetime -from pathlib import Path - -import ujson as json -from nonebot import on_notice, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupDecreaseNoticeEvent, - GroupIncreaseNoticeEvent, -) - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_info import GroupInfo -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger -from utils.depends import GetConfig -from utils.manager import group_manager, plugins2settings_manager, requests_manager -from utils.message_builder import image -from utils.utils import FreqLimiter - -__zx_plugin_name__ = "群事件处理 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"group_welcome": "进群欢迎", "refund_group_remind": "退群提醒"} -Config.add_plugin_config( - "invite_manager", "message", f"请不要未经同意就拉{NICKNAME}入群!告辞!", help_="强制拉群后进群回复的内容.." -) -Config.add_plugin_config( - "invite_manager", "flag", True, help_="被强制拉群后是否直接退出", default_value=True, type=bool -) -Config.add_plugin_config( - "invite_manager", "welcome_msg_cd", 5, help_="群欢迎消息cd", default_value=5, type=int -) -Config.add_plugin_config( - "_task", - "DEFAULT_GROUP_WELCOME", - True, - help_="被动 进群欢迎 进群默认开关状态", - default_value=True, - type=bool, -) -Config.add_plugin_config( - "_task", - "DEFAULT_REFUND_GROUP_REMIND", - True, - help_="被动 退群提醒 进群默认开关状态", - default_value=True, - type=bool, -) - - -_flmt = FreqLimiter(Config.get_config("invite_manager", "welcome_msg_cd") or 5) - - -# 群员增加处理 -group_increase_handle = on_notice(priority=1, block=False) -# 群员减少处理 -group_decrease_handle = on_notice(priority=1, block=False) -# (群管理)加群同意请求 -add_group = on_request(priority=1, block=False) - - -@group_increase_handle.handle() -async def _(bot: Bot, event: GroupIncreaseNoticeEvent): - if event.user_id == int(bot.self_id): - group = await GroupInfo.get_or_none(group_id=str(event.group_id)) - # 群聊不存在或被强制拉群,退出该群 - if (not group or group.group_flag == 0) and Config.get_config( - "invite_manager", "flag" - ): - try: - msg = Config.get_config("invite_manager", "message") - if msg: - await bot.send_group_msg(group_id=event.group_id, message=msg) - await bot.set_group_leave(group_id=event.group_id) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"触发强制入群保护,已成功退出群聊 {event.group_id}...", - ) - logger.info(f"强制拉群或未有群信息,退出群聊成功", "入群检测", group_id=event.group_id) - requests_manager.remove_request("group", event.group_id) - except Exception as e: - logger.info(f"强制拉群或未有群信息,退出群聊失败", "入群检测", group_id=event.group_id, e=e) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", - ) - # 默认群功能开关 - elif event.group_id not in group_manager.get_data().group_manager.keys(): - data = plugins2settings_manager.get_data() - for plugin in data.keys(): - if not data[plugin].default_status: - group_manager.block_plugin(plugin, str(event.group_id)) - admin_default_auth = Config.get_config( - "admin_bot_manage", "ADMIN_DEFAULT_AUTH" - ) - # 即刻刷新权限 - for user_info in await bot.get_group_member_list(group_id=event.group_id): - if ( - user_info["role"] - in [ - "owner", - "admin", - ] - and not await LevelUser.is_group_flag( - user_info["user_id"], event.group_id - ) - and admin_default_auth is not None - ): - await LevelUser.set_level( - user_info["user_id"], - user_info["group_id"], - admin_default_auth, - ) - logger.debug( - f"添加默认群管理员权限: {admin_default_auth}", - "入群检测", - user_info["user_id"], - user_info["group_id"], - ) - if str(user_info["user_id"]) in bot.config.superusers: - await LevelUser.set_level( - user_info["user_id"], user_info["group_id"], 9 - ) - logger.debug( - f"添加超级用户权限: 9", - "入群检测", - user_info["user_id"], - user_info["group_id"], - ) - else: - join_time = datetime.now() - user_info = await bot.get_group_member_info( - group_id=event.group_id, user_id=event.user_id - ) - await GroupInfoUser.update_or_create( - user_id=str(user_info["user_id"]), - group_id=str(user_info["group_id"]), - defaults={"user_name": user_info["nickname"], "user_join_time": join_time}, - ) - logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") - - # 群欢迎消息 - if _flmt.check(event.group_id): - _flmt.start_cd(event.group_id) - msg = "" - img = "" - at_flag = False - custom_welcome_msg_json = ( - Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" - ) - if custom_welcome_msg_json.exists(): - data = json.load(open(custom_welcome_msg_json, "r")) - if data.get(str(event.group_id)): - msg = data[str(event.group_id)] - if "[at]" in msg: - msg = msg.replace("[at]", "") - at_flag = True - if (DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg").exists(): - img = image(DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg") - if msg or img: - msg = msg.strip() + img - msg = "\n" + msg if at_flag else msg - await group_increase_handle.send( - "[[_task|group_welcome]]" + msg, at_sender=at_flag - ) - else: - await group_increase_handle.send( - "[[_task|group_welcome]]新人快跑啊!!本群现状↓(快使用自定义!)" - + image( - IMAGE_PATH - / "qxz" - / random.choice(os.listdir(IMAGE_PATH / "qxz")) - ) - ) - - -@group_decrease_handle.handle() -async def _(bot: Bot, event: GroupDecreaseNoticeEvent): - # 被踢出群 - if event.sub_type == "kick_me": - group_id = event.group_id - operator_id = event.operator_id - if user := await GroupInfoUser.get_or_none( - user_id=str(event.operator_id), group_id=str(event.group_id) - ): - operator_name = user.user_name - else: - operator_name = "None" - group = await GroupInfo.filter(group_id=str(group_id)).first() - group_name = group.group_name if group else "" - coffee = int(list(bot.config.superusers)[0]) - await bot.send_private_msg( - user_id=coffee, - message=f"****呜..一份踢出报告****\n" - f"我被 {operator_name}({operator_id})\n" - f"踢出了 {group_name}({group_id})\n" - f"日期:{str(datetime.now()).split('.')[0]}", - ) - return - if event.user_id == int(bot.self_id): - group_manager.delete_group(event.group_id) - return - if user := await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ): - user_name = user.user_name - else: - user_name = f"{event.user_id}" - await GroupInfoUser.filter( - user_id=str(event.user_id), group_id=str(event.group_id) - ).delete() - logger.info( - f"名称: {user_name} 退出群聊", - "group_decrease_handle", - event.user_id, - event.group_id, - ) - rst = "" - if event.sub_type == "leave": - rst = f"{user_name}离开了我们..." - if event.sub_type == "kick": - operator = await bot.get_group_member_info( - user_id=event.operator_id, group_id=event.group_id - ) - operator_name = operator["card"] if operator["card"] else operator["nickname"] - rst = f"{user_name} 被 {operator_name} 送走了." - try: - await group_decrease_handle.send(f"[[_task|refund_group_remind]]{rst}") - except ActionFailed: - return diff --git a/basic_plugins/help/__init__.py b/basic_plugins/help/__init__.py deleted file mode 100755 index d804cf08..00000000 --- a/basic_plugins/help/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.rule import to_me - -from configs.path_config import DATA_PATH, IMAGE_PATH -from services.log import logger -from utils.message_builder import image - -from ._data_source import create_help_img, get_plugin_help -from ._utils import GROUP_HELP_PATH - -__zx_plugin_name__ = "帮助" - -__plugin_configs__ = { - "TYPE": { - "value": "normal", - "help": "帮助图片样式 ['normal', 'HTML']", - "default_value": "normal", - "type": str, - } -} - -simple_help_image = IMAGE_PATH / "simple_help.png" -if simple_help_image.exists(): - simple_help_image.unlink() - - -simple_help = on_command( - "功能", rule=to_me(), aliases={"help", "帮助"}, priority=1, block=True -) - - -@simple_help.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - is_super = False - if msg: - if "-super" in msg: - if str(event.user_id) in bot.config.superusers: - is_super = True - msg = msg.replace("-super", "").strip() - img_msg = get_plugin_help(msg, is_super) - if img_msg: - await simple_help.send(image(b64=img_msg)) - else: - await simple_help.send("没有此功能的帮助信息...") - logger.info( - f"查看帮助详情: {msg}", "帮助", event.user_id, getattr(event, "group_id", None) - ) - else: - if isinstance(event, GroupMessageEvent): - _image_path = GROUP_HELP_PATH / f"{event.group_id}.png" - if not _image_path.exists(): - await create_help_img(event.group_id) - await simple_help.send(image(_image_path)) - else: - if not simple_help_image.exists(): - if simple_help_image.exists(): - simple_help_image.unlink() - await create_help_img(None) - await simple_help.finish(image("simple_help.png")) diff --git a/basic_plugins/help/_config.py b/basic_plugins/help/_config.py deleted file mode 100644 index 90411d42..00000000 --- a/basic_plugins/help/_config.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Optional, List, Any, Union, Dict -from pydantic import BaseModel - - -class Item(BaseModel): - plugin_name: str - sta: int - - -class PluginList(BaseModel): - plugin_type: str - icon: str - logo: str - items: List[Item] diff --git a/basic_plugins/help/_data_source.py b/basic_plugins/help/_data_source.py deleted file mode 100644 index be61b50b..00000000 --- a/basic_plugins/help/_data_source.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Optional - -from configs.path_config import IMAGE_PATH -from utils.image_utils import BuildImage -from utils.manager import admin_manager, plugin_data_manager, plugins2settings_manager - -from ._utils import HelpImageBuild - -random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help" - -background = IMAGE_PATH / "background" / "0.png" - - -async def create_help_img(group_id: Optional[int]): - """ - 说明: - 生成帮助图片 - 参数: - :param group_id: 群号 - """ - await HelpImageBuild().build_image(group_id) - - -def get_plugin_help(msg: str, is_super: bool = False) -> Optional[str]: - """ - 说明: - 获取功能的帮助信息 - 参数: - :param msg: 功能cmd - :param is_super: 是否为超级用户 - """ - module = plugins2settings_manager.get_plugin_module( - msg - ) or admin_manager.get_plugin_module(msg) - if module and (plugin_data := plugin_data_manager.get(module)): - plugin_data.superuser_usage - if is_super: - result = plugin_data.superuser_usage - else: - result = plugin_data.usage - if result: - width = 0 - for x in result.split("\n"): - _width = len(x) * 24 - width = width if width > _width else _width - height = len(result.split("\n")) * 45 - A = BuildImage(width, height, font_size=24) - bk = BuildImage( - width, - height, - background=IMAGE_PATH / "background" / "1.png", - ) - A.paste(bk, alpha=True) - A.text((int(width * 0.048), int(height * 0.21)), result) - return A.pic2bs4() - return None diff --git a/basic_plugins/help/_utils.py b/basic_plugins/help/_utils.py deleted file mode 100644 index 4a9ddad4..00000000 --- a/basic_plugins/help/_utils.py +++ /dev/null @@ -1,259 +0,0 @@ -import os -import random -from typing import Dict, List, Optional - -from configs.config import Config -from configs.path_config import DATA_PATH, IMAGE_PATH, TEMPLATE_PATH -from utils.decorator import Singleton -from utils.image_utils import BuildImage, build_sort_image, group_image -from utils.manager import group_manager, plugin_data_manager -from utils.manager.models import PluginData, PluginType - -from ._config import Item - -GROUP_HELP_PATH = DATA_PATH / "group_help" -GROUP_HELP_PATH.mkdir(exist_ok=True, parents=True) -for x in os.listdir(GROUP_HELP_PATH): - group_help_image = GROUP_HELP_PATH / x - group_help_image.unlink() - -BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help" - -LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo" - - -@Singleton -class HelpImageBuild: - def __init__(self): - self._data: Dict[str, PluginData] = plugin_data_manager.get_data() - self._sort_data: Dict[str, List[PluginData]] = {} - self._image_list = [] - self.icon2str = { - "normal": "fa fa-cog", - "原神相关": "fa fa-circle-o", - "常规插件": "fa fa-cubes", - "联系管理员": "fa fa-envelope-o", - "抽卡相关": "fa fa-credit-card-alt", - "来点好康的": "fa fa-picture-o", - "数据统计": "fa fa-bar-chart", - "一些工具": "fa fa-shopping-cart", - "商店": "fa fa-shopping-cart", - "其它": "fa fa-tags", - "群内小游戏": "fa fa-gamepad", - } - - def sort_type(self): - """ - 说明: - 对插件按照菜单类型分类 - """ - if not self._sort_data.keys(): - for key in self._data.keys(): - plugin_data = self._data[key] - if plugin_data.plugin_type == PluginType.NORMAL: - if not self._sort_data.get(plugin_data.menu_type[0]): # type: ignore - self._sort_data[plugin_data.menu_type[0]] = [] # type: ignore - self._sort_data[plugin_data.menu_type[0]].append(self._data[key]) # type: ignore - - async def build_image(self, group_id: Optional[int]): - if group_id: - help_image = GROUP_HELP_PATH / f"{group_id}.png" - else: - help_image = IMAGE_PATH / f"simple_help.png" - build_type = Config.get_config("help", "TYPE") - if build_type == "HTML": - byt = await self.build_html_image(group_id) - with open(help_image, "wb") as f: - f.write(byt) - else: - img = await self.build_pil_image(group_id) - img.save(help_image) - - async def build_html_image(self, group_id: Optional[int]) -> bytes: - from nonebot_plugin_htmlrender import template_to_pic - - self.sort_type() - classify = {} - for menu in self._sort_data: - for plugin in self._sort_data[menu]: - sta = 0 - if not plugin.plugin_status.status: - if group_id and plugin.plugin_status.block_type in ["all", "group"]: - sta = 2 - if not group_id and plugin.plugin_status.block_type in [ - "all", - "private", - ]: - sta = 2 - if group_id and not group_manager.get_plugin_super_status( - plugin.model, group_id - ): - sta = 2 - if group_id and not group_manager.get_plugin_status( - plugin.model, group_id - ): - sta = 1 - if classify.get(menu): - classify[menu].append(Item(plugin_name=plugin.name, sta=sta)) - else: - classify[menu] = [Item(plugin_name=plugin.name, sta=sta)] - max_len = 0 - flag_index = -1 - max_data = None - plugin_list = [] - for index, plu in enumerate(classify.keys()): - if plu in self.icon2str.keys(): - icon = self.icon2str[plu] - else: - icon = "fa fa-pencil-square-o" - logo = LOGO_PATH / random.choice(os.listdir(LOGO_PATH)) - data = { - "name": plu if plu != "normal" else "功能", - "items": classify[plu], - "icon": icon, - "logo": str(logo.absolute()), - } - if len(classify[plu]) > max_len: - max_len = len(classify[plu]) - flag_index = index - max_data = data - plugin_list.append(data) - del plugin_list[flag_index] - plugin_list.insert(0, max_data) - pic = await template_to_pic( - template_path=str((TEMPLATE_PATH / "menu").absolute()), - template_name="zhenxun_menu.html", - templates={"plugin_list": plugin_list}, - pages={ - "viewport": {"width": 1903, "height": 975}, - "base_url": f"file://{TEMPLATE_PATH}", - }, - wait=2, - ) - return pic - - async def build_pil_image(self, group_id: Optional[int]) -> BuildImage: - """ - 说明: - 构造帮助图片 - 参数: - :param group_id: 群号 - """ - self._image_list = [] - self.sort_type() - font_size = 24 - build_type = Config.get_config("help", "TYPE") - _image = BuildImage(0, 0, plain_text="1", font_size=font_size) - for idx, menu_type in enumerate(self._sort_data.keys()): - plugin_list = self._sort_data[menu_type] - wh_list = [_image.getsize(x.name) for x in plugin_list] - wh_list.append(_image.getsize(menu_type)) - # sum_height = sum([x[1] for x in wh_list]) - if build_type == "VV": - sum_height = 50 * len(plugin_list) + 10 - else: - sum_height = (font_size + 6) * len(plugin_list) + 10 - max_width = max([x[0] for x in wh_list]) + 20 - bk = BuildImage( - max_width + 40, - sum_height + 50, - font_size=30, - color="#a7d1fc", - font="CJGaoDeGuo.otf", - ) - title_size = bk.getsize(menu_type) - max_width = max_width if max_width > title_size[0] else title_size[0] - B = BuildImage( - max_width + 40, - sum_height, - font_size=font_size, - color="white" if not idx % 2 else "black", - ) - curr_h = 10 - for i, plugin_data in enumerate(plugin_list): - text_color = (255, 255, 255) if idx % 2 else (0, 0, 0) - if group_id and not group_manager.get_plugin_status( - plugin_data.model, group_id - ): - text_color = (252, 75, 13) - pos = None - # 禁用状态划线 - if ( - not plugin_data.plugin_status.status - and plugin_data.plugin_status.block_type in ["group", "all"] - ) or ( - group_id - and not group_manager.get_plugin_super_status( - plugin_data.model, group_id - ) - ): - w = curr_h + int(B.getsize(plugin_data.name)[1] / 2) + 2 - pos = ( - 7, - w, - B.getsize(plugin_data.name)[0] + 35, - w, - ) - if build_type == "VV": - name_image = await self.build_name_image( # type: ignore - max_width, - plugin_data.name, - "black" if not idx % 2 else "white", - text_color, - pos, - ) - await B.apaste( - name_image, (0, curr_h), True, center_type="by_width" - ) - curr_h += name_image.h + 5 - else: - await B.atext( - (10, curr_h), f"{i + 1}.{plugin_data.name}", text_color - ) - if pos: - await B.aline(pos, (236, 66, 7), 3) - curr_h += font_size + 5 - if menu_type == "normal": - menu_type = "功能" - await bk.atext((0, 14), menu_type, center_type="by_width") - await bk.apaste(B, (0, 50)) - await bk.atransparent(2) - # await bk.acircle_corner(point_list=['lt', 'rt']) - self._image_list.append(bk) - image_group, h = group_image(self._image_list) - B = await build_sort_image( - image_group, - h, - background_path=BACKGROUND_PATH, - background_handle=lambda image: image.filter("GaussianBlur", 5), - ) - w = 10 - h = 10 - for msg in [ - "目前支持的功能列表:", - "可以通过 ‘帮助[功能名称]’ 来获取对应功能的使用方法", - ]: - text = BuildImage( - 0, - 0, - plain_text=msg, - font_size=24, - font="HYWenHei-85W.ttf", - ) - B.paste(text, (w, h), True) - h += 50 - if msg == "目前支持的功能列表:": - w += 50 - await B.apaste( - BuildImage( - 0, - 0, - plain_text="注: 红字代表功能被群管理员禁用,红线代表功能正在维护", - font_size=24, - font="HYWenHei-85W.ttf", - font_color=(231, 74, 57), - ), - (300, 10), - True, - ) - return B diff --git a/basic_plugins/hooks/__init__.py b/basic_plugins/hooks/__init__.py deleted file mode 100755 index b236939d..00000000 --- a/basic_plugins/hooks/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from configs.config import Config - -Config.add_plugin_config( - "hook", - "CHECK_NOTICE_INFO_CD", - 300, - name="基础hook配置", - help_="群检测,个人权限检测等各种检测提示信息cd", - default_value=300, - type=int, -) - -Config.add_plugin_config( - "hook", - "MALICIOUS_BAN_TIME", - 30, - help_="恶意命令触发检测触发后ban的时长(分钟)", - default_value=30, - type=int, -) - -Config.add_plugin_config( - "hook", - "MALICIOUS_CHECK_TIME", - 5, - help_="恶意命令触发检测规定时间内(秒)", - default_value=5, - type=int, -) - -Config.add_plugin_config( - "hook", "MALICIOUS_BAN_COUNT", 6, help_="恶意命令触发检测最大触发次数", default_value=6, type=int -) diff --git a/basic_plugins/hooks/_utils.py b/basic_plugins/hooks/_utils.py deleted file mode 100644 index 109c3da2..00000000 --- a/basic_plugins/hooks/_utils.py +++ /dev/null @@ -1,580 +0,0 @@ -import time - -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, - PokeNotifyEvent, - PrivateMessageEvent, -) -from nonebot.exception import ActionFailed, IgnoredException -from nonebot.internal.matcher import Matcher - -from configs.config import Config -from models.bag_user import BagUser -from models.ban_user import BanUser -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.decorator import Singleton -from utils.manager import ( - StaticData, - admin_manager, - group_manager, - plugin_data_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2count_manager, - plugins2settings_manager, - plugins_manager, -) -from utils.manager.models import PluginType -from utils.message_builder import at -from utils.utils import FreqLimiter - -ignore_rst_module = ["ai", "poke", "dialogue"] - -other_limit_plugins = ["poke"] - - -class StatusMessageManager(StaticData): - def __init__(self): - super().__init__(None) - - def add(self, id_: int): - self._data[id_] = time.time() - - def delete(self, id_: int): - if self._data.get(id_): - del self._data[id_] - - def check(self, id_: int, t: int = 30) -> bool: - if self._data.get(id_): - if time.time() - self._data[id_] > t: - del self._data[id_] - return True - return False - return True - - -status_message_manager = StatusMessageManager() - - -def set_block_limit_false(event, module): - """ - 设置用户block为false - :param event: event - :param module: 插件模块 - """ - if plugins2block_manager.check_plugin_block_status(module): - if plugin_block_data := plugins2block_manager.get_plugin_block_data(module): - check_type = plugin_block_data.check_type - limit_type = plugin_block_data.limit_type - if not ( - (isinstance(event, GroupMessageEvent) and check_type == "private") - or (isinstance(event, PrivateMessageEvent) and check_type == "group") - ): - block_type_ = event.user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - block_type_ = event.group_id - plugins2block_manager.set_false(block_type_, module) - - -async def send_msg(msg: str, bot: Bot, event: MessageEvent): - """ - 说明: - 发送信息 - 参数: - :param msg: pass - :param bot: pass - :param event: pass - """ - if "[uname]" in msg: - uname = event.sender.card or event.sender.nickname or "" - msg = msg.replace("[uname]", uname) - if "[nickname]" in msg: - if isinstance(event, GroupMessageEvent): - nickname = await GroupInfoUser.get_user_nickname( - event.user_id, event.group_id - ) - else: - nickname = await FriendUser.get_user_nickname(event.user_id) - msg = msg.replace("[nickname]", nickname) - if "[at]" in msg and isinstance(event, GroupMessageEvent): - msg = msg.replace("[at]", str(at(event.user_id))) - try: - if isinstance(event, GroupMessageEvent): - status_message_manager.add(event.group_id) - await bot.send_group_msg(group_id=event.group_id, message=Message(msg)) - else: - status_message_manager.add(event.user_id) - await bot.send_private_msg(user_id=event.user_id, message=Message(msg)) - except ActionFailed: - pass - - -class IsSuperuserException(Exception): - pass - - -@Singleton -class AuthChecker: - """ - 权限检查 - """ - - def __init__(self): - check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD") - if check_notice_info_cd is None or check_notice_info_cd < 0: - raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0") - self._flmt = FreqLimiter(check_notice_info_cd) - self._flmt_g = FreqLimiter(check_notice_info_cd) - self._flmt_s = FreqLimiter(check_notice_info_cd) - self._flmt_c = FreqLimiter(check_notice_info_cd) - - async def auth(self, matcher: Matcher, bot: Bot, event: Event): - """ - 说明: - 权限检查 - 参数: - :param matcher: matcher - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - try: - if plugin_name := matcher.plugin_name: - # self.auth_hidden(matcher, plugin_name) - cost_gold = await self.auth_cost(plugin_name, bot, event) - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - # if user_id and str(user_id) not in bot.config.superusers: - await self.auth_basic(plugin_name, bot, event) - self.auth_group(plugin_name, bot, event) - await self.auth_admin(plugin_name, matcher, bot, event) - await self.auth_plugin(plugin_name, matcher, bot, event) - await self.auth_limit(plugin_name, bot, event) - if cost_gold and user_id and group_id: - await BagUser.spend_gold(user_id, group_id, cost_gold) - logger.debug(f"调用功能花费金币: {cost_gold}", "HOOK", user_id, group_id) - except IsSuperuserException: - logger.debug(f"超级用户或被ban跳过权限检测...", "HOOK", user_id, group_id) - - # def auth_hidden(self, matcher: Matcher): - # if plugin_data := plugin_data_manager.get(matcher.plugin_name): # type: ignore - - async def auth_limit(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 插件限制 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - if plugins2cd_manager.check_plugin_cd_status(plugin_name): - if ( - plugin_cd_data := plugins2cd_manager.get_plugin_cd_data(plugin_name) - ) and (plugin_data := plugins2cd_manager.get_plugin_data(plugin_name)): - check_type = plugin_cd_data.check_type - limit_type = plugin_cd_data.limit_type - msg = plugin_cd_data.rst - if ( - (isinstance(event, PrivateMessageEvent) and check_type == "private") - or (isinstance(event, GroupMessageEvent) and check_type == "group") - or plugin_data.check_type == "all" - ): - cd_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - cd_type_ = event.group_id - if not plugins2cd_manager.check(plugin_name, cd_type_): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug( - f"{plugin_name} 正在cd中...", "HOOK", user_id, group_id - ) - raise IgnoredException(f"{plugin_name} 正在cd中...") - else: - plugins2cd_manager.start_cd(plugin_name, cd_type_) - # Block - if plugins2block_manager.check_plugin_block_status(plugin_name): - if plugin_block_data := plugins2block_manager.get_plugin_block_data( - plugin_name - ): - check_type = plugin_block_data.check_type - limit_type = plugin_block_data.limit_type - msg = plugin_block_data.rst - if ( - (isinstance(event, PrivateMessageEvent) and check_type == "private") - or (isinstance(event, GroupMessageEvent) and check_type == "group") - or check_type == "all" - ): - block_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - block_type_ = event.group_id - if plugins2block_manager.check(block_type_, plugin_name): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug(f"正在调用{plugin_name}...", "HOOK", user_id, group_id) - raise IgnoredException(f"{user_id}正在调用{plugin_name}....") - else: - plugins2block_manager.set_true(block_type_, plugin_name) - # Count - if ( - plugins2count_manager.check_plugin_count_status(plugin_name) - and user_id not in bot.config.superusers - ): - if plugin_count_data := plugins2count_manager.get_plugin_count_data( - plugin_name - ): - limit_type = plugin_count_data.limit_type - msg = plugin_count_data.rst - count_type_ = user_id - if limit_type == "group" and isinstance(event, GroupMessageEvent): - count_type_ = event.group_id - if not plugins2count_manager.check(plugin_name, count_type_): - if msg: - await send_msg(msg, bot, event) # type: ignore - logger.debug( - f"{plugin_name} count次数限制...", "HOOK", user_id, group_id - ) - raise IgnoredException(f"{plugin_name} count次数限制...") - else: - plugins2count_manager.increase(plugin_name, count_type_) - - async def auth_plugin( - self, plugin_name: str, matcher: Matcher, bot: Bot, event: Event - ): - """ - 说明: - 插件状态 - 参数: - :param plugin_name: 模块名 - :param matcher: matcher - :param bot: bot - :param event: event - """ - if plugin_name in plugins2settings_manager.keys() and matcher.priority not in [ - 1, - 999, - ]: - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - # 戳一戳单独判断 - if ( - isinstance(event, GroupMessageEvent) - or isinstance(event, PokeNotifyEvent) - or matcher.plugin_name in other_limit_plugins - ) and group_id: - if status_message_manager.get(group_id) is None: - status_message_manager.delete(group_id) - if plugins2settings_manager[ - plugin_name - ].level > group_manager.get_group_level(group_id): - try: - if ( - self._flmt_g.check(user_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_g.start_cd(user_id) - await bot.send_group_msg( - group_id=group_id, message="群权限不足..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 群权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("群权限不足") - # 插件状态 - if not group_manager.get_plugin_status(plugin_name, group_id): - try: - if plugin_name not in ignore_rst_module and self._flmt_s.check( - group_id - ): - self._flmt_s.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="该群未开启此功能.." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 未开启此功能...", "HOOK", user_id, group_id) - raise IgnoredException("未开启此功能...") - # 管理员禁用 - if not group_manager.get_plugin_status( - f"{plugin_name}:super", group_id - ): - try: - if ( - self._flmt_s.check(group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_s.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="管理员禁用了此群该功能..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 管理员禁用了此群该功能...", "HOOK", user_id, group_id - ) - raise IgnoredException("管理员禁用了此群该功能...") - # 群聊禁用 - if not plugins_manager.get_plugin_status( - plugin_name, block_type="group" - ): - try: - if ( - self._flmt_c.check(group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_c.start_cd(group_id) - await bot.send_group_msg( - group_id=group_id, message="该功能在群聊中已被禁用..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(group_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 该插件在群聊中已被禁用...", "HOOK", user_id, group_id - ) - raise IgnoredException("该插件在群聊中已被禁用...") - else: - # 私聊禁用 - if not plugins_manager.get_plugin_status( - plugin_name, block_type="private" - ): - try: - if self._flmt_c.check(user_id): - self._flmt_c.start_cd(user_id) - await bot.send_private_msg( - user_id=user_id, message="该功能在私聊中已被禁用..." - ) - except ActionFailed: - pass - if event.is_tome(): - status_message_manager.add(user_id) - set_block_limit_false(event, plugin_name) - logger.debug( - f"{plugin_name} 该插件在私聊中已被禁用...", "HOOK", user_id, group_id - ) - raise IgnoredException("该插件在私聊中已被禁用...") - # 维护 - if not plugins_manager.get_plugin_status(plugin_name, block_type="all"): - if isinstance( - event, GroupMessageEvent - ) and group_manager.check_group_is_white(event.group_id): - raise IsSuperuserException() - try: - if isinstance(event, GroupMessageEvent): - if ( - self._flmt_c.check(event.group_id) - and plugin_name not in ignore_rst_module - ): - self._flmt_c.start_cd(event.group_id) - await bot.send_group_msg( - group_id=event.group_id, message="此功能正在维护..." - ) - else: - await bot.send_private_msg( - user_id=user_id, message="此功能正在维护..." - ) - except ActionFailed: - pass - if event.is_tome(): - id_ = group_id or user_id - status_message_manager.add(id_) - set_block_limit_false(event, plugin_name) - logger.debug(f"{plugin_name} 此功能正在维护...", "HOOK", user_id, group_id) - raise IgnoredException("此功能正在维护...") - - async def auth_admin( - self, plugin_name: str, matcher: Matcher, bot: Bot, event: Event - ): - """ - 说明: - 管理员命令 个人权限 - 参数: - :param plugin_name: 模块名 - :param matcher: matcher - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - group_id = getattr(event, "group_id", None) - if plugin_name in admin_manager.keys() and matcher.priority not in [1, 999]: - if isinstance(event, GroupMessageEvent): - # 个人权限 - if ( - not await LevelUser.check_level( - event.user_id, - event.group_id, - admin_manager.get_plugin_level(plugin_name), - ) - and admin_manager.get_plugin_level(plugin_name) > 0 - ): - try: - if self._flmt.check(event.user_id): - self._flmt.start_cd(event.user_id) - await bot.send_group_msg( - group_id=event.group_id, - message=f"{at(event.user_id)}你的权限不足喔,该功能需要的权限等级:" - f"{admin_manager.get_plugin_level(plugin_name)}", - ) - except ActionFailed: - pass - set_block_limit_false(event, plugin_name) - if event.is_tome(): - status_message_manager.add(event.group_id) - logger.debug(f"{plugin_name} 管理员权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("管理员权限不足") - else: - if not await LevelUser.check_level( - user_id, 0, admin_manager.get_plugin_level(plugin_name) - ): - try: - await bot.send_private_msg( - user_id=user_id, - message=f"你的权限不足喔,该功能需要的权限等级:{admin_manager.get_plugin_level(plugin_name)}", - ) - except ActionFailed: - pass - set_block_limit_false(event, plugin_name) - if event.is_tome(): - status_message_manager.add(user_id) - logger.debug(f"{plugin_name} 管理员权限不足...", "HOOK", user_id, group_id) - raise IgnoredException("权限不足") - - def auth_group(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 群黑名单检测 群总开关检测 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if not group_id: - return - if ( - group_manager.get_group_level(group_id) < 0 - and str(user_id) not in bot.config.superusers - ): - logger.debug(f"{plugin_name} 群黑名单, 群权限-1...", "HOOK", user_id, group_id) - raise IgnoredException("群黑名单") - if not group_manager.check_group_bot_status(group_id): - try: - if str(event.get_message()) != "醒来": - logger.debug( - f"{plugin_name} 功能总开关关闭状态...", "HOOK", user_id, group_id - ) - raise IgnoredException("功能总开关关闭状态") - except ValueError: - logger.debug(f"{plugin_name} 功能总开关关闭状态...", "HOOK", user_id, group_id) - raise IgnoredException("功能总开关关闭状态") - - async def auth_basic(self, plugin_name: str, bot: Bot, event: Event): - """ - 说明: - 检测是否满足超级用户权限,是否被ban等 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return - plugin_setting = plugins2settings_manager.get_plugin_data(plugin_name) - if ( - ( - not isinstance(event, MessageEvent) - and plugin_name not in other_limit_plugins - ) - or await BanUser.is_ban(user_id) - and str(user_id) not in bot.config.superusers - ) or ( - str(user_id) in bot.config.superusers - and plugin_setting - and not plugin_setting.limit_superuser - ): - raise IsSuperuserException() - if plugin_data := plugin_data_manager.get(plugin_name): - if ( - plugin_data.plugin_type == PluginType.SUPERUSER - and str(user_id) in bot.config.superusers - ): - raise IsSuperuserException() - - async def auth_cost(self, plugin_name: str, bot: Bot, event: Event) -> int: - """ - 说明: - 检测是否满足金币条件 - 参数: - :param plugin_name: 模块名 - :param bot: bot - :param event: event - """ - user_id = getattr(event, "user_id", None) - if not user_id: - return 0 - group_id = getattr(event, "group_id", None) - cost_gold = 0 - if isinstance(event, GroupMessageEvent) and ( - psm := plugins2settings_manager.get_plugin_data(plugin_name) - ): - if psm.cost_gold > 0: - if ( - await BagUser.get_gold(event.user_id, event.group_id) - < psm.cost_gold - ): - await send_msg(f"金币不足..该功能需要{psm.cost_gold}金币..", bot, event) - logger.debug( - f"{plugin_name} 金币限制..该功能需要{psm.cost_gold}金币..", - "HOOK", - user_id, - group_id, - ) - raise IgnoredException(f"{plugin_name} 金币限制...") - # 当插件不阻塞超级用户时,超级用户提前扣除金币 - if ( - str(event.user_id) in bot.config.superusers - and not psm.limit_superuser - ): - await BagUser.spend_gold( - event.user_id, event.group_id, psm.cost_gold - ) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=2, - name=plugin_name, - num=1, - spend_gold=psm.cost_gold, - ) - cost_gold = psm.cost_gold - return cost_gold diff --git a/basic_plugins/hooks/auth_hook.py b/basic_plugins/hooks/auth_hook.py deleted file mode 100755 index a51e8fbe..00000000 --- a/basic_plugins/hooks/auth_hook.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Optional - -from nonebot.adapters.onebot.v11 import ( - Bot, - MessageEvent, - Event, -) -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor, run_postprocessor -from nonebot.typing import T_State - -from ._utils import ( - set_block_limit_false, - AuthChecker, -) - - -# # 权限检测 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: Event): - await AuthChecker().auth(matcher, bot, event) - - -# 解除命令block阻塞 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - if not isinstance(event, MessageEvent) and matcher.plugin_name != "poke": - return - module = matcher.plugin_name - set_block_limit_false(event, module) diff --git a/basic_plugins/hooks/ban_hook.py b/basic_plugins/hooks/ban_hook.py deleted file mode 100755 index 40d11b05..00000000 --- a/basic_plugins/hooks/ban_hook.py +++ /dev/null @@ -1,83 +0,0 @@ -from nonebot.adapters.onebot.v11 import ActionFailed, Bot, Event, GroupMessageEvent -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.typing import T_State - -from configs.config import Config -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import at -from utils.utils import FreqLimiter, is_number, static_flmt - -from ._utils import ignore_rst_module, other_limit_plugins - -Config.add_plugin_config( - "hook", - "BAN_RESULT", - "才不会给你发消息.", - help_="对被ban用户发送的消息", -) - -_flmt = FreqLimiter(300) - - -# 检查是否被ban -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: Event, state: T_State): - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if user_id and ( - matcher.priority not in [1, 999] or matcher.plugin_name in other_limit_plugins - ): - if ( - await BanUser.is_super_ban(user_id) - and str(user_id) not in bot.config.superusers - ): - logger.debug(f"用户处于超级黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于超级黑名单中") - if await BanUser.is_ban(user_id) and str(user_id) not in bot.config.superusers: - time = await BanUser.check_ban_time(user_id) - if isinstance(time, int): - time = abs(int(time)) - if time < 60: - time = str(time) + " 秒" - else: - time = str(int(time / 60)) + " 分钟" - else: - time = str(time) + " 分钟" - if isinstance(event, GroupMessageEvent): - if not static_flmt.check(user_id): - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") - static_flmt.start_cd(user_id) - if matcher.priority != 999: - try: - ban_result = Config.get_config("hook", "BAN_RESULT") - if ( - ban_result - and _flmt.check(user_id) - and matcher.plugin_name not in ignore_rst_module - ): - _flmt.start_cd(user_id) - await bot.send_group_msg( - group_id=event.group_id, - message=at(user_id) - + ban_result - + f" 在..在 {time} 后才会理你喔", - ) - except ActionFailed: - pass - else: - if not static_flmt.check(user_id): - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") - static_flmt.start_cd(user_id) - if matcher.priority != 999: - ban_result = Config.get_config("hook", "BAN_RESULT") - if ban_result and matcher.plugin_name not in ignore_rst_module: - await bot.send_private_msg( - user_id=user_id, - message=at(user_id) + ban_result + f" 在..在 {time}后才会理你喔", - ) - logger.debug(f"用户处于黑名单中...", "HOOK", user_id, group_id) - raise IgnoredException("用户处于黑名单中") diff --git a/basic_plugins/hooks/chkdsk_hook.py b/basic_plugins/hooks/chkdsk_hook.py deleted file mode 100755 index 5b506c97..00000000 --- a/basic_plugins/hooks/chkdsk_hook.py +++ /dev/null @@ -1,72 +0,0 @@ -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupMessageEvent, - MessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.typing import T_State - -from configs.config import Config -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import at -from utils.utils import BanCheckLimiter - -malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME") -malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT") - -if not malicious_check_time: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_CHECK_TIME] 为空或小于0") -if not malicious_ban_count: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_COUNT] 为空或小于0") - -_blmt = BanCheckLimiter( - malicious_check_time, - malicious_ban_count, -) - - -# 恶意触发命令检测 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: GroupMessageEvent, state: T_State): - user_id = getattr(event, "user_id", None) - group_id = getattr(event, "group_id", None) - if not isinstance(event, MessageEvent): - return - malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME") - if not malicious_ban_time: - raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") - if matcher.type == "message" and matcher.priority not in [1, 999]: - if state["_prefix"]["raw_command"]: - if _blmt.check(f'{event.user_id}{state["_prefix"]["raw_command"]}'): - await BanUser.ban( - event.user_id, - 9, - malicious_ban_time * 60, - ) - logger.info( - f"触发了恶意触发检测: {matcher.plugin_name}", "HOOK", user_id, group_id - ) - if isinstance(event, GroupMessageEvent): - try: - await bot.send_group_msg( - group_id=event.group_id, - message=at(event.user_id) + "检测到恶意触发命令,您将被封禁 30 分钟", - ) - except ActionFailed: - pass - else: - try: - await bot.send_private_msg( - user_id=event.user_id, - message=at(event.user_id) + "检测到恶意触发命令,您将被封禁 30 分钟", - ) - except ActionFailed: - pass - logger.debug( - f"触发了恶意触发检测: {matcher.plugin_name}", "HOOK", user_id, group_id - ) - raise IgnoredException("检测到恶意触发命令") - _blmt.add(f'{event.user_id}{state["_prefix"]["raw_command"]}') diff --git a/basic_plugins/hooks/other_hook.py b/basic_plugins/hooks/other_hook.py deleted file mode 100755 index 9010c84b..00000000 --- a/basic_plugins/hooks/other_hook.py +++ /dev/null @@ -1,43 +0,0 @@ -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor, IgnoredException -from nonebot.typing import T_State -from ._utils import status_message_manager -from nonebot.adapters.onebot.v11 import ( - Bot, - MessageEvent, - PrivateMessageEvent, - GroupMessageEvent, -) - - -# 为什么AI会自己和自己聊天 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: PrivateMessageEvent, state: T_State): - if not isinstance(event, MessageEvent): - return - if event.user_id == int(bot.self_id): - raise IgnoredException("为什么AI会自己和自己聊天") - - -# 有命令就别说话了 -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): - if not isinstance(event, MessageEvent): - return - if matcher.type == "message" and matcher.plugin_name == "ai": - if ( - isinstance(event, GroupMessageEvent) - and not status_message_manager.check(event.group_id) - ): - status_message_manager.delete(event.group_id) - raise IgnoredException("有命令就别说话了") - elif ( - isinstance(event, PrivateMessageEvent) - and not status_message_manager.check(event.user_id) - ): - status_message_manager.delete(event.user_id) - raise IgnoredException("有命令就别说话了") - - - - diff --git a/basic_plugins/hooks/task_hook.py b/basic_plugins/hooks/task_hook.py deleted file mode 100644 index f346052c..00000000 --- a/basic_plugins/hooks/task_hook.py +++ /dev/null @@ -1,46 +0,0 @@ -import re -from typing import Any, Dict - -from nonebot.adapters.onebot.v11 import Bot, Message, unescape -from nonebot.exception import MockApiException - -from services.log import logger -from utils.manager import group_manager - - -@Bot.on_calling_api -async def _(bot: Bot, api: str, data: Dict[str, Any]): - r = None - task = None - group_id = None - try: - if ( - api == "send_msg" and data.get("message_type") == "group" - ) or api == "send_group_msg": - msg = unescape( - data["message"].strip() - if isinstance(data["message"], str) - else str(data["message"]["text"]).strip() - ) - if r := re.search( - "^\[\[_task\|(.*)]]", - msg, - ): - if r.group(1) in group_manager.get_task_data().keys(): - task = r.group(1) - group_id = data["group_id"] - except Exception as e: - logger.error(f"TaskHook ERROR", "HOOK", e=e) - else: - if task and group_id: - if group_manager.get_group_level( - group_id - ) < 0 or not group_manager.check_task_status(task, group_id): - logger.debug(f"被动技能 {task} 处于关闭状态") - raise MockApiException(f"被动技能 {task} 处于关闭状态...") - else: - msg = str(data["message"]).strip() - msg = msg.replace(f"[[_task|{task}]]", "").replace( - f"[[_task|{task}]]", "" - ) - data["message"] = Message(msg) diff --git a/basic_plugins/hooks/withdraw_message_hook.py b/basic_plugins/hooks/withdraw_message_hook.py deleted file mode 100755 index db7d5651..00000000 --- a/basic_plugins/hooks/withdraw_message_hook.py +++ /dev/null @@ -1,32 +0,0 @@ -import asyncio -from typing import Optional - -from nonebot.adapters.onebot.v11 import Bot, Event -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.typing import T_State - -from services.log import logger -from utils.manager import withdraw_message_manager - - -# 消息撤回 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - tasks = [] - for id_, time in withdraw_message_manager.data: - tasks.append(asyncio.ensure_future(_withdraw_message(bot, id_, time))) - withdraw_message_manager.remove((id_, time)) - await asyncio.gather(*tasks) - - -async def _withdraw_message(bot: Bot, id_: int, time: int): - await asyncio.sleep(time) - logger.debug(f"撤回消息ID: {id_}", "HOOK") - await bot.delete_msg(message_id=id_) diff --git a/basic_plugins/init_plugin_config/__init__.py b/basic_plugins/init_plugin_config/__init__.py deleted file mode 100755 index 355239fe..00000000 --- a/basic_plugins/init_plugin_config/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import Bot - -from configs.path_config import DATA_PATH -from services.log import logger - -from .check_plugin_status import check_plugin_status -from .init import init -from .init_none_plugin_count_manager import init_none_plugin_count_manager -from .init_plugin_info import init_plugin_info -from .init_plugins_config import init_plugins_config -from .init_plugins_data import init_plugins_data, plugins_manager -from .init_plugins_limit import ( - init_plugins_block_limit, - init_plugins_cd_limit, - init_plugins_count_limit, -) -from .init_plugins_resources import init_plugins_resources -from .init_plugins_settings import init_plugins_settings - -__zx_plugin_name__ = "初始化插件数据 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 初始化数据 - """ - config_file = DATA_PATH / "configs" / "plugins2config.yaml" - _flag = not config_file.exists() - init() - init_plugin_info() - init_plugins_settings() - init_plugins_cd_limit() - init_plugins_block_limit() - init_plugins_count_limit() - init_plugins_data() - init_plugins_config() - init_plugins_resources() - init_none_plugin_count_manager() - if _flag: - raise Exception("首次运行,已在configs目录下生成配置文件config.yaml,修改后重启即可...") - logger.info("初始化数据完成...") - - -@driver.on_bot_connect -async def _(bot: Bot): - # await init_group_manager() - await check_plugin_status(bot) diff --git a/basic_plugins/init_plugin_config/check_plugin_status.py b/basic_plugins/init_plugin_config/check_plugin_status.py deleted file mode 100755 index 5f61e0c2..00000000 --- a/basic_plugins/init_plugin_config/check_plugin_status.py +++ /dev/null @@ -1,18 +0,0 @@ -from utils.manager import plugins_manager -from nonebot.adapters.onebot.v11 import Bot - - -async def check_plugin_status(bot: Bot): - """ - 遍历查看插件加载情况 - """ - msg = "" - for plugin in plugins_manager.keys(): - data = plugins_manager.get(plugin) - if data.error: - msg += f'{plugin}:{data.plugin_name}\n' - if msg and bot.config.superusers: - msg = "以下插件加载失败..\n" + msg - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=msg.strip() - ) diff --git a/basic_plugins/init_plugin_config/init.py b/basic_plugins/init_plugin_config/init.py deleted file mode 100644 index d8640828..00000000 --- a/basic_plugins/init_plugin_config/init.py +++ /dev/null @@ -1,14 +0,0 @@ -from utils.manager import plugins2settings_manager - - -def init(): - if plugins2settings_manager.get("update_pic"): - plugins2settings_manager["update_picture"] = plugins2settings_manager["update_pic"] - plugins2settings_manager.delete("update_pic") - if plugins2settings_manager.get("white2black_img"): - plugins2settings_manager["white2black_image"] = plugins2settings_manager["white2black_img"] - plugins2settings_manager.delete("white2black_img") - if plugins2settings_manager.get("send_img"): - plugins2settings_manager["send_image"] = plugins2settings_manager["send_img"] - plugins2settings_manager.delete("send_img") - diff --git a/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py b/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py deleted file mode 100755 index 29d6b3a9..00000000 --- a/basic_plugins/init_plugin_config/init_none_plugin_count_manager.py +++ /dev/null @@ -1,62 +0,0 @@ -from utils.manager import ( - none_plugin_count_manager, - plugins2count_manager, - plugins2cd_manager, - plugins2settings_manager, - plugins2block_manager, - plugins_manager, -) -from services.log import logger -from utils.utils import get_matchers - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -def init_none_plugin_count_manager(): - """ - 清除已删除插件数据 - """ - modules = [x.plugin_name for x in get_matchers(True)] - plugins_manager_list = list(plugins_manager.keys()) - for module in plugins_manager_list: - try: - if module not in modules or none_plugin_count_manager.check(module): - try: - plugin_name = plugins_manager.get(module).plugin_name - except (AttributeError, KeyError): - plugin_name = "" - if none_plugin_count_manager.check(module): - try: - plugins2settings_manager.delete(module) - plugins2count_manager.delete(module) - plugins2cd_manager.delete(module) - plugins2block_manager.delete(module) - plugins_manager.delete(module) - plugins_manager.save() - # resources_manager.remove_resource(module) - none_plugin_count_manager.delete(module) - logger.info(f"{module}:{plugin_name} 插件疑似已删除,清除对应插件数据...") - except Exception as e: - logger.exception( - f"{module}:{plugin_name} 插件疑似已删除,清除对应插件数据失败...{type(e)}:{e}" - ) - else: - none_plugin_count_manager.add_count(module) - logger.info( - f"{module}:{plugin_name} 插件疑似已删除," - f"加载{none_plugin_count_manager._max_count}次失败后将清除对应插件数据," - f"当前次数:{none_plugin_count_manager.get(module)}" - ) - else: - none_plugin_count_manager.reset(module) - except Exception as e: - logger.error(f"清除插件数据错误 {type(e)}:{e}") - plugins2settings_manager.save() - plugins2count_manager.save() - plugins2cd_manager.save() - plugins2block_manager.save() - plugins_manager.save() - none_plugin_count_manager.save() diff --git a/basic_plugins/init_plugin_config/init_plugin_info.py b/basic_plugins/init_plugin_config/init_plugin_info.py deleted file mode 100644 index 744f50d7..00000000 --- a/basic_plugins/init_plugin_config/init_plugin_info.py +++ /dev/null @@ -1,149 +0,0 @@ -import random -from types import ModuleType -from typing import Any, Dict - -from configs.config import Config -from services import logger -from utils.manager import ( - plugin_data_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2count_manager, - plugins2settings_manager, - plugins_manager, -) -from utils.manager.models import ( - Plugin, - PluginBlock, - PluginCd, - PluginCount, - PluginData, - PluginSetting, - PluginType, -) -from utils.utils import get_matchers - - -def get_attr(module: ModuleType, name: str, default: Any = None) -> Any: - """ - 说明: - 获取属性 - 参数: - :param module: module - :param name: name - :param default: default - """ - return getattr(module, name, None) or default - - -def init_plugin_info(): - - for matcher in [x for x in get_matchers(True)]: - try: - if (plugin := matcher.plugin) and matcher.plugin_name: - metadata = plugin.metadata - extra = metadata.extra if metadata else {} - if hasattr(plugin, "module"): - module = plugin.module - plugin_model = matcher.plugin_name - plugin_name = ( - metadata.name - if metadata and metadata.name - else get_attr(module, "__zx_plugin_name__", matcher.plugin_name) - ) - if not plugin_name: - logger.warning(f"配置文件 模块:{plugin_model} 获取 plugin_name 失败...") - continue - if "[Admin]" in plugin_name: - plugin_type = PluginType.ADMIN - plugin_name = plugin_name.replace("[Admin]", "").strip() - elif "[Hidden]" in plugin_name: - plugin_type = PluginType.HIDDEN - plugin_name = plugin_name.replace("[Hidden]", "").strip() - elif "[Superuser]" in plugin_name: - plugin_type = PluginType.SUPERUSER - plugin_name = plugin_name.replace("[Superuser]", "").strip() - else: - plugin_type = PluginType.NORMAL - plugin_usage = ( - metadata.usage - if metadata and metadata.usage - else get_attr(module, "__plugin_usage__") - ) - plugin_des = ( - metadata.description - if metadata and metadata.description - else get_attr(module, "__plugin_des__") - ) - menu_type = get_attr(module, "__plugin_type__") or ("normal",) - plugin_setting = get_attr(module, "__plugin_settings__") - if plugin_setting: - plugin_setting = PluginSetting(**plugin_setting) - plugin_setting.plugin_type = menu_type - plugin_superuser_usage = get_attr( - module, "__plugin_superuser_usage__" - ) - plugin_task = get_attr(module, "__plugin_task__") - plugin_version = extra.get("__plugin_version__") or get_attr( - module, "__plugin_version__" - ) - plugin_author = extra.get("__plugin_author__") or get_attr( - module, "__plugin_author__" - ) - plugin_cd = get_attr(module, "__plugin_cd_limit__") - if plugin_cd: - plugin_cd = PluginCd(**plugin_cd) - plugin_block = get_attr(module, "__plugin_block_limit__") - if plugin_block: - plugin_block = PluginBlock(**plugin_block) - plugin_count = get_attr(module, "__plugin_count_limit__") - if plugin_count: - plugin_count = PluginCount(**plugin_count) - plugin_resources = get_attr(module, "__plugin_resources__") - plugin_configs = get_attr(module, "__plugin_configs__") - if settings := plugins2settings_manager.get(plugin_model): - plugin_setting = settings - if plugin_cd_limit := plugins2cd_manager.get(plugin_model): - plugin_cd = plugin_cd_limit - if plugin_block_limit := plugins2block_manager.get(plugin_model): - plugin_block = plugin_block_limit - if plugin_count_limit := plugins2count_manager.get(plugin_model): - plugin_count = plugin_count_limit - if plugin_cfg := Config.get(plugin_model): - if plugin_configs: - for config_name in plugin_configs: - config: Dict[str, Any] = plugin_configs[config_name] # type: ignore - Config.add_plugin_config( - plugin_model, - config_name, - config.get("value"), - help_=config.get("help"), - default_value=config.get("default_value"), - type=config.get("type"), - ) - plugin_configs = plugin_cfg.configs - plugin_status = plugins_manager.get(plugin_model) - if not plugin_status: - plugin_status = Plugin(plugin_name=plugin_model) - plugin_status.author = plugin_author - plugin_status.version = plugin_version - plugin_data = PluginData( - model=plugin_model, - name=plugin_name.strip(), - plugin_type=plugin_type, - usage=plugin_usage, - superuser_usage=plugin_superuser_usage, - des=plugin_des, - task=plugin_task, - menu_type=menu_type, - plugin_setting=plugin_setting, - plugin_cd=plugin_cd, - plugin_block=plugin_block, - plugin_count=plugin_count, - plugin_resources=plugin_resources, - plugin_configs=plugin_configs, # type: ignore - plugin_status=plugin_status, - ) - plugin_data_manager.add_plugin_info(plugin_data) - except Exception as e: - logger.error(f"构造插件数据失败 {matcher.plugin_name}", e=e) diff --git a/basic_plugins/init_plugin_config/init_plugins_config.py b/basic_plugins/init_plugin_config/init_plugins_config.py deleted file mode 100755 index 203c3941..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_config.py +++ /dev/null @@ -1,173 +0,0 @@ -from pathlib import Path - -from ruamel import yaml -from ruamel.yaml import YAML, round_trip_dump, round_trip_load - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.manager import admin_manager, plugin_data_manager, plugins_manager -from utils.text_utils import prompt2cn -from utils.utils import get_matchers - -_yaml = YAML(typ="safe") - - -def init_plugins_config(): - """ - 初始化插件数据配置 - """ - plugins2config_file = DATA_PATH / "configs" / "plugins2config.yaml" - _data = Config.get_data() - # 优先使用 metadata 数据 - for matcher in get_matchers(True): - if matcher.plugin_name: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - # 插件配置版本更新或为Version为None或不在存储配置内,当使用metadata时,必定更新 - version = plugin_data.plugin_status.version - config = _data.get(matcher.plugin_name) - plugin = plugins_manager.get(matcher.plugin_name) - if plugin_data.plugin_configs and ( - isinstance(version, str) - or ( - version is None - or ( - config - and config.configs.keys() - != plugin_data.plugin_configs.keys() - ) - or version > int(plugin.version or 0) - or matcher.plugin_name not in _data.keys() - ) - ): - plugin_configs = plugin_data.plugin_configs - for key in plugin_configs: - if isinstance(plugin_data.plugin_configs[key], dict): - Config.add_plugin_config( - matcher.plugin_name, - key, - plugin_configs[key].get("value"), - help_=plugin_configs[key].get("help"), - default_value=plugin_configs[key].get("default_value"), - _override=True, - type=plugin_configs[key].get("type"), - ) - else: - config = plugin_configs[key] - Config.add_plugin_config( - matcher.plugin_name, - key, - config.value, - name=config.name, - help_=config.help, - default_value=config.default_value, - _override=True, - type=config.type, - ) - elif plugin_configs := _data.get(matcher.plugin_name): - for key in plugin_configs.configs: - Config.add_plugin_config( - matcher.plugin_name, - key, - plugin_configs.configs[key].value, - help_=plugin_configs.configs[key].help, - default_value=plugin_configs.configs[key].default_value, - _override=True, - type=plugin_configs.configs[key].type, - ) - if not Config.is_empty(): - Config.save() - _data = round_trip_load(open(plugins2config_file, encoding="utf8")) - for plugin in _data.keys(): - try: - plugin_name = plugins_manager.get(plugin).plugin_name - except (AttributeError, TypeError): - plugin_name = plugin - _data[plugin].yaml_set_start_comment(plugin_name, indent=2) - # 初始化未设置的管理员权限等级 - for k, v in Config.get_admin_level_data(): - admin_manager.set_admin_level(k, v) - # 存完插件基本设置 - with open(plugins2config_file, "w", encoding="utf8") as wf: - round_trip_dump( - _data, wf, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - _replace_config() - - -def _replace_config(): - """ - 说明: - 定时任务加载的配置读取替换 - """ - # 再开始读取用户配置 - user_config_file = Path() / "configs" / "config.yaml" - _data = {} - _tmp_data = {} - if user_config_file.exists(): - with open(user_config_file, "r", encoding="utf8") as f: - _data = _yaml.load(f) - # 数据替换 - for plugin in Config.keys(): - _tmp_data[plugin] = {} - for k in Config[plugin].configs.keys(): - try: - if _data.get(plugin) and k in _data[plugin].keys(): - Config.set_config(plugin, k, _data[plugin][k]) - if level2module := Config.get_level2module(plugin, k): - try: - admin_manager.set_admin_level( - level2module, _data[plugin][k] - ) - except KeyError: - logger.warning( - f"{level2module} 设置权限等级失败:{_data[plugin][k]}" - ) - _tmp_data[plugin][k] = Config.get_config(plugin, k) - except AttributeError as e: - raise AttributeError( - f"{e}\n" + prompt2cn("可能为config.yaml配置文件填写不规范", 46) - ) - Config.save() - temp_file = Path() / "configs" / "temp_config.yaml" - try: - with open(temp_file, "w", encoding="utf8") as wf: - yaml.dump(_tmp_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) - with open(temp_file, "r", encoding="utf8") as rf: - _data = round_trip_load(rf) - # 添加注释 - for plugin in _data.keys(): - rst = "" - plugin_name = None - try: - if config_group := Config.get(plugin): - for key in list(config_group.configs.keys()): - try: - if config := config_group.configs[key]: - if config.name: - plugin_name = config.name - except AttributeError: - pass - except (KeyError, AttributeError): - plugin_name = None - if not plugin_name: - try: - plugin_name = plugins_manager.get(plugin).plugin_name - except (AttributeError, TypeError): - plugin_name = plugin - plugin_name = ( - plugin_name.replace("[Hidden]", "") - .replace("[Superuser]", "") - .replace("[Admin]", "") - .strip() - ) - rst += plugin_name + "\n" - for k in _data[plugin].keys(): - rst += f"{k}: {Config[plugin].configs[k].help}" + "\n" - _data[plugin].yaml_set_start_comment(rst[:-1], indent=2) - with open(Path() / "configs" / "config.yaml", "w", encoding="utf8") as wf: - round_trip_dump(_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) - except Exception as e: - logger.error(f"生成简易配置注释错误 {type(e)}:{e}") - if temp_file.exists(): - temp_file.unlink() diff --git a/basic_plugins/init_plugin_config/init_plugins_data.py b/basic_plugins/init_plugin_config/init_plugins_data.py deleted file mode 100755 index 41ded12c..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_data.py +++ /dev/null @@ -1,57 +0,0 @@ - -from ruamel.yaml import YAML -from utils.manager import plugins_manager, plugin_data_manager -from utils.utils import get_matchers -from services.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -_yaml = YAML(typ="safe") - - -def init_plugins_data(): - """ - 初始化插件数据信息 - """ - for matcher in get_matchers(True): - _plugin = matcher.plugin - if not _plugin: - continue - try: - _module = _plugin.module - except AttributeError: - if matcher.plugin_name not in plugins_manager.keys(): - plugins_manager.add_plugin_data( - matcher.plugin_name, matcher.plugin_name, error=True - ) - else: - plugins_manager[matcher.plugin_name].error = True - else: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - try: - plugin_version = plugin_data.plugin_status.version - plugin_name = plugin_data.name - plugin_author = plugin_data.plugin_status.author - if matcher.plugin_name in plugins_manager.keys(): - plugins_manager[matcher.plugin_name].error = False - if matcher.plugin_name not in plugins_manager.keys(): - plugins_manager.add_plugin_data( - matcher.plugin_name, - plugin_name=plugin_name, - author=plugin_author, - version=plugin_version, - ) - elif isinstance(plugin_version, str) or plugins_manager[matcher.plugin_name].version is None or ( - plugin_version is not None - and plugin_version > float(plugins_manager[matcher.plugin_name].version) - ): - plugins_manager[matcher.plugin_name].plugin_name = plugin_name - plugins_manager[matcher.plugin_name].author = plugin_author - plugins_manager[matcher.plugin_name].version = plugin_version - except Exception as e: - logger.error(f"插件数据 {matcher.plugin_name} 加载发生错误 {type(e)}:{e}") - plugins_manager.save() diff --git a/basic_plugins/init_plugin_config/init_plugins_limit.py b/basic_plugins/init_plugin_config/init_plugins_limit.py deleted file mode 100755 index 2de6c010..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_limit.py +++ /dev/null @@ -1,64 +0,0 @@ -from utils.manager import ( - plugins2cd_manager, - plugins2block_manager, - plugins2count_manager, - plugin_data_manager, -) -from utils.utils import get_matchers -from configs.path_config import DATA_PATH - - -def init_plugins_cd_limit(): - """ - 加载 cd 限制 - """ - plugins2cd_file = DATA_PATH / "configs" / "plugins2cd.yaml" - plugins2cd_file.parent.mkdir(exist_ok=True, parents=True) - for matcher in get_matchers(True): - if not plugins2cd_manager.get_plugin_cd_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_cd: - plugins2cd_manager.add_cd_limit( - matcher.plugin_name, plugin_data.plugin_cd - ) - if not plugins2cd_manager.keys(): - plugins2cd_manager.add_cd_limit("这是一个示例") - plugins2cd_manager.save() - plugins2cd_manager.reload_cd_limit() - - -def init_plugins_block_limit(): - """ - 加载阻塞限制 - """ - for matcher in get_matchers(True): - if not plugins2block_manager.get_plugin_block_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_block: - plugins2block_manager.add_block_limit( - matcher.plugin_name, plugin_data.plugin_block - ) - if not plugins2block_manager.keys(): - plugins2block_manager.add_block_limit("这是一个示例") - plugins2block_manager.save() - plugins2block_manager.reload_block_limit() - - -def init_plugins_count_limit(): - """ - 加载次数限制 - """ - for matcher in get_matchers(True): - if not plugins2count_manager.get_plugin_count_data(matcher.plugin_name) and ( - plugin_data := plugin_data_manager.get(matcher.plugin_name) - ): - if plugin_data.plugin_count: - plugins2count_manager.add_count_limit( - matcher.plugin_name, plugin_data.plugin_count - ) - if not plugins2count_manager.keys(): - plugins2count_manager.add_count_limit("这是一个示例") - plugins2count_manager.save() - plugins2count_manager.reload_count_limit() diff --git a/basic_plugins/init_plugin_config/init_plugins_resources.py b/basic_plugins/init_plugin_config/init_plugins_resources.py deleted file mode 100755 index 936d5038..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_resources.py +++ /dev/null @@ -1,25 +0,0 @@ -from utils.manager import resources_manager, plugin_data_manager -from utils.utils import get_matchers -from services.log import logger -from pathlib import Path - - -def init_plugins_resources(): - """ - 资源文件路径的移动 - """ - for matcher in get_matchers(True): - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - try: - _module = matcher.plugin.module - except AttributeError: - logger.warning(f"插件 {matcher.plugin_name} 加载失败...,资源控制未加载...") - else: - if resources := plugin_data.plugin_resources: - path = Path(_module.__getattribute__("__file__")).parent - for resource in resources.keys(): - resources_manager.add_resource( - matcher.plugin_name, path / resource, resources[resource] - ) - resources_manager.save() - resources_manager.start_move() diff --git a/basic_plugins/init_plugin_config/init_plugins_settings.py b/basic_plugins/init_plugin_config/init_plugins_settings.py deleted file mode 100755 index cdf7b372..00000000 --- a/basic_plugins/init_plugin_config/init_plugins_settings.py +++ /dev/null @@ -1,56 +0,0 @@ -import nonebot - -from services.log import logger -from utils.manager import admin_manager, plugin_data_manager, plugins2settings_manager -from utils.manager.models import PluginType -from utils.utils import get_matchers - - -def init_plugins_settings(): - """ - 初始化插件设置,从插件中获取 __zx_plugin_name__,__plugin_cmd__,__plugin_settings__ - """ - # for x in plugins2settings_manager.keys(): - # try: - # _plugin = nonebot.plugin.get_plugin(x) - # _module = _plugin.module - # _module.__getattribute__("__zx_plugin_name__") - # except (KeyError, AttributeError) as e: - # logger.warning(f"配置文件 模块:{x} 获取 plugin_name 失败...{e}") - for matcher in get_matchers(True): - try: - if ( - matcher.plugin_name - and matcher.plugin_name not in plugins2settings_manager.keys() - ): - if _plugin := matcher.plugin: - try: - _module = _plugin.module - except AttributeError: - logger.warning(f"插件 {matcher.plugin_name} 加载失败...,插件控制未加载.") - else: - if plugin_data := plugin_data_manager.get(matcher.plugin_name): - if plugin_settings := plugin_data.plugin_setting: - if ( - name := _module.__getattribute__( - "__zx_plugin_name__" - ) - ) not in plugin_settings.cmd: - plugin_settings.cmd.append(name) - # 管理员命令 - if plugin_data.plugin_type == PluginType.ADMIN: - admin_manager.add_admin_plugin_settings( - matcher.plugin_name, - plugin_settings.cmd, - plugin_settings.level, - ) - else: - plugins2settings_manager.add_plugin_settings( - matcher.plugin_name, plugin_settings - ) - except Exception as e: - logger.error( - f"{matcher.plugin_name} 初始化 plugin_settings 发生错误 {type(e)}:{e}" - ) - plugins2settings_manager.save() - logger.info(f"已成功加载 {len(plugins2settings_manager.get_data())} 个非限制插件.") diff --git a/basic_plugins/invite_manager/utils.py b/basic_plugins/invite_manager/utils.py deleted file mode 100644 index aee7c6f1..00000000 --- a/basic_plugins/invite_manager/utils.py +++ /dev/null @@ -1,87 +0,0 @@ -import time -from dataclasses import dataclass -from typing import Dict - - -@dataclass -class PrivateRequest: - - """ - 好友请求 - """ - - user_id: int - time: float = time.time() - - -@dataclass -class GroupRequest: - - """ - 群聊请求 - """ - - user_id: int - group_id: int - time: float = time.time() - - -class RequestTimeManage: - - """ - 过滤五分钟以内的重复请求 - """ - - def __init__(self): - - self._group: Dict[str, GroupRequest] = {} - self._user: Dict[int, PrivateRequest] = {} - - def add_user_request(self, user_id: int) -> bool: - """ - 添加请求时间 - - Args: - user_id (int): 用户id - - Returns: - bool: 是否满足时间 - """ - if user := self._user.get(user_id): - if time.time() - user.time < 60 * 5: - return False - self._user[user_id] = PrivateRequest(user_id) - return True - - def add_group_request(self, user_id: int, group_id: int) -> bool: - """ - 添加请求时间 - - Args: - user_id (int): 用户id - group_id (int): 邀请群聊 - - Returns: - bool: 是否满足时间 - """ - key = f"{user_id}:{group_id}" - if group := self._group.get(key): - if time.time() - group.time < 60 * 5: - return False - self._group[key] = GroupRequest(user_id=user_id, group_id=group_id) - return True - - def clear(self): - """ - 清理过期五分钟请求 - """ - now = time.time() - for user_id in self._user: - if now - self._user[user_id].time < 60 * 5: - del self._user[user_id] - for key in self._group: - if now - self._group[key].time < 60 * 5: - del self._group[key] - - -time_manager = RequestTimeManage() diff --git a/basic_plugins/nickname.py b/basic_plugins/nickname.py deleted file mode 100755 index d9e104cc..00000000 --- a/basic_plugins/nickname.py +++ /dev/null @@ -1,207 +0,0 @@ -import random -from typing import Any, List, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import CommandArg, RegexGroup -from nonebot.rule import to_me - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.depends import GetConfig - -__zx_plugin_name__ = "昵称系统" -__plugin_usage__ = f""" -usage: - 个人昵称,将替换真寻称呼你的名称,群聊 与 私聊 昵称相互独立,全局昵称设置将更改您目前所有群聊中及私聊的昵称 - 指令: - 以后叫我 [昵称]: 设置当前群聊/私聊的昵称 - 全局昵称设置 [昵称]: 设置当前所有群聊和私聊的昵称 - {NICKNAME}我是谁 -""".strip() -__plugin_des__ = "区区昵称,才不想叫呢!" -__plugin_cmd__ = ["以后叫我 [昵称]", f"{NICKNAME}我是谁", "全局昵称设置 [昵称]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["昵称"], -} -__plugin_configs__ = { - "BLACK_WORD": { - "value": ["爸", "爹", "爷", "父"], - "help": "昵称所屏蔽的关键词,已设置的昵称会被替换为 *,未设置的昵称会在设置时提示", - "default_value": None, - "type": List[str], - } -} - -nickname = on_regex( - "(?:以后)?(?:叫我|请叫我|称呼我)(.*)", - rule=to_me(), - priority=5, - block=True, -) - -my_nickname = on_command( - "我叫什么", aliases={"我是谁", "我的名字"}, rule=to_me(), priority=5, block=True -) - -global_nickname = on_regex("设置全局昵称(.*)", rule=to_me(), priority=5, block=True) - - -cancel_nickname = on_command("取消昵称", rule=to_me(), priority=5, block=True) - - -def CheckNickname(): - """ - 说明: - 检查名称是否合法 - """ - - async def dependency( - bot: Bot, - matcher: Matcher, - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), - black_word: Any = GetConfig(config="BLACK_WORD"), - ): - (msg,) = reg_group - logger.debug(f"昵称检查: {msg}", "昵称设置", event.user_id) - if not msg: - await matcher.finish("叫你空白?叫你虚空?叫你无名??", at_sender=True) - if str(event.user_id) in bot.config.superusers: - logger.debug(f"超级用户设置昵称, 跳过合法检测: {msg}", "昵称设置", event.user_id) - return - if len(msg) > 20: - await nickname.finish("昵称可不能超过20个字!", at_sender=True) - if msg in bot.config.nickname: - await nickname.finish("笨蛋!休想占用我的名字!#", at_sender=True) - if black_word: - for x in msg: - if x in black_word: - logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", event.user_id) - await nickname.finish(f"字符 [{x}] 为禁止字符!", at_sender=True) - for word in black_word: - if word in msg: - logger.debug("昵称设置禁止字符: [{word}]", "昵称设置", event.user_id) - await nickname.finish(f"字符 [{word}] 为禁止字符!", at_sender=True) - - return Depends(dependency) - - -@global_nickname.handle(parameterless=[CheckNickname()]) -async def _( - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), -): - (msg,) = reg_group - await FriendUser.set_user_nickname(event.user_id, msg) - await GroupInfoUser.filter(user_id=str(event.user_id)).update(nickname=msg) - logger.info(f"设置全局昵称成功: {msg}", "设置全局昵称", event.user_id) - await global_nickname.send(f"设置全局昵称成功!亲爱的{msg}") - - -@nickname.handle(parameterless=[CheckNickname()]) -async def _( - event: MessageEvent, - reg_group: Tuple[Any, ...] = RegexGroup(), -): - (msg,) = reg_group - if isinstance(event, GroupMessageEvent): - await GroupInfoUser.set_user_nickname(event.user_id, event.group_id, msg) - if len(msg) < 5: - if random.random() < 0.3: - msg = "~".join(msg) - await nickname.send( - random.choice( - [ - f"好啦好啦,我知道啦,{msg},以后就这么叫你吧", - f"嗯嗯,{NICKNAME}记住你的昵称了哦,{msg}", - f"好突然,突然要叫你昵称什么的...{msg}..", - f"{NICKNAME}会好好记住{msg}的,放心吧", - f"好..好.,那窝以后就叫你{msg}了.", - ] - ) - ) - logger.info(f"设置群昵称成功: {msg}", "昵称设置", event.user_id, event.group_id) - else: - await FriendUser.set_user_nickname(event.user_id, msg) - await nickname.send( - random.choice( - [ - f"好啦好啦,我知道啦,{msg},以后就这么叫你吧", - f"嗯嗯,{NICKNAME}记住你的昵称了哦,{msg}", - f"好突然,突然要叫你昵称什么的...{msg}..", - f"{NICKNAME}会好好记住{msg}的,放心吧", - f"好..好.,那窝以后就叫你{msg}了.", - ] - ) - ) - logger.info(f"设置私聊昵称成功: {msg}", "昵称设置", event.user_id) - - -@my_nickname.handle() -async def _(event: MessageEvent): - nickname_ = None - card = None - if isinstance(event, GroupMessageEvent): - nickname_ = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - card = event.sender.card or event.sender.nickname - else: - nickname_ = await FriendUser.get_user_nickname(event.user_id) - card = event.sender.nickname - if nickname_: - await my_nickname.send( - random.choice( - [ - f"我肯定记得你啊,你是{nickname_}啊", - f"我不会忘记你的,你也不要忘记我!{nickname_}", - f"哼哼,{NICKNAME}记忆力可是很好的,{nickname_}", - f"嗯?你是失忆了嘛...{nickname_}..", - f"不要小看{NICKNAME}的记忆力啊!笨蛋{nickname_}!QAQ", - f"哎?{nickname_}..怎么了吗..突然这样问..", - ] - ) - ) - else: - await my_nickname.send( - random.choice( - ["没..没有昵称嘛,{}", "啊,你是{}啊,我想叫你的昵称!", "是{}啊,有什么事吗?", "你是{}?"] - ).format(card) - ) - - -@cancel_nickname.handle() -async def _(event: MessageEvent): - nickname_ = None - if isinstance(event, GroupMessageEvent): - nickname_ = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - else: - nickname_ = await FriendUser.get_user_nickname(event.user_id) - if nickname_: - await cancel_nickname.send( - random.choice( - [ - f"呜..{NICKNAME}睡一觉就会忘记的..和梦一样..{nickname_}", - f"窝知道了..{nickname_}..", - f"是{NICKNAME}哪里做的不好嘛..好吧..晚安{nickname_}", - f"呃,{nickname_},下次我绝对绝对绝对不会再忘记你!", - f"可..可恶!{nickname_}!太可恶了!呜", - ] - ) - ) - if isinstance(event, GroupMessageEvent): - await GroupInfoUser.set_user_nickname(event.user_id, event.group_id, "") - else: - await FriendUser.set_user_nickname(event.user_id, "") - await BanUser.ban(event.user_id, 9, 60) - else: - await cancel_nickname.send("你在做梦吗?你没有昵称啊", at_sender=True) diff --git a/basic_plugins/plugin_shop/__init__.py b/basic_plugins/plugin_shop/__init__.py deleted file mode 100644 index 0ef17cb9..00000000 --- a/basic_plugins/plugin_shop/__init__.py +++ /dev/null @@ -1,75 +0,0 @@ -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.message_builder import image - -from .data_source import ( - download_json, - install_plugin, - show_plugin_repo, - uninstall_plugin, -) - -__zx_plugin_name__ = "插件商店 [Superuser]" -__plugin_usage__ = """ -usage: - 下载安装插件 - 指令: - 查看插件仓库 - 更新插件仓库 - 安装插件 [name/id] (重新安装等同于更新) - 卸载插件 [name/id] -""".strip() -__plugin_des__ = "从真寻适配仓库中下载插件" -__plugin_cmd__ = ["查看插件仓库", "更新插件仓库", "安装插件 [name/id]", "卸载插件 [name/id]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -show_repo = on_regex("^查看插件仓库$", priority=1, block=True, permission=SUPERUSER) - -update_repo = on_regex("^更新插件仓库$", priority=1, block=True, permission=SUPERUSER) - -install_plugin_matcher = on_command( - "安装插件", priority=1, block=True, permission=SUPERUSER -) - -uninstall_plugin_matcher = on_command( - "卸载插件", priority=1, block=True, permission=SUPERUSER -) - - -@install_plugin_matcher.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - name = arg.extract_plain_text().strip() - msg = await install_plugin(name) - await install_plugin_matcher.send(msg) - logger.info(f"安装插件: {name}", "安装插件", event.user_id) - - -@uninstall_plugin_matcher.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - name = arg.extract_plain_text().strip() - msg = await uninstall_plugin(name) - await install_plugin_matcher.send(msg) - logger.info(f"卸载插件: {name}", "卸载插件", event.user_id) - - -@update_repo.handle() -async def _(bot: Bot, event: MessageEvent): - code = await download_json() - if code == 200: - await update_repo.finish("更新插件仓库信息成功!") - await update_repo.send("更新插件仓库信息失败!") - logger.info("更新插件仓库信息", "更新插件仓库信息", event.user_id) - - -@show_repo.handle() -async def _(bot: Bot, event: MessageEvent): - msg = await show_plugin_repo() - if isinstance(msg, int): - await show_repo.finish("文件下载失败或解压失败..") - await show_repo.send(image(msg)) - logger.info("查看插件仓库", "查看插件仓库", event.user_id) diff --git a/basic_plugins/plugin_shop/data_source.py b/basic_plugins/plugin_shop/data_source.py deleted file mode 100644 index 815527df..00000000 --- a/basic_plugins/plugin_shop/data_source.py +++ /dev/null @@ -1,200 +0,0 @@ -import os -import shutil -import zipfile -from pathlib import Path -from typing import Tuple, Union - -import ujson as json - -from configs.path_config import DATA_PATH, TEMP_PATH -from services import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, text2image -from utils.manager import plugins_manager -from utils.utils import is_number - -path = DATA_PATH / "plugin_shop" -if not path.exists(): - path.mkdir(parents=True, exist_ok=True) - -repo_json_url = "https://github.com/zhenxun-org/nonebot_plugins_zhenxun_bot/archive/refs/heads/index.zip" -repo_zip_path = TEMP_PATH / "plugin_repo_json.zip" -plugin_json = path / "zhenxun_plugins.json" - -extensive_plugin_path = Path() / "extensive_plugin" - -path = DATA_PATH / "plugin_shop" -if not path.exists(): - path.mkdir(parents=True, exist_ok=True) -if not extensive_plugin_path.exists(): - extensive_plugin_path.mkdir(parents=True, exist_ok=True) - -data = {} - - -async def install_plugin(name: str) -> str: - """ - 安装插件 - :param name: 插件名或下标 - """ - try: - if is_number(name): - name, code = await get_plugin_name(int(name)) - if code != 200: - return name - plugin_url = data[name]["download_url"] - url = (await AsyncHttpx.get(plugin_url)).headers.get("Location") - zip_file = TEMP_PATH / f"{name}.zip" - if zip_file.exists(): - zip_file.unlink() - if await AsyncHttpx.download_file(url, zip_file): - logger.debug("开始解压插件压缩包...", "安装插件", target=name) - # 解压 - zf = zipfile.ZipFile(zip_file, "r") - extract_path = TEMP_PATH / f"{name}" - if extract_path.exists(): - shutil.rmtree(extract_path.absolute(), ignore_errors=True) - extract_path.mkdir(exist_ok=True, parents=True) - for file in zf.namelist(): - zf.extract(file, extract_path) - zf.close() - logger.debug("解压插件压缩包完成...", "安装插件", target=name) - logger.debug("开始移动插件文件夹...", "安装插件", target=name) - if (extensive_plugin_path / f"{name}").exists(): - logger.debug( - "extensive_plugin目录下文件夹已存在,删除该目录插件文件夹...", "安装插件", target=name - ) - shutil.rmtree( - (extensive_plugin_path / f"{name}").absolute(), ignore_errors=True - ) - extract_path.rename(extensive_plugin_path / f"{name}") - prompt = "" - if "pyproject.toml" in os.listdir(extensive_plugin_path / f"{name}"): - prompt = "检测到该插件含有额外依赖,当前安装无法保证依赖完全安装成功。" - os.system( - f"poetry run pip install -r {(extensive_plugin_path / f'{name}' / 'pyproject.toml').absolute()}" - ) - elif "requirements.txt" in os.listdir(extensive_plugin_path / f"{name}"): - prompt = "检测到该插件含有额外依赖,当前安装无法保证依赖完全安装成功。" - os.system( - f"poetry run pip install -r {(extensive_plugin_path / f'{name}' / 'requirements.txt').absolute()}" - ) - with open(extensive_plugin_path / f"{name}" / "plugin_info.json", "w") as f: - json.dump(data[name], f, ensure_ascii=False, indent=4) - logger.debug("移动插件文件夹完成...", "安装插件", target=name) - logger.info(f"成功安装插件 {name} 成功!\n{prompt}", "安装插件", target=name) - return f"成功安装插件 {name},请重启真寻!" - except Exception as e: - logger.error(f"安装插失败", "安装插件", target=name, e=e) - return f"安装插件 {name} 失败 {type(e)}:{e}" - - -async def uninstall_plugin(name: str) -> str: - """ - 删除插件 - :param name: 插件名或下标 - """ - try: - if is_number(name): - name, code = await get_plugin_name(int(name)) - if code != 200: - return name - if name not in os.listdir(extensive_plugin_path): - return f"未安装 {name} 插件!" - shutil.rmtree((extensive_plugin_path / name).absolute(), ignore_errors=True) - logger.info(f"插件 {name} 删除成功!") - return f"插件 {name} 删除成功!" - except Exception as e: - logger.error(f"删除插件失败", target=name, e=e) - return f"删除插件 {name} 失败 {type(e)}:{e}" - - -async def show_plugin_repo() -> Union[int, str]: - """ - 获取插件仓库数据并格式化 - """ - if not plugin_json.exists(): - code = await download_json() - if code != 200: - return code - plugin_info = json.load(open(plugin_json, "r", encoding="utf8")) - plugins_data = plugins_manager.get_data() - load_plugin_list = plugins_data.keys() - image_list = [] - w, h = 0, 0 - line_height = 10 - for i, key in enumerate(plugin_info.keys()): - data[key] = { - "名称": plugin_info[key]["plugin_name"], - "模块": key, - "作者": plugin_info[key]["author"], - "版本": plugin_info[key]["version"], - "简介": plugin_info[key]["introduction"], - "download_url": plugin_info[key]["download_url"], - "github_url": plugin_info[key]["github_url"], - } - status = "" - version = "" - if key in load_plugin_list: - status = "[已安装]" - version = f"[{plugins_data[key].version}]" - s = ( - f'id:{i+1}\n名称:{plugin_info[key]["plugin_name"]}' - f" \t\t{status}\n" - f"模块:{key}\n" - f'作者:{plugin_info[key]["author"]}\n' - f'版本:{plugin_info[key]["version"]} \t\t{version}\n' - f'简介:{plugin_info[key]["introduction"]}\n' - f"-------------------" - ) - img = await text2image(s, font_size=20, color="#f9f6f2") - w = w if w > img.w else img.w - h += img.h + line_height - image_list.append(img) - A = BuildImage(w + 50, h + 50, color="#f9f6f2") - cur_h = 25 - for img in image_list: - await A.apaste(img, (25, cur_h)) - cur_h += img.h + line_height - return A.pic2bs4() - - -async def download_json() -> int: - """ - 下载插件库json文件 - """ - try: - url = (await AsyncHttpx.get(repo_json_url)).headers.get("Location") - if repo_zip_path.exists(): - repo_zip_path.unlink() - if await AsyncHttpx.download_file(url, repo_zip_path): - zf = zipfile.ZipFile(repo_zip_path, "r") - extract_path = path / "temp" - for file in zf.namelist(): - zf.extract(file, extract_path) - zf.close() - if plugin_json.exists(): - plugin_json.unlink() - ( - extract_path - / "nonebot_plugins_zhenxun_bot-index" - / "zhenxun_plugins.json" - ).rename(plugin_json) - shutil.rmtree(extract_path.absolute(), ignore_errors=True) - return 200 - except Exception as e: - logger.error(f"下载插件库压缩包失败或解压失败", e=e) - return 999 - - -async def get_plugin_name(index: int) -> Tuple[str, int]: - """ - 通过下标获取插件名 - :param name: 下标 - """ - if not data: - await show_plugin_repo() - if index < 1 or index > len(data.keys()): - return "下标超过上下限!", 999 - name = list(data.keys())[index - 1] - return name, 200 diff --git a/basic_plugins/scripts.py b/basic_plugins/scripts.py deleted file mode 100755 index 1a6bc456..00000000 --- a/basic_plugins/scripts.py +++ /dev/null @@ -1,108 +0,0 @@ -import random -from asyncio.exceptions import TimeoutError - -import nonebot -from nonebot.adapters.onebot.v11 import Bot -from nonebot.drivers import Driver - -from configs.path_config import TEXT_PATH -from models.bag_user import BagUser -from models.group_info import GroupInfo -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import GDict, scheduler - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def update_city(): - """ - 部分插件需要中国省份城市 - 这里直接更新,避免插件内代码重复 - """ - china_city = TEXT_PATH / "china_city.json" - data = {} - if not china_city.exists(): - try: - logger.debug("开始更新城市列表...") - res = await AsyncHttpx.get( - "http://www.weather.com.cn/data/city3jdata/china.html", timeout=5 - ) - res.encoding = "utf8" - provinces_data = json.loads(res.text) - for province in provinces_data.keys(): - data[provinces_data[province]] = [] - res = await AsyncHttpx.get( - f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html", - timeout=5, - ) - res.encoding = "utf8" - city_data = json.loads(res.text) - for city in city_data.keys(): - data[provinces_data[province]].append(city_data[city]) - with open(china_city, "w", encoding="utf8") as f: - json.dump(data, f, indent=4, ensure_ascii=False) - logger.info("自动更新城市列表完成.....") - except TimeoutError as e: - logger.warning("自动更新城市列表超时...", e=e) - except ValueError as e: - logger.warning("自动城市列表失败.....", e=e) - except Exception as e: - logger.error(f"自动城市列表未知错误", e=e) - - -@driver.on_bot_connect -async def _(bot: Bot): - """ - 版本某些需要的变换 - """ - # 清空不存在的群聊信息,并将已所有已存在的群聊group_flag设置为1(认证所有已存在的群) - if not await GroupInfo.get_or_none(group_id=114514): - # 标识符,该功能只需执行一次 - await GroupInfo.create( - group_id=114514, - group_name="114514", - max_member_count=114514, - member_count=114514, - group_flag=1, - ) - group_list = await bot.get_group_list() - group_list = [g["group_id"] for g in group_list] - _gl = [x.group_id for x in await GroupInfo.all()] - if 114514 in _gl: - _gl.remove(114514) - for group_id in _gl: - if group_id in group_list: - if group := await GroupInfo.get_or_none(group_id=group_id): - group.group_flag = 1 - await group.save(update_fields=["group_flag"]) - else: - group_info = await bot.get_group_info(group_id=group_id) - await GroupInfo.create( - group_id=group_info["group_id"], - group_name=group_info["group_name"], - max_member_count=group_info["max_member_count"], - member_count=group_info["member_count"], - group_flag=1, - ) - logger.info(f"已添加群认证...", group_id=group_id) - else: - await GroupInfo.filter(group_id=group_id).delete() - logger.info(f"移除不存在的群聊信息", group_id=group_id) - - -# 自动更新城市列表 -@scheduler.scheduled_job( - "cron", - hour=6, - minute=1, -) -async def _(): - await update_city() diff --git a/basic_plugins/shop/__init__.py b/basic_plugins/shop/__init__.py deleted file mode 100644 index f6a49be9..00000000 --- a/basic_plugins/shop/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from pathlib import Path - -import nonebot -from nonebot.drivers import Driver - -from configs.config import Config -from utils.decorator.shop import shop_register - -driver: Driver = nonebot.get_driver() - - -Config.add_plugin_config( - "shop", - "IMPORT_DEFAULT_SHOP_GOODS", - True, - help_="导入商店自带的三个商品", - default_value=True, - type=bool, -) - - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) - - -@driver.on_bot_connect -async def _(): - await shop_register.load_register() diff --git a/basic_plugins/shop/buy.py b/basic_plugins/shop/buy.py deleted file mode 100644 index 61701e00..00000000 --- a/basic_plugins/shop/buy.py +++ /dev/null @@ -1,109 +0,0 @@ -import time - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from models.bag_user import BagUser -from models.goods_info import GoodsInfo -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.utils import is_number - -__zx_plugin_name__ = "商店 - 购买道具" -__plugin_usage__ = """ -usage: - 购买道具 - 指令: - 购买 [序号或名称] ?[数量=1] - 示例:购买 好感双倍加持卡Ⅰ - 示例:购买 1 4 -""".strip() -__plugin_des__ = "商店 - 购买道具" -__plugin_cmd__ = ["购买 [序号或名称] ?[数量=1]"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "购买道具"], -} -__plugin_cd_limit__ = {"cd": 3} - - -buy = on_command("购买", aliases={"购买道具"}, priority=5, block=True, permission=GROUP) - - -@buy.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - goods = None - if arg.extract_plain_text().strip() in ["神秘药水"]: - await buy.finish("你们看看就好啦,这是不可能卖给你们的~", at_sender=True) - goods_list = [ - x - for x in await GoodsInfo.get_all_goods() - if x.goods_limit_time > time.time() or x.goods_limit_time == 0 - ] - goods_name_list = [x.goods_name for x in goods_list] - msg = arg.extract_plain_text().strip().split() - num = 1 - if len(msg) > 1: - if is_number(msg[1]) and int(msg[1]) > 0: - num = int(msg[1]) - else: - await buy.finish("购买的数量要是数字且大于0!", at_sender=True) - if is_number(msg[0]): - msg = int(msg[0]) - if msg > len(goods_name_list) or msg < 1: - await buy.finish("请输入正确的商品id!", at_sender=True) - goods = goods_list[msg - 1] - else: - if msg[0] in goods_name_list: - for i in range(len(goods_name_list)): - if msg[0] == goods_name_list[i]: - goods = goods_list[i] - break - else: - await buy.finish("请输入正确的商品名称!") - else: - await buy.finish("请输入正确的商品名称!", at_sender=True) - if ( - await BagUser.get_gold(event.user_id, event.group_id) - ) < goods.goods_price * num * goods.goods_discount: - await buy.finish("您的金币好像不太够哦", at_sender=True) - flag, n = await GoodsInfo.check_user_daily_purchase( - goods, event.user_id, event.group_id, num - ) - if flag: - await buy.finish(f"该次购买将超过每日次数限制,目前该道具还可以购买{n}次哦", at_sender=True) - spend_gold = int(goods.goods_discount * goods.goods_price * num) - await BagUser.spend_gold(event.user_id, event.group_id, spend_gold) - await BagUser.add_property(event.user_id, event.group_id, goods.goods_name, num) - await GoodsInfo.add_user_daily_purchase(goods, event.user_id, event.group_id, num) - await buy.send( - f"花费 {goods.goods_price * num * goods.goods_discount} 金币购买 {goods.goods_name} ×{num} 成功!", - at_sender=True, - ) - logger.info( - f"花费 {goods.goods_price*num} 金币购买 {goods.goods_name} ×{num} 成功!", - "购买道具", - event.user_id, - event.group_id, - ) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=0, - name=goods.goods_name, - num=num, - spend_gold=goods.goods_price * num * goods.goods_discount, - ) - # else: - # await buy.send(f"{goods.goods_name} 购买失败!", at_sender=True) - # logger.info( - # f"USER {event.user_id} GROUP {event.group_id} " - # f"花费 {goods.goods_price * num * goods.goods_discount} 金币购买 {goods.goods_name} ×{num} 失败!" - # ) diff --git a/basic_plugins/shop/gold.py b/basic_plugins/shop/gold.py deleted file mode 100644 index dcc3def9..00000000 --- a/basic_plugins/shop/gold.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ActionFailed, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg - -from models.bag_user import BagUser -from utils.data_utils import init_rank -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "商店 - 我的金币" -__plugin_usage__ = """ -usage: - 我的金币 - 指令: - 我的金币 -""".strip() -__plugin_des__ = "商店 - 我的金币" -__plugin_cmd__ = ["我的金币"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "我的金币"], -} - - -my_gold = on_command("我的金币", priority=5, block=True, permission=GROUP) - -gold_rank = on_command("金币排行", priority=5, block=True, permission=GROUP) - - -@my_gold.handle() -async def _(event: GroupMessageEvent): - msg = await BagUser.get_user_total_gold(event.user_id, event.group_id) - try: - await my_gold.send(msg) - except ActionFailed: - await my_gold.send( - image(b64=(await text2image(msg, color="#f9f6f2", padding=10)).pic2bs4()) - ) - - -@gold_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - all_users = await BagUser.filter(group_id=event.group_id) - all_user_id = [user.user_id for user in all_users] - all_user_data = [user.gold for user in all_users] - rank_image = await init_rank( - "金币排行", all_user_id, all_user_data, event.group_id, num - ) - if rank_image: - await gold_rank.finish(image(b64=rank_image.pic2bs4())) diff --git a/basic_plugins/shop/my_props/__init__.py b/basic_plugins/shop/my_props/__init__.py deleted file mode 100644 index 07faf00c..00000000 --- a/basic_plugins/shop/my_props/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from models.bag_user import BagUser -from services.log import logger -from utils.message_builder import image - -from ._data_source import create_bag_image - -__zx_plugin_name__ = "商店 - 我的道具" -__plugin_usage__ = """ -usage: - 我的道具 - 指令: - 我的道具 -""".strip() -__plugin_des__ = "商店 - 我的道具" -__plugin_cmd__ = ["我的道具"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "我的道具"], -} - - -my_props = on_command("我的道具", priority=5, block=True, permission=GROUP) - - -@my_props.handle() -async def _(event: GroupMessageEvent): - props = await BagUser.get_property(str(event.user_id), str(event.group_id)) - if props: - await my_props.send(image(b64=await create_bag_image(props))) - logger.info(f"查看我的道具", "我的道具", event.user_id, event.group_id) - else: - await my_props.finish("您的背包里没有任何的道具噢~", at_sender=True) diff --git a/basic_plugins/shop/my_props/_data_source.py b/basic_plugins/shop/my_props/_data_source.py deleted file mode 100644 index dec64650..00000000 --- a/basic_plugins/shop/my_props/_data_source.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Dict, List, Optional - -from models.bag_user import BagUser -from models.goods_info import GoodsInfo -from utils.image_utils import BuildImage -from configs.path_config import IMAGE_PATH - - -icon_path = IMAGE_PATH / "shop_icon" - - -async def create_bag_image(props: Dict[str, int]): - """ - 说明: - 创建背包道具图片 - 参数: - :param props: 道具仓库字典 - """ - goods_list = await GoodsInfo.get_all_goods() - active_props = await _init_prop(props, [x for x in goods_list if not x.is_passive]) - passive_props = await _init_prop(props, [x for x in goods_list if x.is_passive]) - active_w = 0 - active_h = 0 - passive_w = 0 - passive_h = 0 - if active_props: - img = BuildImage( - active_props.w, - active_props.h + 70, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - await img.apaste(active_props, (0, 70)) - await img.atext((0, 30), "主动道具") - active_props = img - active_w = img.w - active_h = img.h - if passive_props: - img = BuildImage( - passive_props.w, - passive_props.h + 70, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - await img.apaste(passive_props, (0, 70)) - await img.atext((0, 30), "被动道具") - passive_props = img - passive_w = img.w - passive_h = img.h - A = BuildImage( - active_w + passive_w + 100, - max(active_h, passive_h) + 60, - font="CJGaoDeGuo.otf", - font_size=30, - color="#f9f6f2", - ) - curr_w = 50 - if active_props: - await A.apaste(active_props, (curr_w, 0)) - curr_w += active_props.w + 10 - if passive_props: - await A.apaste(passive_props, (curr_w, 0)) - if active_props and passive_props: - await A.aline( - (active_props.w + 45, 70, active_props.w + 45, A.h - 20), fill=(0, 0, 0) - ) - return A.pic2bs4() - - -async def _init_prop( - props: Dict[str, int], _props: List[GoodsInfo] -) -> Optional[BuildImage]: - """ - 说明: - 构造道具列表图片 - 参数: - :param props: 道具仓库字典 - :param _props: 道具列表 - """ - active_name = [x.goods_name for x in _props] - name_list = [x for x in props.keys() if x in active_name] - if not name_list: - return None - temp_img = BuildImage(0, 0, font_size=20) - image_list = [] - num_list = [] - for i, name in enumerate(name_list): - img = BuildImage( - temp_img.getsize(name)[0] + 50, - 30, - font="msyh.ttf", - font_size=20, - color="#f9f6f2", - ) - await img.atext((30, 5), f"{i + 1}.{name}") - goods = [x for x in _props if x.goods_name == name][0] - if goods.icon and (icon_path / goods.icon).exists(): - icon = BuildImage(30, 30, background=icon_path / goods.icon) - await img.apaste(icon, alpha=True) - image_list.append(img) - num_list.append( - BuildImage( - 30, 30, font_size=20, font="msyh.ttf", plain_text=f"×{props[name]}" - ) - ) - max_w = 0 - num_max_w = 0 - h = 0 - for img, num in zip(image_list, num_list): - h += img.h - max_w = max_w if max_w > img.w else img.w - num_max_w = num_max_w if num_max_w > num.w else num.w - A = BuildImage(max_w + num_max_w + 30, h, color="#f9f6f2") - curr_h = 0 - for img, num in zip(image_list, num_list): - await A.apaste(img, (0, curr_h)) - await A.apaste(num, (max_w + 20, curr_h + 5), True) - curr_h += img.h - return A diff --git a/basic_plugins/shop/shop_handle/__init__.py b/basic_plugins/shop/shop_handle/__init__.py deleted file mode 100644 index d436f78b..00000000 --- a/basic_plugins/shop/shop_handle/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from configs.path_config import IMAGE_PATH -from models.bag_user import BagUser -from services.log import logger -from utils.message_builder import image -from utils.utils import is_number, scheduler - -from .data_source import ( - GoodsInfo, - create_shop_help, - delete_goods, - parse_goods_info, - register_goods, - update_goods, -) - -__zx_plugin_name__ = "商店" -__plugin_usage__ = """ -usage: - 商店项目,这可不是奸商 - 指令: - 商店 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 商品操作 - 指令: - 添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时) - 删除商品 [名称或序号] - 修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] - 示例:添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 - 示例:添加商品 name:可疑的药 price:5 des:效果未知 - 示例:删除商品 2 - 示例:修改商品 name:1 price:900 修改序号为1的商品的价格为900 - * 修改商品只需添加需要值即可 * -""".strip() -__plugin_des__ = "商店系统[金币回收计划]" -__plugin_cmd__ = [ - "商店", - "添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时)) [_superuser]", - "删除商品 [名称或序号] [_superuser]", - "修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] [_superuser]", -] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店"], -} -__plugin_block_limit__ = {"limit_type": "group"} - - -shop_help = on_command("商店", priority=5, block=True) - -shop_add_goods = on_command("添加商品", priority=5, permission=SUPERUSER, block=True) - -shop_del_goods = on_command("删除商品", priority=5, permission=SUPERUSER, block=True) - -shop_update_goods = on_command("修改商品", priority=5, permission=SUPERUSER, block=True) - - -@shop_help.handle() -async def _(): - await shop_help.send(image(b64=await create_shop_help())) - - -@shop_add_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - data = parse_goods_info(msg) - if isinstance(data, str): - await shop_add_goods.finish(data) - if not data.get("name") or not data.get("price") or not data.get("des"): - await shop_add_goods.finish("name:price:des 参数不可缺少!") - if await register_goods(**data): - await shop_add_goods.send( - f"添加商品 {data['name']} 成功!\n" - f"名称:{data['name']}\n" - f"价格:{data['price']}金币\n" - f"简介:{data['des']}\n" - f"折扣:{data.get('discount')}\n" - f"限时:{data.get('limit_time')}", - at_sender=True, - ) - logger.info(f"添加商品 {msg} 成功", "添加商品", event.user_id) - else: - await shop_add_goods.send(f"添加商品 {msg} 失败了...", at_sender=True) - logger.warning(f"添加商品 {msg} 失败", "添加商品", event.user_id) - - -@shop_del_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - name = "" - id_ = 0 - if is_number(msg): - id_ = int(msg) - else: - name = msg - rst, goods_name, code = await delete_goods(name, id_) - if code == 200: - await shop_del_goods.send(f"删除商品 {goods_name} 成功了...", at_sender=True) - if os.path.exists(f"{IMAGE_PATH}/shop_help.png"): - os.remove(f"{IMAGE_PATH}/shop_help.png") - logger.info(f"删除商品成功", "删除商品", event.user_id, target=goods_name) - else: - await shop_del_goods.send(f"删除商品 {goods_name} 失败了...", at_sender=True) - logger.info(f"删除商品失败", "删除商品", event.user_id, target=goods_name) - - -@shop_update_goods.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - data = parse_goods_info(msg) - if isinstance(data, str): - await shop_add_goods.finish(data) - if not data.get("name"): - await shop_add_goods.finish("name 参数不可缺少!") - flag, name, text = await update_goods(**data) - if flag: - await shop_update_goods.send(f"修改商品 {name} 成功了...\n{text}", at_sender=True) - logger.info(f"修改商品数据 {text} 成功", "修改商品", event.user_id, target=name) - else: - await shop_update_goods.send(name, at_sender=True) - logger.info(f"修改商品数据 {text} 失败", "修改商品", event.user_id, target=name) - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=0, -) -async def _(): - try: - await GoodsInfo.all().update(daily_purchase_limit={}) - logger.info("商品每日限购次数重置成功...") - except Exception as e: - logger.error(f"商品每日限购次数重置出错", e=e) - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - try: - await BagUser.all().update(get_today_gold=0, spend_today_gold=0) - except Exception as e: - logger.error(f"重置每日金币", "定时任务", e=e) diff --git a/basic_plugins/shop/shop_handle/data_source.py b/basic_plugins/shop/shop_handle/data_source.py deleted file mode 100644 index 2d92e202..00000000 --- a/basic_plugins/shop/shop_handle/data_source.py +++ /dev/null @@ -1,416 +0,0 @@ -import time -from typing import List, Optional, Tuple, Union - -from PIL import Image - -from configs.path_config import IMAGE_PATH -from models.goods_info import GoodsInfo -from utils.image_utils import BuildImage, text2image -from utils.utils import GDict, is_number - -icon_path = IMAGE_PATH / "shop_icon" - -# 创建商店界面 -async def create_shop_help() -> str: - """ - 制作商店图片 - :return: 图片base64 - """ - goods_lst = await GoodsInfo.get_all_goods() - _dc = {} - font_h = BuildImage(0, 0).getsize("正")[1] - h = 10 - _list: List[GoodsInfo] = [] - for goods in goods_lst: - if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time: - _list.append(goods) - # A = BuildImage(1100, h, color="#f9f6f2") - total_n = 0 - image_list = [] - for idx, goods in enumerate(_list): - name_image = BuildImage( - 580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf" - ) - await name_image.atext( - (15, 0), f"{idx + 1}.{goods.goods_name}", center_type="by_height" - ) - await name_image.aline((380, -5, 280, 45), "#a29ad6", 5) - await name_image.atext((390, 0), "售价:", center_type="by_height") - if goods.goods_discount != 1: - discount_price = int(goods.goods_discount * goods.goods_price) - old_price_image = BuildImage( - 0, - 0, - plain_text=str(goods.goods_price), - font_color=(194, 194, 194), - font="CJGaoDeGuo.otf", - font_size=15, - ) - await old_price_image.aline( - ( - 0, - int(old_price_image.h / 2), - old_price_image.w + 1, - int(old_price_image.h / 2), - ), - (0, 0, 0), - ) - await name_image.apaste(old_price_image, (440, 0), True) - await name_image.atext((440, 15), str(discount_price), (255, 255, 255)) - else: - await name_image.atext( - (440, 0), - str(goods.goods_price), - (255, 255, 255), - center_type="by_height", - ) - await name_image.atext( - ( - 440 - + BuildImage(0, 0, plain_text=str(goods.goods_price), font_size=25).w, - 0, - ), - f" 金币", - center_type="by_height", - ) - des_image = None - font_img = BuildImage( - 600, 80, font_size=20, color="#a29ad6", font="CJGaoDeGuo.otf" - ) - p = font_img.getsize("简介:")[0] + 20 - if goods.goods_description: - des_list = goods.goods_description.split("\n") - desc = "" - for des in des_list: - if font_img.getsize(des)[0] > font_img.w - p - 20: - msg = "" - tmp = "" - for i in range(len(des)): - if font_img.getsize(tmp)[0] < font_img.w - p - 20: - tmp += des[i] - else: - msg += tmp + "\n" - tmp = des[i] - desc += msg - if tmp: - desc += tmp - else: - desc += des + "\n" - if desc[-1] == "\n": - desc = desc[:-1] - des_image = await text2image(desc, color="#a29ad6") - goods_image = BuildImage( - 600, - (50 + des_image.h) if des_image else 50, - font_size=20, - color="#a29ad6", - font="CJGaoDeGuo.otf", - ) - if des_image: - await goods_image.atext((15, 50), "简介:") - await goods_image.apaste(des_image, (p, 50)) - await name_image.acircle_corner(5) - await goods_image.apaste(name_image, (0, 5), True, center_type="by_width") - await goods_image.acircle_corner(20) - bk = BuildImage( - 1180, - (50 + des_image.h) if des_image else 50, - font_size=15, - color="#f9f6f2", - font="CJGaoDeGuo.otf", - ) - if goods.icon and (icon_path / goods.icon).exists(): - icon = BuildImage(70, 70, background=icon_path / goods.icon) - await bk.apaste(icon) - await bk.apaste(goods_image, (70, 0), alpha=True) - n = 0 - _w = 650 - # 添加限时图标和时间 - if goods.goods_limit_time > 0: - n += 140 - _limit_time_logo = BuildImage( - 40, 40, background=f"{IMAGE_PATH}/other/time.png" - ) - await bk.apaste(_limit_time_logo, (_w + 50, 0), True) - await bk.apaste( - BuildImage(0, 0, plain_text="限时!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 10), - True, - ) - limit_time = time.strftime( - "%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time) - ).split() - y_m_d = limit_time[0] - _h_m = limit_time[1].split(":") - h_m = _h_m[0] + "时 " + _h_m[1] + "分" - await bk.atext((_w + 55, 38), str(y_m_d)) - await bk.atext((_w + 65, 57), str(h_m)) - _w += 140 - if goods.goods_discount != 1: - n += 140 - _discount_logo = BuildImage( - 30, 30, background=f"{IMAGE_PATH}/other/discount.png" - ) - await bk.apaste(_discount_logo, (_w + 50, 10), True) - await bk.apaste( - BuildImage(0, 0, plain_text="折扣!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 15), - True, - ) - await bk.apaste( - BuildImage( - 0, - 0, - plain_text=f"{10 * goods.goods_discount:.1f} 折", - font_size=30, - font="CJGaoDeGuo.otf", - font_color=(85, 156, 75), - ), - (_w + 50, 44), - True, - ) - _w += 140 - if goods.daily_limit != 0: - n += 140 - _daily_limit_logo = BuildImage( - 35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png" - ) - await bk.apaste(_daily_limit_logo, (_w + 50, 10), True) - await bk.apaste( - BuildImage(0, 0, plain_text="限购!", font_size=23, font="CJGaoDeGuo.otf"), - (_w + 90, 20), - True, - ) - await bk.apaste( - BuildImage( - 0, - 0, - plain_text=f"{goods.daily_limit}", - font_size=30, - font="CJGaoDeGuo.otf", - ), - (_w + 72, 45), - True, - ) - if total_n < n: - total_n = n - if n: - await bk.aline((650, -1, 650 + n, -1), "#a29ad6", 5) - # await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5) - - # 添加限时图标和时间 - image_list.append(bk) - # await A.apaste(bk, (0, current_h), True) - # current_h += 90 - h = 0 - current_h = 0 - for img in image_list: - h += img.h + 10 - A = BuildImage(1100, h, color="#f9f6f2") - for img in image_list: - await A.apaste(img, (0, current_h), True) - current_h += img.h + 10 - w = 950 - if total_n: - w += total_n - h = A.h + 230 + 100 - h = 1000 if h < 1000 else h - shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png") - shop = BuildImage(w, h, font_size=20, color="#f9f6f2") - zx_img = BuildImage(0, 0, background=f"{IMAGE_PATH}/zhenxun/toukan_3.png") - zx_img.transpose(Image.FLIP_LEFT_RIGHT) - zx_img.replace_color_tran(((240, 240, 240), (255, 255, 255)), (249, 246, 242)) - await shop.apaste(zx_img, (0, 100)) - shop.paste(A, (20 + zx_img.w, 230)) - await shop.apaste(shop_logo, (450, 30), True) - shop.text( - (int((1000 - shop.getsize("注【通过 序号 或者 商品名称 购买】")[0]) / 2), 170), - "注【通过 序号 或者 商品名称 购买】", - ) - shop.text((20 + zx_img.w, h - 100), "神秘药水\t\t售价:9999999金币\n\t\t鬼知道会有什么效果~") - return shop.pic2bs4() - - -async def register_goods( - name: str, - price: int, - des: str, - discount: Optional[float] = 1, - limit_time: Optional[int] = 0, - daily_limit: Optional[int] = 0, - is_passive: Optional[bool] = False, - icon: Optional[str] = None, -) -> bool: - """ - 添加商品 - 例如: 折扣:可选参数↓ 限时时间:可选,单位为小时 - 添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 - 添加商品 name:可疑的药 price:5 des:效果未知 - :param name: 商品名称 - :param price: 商品价格 - :param des: 商品简介 - :param discount: 商品折扣 - :param limit_time: 商品限时销售时间,单位为小时 - :param daily_limit: 每日购买次数限制 - :param is_passive: 是否为被动 - :param icon: 图标 - :return: 是否添加成功 - """ - if not await GoodsInfo.get_or_none(goods_name=name): - limit_time_ = float(limit_time) if limit_time else limit_time - discount = discount if discount is not None else 1 - limit_time_ = ( - int(time.time() + limit_time_ * 60 * 60) - if limit_time_ is not None and limit_time_ != 0 - else 0 - ) - await GoodsInfo.create( - goods_name=name, - goods_price=int(price), - goods_description=des, - goods_discount=float(discount), - goods_limit_time=limit_time_, - daily_limit=daily_limit, - is_passive=is_passive, - icon=icon, - ) - return True - return False - - -# 删除商品 -async def delete_goods(name: str, id_: int) -> Tuple[str, str, int]: - """ - 删除商品 - :param name: 商品名称 - :param id_: 商品id - :return: 删除状况 - """ - goods_lst = await GoodsInfo.get_all_goods() - if id_: - if id_ < 1 or id_ > len(goods_lst): - return "序号错误,没有该序号商品...", "", 999 - goods_name = goods_lst[id_ - 1].goods_name - if await GoodsInfo.delete_goods(goods_name): - return f"删除商品 {goods_name} 成功!", goods_name, 200 - else: - return f"删除商品 {goods_name} 失败!", goods_name, 999 - if name: - if await GoodsInfo.delete_goods(name): - return f"删除商品 {name} 成功!", name, 200 - else: - return f"删除商品 {name} 失败!", name, 999 - return "获取商品失败", "", 999 - - -# 更新商品信息 -async def update_goods(**kwargs) -> Tuple[bool, str, str]: - """ - 更新商品信息 - :param kwargs: kwargs - :return: 更新状况 - """ - if kwargs: - goods_lst = await GoodsInfo.get_all_goods() - if is_number(kwargs["name"]): - if int(kwargs["name"]) < 1 or int(kwargs["name"]) > len(goods_lst): - return False, "序号错误,没有该序号的商品...", "" - goods = goods_lst[int(kwargs["name"]) - 1] - else: - goods = await GoodsInfo.filter(goods_name=kwargs["name"]).first() - if not goods: - return False, "名称错误,没有该名称的商品...", "" - name: str = goods.goods_name - price = goods.goods_price - des = goods.goods_description - discount = goods.goods_discount - limit_time = goods.goods_limit_time - daily_limit = goods.daily_limit - is_passive = goods.is_passive - new_time = 0 - tmp = "" - if kwargs.get("price"): - tmp += f'价格:{price} --> {kwargs["price"]}\n' - price = kwargs["price"] - if kwargs.get("des"): - tmp += f'描述:{des} --> {kwargs["des"]}\n' - des = kwargs["des"] - if kwargs.get("discount"): - tmp += f'折扣:{discount} --> {kwargs["discount"]}\n' - discount = kwargs["discount"] - if kwargs.get("limit_time"): - kwargs["limit_time"] = float(kwargs["limit_time"]) - new_time = ( - time.strftime( - "%Y-%m-%d %H:%M:%S", - time.localtime(time.time() + kwargs["limit_time"] * 60 * 60), - ) - if kwargs["limit_time"] != 0 - else 0 - ) - tmp += f"限时至: {new_time}\n" if new_time else "取消了限时\n" - limit_time = kwargs["limit_time"] - if kwargs.get("daily_limit"): - tmp += ( - f'每日购买限制:{daily_limit} --> {kwargs["daily_limit"]}\n' - if daily_limit - else "取消了购买限制\n" - ) - daily_limit = int(kwargs["daily_limit"]) - if kwargs.get("is_passive"): - tmp += f'被动道具:{is_passive} --> {kwargs["is_passive"]}\n' - des = kwargs["is_passive"] - await GoodsInfo.update_goods( - name, - int(price), - des, - float(discount), - int( - time.time() + limit_time * 60 * 60 - if limit_time != 0 and new_time - else 0 - ), - daily_limit, - is_passive, - ) - return ( - True, - name, - tmp[:-1], - ) - - -def parse_goods_info(msg: str) -> Union[dict, str]: - """ - 解析格式数据 - :param msg: 消息 - :return: 解析完毕的数据data - """ - if "name:" not in msg: - return "必须指定修改的商品名称或序号!" - data = {} - for x in msg.split(): - sp = x.split(":", maxsplit=1) - if str(sp[1]).strip(): - sp[1] = sp[1].strip() - if sp[0] == "name": - data["name"] = sp[1] - elif sp[0] == "price": - if not is_number(sp[1]) or int(sp[1]) < 0: - return "price参数不合法,必须大于等于0!" - data["price"] = sp[1] - elif sp[0] == "des": - data["des"] = sp[1] - elif sp[0] == "discount": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "discount参数不合法,必须大于0!" - data["discount"] = sp[1] - elif sp[0] == "limit_time": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "limit_time参数不合法,必须为数字且大于0!" - data["limit_time"] = sp[1] - elif sp[0] == "daily_limit": - if not is_number(sp[1]) or float(sp[1]) < 0: - return "daily_limit参数不合法,必须为数字且大于0!" - data["daily_limit"] = sp[1] - return data diff --git a/basic_plugins/shop/use/__init__.py b/basic_plugins/shop/use/__init__.py deleted file mode 100644 index 33a3a0b5..00000000 --- a/basic_plugins/shop/use/__init__.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup - -from models.bag_user import BagUser -from models.user_shop_gold_log import UserShopGoldLog -from services.log import logger -from utils.decorator.shop import NotMeetUseConditionsException -from utils.utils import is_number - -from .data_source import build_params, effect, func_manager, register_use - -__zx_plugin_name__ = "商店 - 使用道具" -__plugin_usage__ = """ -usage: - 普通的使用道具 - 指令: - 使用道具 [序号或道具名称] ?[数量]=1 ?[其他信息] - 示例:使用道具好感度双倍加持卡 使用道具好感度双倍加持卡 - 示例:使用道具1 使用第一个道具 - 示例:使用道具1 10 使用10个第一个道具 - 示例:使用道具1 1 来点色图 使用第一个道具并附带信息 - * 序号以 ”我的道具“ 为准 * -""".strip() -__plugin_des__ = "商店 - 使用道具" -__plugin_cmd__ = ["使用道具 [序号或道具名称]"] -__plugin_type__ = ("商店",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["商店", "使用道具"], -} - - -use_props = on_command(r"使用道具", priority=5, block=True, permission=GROUP) - - -@use_props.handle() -async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text() - num = 1 - text = "" - prop_n = None - index = None - split = msg.split() - if size := len(split): - if size == 1: - prop_n = split[0].strip() - index = 1 - if size > 1 and is_number(split[1].strip()): - prop_n = split[0].strip() - num = int(split[1].strip()) - index = 2 - else: - await use_props.finish("缺少参数,请查看帮助", at_sender=True) - if index: - text = " ".join(split[index:]) - property_ = await BagUser.get_property(event.user_id, event.group_id, True) - if property_: - name = None - if prop_n and is_number(prop_n): - if 0 < int(prop_n) <= len(property_): - name = list(property_.keys())[int(prop_n) - 1] - else: - await use_props.finish("仔细看看自己的道具仓库有没有这个道具?", at_sender=True) - else: - if prop_n not in property_.keys(): - await use_props.finish("道具名称错误!", at_sender=True) - name = prop_n - if not name: - await use_props.finish("未获取到道具名称", at_sender=True) - _user_prop_count = property_[name] - if num > _user_prop_count: - await use_props.finish(f"道具数量不足,无法使用{num}次!") - if num > (n := func_manager.get_max_num_limit(name)): - await use_props.finish(f"该道具单次只能使用 {n} 个!") - try: - model, kwargs = build_params(bot, event, name, num, text) - except KeyError: - logger.warning(f"{name} 未注册使用函数") - await use_props.finish(f"{name} 未注册使用方法") - else: - try: - await func_manager.run_handle( - type_="before_handle", param=model, **kwargs - ) - except NotMeetUseConditionsException as e: - await use_props.finish(e.get_info(), at_sender=True) - if await BagUser.delete_property(event.user_id, event.group_id, name, num): - if func_manager.check_send_success_message(name): - await use_props.send(f"使用道具 {name} {num} 次成功!", at_sender=True) - if msg := await effect(bot, event, name, num, text, event.message): - await use_props.send(msg, at_sender=True) - logger.info(f"使用道具 {name} {num} 次成功", event.user_id, event.group_id) - await UserShopGoldLog.create( - user_id=event.user_id, - group_id=event.group_id, - type=1, - name=name, - num=num, - ) - else: - await use_props.send(f"使用道具 {name} {num} 次失败!", at_sender=True) - logger.info( - f"使用道具 {name} {num} 次失败", "使用道具", event.user_id, event.group_id - ) - await func_manager.run_handle(type_="after_handle", param=model, **kwargs) - else: - await use_props.send("您的背包里没有任何的道具噢", at_sender=True) diff --git a/basic_plugins/shop/use/data_source.py b/basic_plugins/shop/use/data_source.py deleted file mode 100644 index b52d2c52..00000000 --- a/basic_plugins/shop/use/data_source.py +++ /dev/null @@ -1,235 +0,0 @@ -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageSegment, Message -from services.log import logger -from nonebot.adapters.onebot.v11 import Bot -from pydantic import create_model -from utils.models import ShopParam -from typing import Optional, Union, Callable, List, Tuple, Dict, Any -from types import MappingProxyType -import inspect -import asyncio - - -class GoodsUseFuncManager: - def __init__(self): - self._data = {} - - def register_use_before_handle(self, goods_name: str, fun_list: List[Callable]): - """ - 说明: - 注册商品使用前函数 - 参数: - :param goods_name: 商品名称 - :param fun_list: 函数列表 - """ - if fun_list: - self._data[goods_name]["before_handle"] = fun_list - logger.info(f"register_use_before_handle 成功注册商品:{goods_name} 的{len(fun_list)}个使用前函数") - - def register_use_after_handle(self, goods_name: str, fun_list: List[Callable]): - """ - 说明: - 注册商品使用后函数 - 参数: - :param goods_name: 商品名称 - :param fun_list: 函数列表 - """ - if fun_list: - self._data[goods_name]["after_handle"] = fun_list - logger.info(f"register_use_after_handle 成功注册商品:{goods_name} 的{len(fun_list)}个使用后函数") - - def register_use(self, goods_name: str, **kwargs): - """ - 注册商品使用方法 - :param goods_name: 商品名称 - :param kwargs: kwargs - """ - self._data[goods_name] = kwargs - - def exists(self, goods_name: str) -> bool: - """ - 判断商品使用方法是否被注册 - :param goods_name: 商品名称 - """ - return bool(self._data.get(goods_name)) - - def get_max_num_limit(self, goods_name: str) -> int: - """ - 获取单次商品使用数量 - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return self._data[goods_name]["kwargs"]["max_num_limit"] - return 1 - - def _parse_args(self, args_: MappingProxyType, param: ShopParam, **kwargs): - param_list_ = [] - _bot = param.bot - param.bot = None - param_json = param.dict() - param_json["bot"] = _bot - for par in args_.keys(): - if par in ["shop_param"]: - param_list_.append(param) - elif par not in ["args", "kwargs"]: - param_list_.append(param_json.get(par)) - if kwargs.get(par) is not None: - del kwargs[par] - return param_list_ - - async def use( - self, param: ShopParam, **kwargs - ) -> Optional[Union[str, MessageSegment]]: - """ - 使用道具 - :param param: BaseModel - :param kwargs: kwargs - """ - goods_name = param.goods_name - if self.exists(goods_name): - # 使用方法 - args = inspect.signature(self._data[goods_name]["func"]).parameters - if args and list(args.keys())[0] != "kwargs": - if asyncio.iscoroutinefunction(self._data[goods_name]["func"]): - return await self._data[goods_name]["func"]( - *self._parse_args(args, param, **kwargs) - ) - else: - return self._data[goods_name]["func"]( - *self._parse_args(args, param, **kwargs) - ) - else: - if asyncio.iscoroutinefunction(self._data[goods_name]["func"]): - return await self._data[goods_name]["func"]( - **kwargs, - ) - else: - return self._data[goods_name]["func"]( - **kwargs, - ) - - async def run_handle(self, goods_name: str, type_: str, param: ShopParam, **kwargs): - if self._data.get(goods_name) and self._data[goods_name].get(type_): - for func in self._data[goods_name].get(type_): - args = inspect.signature(func).parameters - if args and list(args.keys())[0] != "kwargs": - if asyncio.iscoroutinefunction(func): - await func(*self._parse_args(args, param, **kwargs)) - else: - func(*self._parse_args(args, param, **kwargs)) - else: - if asyncio.iscoroutinefunction(func): - await func(**kwargs) - else: - func(**kwargs) - - def check_send_success_message(self, goods_name: str) -> bool: - """ - 检查是否发送使用成功信息 - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return bool(self._data[goods_name]["kwargs"]["send_success_msg"]) - return False - - def get_kwargs(self, goods_name: str) -> dict: - """ - 获取商品使用方法的kwargs - :param goods_name: 商品名称 - """ - if self.exists(goods_name): - return self._data[goods_name]["kwargs"] - return {} - - def init_model(self, goods_name: str, bot: Bot, event: GroupMessageEvent, num: int, text: str): - return self._data[goods_name]["model"]( - **{ - "goods_name": goods_name, - "bot": bot, - "event": event, - "user_id": event.user_id, - "group_id": event.group_id, - "num": num, - "message": event.message, - "text": text - } - ) - - -func_manager = GoodsUseFuncManager() - - -def build_params( - bot: Bot, event: GroupMessageEvent, goods_name: str, num: int, text: str -) -> Tuple[ShopParam, Dict[str, Any]]: - """ - 说明: - 构造参数 - 参数: - :param bot: bot - :param event: event - :param goods_name: 商品名称 - :param num: 数量 - :param text: 其他信息 - """ - _kwargs = func_manager.get_kwargs(goods_name) - return ( - func_manager.init_model(goods_name, bot, event, num, text), - { - **_kwargs, - "_bot": bot, - "event": event, - "group_id": event.group_id, - "user_id": event.user_id, - "num": num, - "text": text, - "message": event.message, - "goods_name": goods_name, - }, - ) - - -async def effect( - bot: Bot, event: GroupMessageEvent, goods_name: str, num: int, text: str, message: Message -) -> Optional[Union[str, MessageSegment]]: - """ - 商品生效 - :param bot: Bot - :param event: GroupMessageEvent - :param goods_name: 商品名称 - :param num: 使用数量 - :param text: 其他信息 - :param message: Message - :return: 使用是否成功 - """ - # 优先使用注册的商品插件 - # try: - if func_manager.exists(goods_name): - _kwargs = func_manager.get_kwargs(goods_name) - model, kwargs = build_params(bot, event, goods_name, num, text) - return await func_manager.use(model, **kwargs) - # except Exception as e: - # logger.error(f"use 商品生效函数effect 发生错误 {type(e)}:{e}") - return None - - -def register_use(goods_name: str, func: Callable, **kwargs): - """ - 注册商品使用方法 - :param goods_name: 商品名称 - :param func: 使用函数 - :param kwargs: kwargs - """ - if func_manager.exists(goods_name): - raise ValueError("该商品使用函数已被注册!") - # 发送使用成功信息 - kwargs["send_success_msg"] = kwargs.get("send_success_msg", True) - kwargs["max_num_limit"] = kwargs.get("max_num_limit", 1) - func_manager.register_use( - goods_name, - **{ - "func": func, - "model": create_model(f"{goods_name}_model", __base__=ShopParam, **kwargs), - "kwargs": kwargs, - }, - ) - logger.info(f"register_use 成功注册商品:{goods_name} 的使用函数") diff --git a/basic_plugins/super_cmd/bot_friend_group.py b/basic_plugins/super_cmd/bot_friend_group.py deleted file mode 100755 index eef776d3..00000000 --- a/basic_plugins/super_cmd/bot_friend_group.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from models.group_info import GroupInfo -from services.log import logger -from utils.depends import OneCommand -from utils.manager import requests_manager -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "显示所有好友群组 [Superuser]" -__plugin_usage__ = """ -usage: - 显示所有好友群组 - 指令: - 查看所有好友/查看所有群组 - 同意好友请求 [id] - 拒绝好友请求 [id] - 同意群聊请求 [id] - 拒绝群聊请求 [id] - 查看所有请求 - 清空所有请求 -""".strip() -__plugin_des__ = "显示所有好友群组" -__plugin_cmd__ = [ - "查看所有好友/查看所有群组", - "同意好友请求 [id]", - "拒绝好友请求 [id]", - "同意群聊请求 [id]", - "拒绝群聊请求 [id]", - "查看所有请求", - "清空所有请求", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -cls_group = on_command( - "查看所有群组", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) -cls_friend = on_command( - "查看所有好友", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - -friend_handle = on_command( - "同意好友请求", aliases={"拒绝好友请求"}, permission=SUPERUSER, priority=1, block=True -) - -group_handle = on_command( - "同意群聊请求", aliases={"拒绝群聊请求"}, permission=SUPERUSER, priority=1, block=True -) - -clear_request = on_command("清空所有请求", permission=SUPERUSER, priority=1, block=True) - -cls_request = on_command("查看所有请求", permission=SUPERUSER, priority=1, block=True) - - -@cls_group.handle() -async def _(bot: Bot): - gl = await bot.get_group_list() - msg = ["{group_id} {group_name}".format_map(g) for g in gl] - msg = "\n".join(msg) - msg = f"bot:{bot.self_id}\n| 群号 | 群名 | 共{len(gl)}个群\n" + msg - await cls_group.send(msg) - - -@cls_friend.handle() -async def _(bot: Bot): - gl = await bot.get_friend_list() - msg = ["{user_id} {nickname}".format_map(g) for g in gl] - msg = "\n".join(msg) - msg = f"| QQ号 | 昵称 | 共{len(gl)}个好友\n" + msg - await cls_friend.send(msg) - - -@friend_handle.handle() -async def _(bot: Bot, cmd: str = OneCommand(), arg: Message = CommandArg()): - id_ = arg.extract_plain_text().strip() - if is_number(id_): - id_ = int(id_) - if cmd[:2] == "同意": - flag = await requests_manager.approve(bot, id_, "private") - else: - flag = await requests_manager.refused(bot, id_, "private") - if flag == 1: - await friend_handle.send(f"{cmd[:2]}好友请求失败,该请求已失效..") - requests_manager.delete_request(id_, "private") - elif flag == 2: - await friend_handle.send(f"{cmd[:2]}好友请求失败,未找到此id的请求..") - else: - await friend_handle.send(f"{cmd[:2]}好友请求成功!") - else: - await friend_handle.send("id必须为纯数字!") - - -@group_handle.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - id_ = arg.extract_plain_text().strip() - flag = None - if is_number(id_): - id_ = int(id_) - if cmd[:2] == "同意": - rid = requests_manager.get_group_id(id_) - if rid: - if group := await GroupInfo.get_or_none(group_id=str(rid)): - group.group_flag = 1 - await group.save(update_fields=["group_flag"]) - else: - group_info = await bot.get_group_info(group_id=rid) - await GroupInfo.create( - group_id=str(rid), - group_name=group_info["group_name"], - max_member_count=group_info["max_member_count"], - member_count=group_info["member_count"], - group_flag=1, - ) - flag = await requests_manager.approve(bot, id_, "group") - else: - await group_handle.send("同意群聊请求失败,未找到此id的请求..") - logger.info("同意群聊请求失败,未找到此id的请求..", cmd, event.user_id) - else: - flag = await requests_manager.refused(bot, id_, "group") - if flag == 1: - await group_handle.send(f"{cmd[:2]}群聊请求失败,该请求已失效..") - requests_manager.delete_request(id_, "group") - elif flag == 2: - await group_handle.send(f"{cmd[:2]}群聊请求失败,未找到此id的请求..") - else: - await group_handle.send(f"{cmd[:2]}群聊请求成功!") - else: - await group_handle.send("id必须为纯数字!") - - -@cls_request.handle() -async def _(): - _str = "" - for type_ in ["private", "group"]: - msg = await requests_manager.show(type_) - if msg: - _str += image(msg) - else: - _str += "没有任何好友请求.." if type_ == "private" else "没有任何群聊请求.." - if type_ == "private": - _str += "\n--------------------\n" - await cls_request.send(Message(_str)) - - -@clear_request.handle() -async def _(): - requests_manager.clear() - await clear_request.send("已清空所有好友/群聊请求..") diff --git a/basic_plugins/super_cmd/clear_data.py b/basic_plugins/super_cmd/clear_data.py deleted file mode 100755 index effca772..00000000 --- a/basic_plugins/super_cmd/clear_data.py +++ /dev/null @@ -1,75 +0,0 @@ -import asyncio -import os -import time - -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.manager import resources_manager -from utils.utils import scheduler - -__zx_plugin_name__ = "清理临时数据 [Superuser]" -__plugin_usage__ = """ -usage: - 清理临时数据 - 指令: - 清理临时数据 -""".strip() -__plugin_des__ = "清理临时数据" -__plugin_cmd__ = [ - "清理临时数据", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -clear_data = on_command( - "清理临时数据", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -resources_manager.add_temp_dir(TEMP_PATH) - - -@clear_data.handle() -async def _(): - await clear_data.send("开始清理临时数据....") - size = await asyncio.get_event_loop().run_in_executor(None, _clear_data) - await clear_data.send("共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) - logger.info("清理临时数据完成," + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) - - -def _clear_data() -> float: - logger.debug("开始清理临时文件...") - size = 0 - dir_list = [dir_ for dir_ in resources_manager.get_temp_data_dir() if dir_.exists()] - for dir_ in dir_list: - logger.debug(f"尝试清理文件夹: {dir_.absolute()}", "清理临时数据") - dir_size = 0 - for file in os.listdir(dir_): - file = dir_ / file - if file.is_file(): - try: - if time.time() - os.path.getatime(file) > 10: - file_size = os.path.getsize(file) - file.unlink() - size += file_size - dir_size += file_size - logger.debug(f"移除临时文件: {file.absolute()}", "清理临时数据") - except Exception as e: - logger.error(f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", "清理临时数据", e=e) - logger.debug("清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据") - return float(size) - - -@scheduler.scheduled_job( - "cron", - hour=1, - minute=1, -) -async def _(): - size = await asyncio.get_event_loop().run_in_executor(None, _clear_data) - logger.info("自动清理临时数据完成," + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)) diff --git a/basic_plugins/super_cmd/exec_sql.py b/basic_plugins/super_cmd/exec_sql.py deleted file mode 100644 index 26763de9..00000000 --- a/basic_plugins/super_cmd/exec_sql.py +++ /dev/null @@ -1,92 +0,0 @@ -import asyncio - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me -from tortoise import Tortoise - -from services.db_context import TestSQL -from services.log import logger -from utils.message_builder import custom_forward_msg - -__zx_plugin_name__ = "执行sql [Superuser]" -__plugin_usage__ = """ -usage: - 执行一段sql语句 - 指令: - exec [sql语句] ([查询页数,19条/页]) - 查看所有表 -""".strip() -__plugin_des__ = "执行一段sql语句" -__plugin_cmd__ = ["exec [sql语句]", "查看所有表"] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" - - -exec_ = on_command("exec", rule=to_me(), permission=SUPERUSER, priority=1, block=True) -tables = on_command("查看所有表", rule=to_me(), permission=SUPERUSER, priority=1, block=True) - - -@exec_.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - sql = arg.extract_plain_text().strip() - if not sql: - await exec_.finish("未接受到sql语句") - db = Tortoise.get_connection("default") - # try: - # 判断是否为SELECT语句 - if sql.lower().startswith("select"): - pass - # # 分割语句 - try: - page = int(sql.split(" ")[-1]) - 1 - sql_list = sql.split(" ")[:-1] - except ValueError: - page = 0 - sql_list = sql.split(" ") - # 拼接语句 - sql = " ".join(sql_list) - res = await db.execute_query_dict(sql) - msg_list = [f"第{page+1}页查询结果:"] - # logger.info(res) - # 获取所有字段 - keys = res[0].keys() - # 每页10条 - for i in res[page * 10 : (page + 1) * 10]: - msg = "" - for key in keys: - msg += f"{key}: {i[key]}\n" - msg += f"第{page+1}页第{res.index(i)+1}条" - msg_list.append(msg) - # 检查是私聊还是群聊 - if isinstance(event, GroupMessageEvent): - forward_msg_list = custom_forward_msg(msg_list, bot.self_id) - await bot.send_group_forward_msg( - group_id=event.group_id, messages=forward_msg_list - ) - else: - for msg in msg_list: - await exec_.send(msg) - await asyncio.sleep(0.2) - return - else: - await TestSQL.raw(sql) - await exec_.send("执行 sql 语句成功.") - # except Exception as e: - # await exec_.send(f"执行 sql 语句失败 {type(e)}:{e}") - # logger.error(f"执行 sql 语句失败 {type(e)}:{e}") - - -@tables.handle() -async def _(bot: Bot, event: MessageEvent): - # 获取所有表 - db = Tortoise.get_connection("default") - query = await db.execute_query_dict( - "select tablename from pg_tables where schemaname = 'public'" - ) - msg = "数据库中的所有表名:\n" - for tablename in query: - msg += str(tablename["tablename"]) + "\n" - await tables.finish(msg) diff --git a/basic_plugins/super_cmd/manager_group.py b/basic_plugins/super_cmd/manager_group.py deleted file mode 100755 index f1a1195b..00000000 --- a/basic_plugins/super_cmd/manager_group.py +++ /dev/null @@ -1,235 +0,0 @@ -from typing import Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - GROUP, - Bot, - GroupMessageEvent, - Message, - MessageEvent, -) -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME -from models.group_info import GroupInfo -from services.log import logger -from utils.depends import OneCommand -from utils.image_utils import text2image -from utils.manager import group_manager, plugins2settings_manager -from utils.message_builder import image -from utils.utils import is_number - -__zx_plugin_name__ = "管理群操作 [Superuser]" -__plugin_usage__ = """ -usage: - 群权限 | 群白名单 | 退出群 操作 - 退群,添加/删除群白名单,添加/删除群认证,当在群聊中这五个命令且没有指定群号时,默认指定当前群聊 - 指令: - 退群 ?[group_id] - 修改群权限 [group_id] [等级] - 修改群权限 [等级]: 该命令仅在群聊时生效,默认修改当前群聊 - 添加群白名单 ?*[group_id] - 删除群白名单 ?*[group_id] - 添加群认证 ?*[group_id] - 删除群认证 ?*[group_id] - 查看群白名单 -""".strip() -__plugin_des__ = "管理群操作" -__plugin_cmd__ = [ - "退群 [group_id]", - "修改群权限 [group_id] [等级]", - "添加群白名单 *[group_id]", - "删除群白名单 *[group_id]", - "添加群认证 *[group_id]", - "删除群认证 *[group_id]", - "查看群白名单", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -del_group = on_command("退群", rule=to_me(), permission=SUPERUSER, priority=1, block=True) - -add_group_level = on_command("修改群权限", priority=1, permission=SUPERUSER, block=True) -my_group_level = on_command( - "查看群权限", aliases={"群权限"}, priority=5, permission=GROUP, block=True -) -what_up_group_level = on_regex( - "(:?提高|提升|升高|增加|加上).*?群权限", - rule=to_me(), - priority=5, - permission=GROUP, - block=True, -) -manager_group_whitelist = on_command( - "添加群白名单", aliases={"删除群白名单"}, priority=1, permission=SUPERUSER, block=True -) - -show_group_whitelist = on_command( - "查看群白名单", priority=1, permission=SUPERUSER, block=True -) - -group_auth = on_command( - "添加群认证", aliases={"删除群认证"}, priority=1, permission=SUPERUSER, block=True -) - - -@del_group.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - group_id = arg.extract_plain_text().strip() - if not group_id and isinstance(event, GroupMessageEvent): - group_id = event.group_id - if group_id: - if is_number(group_id): - group_list = [x["group_id"] for x in await bot.get_group_list()] - group_id = int(group_id) - if group_id not in group_list: - logger.debug("群聊不存在", "退群", event.user_id, target=group_id) - await del_group.finish(f"{NICKNAME}未在该群聊中...") - try: - await bot.set_group_leave(group_id=group_id) - logger.info(f"{NICKNAME}退出群聊成功", "退群", event.user_id, target=group_id) - await del_group.send(f"退出群聊 {group_id} 成功", at_sender=True) - group_manager.delete_group(group_id) - await GroupInfo.filter(group_id=group_id).delete() - except Exception as e: - logger.error(f"退出群聊失败", "退群", event.user_id, target=group_id, e=e) - await del_group.send(f"退出群聊 {group_id} 失败", at_sender=True) - else: - await del_group.send(f"请输入正确的群号", at_sender=True) - else: - await del_group.send(f"请输入群号", at_sender=True) - - -@add_group_level.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - msg = msg.split() - group_id = 0 - level = 0 - if isinstance(event, GroupMessageEvent) and len(msg) == 1: - msg = [event.group_id, msg[0]] - if not msg: - await add_group_level.finish("缺失参数...") - if len(msg) < 2: - await add_group_level.finish("缺失参数...") - if is_number(msg[0]) and is_number(msg[1]): - group_id = str(msg[0]) - level = int(msg[1]) - else: - await add_group_level.finish("参数错误...群号和等级必须是数字..") - old_level = group_manager.get_group_level(group_id) - group_manager.set_group_level(group_id, level) - await add_group_level.send("修改成功...", at_sender=True) - if level > -1: - await bot.send_group_msg( - group_id=int(group_id), message=f"管理员修改了此群权限:{old_level} -> {level}" - ) - logger.info(f"修改群权限:{level}", "修改群权限", event.user_id, target=group_id) - - -@my_group_level.handle() -async def _(event: GroupMessageEvent): - level = group_manager.get_group_level(event.group_id) - tmp = "" - data = plugins2settings_manager.get_data() - for module in data: - if data[module].level > level: - plugin_name = data[module].cmd[0] - if plugin_name == "pixiv": - plugin_name = "搜图 p站排行" - tmp += f"{plugin_name}\n" - if not tmp: - await my_group_level.finish(f"当前群权限:{level}") - await my_group_level.finish( - f"当前群权限:{level}\n目前无法使用的功能:\n" - + image(await text2image(tmp, padding=10, color="#f9f6f2")) - ) - - -@what_up_group_level.handle() -async def _(): - await what_up_group_level.finish( - f"[此功能用于防止内鬼,如果引起不便那真是抱歉了]\n" f"目前提高群权限的方法:\n" f"\t1.超级管理员修改权限" - ) - - -@manager_group_whitelist.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - msg = arg.extract_plain_text().strip().split() - if not msg and isinstance(event, GroupMessageEvent): - msg = [event.group_id] - if not msg: - await manager_group_whitelist.finish("请输入群号") - all_group = [g["group_id"] for g in await bot.get_group_list()] - error_group = [] - group_list = [] - for group_id in msg: - if is_number(group_id) and int(group_id) in all_group: - group_list.append(int(group_id)) - else: - logger.debug(f"群号不合法或不存在", cmd, target=group_id) - error_group.append(group_id) - if group_list: - for group_id in group_list: - if cmd in ["添加群白名单"]: - group_manager.add_group_white_list(group_id) - else: - group_manager.delete_group_white_list(group_id) - group_list = [str(x) for x in group_list] - await manager_group_whitelist.send("已成功将 " + "\n".join(group_list) + " " + cmd) - group_manager.save() - if error_group: - await manager_group_whitelist.send("以下群聊不合法或不存在:\n" + "\n".join(error_group)) - - -@show_group_whitelist.handle() -async def _(): - group = [str(g) for g in group_manager.get_group_white_list()] - if not group: - await show_group_whitelist.finish("没有任何群在群白名单...") - await show_group_whitelist.send("目前的群白名单:\n" + "\n".join(group)) - - -@group_auth.handle() -async def _( - bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg() -): - msg = arg.extract_plain_text().strip().split() - if isinstance(event, GroupMessageEvent) and not msg: - msg = [event.group_id] - if not msg: - await manager_group_whitelist.finish("请输入群号") - error_group = [] - for group_id in msg: - group_id = int(group_id) - if cmd[:2] == "添加": - try: - await GroupInfo.update_or_create( - group_id=group_id, - defaults={ - "group_flag": 1, - }, - ) - except Exception as e: - await group_auth.send(f"添加群认证 {group_id} 发生错误!") - logger.error(f"添加群认证发生错误", cmd, target=group_id, e=e) - else: - await group_auth.send(f"已为 {group_id} {cmd[:2]}群认证..") - logger.info(f"添加群认证成功", cmd, target=group_id) - else: - if group := await GroupInfo.filter(group_id=group_id).first(): - await group.update_or_create( - group_id=group_id, defaults={"group_flag": 0} - ) - await group_auth.send(f"已删除 {group_id} 群认证..") - logger.info(f"删除群认证成功", cmd, target=group_id) - else: - await group_auth.send(f"未查找到群聊: {group_id}") - logger.info(f"未找到群聊", cmd, target=group_id) - if error_group: - await manager_group_whitelist.send("以下群聊不合法或不存在:\n" + "\n".join(error_group)) diff --git a/basic_plugins/super_cmd/reload_setting.py b/basic_plugins/super_cmd/reload_setting.py deleted file mode 100755 index c6460b40..00000000 --- a/basic_plugins/super_cmd/reload_setting.py +++ /dev/null @@ -1,64 +0,0 @@ -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import Config -from services.log import logger -from utils.manager import ( - group_manager, - plugins2block_manager, - plugins2cd_manager, - plugins2settings_manager, -) -from utils.utils import scheduler - -__zx_plugin_name__ = "重载配置 [Superuser]" -__plugin_usage__ = """ -usage: - 重载配置 - plugins2settings, - plugins2cd - plugins2block - group_manager - 指令: - 重载配置 -""".strip() -__plugin_des__ = "重载配置" -__plugin_cmd__ = [ - "重载配置", -] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "AUTO_RELOAD": {"value": False, "help": "自动重载配置文件", "default_value": False, "type": bool}, - "AUTO_RELOAD_TIME": {"value": 180, "help": "控制自动重载配置文件时长", "default_value": 180, "type": int}, -} - - -reload_plugins_manager = on_command( - "重载配置", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@reload_plugins_manager.handle() -async def _(): - plugins2settings_manager.reload() - plugins2cd_manager.reload() - plugins2block_manager.reload() - group_manager.reload() - Config.reload() - await reload_plugins_manager.send("重载完成...") - - -@scheduler.scheduled_job( - "interval", - seconds=Config.get_config("reload_setting", "AUTO_RELOAD_TIME", 180), -) -async def _(): - if Config.get_config("reload_setting", "AUTO_RELOAD"): - plugins2settings_manager.reload() - plugins2cd_manager.reload() - plugins2block_manager.reload() - group_manager.reload() - Config.reload() - logger.debug("已自动重载所有配置文件...") diff --git a/basic_plugins/super_cmd/set_admin_permissions.py b/basic_plugins/super_cmd/set_admin_permissions.py deleted file mode 100755 index 03835f76..00000000 --- a/basic_plugins/super_cmd/set_admin_permissions.py +++ /dev/null @@ -1,114 +0,0 @@ -from typing import List, Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.exception import ActionFailed -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from models.level_user import LevelUser -from services.log import logger -from utils.depends import AtList, OneCommand -from utils.message_builder import at -from utils.utils import is_number - -__zx_plugin_name__ = "用户权限管理 [Superuser]" -__plugin_usage__ = """ -usage: - 增删改用户的权限 - 指令: - 添加权限 [at] [权限] - 添加权限 [qq] [group_id] [权限] - 删除权限 [at] - 删除权限 [qq] [group_id] -""".strip() -__plugin_des__ = "增删改用户的权限" -__plugin_cmd__ = [ - "添加权限 [at] [权限]", - "添加权限 [qq] [group_id] [权限]", - "删除权限 [at]", - "删除权限 [qq] [group_id]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -super_cmd = on_command( - "添加管理", - aliases={"删除管理", "添加权限", "删除权限"}, - priority=1, - permission=SUPERUSER, - block=True, -) - - -@super_cmd.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), - at_list: List[int] = AtList(), -): - group_id = event.group_id if isinstance(event, GroupMessageEvent) else -1 - level = None - args = arg.extract_plain_text().strip().split() - flag = 2 - qq = None - try: - if at_list: - qq = at_list[0] - if cmd[:2] == "添加" and args and is_number(args[0]): - level = int(args[0]) - else: - if cmd[:2] == "添加": - if ( - len(args) > 2 - and is_number(args[0]) - and is_number(args[1]) - and is_number(args[2]) - ): - qq = int(args[0]) - group_id = int(args[1]) - level = int(args[2]) - else: - if len(args) > 1 and is_number(args[0]) and is_number(args[1]): - qq = int(args[0]) - group_id = int(args[1]) - flag = 1 - level = -1 if cmd[:2] == "删除" else level - if group_id == -1 or not level or not qq: - raise IndexError() - except IndexError: - await super_cmd.finish(__plugin_usage__) - if not qq: - await super_cmd.finish("未指定对象...") - try: - if cmd[:2] == "添加": - await LevelUser.set_level(qq, group_id, level, 1) - result = f"设置权限成功, 权限: {level}" - else: - if await LevelUser.delete_level(qq, group_id): - result = "删除管理成功!" - else: - result = "该账号无管理权限!" - if flag == 2: - await super_cmd.send(result) - elif flag == 1: - try: - await bot.send_group_msg( - group_id=group_id, - message=Message( - f"{at(qq)}管理员修改了你的权限" - f"\n--------\n你当前的权限等级:{level if level != -1 else 0}" - ), - ) - except ActionFailed: - pass - await super_cmd.send("修改成功") - logger.info( - f"修改权限: {level if level != -1 else 0}", cmd, event.user_id, group_id, qq - ) - except Exception as e: - await super_cmd.send("执行指令失败!") - logger.error(f"执行指令失败", cmd, event.user_id, group_id, qq, e=e) diff --git a/basic_plugins/super_cmd/update_friend_group_info.py b/basic_plugins/super_cmd/update_friend_group_info.py deleted file mode 100755 index 5d4b1089..00000000 --- a/basic_plugins/super_cmd/update_friend_group_info.py +++ /dev/null @@ -1,77 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger - -__zx_plugin_name__ = "更新群/好友信息 [Superuser]" -__plugin_usage__ = """ -usage: - 更新群/好友信息 - 指令: - 更新群信息 - 更新好友信息 -""".strip() -__plugin_des__ = "更新群/好友信息" -__plugin_cmd__ = [ - "更新群信息", - "更新好友信息", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -update_group_info = on_command( - "更新群信息", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) -update_friend_info = on_command( - "更新好友信息", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@update_group_info.handle() -async def _(bot: Bot, event: MessageEvent): - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - num = 0 - for g in gl: - try: - group_info = await bot.get_group_info(group_id=g) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - }, - ) - num += 1 - logger.debug( - "群聊信息更新成功", "更新群信息", event.user_id, target=group_info["group_id"] - ) - except Exception as e: - logger.error(f"更新群聊信息失败", "更新群信息", event.user_id, target=g, e=e) - await update_group_info.send(f"成功更新了 {len(gl)} 个群的信息") - logger.info(f"更新群聊信息完成,共更新了 {len(gl)} 个群的信息", "更新群信息", event.user_id) - - -@update_friend_info.handle() -async def _(bot: Bot, event: MessageEvent): - num = 0 - error_list = [] - fl = await bot.get_friend_list() - for f in fl: - try: - await FriendUser.update_or_create( - user_id=str(f["user_id"]), defaults={"nickname": f["nickname"]} - ) - logger.debug(f"更新好友信息成功", "更新好友信息", event.user_id, target=f["user_id"]) - num += 1 - except Exception as e: - logger.error(f"更新好友信息失败", "更新好友信息", event.user_id, target=f["user_id"], e=e) - await update_friend_info.send(f"成功更新了 {num} 个好友的信息") - if error_list: - await update_friend_info.send(f"以下好友更新失败:\n" + "\n".join(error_list)) - logger.info(f"更新好友信息完成,共更新了 {num} 个群的信息", "更新好友信息", event.user_id) diff --git a/basic_plugins/super_help/__init__.py b/basic_plugins/super_help/__init__.py deleted file mode 100755 index 574b3d1b..00000000 --- a/basic_plugins/super_help/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from utils.message_builder import image - -from .data_source import SUPERUSER_HELP_IMAGE, create_help_image - -__zx_plugin_name__ = "超级用户帮助 [Superuser]" - - -if SUPERUSER_HELP_IMAGE.exists(): - SUPERUSER_HELP_IMAGE.unlink() - -super_help = on_command( - "超级用户帮助", rule=to_me(), priority=1, permission=SUPERUSER, block=True -) - - -@super_help.handle() -async def _(): - if not SUPERUSER_HELP_IMAGE.exists(): - await create_help_image() - await super_help.finish(image(SUPERUSER_HELP_IMAGE)) diff --git a/basic_plugins/super_help/data_source.py b/basic_plugins/super_help/data_source.py deleted file mode 100755 index 655ea5a1..00000000 --- a/basic_plugins/super_help/data_source.py +++ /dev/null @@ -1,81 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_template import help_template -from utils.image_utils import BuildImage, build_sort_image, group_image, text2image -from utils.manager import plugin_data_manager -from utils.manager.models import PluginType - -driver: Driver = nonebot.get_driver() - -SUPERUSER_HELP_IMAGE = IMAGE_PATH / "superuser_help.png" - - -@driver.on_bot_connect -async def create_help_image(): - """ - 创建超级用户帮助图片 - """ - if SUPERUSER_HELP_IMAGE.exists(): - return - plugin_data_ = plugin_data_manager.get_data() - image_list = [] - task_list = [] - for plugin_data in [ - plugin_data_[x] - for x in plugin_data_ - if plugin_data_[x].name != "超级用户帮助 [Superuser]" - ]: - try: - if plugin_data.plugin_type in [PluginType.SUPERUSER, PluginType.ADMIN]: - usage = None - if ( - plugin_data.plugin_type == PluginType.SUPERUSER - and plugin_data.usage - ): - usage = await text2image( - plugin_data.usage, padding=5, color=(204, 196, 151) - ) - if plugin_data.superuser_usage: - usage = await text2image( - plugin_data.superuser_usage, padding=5, color=(204, 196, 151) - ) - if usage: - await usage.acircle_corner() - image = await help_template(plugin_data.name, usage) - image_list.append(image) - if plugin_data.task: - for x in plugin_data.task.keys(): - task_list.append(plugin_data.task[x]) - except Exception as e: - logger.warning( - f"获取超级用户插件 {plugin_data.model}: {plugin_data.name} 设置失败...", e=e - ) - task_str = "\n".join(task_list) - task_str = "通过私聊 开启被动/关闭被动 + [被动名称] 来控制全局被动\n----------\n" + task_str - task_image = await text2image(task_str, padding=5, color=(204, 196, 151)) - task_image = await help_template("被动任务", task_image) - image_list.append(task_image) - image_group, _ = group_image(image_list) - A = await build_sort_image(image_group, color="#f9f6f2", padding_top=180) - await A.apaste( - BuildImage(0, 0, font="CJGaoDeGuo.otf", plain_text="超级用户帮助", font_size=50), - (50, 30), - True, - ) - await A.apaste( - BuildImage( - 0, - 0, - font="CJGaoDeGuo.otf", - plain_text="注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", - font_size=30, - font_color="red", - ), - (50, 90), - True, - ) - await A.asave(SUPERUSER_HELP_IMAGE) - logger.info(f"已成功加载 {len(image_list)} 条超级用户命令") diff --git a/basic_plugins/update_info.py b/basic_plugins/update_info.py deleted file mode 100755 index a5896f23..00000000 --- a/basic_plugins/update_info.py +++ /dev/null @@ -1,32 +0,0 @@ -from nonebot import on_command - -from utils.message_builder import image - -__zx_plugin_name__ = "更新信息" -__plugin_usage__ = """ -usage: - 更新信息 - 指令: - 更新信息 -""".strip() -__plugin_des__ = "当前版本的更新信息" -__plugin_cmd__ = ["更新信息"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["更新信息"], -} - - -update_info = on_command("更新信息", aliases={"更新日志"}, priority=5, block=True) - - -@update_info.handle() -async def _(): - if img := image("update_info.png"): - await update_info.finish(img) - else: - await update_info.finish("目前没有更新信息哦") diff --git a/bot.py b/bot.py index 4ff6efac..71a794b0 100644 --- a/bot.py +++ b/bot.py @@ -1,20 +1,25 @@ import nonebot -from nonebot.adapters.onebot.v11 import Adapter -from services.db_context import init, disconnect + +# from nonebot.adapters.discord import Adapter as DiscordAdapter +from nonebot.adapters.dodo import Adapter as DoDoAdapter +from nonebot.adapters.kaiheila import Adapter as KaiheilaAdapter +from nonebot.adapters.onebot.v11 import Adapter as OneBotV11Adapter + +from zhenxun.services.db_context import disconnect, init nonebot.init() driver = nonebot.get_driver() -driver.register_adapter(Adapter) -config = driver.config +driver.register_adapter(OneBotV11Adapter) +driver.register_adapter(KaiheilaAdapter) +driver.register_adapter(DoDoAdapter) +# driver.register_adapter(DiscordAdapter) + + driver.on_startup(init) driver.on_shutdown(disconnect) -# 优先加载定时任务 -nonebot.load_plugin("nonebot_plugin_apscheduler") -nonebot.load_plugins("basic_plugins") -nonebot.load_plugins("plugins") -nonebot.load_plugins("extensive_plugin") -# 最后加载权限控制 -nonebot.load_plugins("basic_plugins/hooks") + +nonebot.load_builtin_plugins("echo") # 内置插件 +nonebot.load_plugins("zhenxun/builtin_plugins") if __name__ == "__main__": diff --git a/configs/path_config.py b/configs/path_config.py deleted file mode 100644 index 7f9b46bf..00000000 --- a/configs/path_config.py +++ /dev/null @@ -1,47 +0,0 @@ -from pathlib import Path -import os - -# 图片路径 -IMAGE_PATH = Path() / "resources" / "image" -# 语音路径 -RECORD_PATH = Path() / "resources" / "record" -# 文本路径 -TEXT_PATH = Path() / "resources" / "text" -# 日志路径 -LOG_PATH = Path() / "log" -# 字体路径 -FONT_PATH = Path() / "resources" / "font" -# 数据路径 -DATA_PATH = Path() / "data" -# 临时数据路径 -TEMP_PATH = Path() / "resources" / "temp" -# 网页模板路径 -TEMPLATE_PATH = Path() / "resources" / "template" - - -def load_path(): - old_img_dir = Path() / "resources" / "img" - if not IMAGE_PATH.exists() and old_img_dir.exists(): - os.rename(old_img_dir, IMAGE_PATH) - old_voice_dir = Path() / "resources" / "voice" - if not RECORD_PATH.exists() and old_voice_dir.exists(): - os.rename(old_voice_dir, RECORD_PATH) - old_ttf_dir = Path() / "resources" / "ttf" - if not FONT_PATH.exists() and old_ttf_dir.exists(): - os.rename(old_ttf_dir, FONT_PATH) - old_txt_dir = Path() / "resources" / "txt" - if not TEXT_PATH.exists() and old_txt_dir.exists(): - os.rename(old_txt_dir, TEXT_PATH) - IMAGE_PATH.mkdir(parents=True, exist_ok=True) - RECORD_PATH.mkdir(parents=True, exist_ok=True) - TEXT_PATH.mkdir(parents=True, exist_ok=True) - LOG_PATH.mkdir(parents=True, exist_ok=True) - FONT_PATH.mkdir(parents=True, exist_ok=True) - DATA_PATH.mkdir(parents=True, exist_ok=True) - TEMP_PATH.mkdir(parents=True, exist_ok=True) - - -load_path() - - - diff --git a/models/bag_user.py b/models/bag_user.py deleted file mode 100755 index 283a49f2..00000000 --- a/models/bag_user.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Dict, Union - -from tortoise import fields - -from services.db_context import Model - -from .goods_info import GoodsInfo - - -class BagUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - gold = fields.IntField(default=100) - """金币数量""" - spend_total_gold = fields.IntField(default=0) - """花费金币总数""" - get_total_gold = fields.IntField(default=0) - """获取金币总数""" - get_today_gold = fields.IntField(default=0) - """今日获取金币""" - spend_today_gold = fields.IntField(default=0) - """今日获取金币""" - property: Dict[str, int] = fields.JSONField(default={}) - """道具""" - - class Meta: - table = "bag_users" - table_description = "用户道具数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def get_user_total_gold(cls, user_id: Union[int, str], group_id: Union[int, str]) -> str: - """ - 说明: - 获取金币概况 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - return ( - f"当前金币:{user.gold}\n今日获取金币:{user.get_today_gold}\n今日花费金币:{user.spend_today_gold}" - f"\n今日收益:{user.get_today_gold - user.spend_today_gold}" - f"\n总赚取金币:{user.get_total_gold}\n总花费金币:{user.spend_total_gold}" - ) - - @classmethod - async def get_gold(cls, user_id: Union[int, str], group_id: Union[int, str]) -> int: - """ - 说明: - 获取当前金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - return user.gold - - @classmethod - async def get_property( - cls, user_id: Union[int, str], group_id: Union[int, str], only_active: bool = False - ) -> Dict[str, int]: - """ - 说明: - 获取当前道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param only_active: 仅仅获取主动使用的道具 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - if only_active and user.property: - data = {} - name_list = [ - x.goods_name - for x in await GoodsInfo.get_all_goods() - if not x.is_passive - ] - for key in [x for x in user.property if x in name_list]: - data[key] = user.property[key] - return data - return user.property - - @classmethod - async def add_gold(cls, user_id: Union[int, str], group_id: Union[int, str], num: int): - """ - 说明: - 增加金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param num: 金币数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.gold = user.gold + num - user.get_total_gold = user.get_total_gold + num - user.get_today_gold = user.get_today_gold + num - await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) - - @classmethod - async def spend_gold(cls, user_id: Union[int, str], group_id: Union[int, str], num: int): - """ - 说明: - 花费金币 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param num: 金币数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.gold = user.gold - num - user.spend_total_gold = user.spend_total_gold + num - user.spend_today_gold = user.spend_today_gold + num - await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) - - @classmethod - async def add_property(cls, user_id: Union[int, str], group_id: Union[int, str], name: str, num: int = 1): - """ - 说明: - 增加道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param name: 道具名称 - :param num: 道具数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - property_ = user.property - if property_.get(name) is None: - property_[name] = 0 - property_[name] += num - user.property = property_ - await user.save(update_fields=["property"]) - - @classmethod - async def delete_property( - cls, user_id: Union[int, str], group_id: Union[int, str], name: str, num: int = 1 - ) -> bool: - """ - 说明: - 使用/删除 道具 - 参数: - :param user_id: 用户id - :param group_id: 所在群组id - :param name: 道具名称 - :param num: 使用个数 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - property_ = user.property - if name in property_: - if (n := property_.get(name, 0)) < num: - return False - if n == num: - del property_[name] - else: - property_[name] -= num - await user.save(update_fields=["property"]) - return True - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE bag_users DROP props;", # 删除 props 字段 - "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/ban_user.py b/models/ban_user.py deleted file mode 100755 index e75c9552..00000000 --- a/models/ban_user.py +++ /dev/null @@ -1,131 +0,0 @@ -import time -from typing import Union - -from tortoise import fields - -from services.db_context import Model -from services.log import logger - - -class BanUser(Model): - - user_id = fields.CharField(255, pk=True) - """用户id""" - ban_level = fields.IntField() - """使用ban命令的用户等级""" - ban_time = fields.BigIntField() - """ban开始的时间""" - duration = fields.BigIntField() - """ban时长""" - - class Meta: - table = "ban_users" - table_description = ".ban/b了 封禁人员数据表" - - @classmethod - async def check_ban_level(cls, user_id: Union[int, str], level: int) -> bool: - """ - 说明: - 检测ban掉目标的用户与unban用户的权限等级大小 - 参数: - :param user_id: unban用户的用户id - :param level: ban掉目标用户的权限等级 - """ - user = await cls.filter(user_id=str(user_id)).first() - if user: - logger.debug( - f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", - target=str(user_id), - ) - return bool(user and user.ban_level > level) - return False - - @classmethod - async def check_ban_time(cls, user_id: Union[int, str]) -> Union[str, int]: - """ - 说明: - 检测用户被ban时长 - 参数: - :param user_id: 用户id - """ - logger.debug(f"获取用户ban时长", target=str(user_id)) - if user := await cls.filter(user_id=str(user_id)).first(): - if ( - time.time() - (user.ban_time + user.duration) > 0 - and user.duration != -1 - ): - return "" - if user.duration == -1: - return "∞" - return int(time.time() - user.ban_time - user.duration) - return "" - - @classmethod - async def is_ban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - 判断用户是否被ban - 参数: - :param user_id: 用户id - """ - logger.debug(f"检测是否被ban", target=str(user_id)) - if await cls.check_ban_time(str(user_id)): - return True - else: - await cls.unban(user_id) - return False - - @classmethod - async def is_super_ban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - 判断用户是否被超级用户ban / b了 - 参数: - :param user_id: 用户id - """ - logger.debug(f"检测是否被超级用户权限封禁", target=str(user_id)) - if user := await cls.filter(user_id=str(user_id)).first(): - if user.ban_level == 10: - return True - return False - - @classmethod - async def ban(cls, user_id: Union[int, str], ban_level: int, duration: int): - """ - 说明: - ban掉目标用户 - 参数: - :param user_id: 目标用户id - :param ban_level: 使用ban命令用户的权限 - :param duration: ban时长,秒 - """ - logger.debug(f"封禁用户,等级:{ban_level},时长: {duration}", target=str(user_id)) - if await cls.filter(user_id=str(user_id)).first(): - await cls.unban(user_id) - await cls.create( - user_id=str(user_id), - ban_level=ban_level, - ban_time=time.time(), - duration=duration, - ) - - @classmethod - async def unban(cls, user_id: Union[int, str]) -> bool: - """ - 说明: - unban用户 - 参数: - :param user_id: 用户id - """ - if user := await cls.filter(user_id=str(user_id)).first(): - logger.debug("解除封禁", target=str(user_id)) - await user.delete() - return True - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE ban_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE ban_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - ] diff --git a/models/chat_history.py b/models/chat_history.py deleted file mode 100644 index 60afd130..00000000 --- a/models/chat_history.py +++ /dev/null @@ -1,126 +0,0 @@ -from datetime import datetime, timedelta -from typing import Any, List, Literal, Optional, Tuple, Union - -from tortoise import fields -from tortoise.functions import Count - -from services.db_context import Model - - -class ChatHistory(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255, null=True) - """群聊id""" - text = fields.TextField(null=True) - """文本内容""" - plain_text = fields.TextField(null=True) - """纯文本""" - create_time = fields.DatetimeField(auto_now_add=True) - """创建时间""" - bot_id = fields.CharField(255, null=True) - """bot记录id""" - - class Meta: - table = "chat_history" - table_description = "聊天记录数据表" - - @classmethod - async def get_group_msg_rank( - cls, - gid: Union[int, str], - limit: int = 10, - order: str = "DESC", - date_scope: Optional[Tuple[datetime, datetime]] = None, - ) -> List["ChatHistory"]: - """ - 说明: - 获取排行数据 - 参数: - :param gid: 群号 - :param limit: 获取数量 - :param order: 排序类型,desc,des - :param date_scope: 日期范围 - """ - o = "-" if order == "DESC" else "" - query = cls.filter(group_id=str(gid)) - if date_scope: - query = query.filter(create_time__range=date_scope) - return list( - await query.annotate(count=Count("user_id")) - .order_by(o + "count") - .group_by("user_id") - .limit(limit) - .values_list("user_id", "count") - ) # type: ignore - - @classmethod - async def get_group_first_msg_datetime( - cls, group_id: Union[int, str] - ) -> Optional[datetime]: - """ - 说明: - 获取群第一条记录消息时间 - 参数: - :param group_id: 群组id - """ - if ( - message := await cls.filter(group_id=str(group_id)) - .order_by("create_time") - .first() - ): - return message.create_time - - @classmethod - async def get_message( - cls, - uid: Union[int, str], - gid: Union[int, str], - type_: Literal["user", "group"], - msg_type: Optional[Literal["private", "group"]] = None, - days: Optional[Union[int, Tuple[datetime, datetime]]] = None, - ) -> List["ChatHistory"]: - """ - 说明: - 获取消息查询query - 参数: - :param uid: 用户id - :param gid: 群聊id - :param type_: 类型,私聊或群聊 - :param msg_type: 消息类型,用户或群聊 - :param days: 限制日期 - """ - if type_ == "user": - query = cls.filter(user_id=str(uid)) - if msg_type == "private": - query = query.filter(group_id__isnull=True) - elif msg_type == "group": - query = query.filter(group_id__not_isnull=True) - else: - query = cls.filter(group_id=str(gid)) - if uid: - query = query.filter(user_id=str(uid)) - if days: - if isinstance(days, int): - query = query.filter( - create_time__gte=datetime.now() - timedelta(days=days) - ) - elif isinstance(days, tuple): - query = query.filter(create_time__range=days) - return await query.all() # type: ignore - - @classmethod - async def _run_script(cls): - return [ - "alter table chat_history alter group_id drop not null;", # 允许 group_id 为空 - "alter table chat_history alter text drop not null;", # 允许 text 为空 - "alter table chat_history alter plain_text drop not null;", # 允许 plain_text 为空 - "ALTER TABLE chat_history RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE chat_history ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE chat_history ALTER COLUMN group_id TYPE character varying(255);", - "ALTER TABLE chat_history ADD bot_id VARCHAR(255);", # 添加bot_id字段 - "ALTER TABLE chat_history ALTER COLUMN bot_id TYPE character varying(255);", - ] diff --git a/models/goods_info.py b/models/goods_info.py deleted file mode 100644 index d0afcef2..00000000 --- a/models/goods_info.py +++ /dev/null @@ -1,205 +0,0 @@ -from typing import Dict, List, Optional, Tuple, Union - -from tortoise import fields - -from services.db_context import Model - - -class GoodsInfo(Model): - __tablename__ = "goods_info" - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - goods_name = fields.CharField(255, unique=True) - """商品名称""" - goods_price = fields.IntField() - """价格""" - goods_description = fields.TextField() - """描述""" - goods_discount = fields.FloatField(default=1) - """折扣""" - goods_limit_time = fields.BigIntField(default=0) - """限时""" - daily_limit = fields.IntField(default=0) - """每日限购""" - daily_purchase_limit: Dict[str, Dict[str, int]] = fields.JSONField(default={}) - """用户限购记录""" - is_passive = fields.BooleanField(default=False) - """是否为被动道具""" - icon = fields.TextField(null=True) - """图标路径""" - - class Meta: - table = "goods_info" - table_description = "商品数据表" - - @classmethod - async def add_goods( - cls, - goods_name: str, - goods_price: int, - goods_description: str, - goods_discount: float = 1, - goods_limit_time: int = 0, - daily_limit: int = 0, - is_passive: bool = False, - icon: Optional[str] = None, - ): - """ - 说明: - 添加商品 - 参数: - :param goods_name: 商品名称 - :param goods_price: 商品价格 - :param goods_description: 商品简介 - :param goods_discount: 商品折扣 - :param goods_limit_time: 商品限时 - :param daily_limit: 每日购买限制 - :param is_passive: 是否为被动道具 - :param icon: 图标 - """ - if not await cls.filter(goods_name=goods_name).first(): - await cls.create( - goods_name=goods_name, - goods_price=goods_price, - goods_description=goods_description, - goods_discount=goods_discount, - goods_limit_time=goods_limit_time, - daily_limit=daily_limit, - is_passive=is_passive, - icon=icon, - ) - - @classmethod - async def delete_goods(cls, goods_name: str) -> bool: - """ - 说明: - 删除商品 - 参数: - :param goods_name: 商品名称 - """ - if goods := await cls.get_or_none(goods_name=goods_name): - await goods.delete() - return True - return False - - @classmethod - async def update_goods( - cls, - goods_name: str, - goods_price: Optional[int] = None, - goods_description: Optional[str] = None, - goods_discount: Optional[float] = None, - goods_limit_time: Optional[int] = None, - daily_limit: Optional[int] = None, - is_passive: Optional[bool] = None, - icon: Optional[str] = None, - ): - """ - 说明: - 更新商品信息 - 参数: - :param goods_name: 商品名称 - :param goods_price: 商品价格 - :param goods_description: 商品简介 - :param goods_discount: 商品折扣 - :param goods_limit_time: 商品限时时间 - :param daily_limit: 每日次数限制 - :param is_passive: 是否为被动 - :param icon: 图标 - """ - if goods := await cls.get_or_none(goods_name=goods_name): - await cls.update_or_create( - goods_name=goods_name, - defaults={ - "goods_price": goods_price or goods.goods_price, - "goods_description": goods_description or goods.goods_description, - "goods_discount": goods_discount or goods.goods_discount, - "goods_limit_time": goods_limit_time - if goods_limit_time is not None - else goods.goods_limit_time, - "daily_limit": daily_limit - if daily_limit is not None - else goods.daily_limit, - "is_passive": is_passive - if is_passive is not None - else goods.is_passive, - "icon": icon or goods.icon, - }, - ) - - @classmethod - async def get_all_goods(cls) -> List["GoodsInfo"]: - """ - 说明: - 获得全部有序商品对象 - """ - query = await cls.all() - id_lst = [x.id for x in query] - goods_lst = [] - for _ in range(len(query)): - min_id = min(id_lst) - goods_lst.append([x for x in query if x.id == min_id][0]) - id_lst.remove(min_id) - return goods_lst - - @classmethod - async def add_user_daily_purchase( - cls, goods: "GoodsInfo", user_id_: Union[int, str], group_id_: Union[int, str], num: int = 1 - ): - """ - 说明: - 添加用户明日购买限制 - 参数: - :param goods: 商品 - :param user_id: 用户id - :param group_id: 群号 - :param num: 数量 - """ - user_id = str(user_id_) - group_id = str(group_id_) - if goods and goods.daily_limit and goods.daily_limit > 0: - if not goods.daily_purchase_limit.get(group_id): - goods.daily_purchase_limit[group_id] = {} - if not goods.daily_purchase_limit[group_id].get(user_id): - goods.daily_purchase_limit[group_id][user_id] = 0 - goods.daily_purchase_limit[group_id][user_id] += num - await goods.save(update_fields=["daily_purchase_limit"]) - - @classmethod - async def check_user_daily_purchase( - cls, goods: "GoodsInfo", user_id_: Union[int, str], group_id_: Union[int, str], num: int = 1 - ) -> Tuple[bool, int]: - """ - 说明: - 检测用户每日购买上限 - 参数: - :param goods: 商品 - :param user_id: 用户id - :param group_id: 群号 - :param num: 数量 - """ - user_id = str(user_id_) - group_id = str(group_id_) - if goods and goods.daily_limit > 0: - if ( - not goods.daily_limit - or not goods.daily_purchase_limit.get(group_id) - or not goods.daily_purchase_limit[group_id].get(user_id) - ): - return goods.daily_limit - num < 0, goods.daily_limit - if goods.daily_purchase_limit[group_id][user_id] + num > goods.daily_limit: - return ( - True, - goods.daily_limit - goods.daily_purchase_limit[group_id][user_id], - ) - return False, 0 - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;", - "ALTER TABLE goods_info ADD daily_purchase_limit Json DEFAULT '{}';", - "ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;", - "ALTER TABLE goods_info ADD icon VARCHAR(255);", - ] diff --git a/models/level_user.py b/models/level_user.py deleted file mode 100755 index 0c67d8ac..00000000 --- a/models/level_user.py +++ /dev/null @@ -1,108 +0,0 @@ -from tortoise import fields - -from services.db_context import Model -from typing import Union - -class LevelUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - user_level = fields.BigIntField() - """用户权限等级""" - group_flag = fields.IntField(default=0) - """特殊标记,是否随群管理员变更而设置权限""" - - class Meta: - table = "level_users" - table_description = "用户权限数据库" - unique_together = ("user_id", "group_id") - - @classmethod - async def get_user_level(cls, user_id: Union[int, str], group_id: Union[int, str]) -> int: - """ - 说明: - 获取用户在群内的等级 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.user_level - return -1 - - @classmethod - async def set_level( - cls, user_id: Union[int, str], group_id: Union[int, str], level: int, group_flag: int = 0 - ): - """ - 说明: - 设置用户在群内的权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - :param level: 权限等级 - :param group_flag: 是否被自动更新刷新权限 0:是,1:否 - """ - await cls.update_or_create( - user_id=str(user_id), - group_id=str(group_id), - defaults={"user_level": level, "group_flag": group_flag}, - ) - - @classmethod - async def delete_level(cls, user_id: Union[int, str], group_id: Union[int, str]) -> bool: - """ - 说明: - 删除用户权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - await user.delete() - return True - return False - - @classmethod - async def check_level(cls, user_id: Union[int, str], group_id: Union[int, str], level: int) -> bool: - """ - 说明: - 检查用户权限等级是否大于 level - 参数: - :param user_id: 用户id - :param group_id: 群组id - :param level: 权限等级 - """ - if group_id: - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.user_level >= level - else: - user_list = await cls.filter(user_id=str(user_id)).all() - user = max(user_list, key=lambda x: x.user_level) - return user.user_level >= level - return False - - @classmethod - async def is_group_flag(cls, user_id: Union[int, str], group_id: Union[int, str]) -> bool: - """ - 说明: - 检测是否会被自动更新刷新权限 - 参数: - :param user_id: 用户id - :param group_id: 群组id - """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): - return user.group_flag == 1 - return False - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE level_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/sign_group_user.py b/models/sign_group_user.py deleted file mode 100755 index 702ff4ae..00000000 --- a/models/sign_group_user.py +++ /dev/null @@ -1,80 +0,0 @@ -from datetime import datetime -from typing import List, Literal, Optional, Tuple, Union - -from tortoise import fields - -from services.db_context import Model - - -class SignGroupUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - checkin_count = fields.IntField(default=0) - """签到次数""" - checkin_time_last = fields.DatetimeField(default=datetime.min) - """最后签到时间""" - impression = fields.DecimalField(10, 3, default=0) - """好感度""" - add_probability = fields.DecimalField(10, 3, default=0) - """双倍签到增加概率""" - specify_probability = fields.DecimalField(10, 3, default=0) - """使用指定双倍概率""" - # specify_probability = fields.DecimalField(10, 3, default=0) - - class Meta: - table = "sign_group_users" - table_description = "群员签到数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def sign(cls, user: "SignGroupUser", impression: float): - """ - 说明: - 签到 - 说明: - :param user: 用户 - :param impression: 增加的好感度 - """ - user.checkin_time_last = datetime.now() - user.checkin_count = user.checkin_count + 1 - user.add_probability = 0 - user.specify_probability = 0 - user.impression = float(user.impression) + impression - await user.save() - - @classmethod - async def get_all_impression( - cls, group_id: Union[int, str] - ) -> Tuple[List[str], List[float], List[str]]: - """ - 说明: - 获取该群所有用户 id 及对应 好感度 - 参数: - :param group_id: 群号 - """ - if group_id: - query = cls.filter(group_id=str(group_id)) - else: - query = cls - value_list = await query.all().values_list("user_id", "group_id", "impression") # type: ignore - user_list = [] - group_list = [] - impression_list = [] - for value in value_list: - user_list.append(value[0]) - group_list.append(value[1]) - impression_list.append(float(value[2])) - return user_list, impression_list, group_list - - @classmethod - async def _run_script(cls): - return ["ALTER TABLE sign_group_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id - "ALTER TABLE sign_group_users ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE sign_group_users ALTER COLUMN group_id TYPE character varying(255);" - ] diff --git a/models/statistics.py b/models/statistics.py deleted file mode 100644 index 28ce3814..00000000 --- a/models/statistics.py +++ /dev/null @@ -1,31 +0,0 @@ - - -from tortoise import fields - -from services.db_context import Model - - -class Statistics(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255, null=True) - """群聊id""" - plugin_name = fields.CharField(255) - """插件名称""" - create_time = fields.DatetimeField(auto_now=True) - """添加日期""" - - class Meta: - table = "statistics" - table_description = "用户权限数据库" - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE statistics RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE statistics ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE statistics ALTER COLUMN group_id TYPE character varying(255);", - ] \ No newline at end of file diff --git a/models/user_shop_gold_log.py b/models/user_shop_gold_log.py deleted file mode 100644 index 41c980f6..00000000 --- a/models/user_shop_gold_log.py +++ /dev/null @@ -1,38 +0,0 @@ -from datetime import datetime - -from tortoise import fields - -from services.db_context import Model - - -class UserShopGoldLog(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - type = fields.IntField() - """金币使用类型 0: 购买, 1: 使用, 2: 插件""" - name = fields.CharField(255) - """商品/插件 名称""" - spend_gold = fields.IntField(default=0) - """花费金币""" - num = fields.IntField() - """数量""" - create_time = fields.DatetimeField(auto_now_add=True) - """创建时间""" - - class Meta: - table = "user_shop_gold_log" - table_description = "金币使用日志表" - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE user_shop_gold_log RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE user_shop_gold_log ALTER COLUMN user_id TYPE character varying(255);", - # 将user_id字段类型改为character varying(255) - "ALTER TABLE user_shop_gold_log ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/__init__.py b/plugins/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/plugins/about.py b/plugins/about.py deleted file mode 100644 index 5feb5941..00000000 --- a/plugins/about.py +++ /dev/null @@ -1,43 +0,0 @@ -from nonebot import on_regex -from nonebot.rule import to_me -from pathlib import Path - - -__zx_plugin_name__ = "关于" -__plugin_usage__ = """ -usage: - 想要更加了解真寻吗 - 指令: - 关于 -""".strip() -__plugin_des__ = "想要更加了解真寻吗" -__plugin_cmd__ = ["关于"] -__plugin_version__ = 0.1 -__plugin_type__ = ("其他",) -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 1, - "default_status": True, - "limit_superuser": False, - "cmd": ["关于"], -} - - -about = on_regex("^关于$", priority=5, block=True, rule=to_me()) - - -@about.handle() -async def _(): - ver_file = Path() / '__version__' - version = None - if ver_file.exists(): - with open(ver_file, 'r', encoding='utf8') as f: - version = f.read().split(':')[-1].strip() - msg = f""" -『绪山真寻Bot』 -版本:{version} -简介:基于Nonebot2与go-cqhttp开发,是一个非常可爱的Bot呀,希望与大家要好好相处 -项目地址:https://github.com/HibiKier/zhenxun_bot -文档地址:https://hibikier.github.io/zhenxun_bot/ - """.strip() - await about.send(msg) diff --git a/plugins/aconfig/__init__.py b/plugins/aconfig/__init__.py deleted file mode 100755 index 3397495b..00000000 --- a/plugins/aconfig/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import random - -from nonebot import on_command, on_keyword -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.rule import to_me - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from utils.message_builder import image -from utils.utils import FreqLimiter - -__zx_plugin_name__ = "基本设置 [Hidden]" -__plugin_usage__ = "用法: 无" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -_flmt = FreqLimiter(300) - - -config_play_game = on_keyword({"打游戏"}, permission=GROUP, priority=1, block=True) - - -@config_play_game.handle() -async def _(event: GroupMessageEvent): - if not _flmt.check(event.group_id): - return - _flmt.start_cd(event.group_id) - await config_play_game.finish( - image(IMAGE_PATH / random.choice(os.listdir(IMAGE_PATH / "dayouxi"))) - ) - - -self_introduction = on_command( - "自我介绍", aliases={"介绍", "你是谁", "你叫什么"}, rule=to_me(), priority=5, block=True -) - - -@self_introduction.handle() -async def _(): - if NICKNAME.find("真寻") != -1: - result = ( - "我叫绪山真寻\n" - "你们可以叫我真寻,小真寻,哪怕你们叫我小寻子我也能接受!\n" - "年龄的话我还是个**岁初中生(至少现在是)\n" - "身高保密!!!(也就比美波里(姐姐..(妹妹))矮一点)\n" - "我生日是在3月6号, 能记住的话我会很高兴的\n现在是自宅警备系的现役JC\n" - "最好的朋友是椛!\n" + image("zhenxun.jpg") - ) - await self_introduction.finish(result) - - -my_wife = on_keyword({"老婆"}, rule=to_me(), priority=5, block=True) - - -@my_wife.handle() -async def _(): - await my_wife.finish(image(IMAGE_PATH / "other" / "laopo.jpg")) diff --git a/plugins/ai/__init__.py b/plugins/ai/__init__.py deleted file mode 100755 index 15ce091c..00000000 --- a/plugins/ai/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import List - -from nonebot import on_message -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.rule import to_me - -from configs.config import NICKNAME, Config -from models.friend_user import FriendUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.utils import get_message_img, get_message_text - -from .data_source import get_chat_result, hello, no_result - -__zx_plugin_name__ = "AI" -__plugin_usage__ = f""" -usage: - 与{NICKNAME}普普通通的对话吧! -""" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "cmd": ["Ai", "ai", "AI", "aI"], -} -__plugin_configs__ = { - "TL_KEY": {"value": [], "help": "图灵Key", "type": List[str]}, - "ALAPI_AI_CHECK": { - "value": False, - "help": "是否检测青云客骂娘回复", - "default_value": False, - "type": bool, - }, - "TEXT_FILTER": { - "value": ["鸡", "口交"], - "help": "文本过滤器,将敏感词更改为*", - "default_value": [], - "type": List[str], - }, -} -Config.add_plugin_config( - "alapi", "ALAPI_TOKEN", None, help_="在 https://admin.alapi.cn/user/login 登录后获取token" -) - -ai = on_message(rule=to_me(), priority=998) - - -@ai.handle() -async def _(bot: Bot, event: MessageEvent): - msg = get_message_text(event.json()) - img = get_message_img(event.json()) - if "CQ:xml" in str(event.get_message()): - return - # 打招呼 - if (not msg and not img) or msg in [ - "你好啊", - "你好", - "在吗", - "在不在", - "您好", - "您好啊", - "你好", - "在", - ]: - await ai.finish(hello()) - img = img[0] if img else "" - if isinstance(event, GroupMessageEvent): - nickname = await GroupInfoUser.get_user_nickname(event.user_id, event.group_id) - else: - nickname = await FriendUser.get_user_nickname(event.user_id) - if not nickname: - if isinstance(event, GroupMessageEvent): - nickname = event.sender.card or event.sender.nickname - else: - nickname = event.sender.nickname - result = await get_chat_result(msg, img, event.user_id, nickname) - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else ''} " - f"问题:{msg} ---- 回答:{result}" - ) - if result: - result = str(result) - for t in Config.get_config("ai", "TEXT_FILTER"): - result = result.replace(t, "*") - await ai.finish(Message(result)) - else: - await ai.finish(no_result()) diff --git a/plugins/ai/data_source.py b/plugins/ai/data_source.py deleted file mode 100755 index 295b5b40..00000000 --- a/plugins/ai/data_source.py +++ /dev/null @@ -1,222 +0,0 @@ -import os -import random -import re - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.message_builder import face, image - -from .utils import ai_message_manager - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -url = "http://openapi.tuling123.com/openapi/api/v2" - -check_url = "https://v2.alapi.cn/api/censor/text" - -index = 0 - -anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) - - -async def get_chat_result(text: str, img_url: str, user_id: int, nickname: str) -> str: - """ - 获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 - :param text: 问题 - :param img_url: 图片链接 - :param user_id: 用户id - :param nickname: 用户昵称 - :return: 回答 - """ - global index - ai_message_manager.add_message(user_id, text) - special_rst = await ai_message_manager.get_result(user_id, nickname) - if special_rst: - ai_message_manager.add_result(user_id, special_rst) - return special_rst - if index == 5: - index = 0 - if len(text) < 6 and random.random() < 0.6: - keys = anime_data.keys() - for key in keys: - if text.find(key) != -1: - return random.choice(anime_data[key]).replace("你", nickname) - rst = await tu_ling(text, img_url, user_id) - if not rst: - rst = await xie_ai(text) - if not rst: - return no_result() - if nickname: - if len(nickname) < 5: - if random.random() < 0.5: - nickname = "~".join(nickname) + "~" - if random.random() < 0.2: - if nickname.find("大人") == -1: - nickname += "大~人~" - rst = str(rst).replace("小主人", nickname).replace("小朋友", nickname) - ai_message_manager.add_result(user_id, rst) - return rst - - -# 图灵接口 -async def tu_ling(text: str, img_url: str, user_id: int) -> str: - """ - 获取图灵接口的回复 - :param text: 问题 - :param img_url: 图片链接 - :param user_id: 用户id - :return: 图灵回复 - """ - global index - TL_KEY = Config.get_config("ai", "TL_KEY") - req = None - if not TL_KEY: - return "" - try: - if text: - req = { - "perception": { - "inputText": {"text": text}, - "selfInfo": { - "location": {"city": "陨石坑", "province": "火星", "street": "第5坑位"} - }, - }, - "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, - } - elif img_url: - req = { - "reqType": 1, - "perception": { - "inputImage": {"url": img_url}, - "selfInfo": { - "location": {"city": "陨石坑", "province": "火星", "street": "第5坑位"} - }, - }, - "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, - } - except IndexError: - index = 0 - return "" - text = "" - response = await AsyncHttpx.post(url, json=req) - if response.status_code != 200: - return no_result() - resp_payload = json.loads(response.text) - if int(resp_payload["intent"]["code"]) in [4003]: - return "" - if resp_payload["results"]: - for result in resp_payload["results"]: - if result["resultType"] == "text": - text = result["values"]["text"] - if "请求次数超过" in text: - text = "" - return text - - -# 屑 AI -async def xie_ai(text: str) -> str: - """ - 获取青云客回复 - :param text: 问题 - :return: 青云可回复 - """ - res = await AsyncHttpx.get( - f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={text}" - ) - content = "" - try: - data = json.loads(res.text) - if data["result"] == 0: - content = data["content"] - if "菲菲" in content: - content = content.replace("菲菲", NICKNAME) - if "艳儿" in content: - content = content.replace("艳儿", NICKNAME) - if "公众号" in content: - content = "" - if "{br}" in content: - content = content.replace("{br}", "\n") - if "提示" in content: - content = content[: content.find("提示")] - if "淘宝" in content or "taobao.com" in content: - return "" - while True: - r = re.search("{face:(.*)}", content) - if r: - id_ = r.group(1) - content = content.replace( - "{" + f"face:{id_}" + "}", str(face(int(id_))) - ) - else: - break - return ( - content - if not content and not Config.get_config("ai", "ALAPI_AI_CHECK") - else await check_text(content) - ) - except Exception as e: - logger.error(f"Ai xie_ai 发生错误 {type(e)}:{e}") - return "" - - -def hello() -> str: - """ - 一些打招呼的内容 - """ - result = random.choice( - ( - "哦豁?!", - "你好!Ov<", - f"库库库,呼唤{NICKNAME}做什么呢", - "我在呢!", - "呼呼,叫俺干嘛", - ) - ) - img = random.choice(os.listdir(IMAGE_PATH / "zai")) - if img[-4:] == ".gif": - result += image(IMAGE_PATH / "zai" / img) - else: - result += image(IMAGE_PATH / "zai" / img) - return result - - -# 没有回答时回复内容 -def no_result() -> str: - """ - 没有回答时的回复 - """ - return random.choice( - [ - "你在说啥子?", - f"纯洁的{NICKNAME}没听懂", - "下次再告诉你(下次一定)", - "你觉得我听懂了吗?嗯?", - "我!不!知!道!", - ] - ) + image( - IMAGE_PATH / "noresult" / random.choice(os.listdir(IMAGE_PATH / "noresult")) - ) - - -async def check_text(text: str) -> str: - """ - ALAPI文本检测,主要针对青云客API,检测为恶俗文本改为无回复的回答 - :param text: 回复 - """ - if not Config.get_config("alapi", "ALAPI_TOKEN"): - return text - params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} - try: - data = (await AsyncHttpx.get(check_url, timeout=2, params=params)).json() - if data["code"] == 200: - if data["data"]["conclusion_type"] == 2: - return "" - except Exception as e: - logger.error(f"检测违规文本错误...{type(e)}:{e}") - return text diff --git a/plugins/ai/utils.py b/plugins/ai/utils.py deleted file mode 100755 index 3b00941b..00000000 --- a/plugins/ai/utils.py +++ /dev/null @@ -1,140 +0,0 @@ -from utils.manager import StaticData -from configs.config import NICKNAME -from models.ban_user import BanUser -from typing import Optional -import random -import time - - -class AiMessageManager(StaticData): - def __init__(self): - super().__init__(None) - self._same_message = [ - "为什么要发一样的话?", - "请不要再重复对我说一句话了,不然我就要生气了!", - "别再发这句话了,我已经知道了...", - "你是只会说这一句话吗?", - "[*],你发我也发!", - "[uname],[*]", - f"救命!有笨蛋一直给{NICKNAME}发一样的话!", - "这句话你已经给我发了{}次了,再发就生气!", - ] - self._repeat_message = [ - f"请不要学{NICKNAME}说话", - f"为什么要一直学{NICKNAME}说话?", - "你再学!你再学我就生气了!", - f"呜呜,你是想欺负{NICKNAME}嘛..", - "[uname]不要再学我说话了!", - "再学我说话,我就把你拉进黑名单(生气", - "你再学![uname]是个笨蛋!", - "你已经学我说话{}次了!别再学了!", - ] - - def add_message(self, user_id: int, message: str): - """ - 添加用户消息 - :param user_id: 用户id - :param message: 消息内容 - """ - if message: - if self._data.get(user_id) is None: - self._data[user_id] = { - "time": time.time(), - "message": [], - "result": [], - "repeat_count": 0, - } - if time.time() - self._data[user_id]["time"] > 60 * 10: - self._data[user_id]["message"].clear() - self._data[user_id]["time"] = time.time() - self._data[user_id]["message"].append(message.strip()) - - def add_result(self, user_id: int, message: str): - """ - 添加回复用户的消息 - :param user_id: 用户id - :param message: 回复消息内容 - """ - if message: - if self._data.get(user_id) is None: - self._data[user_id] = { - "time": time.time(), - "message": [], - "result": [], - "repeat_count": 0, - } - if time.time() - self._data[user_id]["time"] > 60 * 10: - self._data[user_id]["result"].clear() - self._data[user_id]["repeat_count"] = 0 - self._data[user_id]["time"] = time.time() - self._data[user_id]["result"].append(message.strip()) - - async def get_result(self, user_id: int, nickname: str) -> Optional[str]: - """ - 特殊消息特殊回复 - :param user_id: 用户id - :param nickname: 用户昵称 - """ - try: - if len(self._data[user_id]["message"]) < 2: - return None - except KeyError: - return None - msg = await self._get_user_repeat_message_result(user_id) - if not msg: - msg = await self._get_user_same_message_result(user_id) - if msg: - if "[uname]" in msg: - msg = msg.replace("[uname]", nickname) - if not msg.startswith("生气了!你好烦,闭嘴!") and "[*]" in msg: - msg = msg.replace("[*]", self._data[user_id]["message"][-1]) - return msg - - async def _get_user_same_message_result(self, user_id: int) -> Optional[str]: - """ - 重复消息回复 - :param user_id: 用户id - """ - msg = self._data[user_id]["message"][-1] - cnt = 0 - _tmp = self._data[user_id]["message"][:-1] - _tmp.reverse() - for s in _tmp: - if s == msg: - cnt += 1 - else: - break - if cnt > 1: - if random.random() < 0.5 and cnt > 3: - rand = random.randint(60, 300) - await BanUser.ban(user_id, 9, rand) - self._data[user_id]["message"].clear() - return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" - return random.choice(self._same_message).format(cnt) - return None - - async def _get_user_repeat_message_result(self, user_id: int) -> Optional[str]: - """ - 复读真寻的消息回复 - :param user_id: 用户id - """ - msg = self._data[user_id]["message"][-1] - if self._data[user_id]["result"]: - rst = self._data[user_id]["result"][-1] - else: - return None - if msg == rst: - self._data[user_id]["repeat_count"] += 1 - cnt = self._data[user_id]["repeat_count"] - if cnt > 1: - if random.random() < 0.5 and cnt > 3: - rand = random.randint(60, 300) - await BanUser.ban(user_id, 9, rand) - self._data[user_id]["result"].clear() - self._data[user_id]["repeat_count"] = 0 - return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" - return random.choice(self._repeat_message).format(cnt) - return None - - -ai_message_manager = AiMessageManager() diff --git a/plugins/alapi/__init__.py b/plugins/alapi/__init__.py deleted file mode 100755 index 749529c7..00000000 --- a/plugins/alapi/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "alapi", "ALAPI_TOKEN", None, help_="在https://admin.alapi.cn/user/login登录后获取token" -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/alapi/_data_source.py b/plugins/alapi/_data_source.py deleted file mode 100644 index 916143e2..00000000 --- a/plugins/alapi/_data_source.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional, Tuple, Union -from configs.config import Config -from utils.http_utils import AsyncHttpx - - -async def get_data(url: str, params: Optional[dict] = None) -> Tuple[Union[dict, str], int]: - """ - 获取ALAPI数据 - :param url: 请求链接 - :param params: 参数 - """ - if not params: - params = {} - params["token"] = Config.get_config("alapi", "ALAPI_TOKEN") - try: - data = (await AsyncHttpx.get(url, params=params, timeout=5)).json() - if data["code"] == 200: - if not data["data"]: - return "没有搜索到...", 997 - return data, 200 - else: - if data["code"] == 101: - return "缺失ALAPI TOKEN,请在配置文件中填写!", 999 - return f'发生了错误...code:{data["code"]}', 999 - except TimeoutError: - return "超时了....", 998 diff --git a/plugins/alapi/comments_163.py b/plugins/alapi/comments_163.py deleted file mode 100755 index 2176162f..00000000 --- a/plugins/alapi/comments_163.py +++ /dev/null @@ -1,45 +0,0 @@ -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from ._data_source import get_data -from services.log import logger - -__zx_plugin_name__ = "网易云热评" -__plugin_usage__ = """ -usage: - 到点了,还是防不了下塔 - 指令: - 网易云热评/到点了/12点了 -""".strip() -__plugin_des__ = "生了个人,我很抱歉" -__plugin_cmd__ = ["网易云热评", "到点了", "12点了"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["网易云热评", "网易云评论", "到点了", "12点了"], -} - - -comments_163 = on_regex( - "^(网易云热评|网易云评论|到点了|12点了)$", priority=5, block=True -) - - -comments_163_url = "https://v2.alapi.cn/api/comment" - - -@comments_163.handle() -async def _(event: MessageEvent): - data, code = await get_data(comments_163_url) - if code != 200: - await comments_163.finish(data, at_sender=True) - data = data["data"] - comment = data["comment_content"] - song_name = data["title"] - await comments_163.send(f"{comment}\n\t——《{song_name}》") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送网易云热评: {comment} \n\t\t————{song_name}" - ) diff --git a/plugins/alapi/cover.py b/plugins/alapi/cover.py deleted file mode 100755 index 75cd3713..00000000 --- a/plugins/alapi/cover.py +++ /dev/null @@ -1,47 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, Message, GroupMessageEvent -from utils.message_builder import image -from nonebot.params import CommandArg -from ._data_source import get_data -from services.log import logger - -__zx_plugin_name__ = "b封面" -__plugin_usage__ = """ -usage: - b封面 [链接/av/bv/cv/直播id] - 示例:b封面 av86863038 -""".strip() -__plugin_des__ = "快捷的b站视频封面获取方式" -__plugin_cmd__ = ["b封面/B封面"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["b封面", "B封面"], -} - - -cover = on_command("b封面", aliases={"B封面"}, priority=5, block=True) - - -cover_url = "https://v2.alapi.cn/api/bilibili/cover" - - -@cover.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - params = {"c": msg} - data, code = await get_data(cover_url, params) - if code != 200: - await cover.finish(data, at_sender=True) - data = data["data"] - title = data["title"] - img = data["cover"] - await cover.send(Message(f"title:{title}\n{image(img)}")) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 获取b站封面: {title} url:{img}" - ) diff --git a/plugins/alapi/jitang.py b/plugins/alapi/jitang.py deleted file mode 100755 index 4d907684..00000000 --- a/plugins/alapi/jitang.py +++ /dev/null @@ -1,45 +0,0 @@ -from nonebot import on_regex -from services.log import logger -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from ._data_source import get_data - - -__zx_plugin_name__ = "鸡汤" -__plugin_usage__ = """ -usage: - 不喝点什么感觉有点不舒服 - 指令: - 鸡汤 -""".strip() -__plugin_des__ = "喏,亲手为你煮的鸡汤" -__plugin_cmd__ = ["鸡汤"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["鸡汤", "毒鸡汤"], -} - -url = "https://v2.alapi.cn/api/soul" - - -jitang = on_regex("^毒?鸡汤$", priority=5, block=True) - - -@jitang.handle() -async def _(event: MessageEvent): - try: - data, code = await get_data(url) - if code != 200: - await jitang.finish(data, at_sender=True) - await jitang.send(data["data"]["content"]) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送鸡汤:" + data["data"]["content"] - ) - except Exception as e: - await jitang.send("鸡汤煮坏掉了...") - logger.error(f"鸡汤煮坏掉了 {type(e)}:{e}") diff --git a/plugins/alapi/poetry.py b/plugins/alapi/poetry.py deleted file mode 100755 index 1c6ff10d..00000000 --- a/plugins/alapi/poetry.py +++ /dev/null @@ -1,41 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from services.log import logger -from ._data_source import get_data - -__zx_plugin_name__ = "古诗" -__plugin_usage__ = """usage: - 平白无故念首诗 - 示例:念诗/来首诗/念首诗 -""" -__plugin_des__ = "为什么突然文艺起来了!" -__plugin_cmd__ = ["念诗/来首诗/念首诗"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["念诗", "来首诗", "念首诗"], -} - -poetry = on_command("念诗", aliases={"来首诗", "念首诗"}, priority=5, block=True) - - -poetry_url = "https://v2.alapi.cn/api/shici" - - -@poetry.handle() -async def _(event: MessageEvent): - data, code = await get_data(poetry_url) - if code != 200: - await poetry.finish(data, at_sender=True) - data = data["data"] - content = data["content"] - title = data["origin"] - author = data["author"] - await poetry.send(f"{content}\n\t——{author}《{title}》") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送古诗: f'{content}\n\t--{author}《{title}》'" - ) diff --git a/plugins/bilibili_sub/__init__.py b/plugins/bilibili_sub/__init__.py deleted file mode 100755 index 34879661..00000000 --- a/plugins/bilibili_sub/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -from typing import Any, Optional, Tuple - -import nonebot -from nonebot import Driver, on_command, on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import ArgStr, CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from models.level_user import LevelUser -from services.log import logger -from utils.depends import GetConfig -from utils.image_utils import text2image -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_bot, is_number, scheduler - -from .data_source import ( - BilibiliSub, - SubManager, - add_live_sub, - add_season_sub, - add_up_sub, - delete_sub, - get_media_id, - get_sub_status, -) - -__zx_plugin_name__ = "B站订阅" -__plugin_usage__ = """ -usage: - B站直播,番剧,UP动态开播等提醒 - 主播订阅相当于 直播间订阅 + UP订阅 - 指令:[示例Id乱打的,仅做示例] - 添加订阅 ['主播'/'UP'/'番剧'] [id/链接/番名] - 删除订阅 ['主播'/'UP'/'id'] [id] - 查看订阅 - 示例:添加订阅主播 2345344 <-(直播房间id) - 示例:添加订阅UP 2355543 <-(个人主页id) - 示例:添加订阅番剧 史莱姆 <-(支持模糊搜索) - 示例:添加订阅番剧 125344 <-(番剧id) - 示例:删除订阅id 2324344 <-(任意id,通过查看订阅获取) -""".strip() -__plugin_des__ = "非常便利的B站订阅通知" -__plugin_cmd__ = ["添加订阅 [主播/UP/番剧] [id/链接/番名]", "删除订阅 ['主播'/'UP'/'id'] [id]", "查看订阅"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier & NumberSir" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["B站订阅", "b站订阅", "添加订阅", "删除订阅", "查看订阅"], -} -__plugin_configs__ = { - "GROUP_BILIBILI_SUB_LEVEL": { - "value": 5, - "help": "群内bilibili订阅需要管理的权限", - "default_value": 5, - "type": int, - }, - "LIVE_MSG_AT_ALL": { - "value": False, - "help": "直播提醒是否AT全体(仅在真寻是管理员时生效)", - "default_value": False, - "type": bool, - }, - "UP_MSG_AT_ALL": { - "value": False, - "help": "UP动态投稿提醒是否AT全体(仅在真寻是管理员时生效)", - "default_value": False, - "type": bool, - }, - "DOWNLOAD_DYNAMIC_IMAGE": { - "value": True, - "help": "下载动态中的图片并发送在提醒消息中", - "default_value": True, - "type": bool, - }, -} - -add_sub = on_command("添加订阅", priority=5, block=True) -del_sub = on_command("删除订阅", priority=5, block=True) -show_sub_info = on_regex("^查看订阅$", priority=5, block=True) - -driver: Driver = nonebot.get_driver() - - -sub_manager: SubManager - - -@driver.on_startup -async def _(): - global sub_manager - sub_manager = SubManager() - - -@add_sub.handle() -@del_sub.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - sub_level: Optional[int] = GetConfig(config="GROUP_BILIBILI_SUB_LEVEL"), -): - msg = arg.extract_plain_text().strip().split() - if len(msg) < 2: - await add_sub.finish("参数不完全,请查看订阅帮助...") - sub_type = msg[0] - id_ = "" - if isinstance(event, GroupMessageEvent): - if not await LevelUser.check_level( - event.user_id, - event.group_id, - sub_level, # type: ignore - ): - await add_sub.finish( - f"您的权限不足,群内订阅的需要 {sub_level} 级权限..", - at_sender=True, - ) - sub_user = f"{event.user_id}:{event.group_id}" - else: - sub_user = f"{event.user_id}" - state["sub_type"] = sub_type - state["sub_user"] = sub_user - if len(msg) > 1: - if "http" in msg[1]: - msg[1] = msg[1].split("?")[0] - msg[1] = msg[1][:-1] if msg[1][-1] == "/" else msg[1] - msg[1] = msg[1].split("/")[-1] - id_ = msg[1][2:] if msg[1].startswith("md") else msg[1] - if not is_number(id_): - if sub_type in ["season", "动漫", "番剧"]: - rst = "*以为您找到以下番剧,请输入Id选择:*\n" - state["season_data"] = await get_media_id(id_) - if len(state["season_data"]) == 0: # type: ignore - await add_sub.finish(f"未找到番剧:{msg}") - for i, x in enumerate(state["season_data"]): # type: ignore - rst += f'{i + 1}.{state["season_data"][x]["title"]}\n----------\n' # type: ignore - await add_sub.send("\n".join(rst.split("\n")[:-1])) - else: - await add_sub.finish("Id 必须为全数字!") - else: - state["id"] = int(id_) - - -@add_sub.got("sub_type") -@add_sub.got("sub_user") -@add_sub.got("id") -async def _( - event: MessageEvent, - state: T_State, - id_: str = ArgStr("id"), - sub_type: str = ArgStr("sub_type"), - sub_user: str = ArgStr("sub_user"), -): - if sub_type in ["season", "动漫", "番剧"] and state.get("season_data"): - season_data = state["season_data"] - if not is_number(id_) or int(id_) < 1 or int(id_) > len(season_data): - await add_sub.reject_arg("id", "Id必须为数字且在范围内!请重新输入...") - id_ = season_data[int(id_) - 1]["media_id"] - if sub_type in ["主播", "直播"]: - await add_sub.send(await add_live_sub(id_, sub_user)) - elif sub_type.lower() in ["up", "用户"]: - await add_sub.send(await add_up_sub(id_, sub_user)) - elif sub_type in ["season", "动漫", "番剧"]: - await add_sub.send(await add_season_sub(id_, sub_user)) - else: - await add_sub.finish("参数错误,第一参数必须为:主播/up/番剧!") - logger.info( - f"添加订阅:{sub_type} -> {sub_user} -> {id_}", - "添加订阅", - event.user_id, - getattr(event, "group_id", None), - ) - - -@del_sub.got("sub_type") -@del_sub.got("sub_user") -@del_sub.got("id") -async def _( - event: MessageEvent, - id_: str = ArgStr("id"), - sub_type: str = ArgStr("sub_type"), - sub_user: str = ArgStr("sub_user"), -): - if sub_type in ["主播", "直播"]: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user, "live") - elif sub_type.lower() in ["up", "用户"]: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user, "up") - else: - result = await BilibiliSub.delete_bilibili_sub(id_, sub_user) - if result: - await del_sub.send(f"删除订阅id:{id_} 成功...") - logger.info( - f"删除订阅 {id_}", - "删除订阅", - event.user_id, - getattr(event, "group_id", None), - ) - else: - await del_sub.send(f"删除订阅id:{id_} 失败...") - logger.info( - f"删除订阅 {id_} 失败", - "删除订阅", - event.user_id, - getattr(event, "group_id", None), - ) - - -@show_sub_info.handle() -async def _(event: MessageEvent): - if isinstance(event, GroupMessageEvent): - id_ = f"{event.group_id}" - else: - id_ = f"{event.user_id}" - data = await BilibiliSub.filter(sub_users__contains=id_).all() - live_rst = "" - up_rst = "" - season_rst = "" - for x in data: - if x.sub_type == "live": - live_rst += ( - f"\t直播间id:{x.sub_id}\n" f"\t名称:{x.uname}\n" f"------------------\n" - ) - if x.sub_type == "up": - up_rst += f"\tUP:{x.uname}\n" f"\tuid:{x.uid}\n" f"------------------\n" - if x.sub_type == "season": - season_rst += ( - f"\t番剧id:{x.sub_id}\n" - f"\t番名:{x.season_name}\n" - f"\t当前集数:{x.season_current_episode}\n" - f"------------------\n" - ) - live_rst = "当前订阅的直播:\n" + live_rst if live_rst else live_rst - up_rst = "当前订阅的UP:\n" + up_rst if up_rst else up_rst - season_rst = "当前订阅的番剧:\n" + season_rst if season_rst else season_rst - if not live_rst and not up_rst and not season_rst: - live_rst = ( - "该群目前没有任何订阅..." if isinstance(event, GroupMessageEvent) else "您目前没有任何订阅..." - ) - await show_sub_info.send( - image( - await text2image( - live_rst + up_rst + season_rst, padding=10, color="#f9f6f2" - ) - ) - ) - - -# 推送 -@scheduler.scheduled_job( - "interval", - seconds=30, -) -async def _(): - bot = get_bot() - sub = None - if bot: - await sub_manager.reload_sub_data() - sub = await sub_manager.random_sub_data() - if sub: - try: - logger.debug(f"Bilibili订阅开始检测:{sub.sub_id}") - rst = await get_sub_status(sub.sub_id, sub.sub_id, sub.sub_type) - await send_sub_msg(rst, sub, bot) # type: ignore - if sub.sub_type == "live": - rst += "\n" + await get_sub_status(sub.uid, sub.sub_id, "up") - await send_sub_msg(rst, sub, bot) # type: ignore - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id}", e=e) - - -async def send_sub_msg(rst: str, sub: BilibiliSub, bot: Bot): - """ - 推送信息 - :param rst: 回复 - :param sub: BilibiliSub - :param bot: Bot - """ - temp_group = [] - if rst and rst.strip(): - for x in sub.sub_users.split(",")[:-1]: - try: - if ":" in x and x.split(":")[1] not in temp_group: - group_id = int(x.split(":")[1]) - temp_group.append(group_id) - if ( - await bot.get_group_member_info( - group_id=group_id, user_id=int(bot.self_id), no_cache=True - ) - )["role"] in ["owner", "admin"]: - if ( - sub.sub_type == "live" - and Config.get_config("bilibili_sub", "LIVE_MSG_AT_ALL") - ) or ( - sub.sub_type == "up" - and Config.get_config("bilibili_sub", "UP_MSG_AT_ALL") - ): - rst = "[CQ:at,qq=all]\n" + rst - if group_manager.get_plugin_status("bilibili_sub", group_id): - await bot.send_group_msg( - group_id=group_id, message=Message(rst) - ) - else: - await bot.send_private_msg(user_id=int(x), message=Message(rst)) - except Exception as e: - logger.error(f"B站订阅推送发生错误 sub_id: {sub.sub_id}", e=e) diff --git a/plugins/bilibili_sub/data_source.py b/plugins/bilibili_sub/data_source.py deleted file mode 100755 index d0958357..00000000 --- a/plugins/bilibili_sub/data_source.py +++ /dev/null @@ -1,451 +0,0 @@ -import random -from asyncio.exceptions import TimeoutError -from datetime import datetime -from typing import Optional, Tuple, Union - -# from .utils import get_videos -from bilireq import dynamic -from bilireq.exceptions import ResponseCodeError -from bilireq.grpc.dynamic import grpc_get_user_dynamics -from bilireq.grpc.protos.bilibili.app.dynamic.v2.dynamic_pb2 import DynamicType -from bilireq.live import get_room_info_by_id -from bilireq.user import get_videos -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.browser import get_browser -from utils.http_utils import AsyncHttpx, AsyncPlaywright -from utils.manager import resources_manager -from utils.message_builder import image -from utils.utils import get_bot, get_local_proxy - -from .model import BilibiliSub -from .utils import get_meta, get_user_card - -SEARCH_URL = "https://api.bilibili.com/x/web-interface/search/all/v2" - -DYNAMIC_PATH = IMAGE_PATH / "bilibili_sub" / "dynamic" -DYNAMIC_PATH.mkdir(exist_ok=True, parents=True) - - -TYPE2MSG = { - 0: "发布了新动态", - DynamicType.forward: "转发了一条动态", - DynamicType.word: "发布了新文字动态", - DynamicType.draw: "发布了新图文动态", - DynamicType.av: "发布了新投稿", - DynamicType.article: "发布了新专栏", - DynamicType.music: "发布了新音频", -} - - -resources_manager.add_temp_dir(DYNAMIC_PATH) - - -async def add_live_sub(live_id: str, sub_user: str) -> str: - """ - 添加直播订阅 - :param live_id: 直播房间号 - :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) - :return: - """ - try: - if await BilibiliSub.exists( - sub_type="live", sub_id=live_id, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.live库的LiveRoom类中get_room_info改为bilireq.live库的get_room_info_by_id方法""" - live_info = await get_room_info_by_id(live_id) - except ResponseCodeError: - return f"未找到房间号Id:{live_id} 的信息,请检查Id是否正确" - uid = str(live_info["uid"]) - room_id = live_info["room_id"] - short_id = live_info["short_id"] - title = live_info["title"] - live_status = live_info["live_status"] - try: - user_info = await get_user_card(uid) - except ResponseCodeError: - return f"未找到UpId:{uid} 的信息,请检查Id是否正确" - uname = user_info["name"] - dynamic_info = await dynamic.get_user_dynamics(int(uid)) - dynamic_upload_time = 0 - if dynamic_info.get("cards"): - dynamic_upload_time = dynamic_info["cards"][0]["desc"]["dynamic_id"] - if await BilibiliSub.sub_handle( - room_id, - "live", - sub_user, - uid=uid, - live_short_id=short_id, - live_status=live_status, - uname=uname, - dynamic_upload_time=dynamic_upload_time, - ): - if data := await BilibiliSub.get_or_none(sub_id=room_id): - uname = data.uname - return ( - "已成功订阅主播:\n" - f"\ttitle:{title}\n" - f"\tname: {uname}\n" - f"\tlive_id:{room_id}\n" - f"\tuid:{uid}" - ) - return "添加订阅失败..." - except Exception as e: - logger.error(f"订阅主播live_id: {live_id} 错误", e=e) - return "添加订阅失败..." - - -async def add_up_sub(uid: str, sub_user: str) -> str: - """ - 添加订阅 UP - :param uid: UP uid - :param sub_user: 订阅用户 - """ - uname = uid - dynamic_upload_time = 0 - latest_video_created = 0 - try: - if await BilibiliSub.exists( - sub_type="up", sub_id=uid, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.user库中User类的get_user_info改为bilireq.user库的get_user_info方法""" - user_info = await get_user_card(uid) - except ResponseCodeError: - return f"未找到UpId:{uid} 的信息,请检查Id是否正确" - uname = user_info["name"] - """bilibili_api.user库中User类的get_dynamics改为bilireq.dynamic库的get_user_dynamics方法""" - dynamic_info = await dynamic.get_user_dynamics(int(uid)) - if dynamic_info.get("cards"): - dynamic_upload_time = dynamic_info["cards"][0]["desc"]["dynamic_id"] - except Exception as e: - logger.error(f"订阅Up uid: {uid} 错误", e=e) - if await BilibiliSub.sub_handle( - uid, - "up", - sub_user, - uid=uid, - uname=uname, - dynamic_upload_time=dynamic_upload_time, - latest_video_created=latest_video_created, - ): - return "已成功订阅UP:\n" f"\tname: {uname}\n" f"\tuid:{uid}" - else: - return "添加订阅失败..." - - -async def add_season_sub(media_id: str, sub_user: str) -> str: - """ - 添加订阅 UP - :param media_id: 番剧 media_id - :param sub_user: 订阅用户 - """ - try: - if await BilibiliSub.exists( - sub_type="season", sub_id=media_id, sub_users__contains=sub_user + "," - ): - return "该订阅Id已存在..." - try: - """bilibili_api.bangumi库中get_meta改为bilireq.bangumi库的get_meta方法""" - season_info = await get_meta(media_id) - except ResponseCodeError: - return f"未找到media_id:{media_id} 的信息,请检查Id是否正确" - season_id = season_info["media"]["season_id"] - season_current_episode = season_info["media"]["new_ep"]["index"] - season_name = season_info["media"]["title"] - if await BilibiliSub.sub_handle( - media_id, - "season", - sub_user, - season_name=season_name, - season_id=season_id, - season_current_episode=season_current_episode, - ): - return ( - "已成功订阅番剧:\n" - f"\ttitle: {season_name}\n" - f"\tcurrent_episode: {season_current_episode}" - ) - else: - return "添加订阅失败..." - except Exception as e: - logger.error(f"订阅番剧 media_id: {media_id} 错误", e=e) - return "添加订阅失败..." - - -async def delete_sub(sub_id: str, sub_user: str) -> str: - """ - 删除订阅 - :param sub_id: 订阅 id - :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) - """ - if await BilibiliSub.delete_bilibili_sub(sub_id, sub_user): - return f"已成功取消订阅:{sub_id}" - else: - return f"取消订阅:{sub_id} 失败,请检查是否订阅过该Id...." - - -async def get_media_id(keyword: str) -> Optional[dict]: - """ - 获取番剧的 media_id - :param keyword: 番剧名称 - """ - params = {"keyword": keyword} - for _ in range(3): - try: - _season_data = {} - response = await AsyncHttpx.get(SEARCH_URL, params=params, timeout=5) - if response.status_code == 200: - data = response.json() - if data.get("data"): - for item in data["data"]["result"]: - if item["result_type"] == "media_bangumi": - idx = 0 - for x in item["data"]: - _season_data[idx] = { - "media_id": x["media_id"], - "title": x["title"] - .replace('', "") - .replace("", ""), - } - idx += 1 - return _season_data - except TimeoutError: - pass - return {} - - -async def get_sub_status(id_: str, sub_id: str, sub_type: str) -> Union[Message, str]: - """ - 获取订阅状态 - :param id_: 订阅 id - :param sub_type: 订阅类型 - """ - try: - if sub_type == "live": - return await _get_live_status(id_) - elif sub_type == "up": - return await _get_up_status(id_, sub_id) - elif sub_type == "season": - return await _get_season_status(id_) - except ResponseCodeError as e: - logger.error(f"Id:{id_} 获取信息失败...", e=e) - # return f"Id:{id_} 获取信息失败...请检查订阅Id是否存在或稍后再试..." - except Exception as e: - logger.error(f"获取订阅状态发生预料之外的错误 Id_:{id_}", e=e) - # return "发生了预料之外的错误..请稍后再试或联系管理员....." - return "" - - -async def _get_live_status(id_: str) -> str: - """ - 获取直播订阅状态 - :param id_: 直播间 id - """ - """bilibili_api.live库的LiveRoom类中get_room_info改为bilireq.live库的get_room_info_by_id方法""" - live_info = await get_room_info_by_id(id_) - title = live_info["title"] - room_id = live_info["room_id"] - live_status = live_info["live_status"] - cover = live_info["user_cover"] - if sub := await BilibiliSub.get_or_none(sub_id=id_): - if sub.live_status != live_status: - await BilibiliSub.sub_handle(id_, live_status=live_status) - if sub.live_status in [0, 2] and live_status == 1: - return ( - f"" - f"{image(cover)}\n" - f"{sub.uname} 开播啦!\n" - f"标题:{title}\n" - f"直链:https://live.bilibili.com/{room_id}" - ) - return "" - - -async def _get_up_status( - id_: str, live_id: Optional[str] = None -) -> Union[Message, str]: - """获取up动态 - - 参数: - id_: up的id - live_id: 直播间id,当订阅直播间时才有. - - 返回: - Union[Message, str]: 消息 - """ - rst = "" - if _user := await BilibiliSub.get_or_none(sub_id=live_id or id_): - dynamics = None - dynamic = None - uname = "" - try: - dynamics = ( - await grpc_get_user_dynamics(int(id_), proxy=get_local_proxy()) - ).list - except Exception as e: - logger.error("获取动态失败...", target=id_, e=e) - if dynamics: - uname = dynamics[0].modules[0].module_author.author.name - for dyn in dynamics: - if int(dyn.extend.dyn_id_str) > _user.dynamic_upload_time: - dynamic = dyn - break - if not dynamic: - logger.debug(f"{_user.sub_type}:{id_} 未有任何动态, 已跳过....") - return "" - if _user.uname != uname: - await BilibiliSub.sub_handle(live_id or id_, uname=uname) - dynamic_img, link = await get_user_dynamic(dynamic.extend.dyn_id_str, _user) - if not dynamic_img: - logger.debug(f"{id_} 未发布新动态或截图失败, 已跳过....") - return "" - await BilibiliSub.sub_handle( - live_id or id_, dynamic_upload_time=int(dynamic.extend.dyn_id_str) - ) - rst += ( - f"{uname} {TYPE2MSG.get(dynamic.card_type, TYPE2MSG[0])}!\n" - + dynamic_img - + f"\n{link}\n" - ) - video_info = "" - if video_list := [ - module - for module in dynamic.modules - if str(module.module_dynamic.dyn_archive) - ]: - video = video_list[0].module_dynamic.dyn_archive - video_info = ( - image(video.cover) - + f"标题: {video.title}\nBvid: {video.bvid}\n直链: https://www.bilibili.com/video/{video.bvid}" - ) - rst += video_info + "\n" - download_dynamic_image = Config.get_config( - "bilibili_sub", "DOWNLOAD_DYNAMIC_IMAGE" - ) - draw_info = "" - if download_dynamic_image and ( - draw_list := [ - module.module_dynamic.dyn_draw - for module in dynamic.modules - if str(module.module_dynamic.dyn_draw) - ] - ): - idx = 0 - for draws in draw_list: - for draw in list(draws.items): - path = ( - TEMP_PATH - / f"{_user.uid}_{dynamic.extend.dyn_id_str}_draw_{idx}.jpg" - ) - if await AsyncHttpx.download_file(draw.src, path): - draw_info += image(path) - idx += 1 - if draw_info: - rst += "动态图片\n" + draw_info + "\n" - return rst - - -async def _get_season_status(id_: str) -> str: - """ - 获取 番剧 更新状态 - :param id_: 番剧 id - """ - """bilibili_api.bangumi库中get_meta改为bilireq.bangumi库的get_meta方法""" - season_info = await get_meta(id_) - title = season_info["media"]["title"] - if data := await BilibiliSub.get_or_none(sub_id=id_): - _idx = data.season_current_episode - new_ep = season_info["media"]["new_ep"]["index"] - if new_ep != _idx: - await BilibiliSub.sub_handle( - id_, season_current_episode=new_ep, season_update_time=datetime.now() - ) - return ( - f'{image(season_info["media"]["cover"])}\n' - f"[{title}]更新啦\n" - f"最新集数:{new_ep}" - ) - return "" - - -async def get_user_dynamic( - dynamic_id: str, local_user: BilibiliSub -) -> Tuple[Optional[MessageSegment], str]: - """ - 获取用户动态 - :param dynamic_id: 动态id - :param local_user: 数据库存储的用户数据 - :return: 最新动态截图与时间 - """ - if local_user.dynamic_upload_time < int(dynamic_id): - image = await AsyncPlaywright.screenshot( - f"https://t.bilibili.com/{dynamic_id}", - DYNAMIC_PATH / f"sub_{local_user.sub_id}.png", - ".bili-dyn-item__main", - wait_until="networkidle", - ) - return ( - image, - f"https://t.bilibili.com/{dynamic_id}", - ) - return None, "" - - -class SubManager: - def __init__(self): - self.live_data = [] - self.up_data = [] - self.season_data = [] - self.current_index = -1 - - async def reload_sub_data(self): - """ - 重载数据 - """ - if not self.live_data or not self.up_data or not self.season_data: - ( - _live_data, - _up_data, - _season_data, - ) = await BilibiliSub.get_all_sub_data() - if not self.live_data: - self.live_data = _live_data - if not self.up_data: - self.up_data = _up_data - if not self.season_data: - self.season_data = _season_data - - async def random_sub_data(self) -> Optional[BilibiliSub]: - """ - 随机获取一条数据 - :return: - """ - sub = None - if not self.live_data and not self.up_data and not self.season_data: - return sub - self.current_index += 1 - if self.current_index == 0: - if self.live_data: - sub = random.choice(self.live_data) - self.live_data.remove(sub) - elif self.current_index == 1: - if self.up_data: - sub = random.choice(self.up_data) - self.up_data.remove(sub) - elif self.current_index == 2: - if self.season_data: - sub = random.choice(self.season_data) - self.season_data.remove(sub) - else: - self.current_index = -1 - if sub: - return sub - await self.reload_sub_data() - return await self.random_sub_data() diff --git a/plugins/bilibili_sub/model.py b/plugins/bilibili_sub/model.py deleted file mode 100755 index 28c6cf7f..00000000 --- a/plugins/bilibili_sub/model.py +++ /dev/null @@ -1,207 +0,0 @@ -from datetime import datetime -from typing import List, Optional, Tuple - -from tortoise import fields - -from services.db_context import Model -from services.log import logger - - -class BilibiliSub(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - sub_id = fields.CharField(255) - """订阅id""" - sub_type = fields.CharField(255) - """订阅类型""" - sub_users = fields.TextField() - """订阅用户""" - live_short_id = fields.CharField(255, null=True) - """直播短id""" - live_status = fields.IntField(null=True) - """直播状态 0: 停播 1: 直播""" - uid = fields.CharField(255, null=True) - """主播/UP UID""" - uname = fields.CharField(255, null=True) - """主播/UP 名称""" - latest_video_created = fields.BigIntField(null=True) - """最后视频上传时间""" - dynamic_upload_time = fields.BigIntField(null=True, default=0) - """动态发布时间""" - season_name = fields.CharField(255, null=True) - """番剧名称""" - season_id = fields.IntField(null=True) - """番剧id""" - season_current_episode = fields.CharField(255, null=True) - """番剧最新集数""" - season_update_time = fields.DateField(null=True) - """番剧更新日期""" - - class Meta: - table = "bilibili_sub" - table_description = "B站订阅数据表" - unique_together = ("sub_id", "sub_type") - - @classmethod - async def sub_handle( - cls, - sub_id: str, - sub_type: Optional[str] = None, - sub_user: str = "", - *, - live_short_id: Optional[str] = None, - live_status: Optional[int] = None, - dynamic_upload_time: int = 0, - uid: Optional[str] = None, - uname: Optional[str] = None, - latest_video_created: Optional[int] = None, - season_name: Optional[str] = None, - season_id: Optional[int] = None, - season_current_episode: Optional[str] = None, - season_update_time: Optional[datetime] = None, - ) -> bool: - """ - 说明: - 添加订阅 - 参数: - :param sub_id: 订阅名称,房间号,番剧号等 - :param sub_type: 订阅类型 - :param sub_user: 订阅此条目的用户 - :param live_short_id: 直接短 id - :param live_status: 主播开播状态 - :param dynamic_upload_time: 主播/UP最新动态时间 - :param uid: 主播/UP uid - :param uname: 用户名称 - :param latest_video_created: 最新视频上传时间 - :param season_name: 番剧名称 - :param season_id: 番剧 season_id - :param season_current_episode: 番剧最新集数 - :param season_update_time: 番剧更新时间 - """ - sub_id = str(sub_id) - try: - data = { - "sub_type": sub_type, - "sub_user": sub_user, - "live_short_id": live_short_id, - "live_status": live_status, - "dynamic_upload_time": dynamic_upload_time, - "uid": uid, - "uname": uname, - "latest_video_created": latest_video_created, - "season_name": season_name, - "season_id": season_id, - "season_current_episode": season_current_episode, - "season_update_time": season_update_time, - } - if sub_user: - sub_user = sub_user if sub_user[-1] == "," else f"{sub_user}," - sub = None - if sub_type: - sub = await cls.get_or_none(sub_id=sub_id, sub_type=sub_type) - else: - sub = await cls.get_or_none(sub_id=sub_id) - if sub: - sub_users = sub.sub_users + sub_user - data["sub_type"] = sub_type or sub.sub_type - data["sub_users"] = sub_users - data["live_short_id"] = live_short_id or sub.live_short_id - data["live_status"] = ( - live_status if live_status is not None else sub.live_status - ) - data["dynamic_upload_time"] = ( - dynamic_upload_time or sub.dynamic_upload_time - ) - data["uid"] = uid or sub.uid - data["uname"] = uname or sub.uname - data["latest_video_created"] = ( - latest_video_created or sub.latest_video_created - ) - data["season_name"] = season_name or sub.season_name - data["season_id"] = season_id or sub.season_id - data["season_current_episode"] = ( - season_current_episode or sub.season_current_episode - ) - data["season_update_time"] = ( - season_update_time or sub.season_update_time - ) - else: - await cls.create(sub_id=sub_id, sub_type=sub_type, sub_users=sub_user) - await cls.update_or_create(sub_id=sub_id, defaults=data) - return True - except Exception as e: - logger.error(f"添加订阅 Id: {sub_id} 错误", e=e) - return False - - @classmethod - async def delete_bilibili_sub( - cls, sub_id: str, sub_user: str, sub_type: Optional[str] = None - ) -> bool: - """ - 说明: - 删除订阅 - 参数: - :param sub_id: 订阅名称 - :param sub_user: 删除此条目的用户 - """ - try: - group_id = None - contains_str = sub_user - if ":" in sub_user: - group_id = sub_user.split(":")[1] - contains_str = f":{group_id}" - if sub_type: - sub = await cls.get_or_none( - sub_id=sub_id, sub_type=sub_type, sub_users__contains=contains_str - ) - else: - sub = await cls.get_or_none( - sub_id=sub_id, sub_users__contains=contains_str - ) - if not sub: - return False - if group_id: - sub.sub_users = ",".join( - [s for s in sub.sub_users.split(",") if f":{group_id}" not in s] - ) - else: - sub.sub_users = sub.sub_users.replace(f"{sub_user},", "") - if sub.sub_users.strip(): - await sub.save(update_fields=["sub_users"]) - else: - await sub.delete() - return True - except Exception as e: - logger.error(f"bilibili_sub 删除订阅错误", target=sub_id, e=e) - return False - - @classmethod - async def get_all_sub_data( - cls, - ) -> Tuple[List["BilibiliSub"], List["BilibiliSub"], List["BilibiliSub"]]: - """ - 说明: - 分类获取所有数据 - """ - live_data = [] - up_data = [] - season_data = [] - query = await cls.all() - for x in query: - if x.sub_type == "live": - live_data.append(x) - if x.sub_type == "up": - up_data.append(x) - if x.sub_type == "season": - season_data.append(x) - return live_data, up_data, season_data - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE bilibili_sub ALTER COLUMN season_update_time TYPE timestamp with time zone USING season_update_time::timestamp with time zone;", - "alter table bilibili_sub alter COLUMN sub_id type varchar(255);", # 将sub_id字段改为字符串 - "alter table bilibili_sub alter COLUMN live_short_id type varchar(255);", # 将live_short_id字段改为字符串 - "alter table bilibili_sub alter COLUMN uid type varchar(255);", # 将live_short_id字段改为字符串 - ] diff --git a/plugins/bilibili_sub/utils.py b/plugins/bilibili_sub/utils.py deleted file mode 100755 index ee2094f4..00000000 --- a/plugins/bilibili_sub/utils.py +++ /dev/null @@ -1,150 +0,0 @@ -from io import BytesIO - -# from bilibili_api import user -from bilireq.user import get_user_info -from httpx import AsyncClient - -from configs.path_config import IMAGE_PATH -from utils.http_utils import AsyncHttpx, get_user_agent -from utils.image_utils import BuildImage - -BORDER_PATH = IMAGE_PATH / "border" -BORDER_PATH.mkdir(parents=True, exist_ok=True) -BASE_URL = "https://api.bilibili.com" - - -async def get_pic(url: str) -> bytes: - """ - 获取图像 - :param url: 图像链接 - :return: 图像二进制 - """ - return (await AsyncHttpx.get(url, timeout=10)).content - - -async def create_live_des_image(uid: int, title: str, cover: str, tags: str, des: str): - """ - 生成主播简介图片 - :param uid: 主播 uid - :param title: 直播间标题 - :param cover: 直播封面 - :param tags: 直播标签 - :param des: 直播简介 - :return: - """ - - user_info = await get_user_info(uid) - name = user_info["name"] - sex = user_info["sex"] - face = user_info["face"] - sign = user_info["sign"] - ava = BuildImage(100, 100, background=BytesIO(await get_pic(face))) - ava.circle() - cover = BuildImage(470, 265, background=BytesIO(await get_pic(cover))) - - -def _create_live_des_image( - title: str, - cover: BuildImage, - tags: str, - des: str, - user_name: str, - sex: str, - sign: str, - ava: BuildImage, -): - """ - 生成主播简介图片 - :param title: 直播间标题 - :param cover: 直播封面 - :param tags: 直播标签 - :param des: 直播简介 - :param user_name: 主播名称 - :param sex: 主播性别 - :param sign: 主播签名 - :param ava: 主播头像 - :return: - """ - border = BORDER_PATH / "0.png" - border_img = None - if border.exists(): - border_img = BuildImage(1772, 2657, background=border) - bk = BuildImage(1772, 2657, font_size=30) - bk.paste(cover, (0, 100), center_type="by_width") - - -async def get_meta(media_id: str, auth=None, reqtype="both", **kwargs): - """ - 根据番剧 ID 获取番剧元数据信息, - 作为bilibili_api和bilireq的替代品。 - 如果bilireq.bangumi更新了,可以转为调用bilireq.bangumi的get_meta方法,两者完全一致。 - """ - from bilireq.utils import get - - url = f"{BASE_URL}/pgc/review/user" - params = {"media_id": media_id} - raw_json = await get( - url, raw=True, params=params, auth=auth, reqtype=reqtype, **kwargs - ) - return raw_json["result"] - - -async def get_videos( - uid: int, tid: int = 0, pn: int = 1, keyword: str = "", order: str = "pubdate" -): - """ - 获取用户投该视频信息 - 作为bilibili_api和bilireq的替代品。 - 如果bilireq.user更新了,可以转为调用bilireq.user的get_videos方法,两者完全一致。 - - :param uid: 用户 UID - :param tid: 分区 ID - :param pn: 页码 - :param keyword: 搜索关键词 - :param order: 排序方式,可以为 “pubdate(上传日期从新到旧), stow(收藏从多到少), click(播放量从多到少)” - """ - from bilireq.utils import ResponseCodeError - - url = f"{BASE_URL}/x/space/arc/search" - headers = get_user_agent() - headers["Referer"] = f"https://space.bilibili.com/{uid}/video" - async with AsyncClient() as client: - r = await client.head( - "https://space.bilibili.com", - headers=headers, - ) - params = { - "mid": uid, - "ps": 30, - "tid": tid, - "pn": pn, - "keyword": keyword, - "order": order, - } - raw_json = ( - await client.get(url, params=params, headers=headers, cookies=r.cookies) - ).json() - if raw_json["code"] != 0: - raise ResponseCodeError( - code=raw_json["code"], - msg=raw_json["message"], - data=raw_json.get("data", None), - ) - return raw_json["data"] - - -async def get_user_card( - mid: str, photo: bool = False, auth=None, reqtype="both", **kwargs -): - from bilireq.utils import get - - url = f"{BASE_URL}/x/web-interface/card" - return ( - await get( - url, - params={"mid": mid, "photo": photo}, - auth=auth, - reqtype=reqtype, - **kwargs, - ) - )["card"] diff --git a/plugins/black_word/__init__.py b/plugins/black_word/__init__.py deleted file mode 100644 index fde8e4dd..00000000 --- a/plugins/black_word/__init__.py +++ /dev/null @@ -1,278 +0,0 @@ -from datetime import datetime -from typing import Any, List, Tuple - -from nonebot import on_command, on_message, on_regex -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER - -from configs.config import NICKNAME, Config -from models.ban_user import BanUser -from services.log import logger -from utils.image_utils import BuildImage -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_message_text, is_number - -from .data_source import set_user_punish, show_black_text_image -from .model import BlackWord -from .utils import black_word_manager - -__zx_plugin_name__ = "敏感词检测" -__plugin_usage__ = """ -usage: - 注意你的发言! - 指令: - 惩罚机制 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 查看和设置惩罚 - Regex:^记录名单(u:\d*)?(g:\d*)?(d[=><]\d*-\d{1,2}-\d{1,2})?$ - 设置惩罚id需要通过 '记录名单u:xxxxxxxx' 获取 - 指令: - 记录名单 - 设置惩罚 [user_id] [下标] [惩罚等级] - 示例:记录名单 - 示例:记录名单u:12345678 - 示例:设置惩罚 12345678 1 4 -""".strip() -__plugin_des__ = "请注意你的发言!!" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_cmd__ = ["惩罚机制", "记录名单 [_superuser]", "设置惩罚 [_superuser]"] -__plugin_settings__ = { - "cmd": ["敏感词检测"], -} - - -Config.add_plugin_config( - "black_word", - "CYCLE_DAYS", - 30, - name="敏感词检测与惩罚", - help_="黑名单词汇记录周期", - default_value=30, - type=int, -) - -Config.add_plugin_config( - "black_word", - "TOLERATE_COUNT", - [5, 1, 1, 1, 1], - help_="各个级别惩罚的容忍次数,依次为:1, 2, 3, 4, 5", - default_value=[5, 1, 1, 1, 1], - type=List[int], -) - -Config.add_plugin_config( - "black_word", "AUTO_PUNISH", True, help_="是否启动自动惩罚机制", default_value=True, type=bool -) - -# Config.add_plugin_config( -# "black_word", "IGNORE_GROUP", [], help_="退出群聊惩罚中忽略的群聊,即不会退出的群聊", default_value=[] -# ) - -Config.add_plugin_config( - "black_word", - "BAN_4_DURATION", - 360, - help_="Union[int, List[int, int]]Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", - default_value=360, - type=int, -) - -Config.add_plugin_config( - "black_word", - "BAN_3_DURATION", - 7, - help_="Union[int, List[int, int]]Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", - default_value=7, - type=int, -) - -Config.add_plugin_config( - "black_word", - "WARNING_RESULT", - f"请注意对{NICKNAME}的发言内容", - help_="口头警告内容", - default_value=f"请注意对{NICKNAME}的发言内容", -) - -Config.add_plugin_config( - "black_word", - "AUTO_ADD_PUNISH_LEVEL", - True, - help_="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", - default_value=True, - type=bool, -) - -Config.add_plugin_config( - "black_word", - "ADD_PUNISH_LEVEL_TO_COUNT", - 3, - help_="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", - default_value=3, - type=int, -) - -Config.add_plugin_config( - "black_word", - "ALAPI_CHECK_FLAG", - False, - help_="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", - default_value=False, - type=bool, -) - -Config.add_plugin_config( - "black_word", - "CONTAIN_BLACK_STOP_PROPAGATION", - True, - help_="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", - default_value=True, - type=bool, -) - -message_matcher = on_message(priority=1, block=False) - -set_punish = on_command("设置惩罚", priority=1, permission=SUPERUSER, block=True) - -show_black = on_regex( - r"^记录名单(u:\d*)?(g:\d*)?(d[=><]\d*-\d{1,2}-\d{1,2})?$", - priority=1, - permission=SUPERUSER, - block=True, -) - -show_punish = on_command("惩罚机制", aliases={"敏感词检测"}, priority=1, block=True) - - -# 黑名单词汇检测 -@run_preprocessor -async def _( - bot: Bot, - matcher: Matcher, - event: Event, -): - msg = get_message_text(event.json()) - if ( - isinstance(event, MessageEvent) - and event.is_tome() - and not msg.startswith("原神绑定") - ): - if str(event.user_id) in bot.config.superusers: - return logger.debug(f"超级用户跳过黑名单词汇检查 Message: {msg}", target=event.user_id) - if ( - event.is_tome() - and matcher.plugin_name == "black_word" - and not await BanUser.is_ban(event.user_id) - ): - # 屏蔽群权限-1的群 - if ( - isinstance(event, GroupMessageEvent) - and group_manager.get_group_level(event.group_id) < 0 - ): - return - user_id = str(event.user_id) - group_id = str(event.group_id) if isinstance(event, GroupMessageEvent) else None - msg = get_message_text(event.json()) - if await black_word_manager.check( - user_id, group_id, msg - ) and Config.get_config("black_word", "CONTAIN_BLACK_STOP_PROPAGATION"): - matcher.stop_propagation() - - -@show_black.handle() -async def _(bot: Bot, reg_group: Tuple[Any, ...] = RegexGroup()): - user_id, group_id, date = reg_group - date_type = "=" - if date: - date_type = date[1] - date = date[2:] - try: - date = datetime.strptime(date, "%Y-%m-%d") - except ValueError: - await show_black.finish("日期格式错误,需要:年-月-日") - pic = await show_black_text_image( - bot, - user_id.split(":")[1] if user_id else None, - group_id.split(":")[1] if group_id else None, - date, - date_type, - ) - await show_black.send(image(b64=pic.pic2bs4())) - - -@show_punish.handle() -async def _(): - text = f""" - ** 惩罚机制 ** - - 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 - - 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 - - 目前公开的惩罚等级: - - 1级:永久ban - - 2级:删除好友 - - 3级:ban指定/随机天数 - - 4级:ban指定/随机时长 - - 5级:警告 - - 备注: - - 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 - - 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 - - 关于敏感词: - - 记住不要骂{NICKNAME}就对了! - """.strip() - max_width = 0 - for m in text.split("\n"): - max_width = len(m) * 20 if len(m) * 20 > max_width else max_width - max_height = len(text.split("\n")) * 24 - A = BuildImage( - max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" - ) - A.text((10, 10), text) - await show_punish.send(image(b64=A.pic2bs4())) - - -@set_punish.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if ( - len(msg) < 3 - or not is_number(msg[0]) - or not is_number(msg[1]) - or not is_number(msg[2]) - ): - await set_punish.finish("参数错误,请查看帮助...", at_sender=True) - uid = msg[0] - id_ = int(msg[1]) - punish_level = int(msg[2]) - rst = await set_user_punish(uid, id_, punish_level) - await set_punish.send(rst) - logger.info( - f"设置惩罚 uid:{uid} id_:{id_} punish_level:{punish_level} --> {rst}", - "设置惩罚", - event.user_id, - ) diff --git a/plugins/black_word/data_source.py b/plugins/black_word/data_source.py deleted file mode 100644 index 2da848d2..00000000 --- a/plugins/black_word/data_source.py +++ /dev/null @@ -1,121 +0,0 @@ -from datetime import datetime -from typing import Optional - -from nonebot.adapters.onebot.v11 import Bot - -from services.log import logger -from utils.image_utils import BuildImage, text2image - -from .model import BlackWord -from .utils import Config, _get_punish - - -async def show_black_text_image( - bot: Bot, - user_id: Optional[str], - group_id: Optional[str], - date: Optional[datetime], - data_type: str = "=", -) -> BuildImage: - """ - 展示记录名单 - :param bot: bot - :param user: 用户qq - :param group_id: 群聊 - :param date: 日期 - :param data_type: 日期搜索类型 - :return: - """ - data = await BlackWord.get_black_data(user_id, group_id, date, data_type) - A = BuildImage(0, 0, color="#f9f6f2", font_size=20) - image_list = [] - friend_str = await bot.get_friend_list() - id_str = "" - uname_str = "" - uid_str = "" - gid_str = "" - plant_text_str = "" - black_word_str = "" - punish_str = "" - punish_level_str = "" - create_time_str = "" - for i, x in enumerate(data): - try: - if x.group_id: - user_name = ( - await bot.get_group_member_info( - group_id=int(x.group_id), user_id=int(x.user_id) - ) - )["card"] - else: - user_name = [ - u["nickname"] for u in friend_str if u["user_id"] == int(x.user_id) - ][0] - except Exception as e: - logger.warning( - f"show_black_text_image 获取 USER {x.user_id} user_name 失败", e=e - ) - user_name = x.user_id - id_str += f"{i}\n" - uname_str += f"{user_name}\n" - uid_str += f"{x.user_id}\n" - gid_str += f"{x.group_id}\n" - plant_text = " ".join(x.plant_text.split("\n")) - if A.getsize(plant_text)[0] > 200: - plant_text = plant_text[:20] + "..." - plant_text_str += f"{plant_text}\n" - black_word_str += f"{x.black_word}\n" - punish_str += f"{x.punish}\n" - punish_level_str += f"{x.punish_level}\n" - create_time_str += f"{x.create_time.replace(microsecond=0)}\n" - _tmp_img = BuildImage(0, 0, font_size=35, font="CJGaoDeGuo.otf") - for s, type_ in [ - (id_str, "Id"), - (uname_str, "昵称"), - (uid_str, "UID"), - (gid_str, "GID"), - (plant_text_str, "文本"), - (black_word_str, "检测"), - (punish_str, "惩罚"), - (punish_level_str, "等级"), - (create_time_str, "记录日期"), - ]: - img = await text2image(s, color="#f9f6f2", _add_height=2.1) - w = _tmp_img.getsize(type_)[0] if _tmp_img.getsize(type_)[0] > img.w else img.w - A = BuildImage(w + 11, img.h + 50, color="#f9f6f2", font_size=35, font="CJGaoDeGuo.otf") - await A.atext((10, 10), type_) - await A.apaste(img, (0, 50)) - image_list.append(A) - horizontal_line = [] - w, h = 0, 0 - for img in image_list: - w += img.w + 20 - h = img.h if img.h > h else h - horizontal_line.append(img.w) - A = BuildImage(w, h, color="#f9f6f2") - current_w = 0 - for img in image_list: - await A.apaste(img, (current_w, 0)) - current_w += img.w + 20 - return A - - -async def set_user_punish(user_id: str, id_: int, punish_level: int) -> str: - """ - 设置惩罚 - :param user_id: 用户id - :param id_: 记录下标 - :param punish_level: 惩罚等级 - """ - result = await _get_punish(punish_level, user_id) - punish = { - 1: "永久ban", - 2: "删除好友", - 3: f"ban {result} 天", - 4: f"ban {result} 分钟", - 5: "口头警告" - } - if await BlackWord.set_user_punish(user_id, punish[punish_level], id_=id_): - return f"已对 USER {user_id} 进行 {punish[punish_level]} 处罚。" - else: - return "操作失败,可能未找到用户,id或敏感词" diff --git a/plugins/black_word/model.py b/plugins/black_word/model.py deleted file mode 100644 index d3cd3afa..00000000 --- a/plugins/black_word/model.py +++ /dev/null @@ -1,149 +0,0 @@ -from datetime import datetime, timedelta -from typing import List, Optional - -from tortoise import fields - -from services.db_context import Model - - -class BlackWord(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255, null=True) - """群聊id""" - plant_text = fields.TextField() - """检测文本""" - black_word = fields.TextField() - """黑名单词语""" - punish = fields.TextField(default="") - """惩罚内容""" - punish_level = fields.IntField() - """惩罚等级""" - create_time = fields.DatetimeField(auto_now_add=True) - """创建时间""" - - class Meta: - table = "black_word" - table_description = "惩罚机制数据表" - - @classmethod - async def set_user_punish( - cls, - user_id: str, - punish: str, - black_word: Optional[str] = None, - id_: Optional[int] = None, - ) -> bool: - """ - 说明: - 设置处罚 - 参数: - :param user_id: 用户id - :param punish: 处罚 - :param black_word: 黑名单词汇 - :param id_: 记录下标 - """ - user = None - if (not black_word and id_ is None) or not punish: - return False - if black_word: - user = ( - await cls.filter(user_id=user_id, black_word=black_word, punish="") - .order_by("id") - .first() - ) - elif id_ is not None: - user_list = await cls.filter(user_id=user_id).order_by("id").all() - if len(user_list) == 0 or (id_ < 0 or id_ > len(user_list)): - return False - user = user_list[id_] - if not user: - return False - user.punish = f"{user.punish}{punish} " - await user.save(update_fields=["punish"]) - return True - - @classmethod - async def get_user_count( - cls, user_id: str, days: int = 7, punish_level: Optional[int] = None - ) -> int: - """ - 说明: - 获取用户规定周期内的犯事次数 - 参数: - :param user_id: 用户id - :param days: 周期天数 - :param punish_level: 惩罚等级 - """ - query = cls.filter( - user_id=user_id, - create_time__gte=datetime.now() - timedelta(days=days), - punish_level__not_in=[-1], - ) - if punish_level is not None: - query = query.filter(punish_level=punish_level) - return await query.count() - - @classmethod - async def get_user_punish_level(cls, user_id: str, days: int = 7) -> Optional[int]: - """ - 说明: - 获取用户最近一次的惩罚记录等级 - 参数: - :param user_id: 用户id - :param days: 周期天数 - """ - if ( - user := await cls.filter( - user_id=user_id, - create_time__gte=datetime.now() - timedelta(days=days), - ) - .order_by("id") - .first() - ): - return user.punish_level - return None - - @classmethod - async def get_black_data( - cls, - user_id: Optional[str], - group_id: Optional[str], - date: Optional[datetime], - date_type: str = "=", - ) -> List["BlackWord"]: - """ - 说明: - 通过指定条件查询数据 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param date: 日期 - :param date_type: 日期查询类型 - """ - query = cls - if user_id: - query = query.filter(user_id=user_id) - if group_id: - query = query.filter(group_id=group_id) - if date: - if date_type == "=": - query = query.filter( - create_time__range=[date, date + timedelta(days=1)] - ) - elif date_type == ">": - query = query.filter(create_time__gte=date) - elif date_type == "<": - query = query.filter(create_time__lte=date) - return await query.all().order_by("id") # type: ignore - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE black_word RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE black_word ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE black_word ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/black_word/utils.py b/plugins/black_word/utils.py deleted file mode 100644 index f1b5842d..00000000 --- a/plugins/black_word/utils.py +++ /dev/null @@ -1,343 +0,0 @@ -import random -from pathlib import Path -from typing import Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import ActionFailed - -from configs.config import Config -from configs.path_config import DATA_PATH -from models.ban_user import BanUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import cn2py, get_bot - -from .model import BlackWord - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -class BlackWordManager: - - """ - 敏感词管理( 拒绝恶意 - """ - - def __init__(self, word_file: Path, py_file: Path): - self._word_list = { - "1": [], - "2": [], - "3": [], - "4": ["sb", "nmsl", "mdzz", "2b", "jb", "操", "废物", "憨憨", "cnm", "rnm"], - "5": [], - } - self._py_list = { - "1": [], - "2": [], - "3": [], - "4": [ - "shabi", - "wocaonima", - "sima", - "sabi", - "zhizhang", - "naocan", - "caonima", - "rinima", - "simadongxi", - "simawanyi", - "hanbi", - "hanpi", - "laji", - "fw", - ], - "5": [], - } - word_file.parent.mkdir(parents=True, exist_ok=True) - if word_file.exists(): - # 清空默认配置 - with open(word_file, "r", encoding="utf8") as f: - self._word_list = json.load(f) - else: - with open(word_file, "w", encoding="utf8") as f: - json.dump( - self._word_list, - f, - ensure_ascii=False, - indent=4, - ) - if py_file.exists(): - # 清空默认配置 - with open(py_file, "r", encoding="utf8") as f: - self._py_list = json.load(f) - else: - with open(py_file, "w", encoding="utf8") as f: - json.dump( - self._py_list, - f, - ensure_ascii=False, - indent=4, - ) - - async def check( - self, user_id: str, group_id: Optional[str], message: str - ) -> Optional[Union[str, bool]]: - """ - 检查是否包含黑名单词汇 - :param user_id: 用户id - :param group_id: 群号 - :param message: 消息 - """ - logger.debug(f"检查文本是否含有黑名单词汇: {message}", "敏感词检测", user_id, group_id) - if data := self._check(message): - if data[0]: - await _add_user_black_word( - user_id, group_id, data[0], message, int(data[1]) - ) - return True - if Config.get_config("black_word", "ALAPI_CHECK_FLAG") and not await check_text( - message - ): - await send_msg( - 0, None, f"USER {user_id} GROUP {group_id} ALAPI 疑似检测:{message}" - ) - return False - - def _check(self, message: str) -> Tuple[Optional[str], int]: - """ - 检测文本是否违规 - :param message: 检测消息 - """ - # 移除空格 - message = message.replace(" ", "") - py_msg = cn2py(message).lower() - # 完全匹配 - for x in [self._word_list, self._py_list]: - for level in x: - if message in x[level] or py_msg in x[level]: - return message if message in x[level] else py_msg, int(level) - # 模糊匹配 - for x in [self._word_list, self._py_list]: - for level in x: - for m in x[level]: - if m in message or m in py_msg: - return m, -1 - return None, 0 - - -async def _add_user_black_word( - user_id: str, - group_id: Optional[str], - black_word: str, - message: str, - punish_level: int, -): - """ - 添加敏感词数据 - :param user_id: 用户id - :param group_id: 群号 - :param black_word: 触发的黑名单词汇 - :param message: 原始文本 - :param punish_level: 惩罚等级 - """ - cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7 - user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level) - add_punish_level_to_count = Config.get_config( - "black_word", "ADD_PUNISH_LEVEL_TO_COUNT" - ) - # 周期内超过次数直接提升惩罚 - if ( - Config.get_config("black_word", "AUTO_ADD_PUNISH_LEVEL") - and add_punish_level_to_count - ): - punish_level -= 1 - await BlackWord.create( - user_id=user_id, - group_id=group_id, - plant_text=message, - black_word=black_word, - punish_level=punish_level, - ) - logger.info( - f"已将 USER {user_id} GROUP {group_id} 添加至黑名单词汇记录 Black_word:{black_word} Plant_text:{message}" - ) - # 自动惩罚 - if Config.get_config("black_word", "AUTO_PUNISH") and punish_level != -1: - await _punish_handle(user_id, group_id, punish_level, black_word) - - -async def _punish_handle( - user_id: str, group_id: Optional[str], punish_level: int, black_word: str -): - """ - 惩罚措施,级别越低惩罚越严 - :param user_id: 用户id - :param group_id: 群号 - :param black_word: 触发的黑名单词汇 - """ - logger.info(f"BlackWord USER {user_id} 触发 {punish_level} 级惩罚...") - # 周期天数 - cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7 - # 用户周期内触发punish_level级惩罚的次数 - user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level) - # 获取最近一次的惩罚等级,将在此基础上增加 - punish_level = ( - await BlackWord.get_user_punish_level(user_id, cycle_days) or punish_level - ) - # 容忍次数:List[int] - tolerate_count = Config.get_config("black_word", "TOLERATE_COUNT") - if not tolerate_count or len(tolerate_count) < 5: - tolerate_count = [5, 2, 2, 2, 2] - if punish_level == 1 and user_count > tolerate_count[punish_level - 1]: - # 永久ban - await _get_punish(1, user_id, group_id) - await BlackWord.set_user_punish(user_id, "永久ban 删除好友", black_word) - elif punish_level == 2 and user_count > tolerate_count[punish_level - 1]: - # 删除好友 - await _get_punish(2, user_id, group_id) - await BlackWord.set_user_punish(user_id, "删除好友", black_word) - elif punish_level == 3 and user_count > tolerate_count[punish_level - 1]: - # 永久ban - ban_day = await _get_punish(3, user_id, group_id) - await BlackWord.set_user_punish(user_id, f"ban {ban_day} 天", black_word) - elif punish_level == 4 and user_count > tolerate_count[punish_level - 1]: - # ban指定时长 - ban_time = await _get_punish(4, user_id, group_id) - await BlackWord.set_user_punish(user_id, f"ban {ban_time} 分钟", black_word) - elif punish_level == 5 and user_count > tolerate_count[punish_level - 1]: - # 口头警告 - warning_result = await _get_punish(5, user_id, group_id) - await BlackWord.set_user_punish(user_id, f"口头警告:{warning_result}", black_word) - else: - await BlackWord.set_user_punish(user_id, f"提示!", black_word) - await send_msg( - user_id, - group_id, - f"BlackWordChecker:该条发言已被记录,目前你在{cycle_days}天内的发表{punish_level}级" - f"言论记录次数为:{user_count}次,请注意你的发言\n" - f"* 如果你不清楚惩罚机制,请发送“惩罚机制” *", - ) - - -async def _get_punish( - id_: int, user_id: str, group_id: Optional[str] = None -) -> Optional[Union[int, str]]: - """ - 通过id_获取惩罚 - :param id_: id - :param user_id: 用户id - :param group_id: 群号 - """ - bot = get_bot() - # 忽略的群聊 - # _ignore_group = Config.get_config("black_word", "IGNORE_GROUP") - # 处罚 id 4 ban 时间:int,List[int] - ban_3_duration = Config.get_config("black_word", "BAN_3_DURATION") or 7 - # 处罚 id 4 ban 时间:int,List[int] - ban_4_duration = Config.get_config("black_word", "BAN_4_DURATION") or 360 - # 口头警告内容 - warning_result = Config.get_config("black_word", "WARNING_RESULT") - if user := await GroupInfoUser.get_or_none(user_id=user_id, group_id=group_id): - uname = user.user_name - else: - uname = user_id - # 永久ban - if id_ == 1: - if str(user_id) not in bot.config.superusers: - await BanUser.ban(user_id, 10, 99999999) - await send_msg( - user_id, group_id, f"BlackWordChecker 永久ban USER {uname}({user_id})" - ) - logger.info(f"BlackWord 永久封禁 USER {user_id}...") - # 删除好友(有的话 - elif id_ == 2: - if str(user_id) not in bot.config.superusers: - try: - await bot.delete_friend(user_id=user_id) - await send_msg( - user_id, group_id, f"BlackWordChecker 删除好友 USER {uname}({user_id})" - ) - logger.info(f"BlackWord 删除好友 {user_id}...") - except ActionFailed: - pass - # 封禁用户指定时间,默认7天 - elif id_ == 3: - if isinstance(ban_3_duration, list): - ban_3_duration = random.randint(ban_3_duration[0], ban_3_duration[1]) - await BanUser.ban(user_id, 9, ban_4_duration * 60 * 60 * 24) - await send_msg( - user_id, - group_id, - f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_3_duration} 天处罚。", - ) - logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_3_duration} 天...") - return ban_3_duration - # 封禁用户指定时间,默认360分钟 - elif id_ == 4: - if isinstance(ban_4_duration, list): - ban_4_duration = random.randint(ban_4_duration[0], ban_4_duration[1]) - await BanUser.ban(user_id, 9, ban_4_duration * 60) - await send_msg( - user_id, - group_id, - f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_4_duration} 分钟处罚。", - ) - logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_4_duration} 分钟...") - return ban_4_duration - # 口头警告 - elif id_ == 5: - if group_id: - await bot.send_group_msg(group_id=int(group_id), message=warning_result) - else: - await bot.send_private_msg(user_id=int(user_id), message=warning_result) - logger.info(f"BlackWord 口头警告 USER {user_id}") - return warning_result - return None - - -async def send_msg( - user_id: Union[str, int], group_id: Optional[Union[str, int]], message: str -): - """ - 发送消息 - :param user_id: user_id - :param group_id: group_id - :param message: message - """ - if bot := get_bot(): - if not user_id: - user_id = list(bot.config.superusers)[0] - if group_id: - await bot.send_group_msg(group_id=int(group_id), message=message) - else: - await bot.send_private_msg(user_id=int(user_id), message=message) - - -async def check_text(text: str) -> bool: - """ - ALAPI文本检测,检测输入违规 - :param text: 回复 - """ - if not Config.get_config("alapi", "ALAPI_TOKEN"): - return True - params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} - try: - data = ( - await AsyncHttpx.get( - "https://v2.alapi.cn/api/censor/text", timeout=4, params=params - ) - ).json() - if data["code"] == 200: - return data["data"]["conclusion_type"] == 2 - except Exception as e: - logger.error(f"检测违规文本错误...{type(e)}:{e}") - return True - - -black_word_manager = BlackWordManager( - DATA_PATH / "black_word" / "black_word.json", - DATA_PATH / "black_word" / "black_py.json", -) diff --git a/plugins/bt/__init__.py b/plugins/bt/__init__.py deleted file mode 100755 index d2571441..00000000 --- a/plugins/bt/__init__.py +++ /dev/null @@ -1,91 +0,0 @@ -from asyncio.exceptions import TimeoutError - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message, PrivateMessageEvent -from nonebot.adapters.onebot.v11.permission import PRIVATE -from nonebot.params import ArgStr, CommandArg -from nonebot.typing import T_State - -from services.log import logger -from utils.utils import is_number - -from .data_source import get_bt_info - -__zx_plugin_name__ = "磁力搜索" -__plugin_usage__ = """ -usage: - * 请各位使用后不要转发 * - * 拒绝反冲斗士! * - 指令: - bt [关键词] ?[页数] - 示例:bt 钢铁侠 - 示例:bt 钢铁侠 3 -""".strip() -__plugin_des__ = "bt(磁力搜索)[仅支持私聊,懂的都懂]" -__plugin_cmd__ = ["bt [关键词] ?[页数]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["bt", "磁力搜索", "Bt", "BT"], -} -__plugin_block_limit__ = {"rst": "您有bt任务正在进行,请等待结束."} -__plugin_configs__ = { - "BT_MAX_NUM": { - "value": 10, - "help": "单次BT搜索返回最大消息数量", - "default_value": 10, - "type": int, - } -} - - -bt = on_command("bt", permission=PRIVATE, priority=5, block=True) - - -@bt.handle() -async def _(state: T_State, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if msg: - keyword = None - page = 1 - if n := len(msg): - keyword = msg[0] - if n > 1 and is_number(msg[1]) and int(msg[1]) > 0: - page = int(msg[1]) - state["keyword"] = keyword - state["page"] = page - else: - state["page"] = 1 - - -@bt.got("keyword", prompt="请输入要查询的内容!") -async def _( - event: PrivateMessageEvent, - state: T_State, - keyword: str = ArgStr("keyword"), - page: str = ArgStr("page"), -): - send_flag = False - try: - async for title, type_, create_time, file_size, link in get_bt_info( - keyword, page - ): - await bt.send( - f"标题:{title}\n" - f"类型:{type_}\n" - f"创建时间:{create_time}\n" - f"文件大小:{file_size}\n" - f"种子:{link}" - ) - send_flag = True - except TimeoutError: - await bt.finish(f"搜索 {keyword} 超时...") - except Exception as e: - logger.error(f"bt 错误 {type(e)}:{e}") - await bt.finish(f"bt 其他未知错误..") - if not send_flag: - await bt.send(f"{keyword} 未搜索到...") - logger.info(f"USER {event.user_id} BT搜索 {keyword} 第 {page} 页") diff --git a/plugins/bt/data_source.py b/plugins/bt/data_source.py deleted file mode 100755 index 1b3ba7cd..00000000 --- a/plugins/bt/data_source.py +++ /dev/null @@ -1,42 +0,0 @@ -from bs4 import BeautifulSoup - -from configs.config import Config -from utils.http_utils import AsyncHttpx - -url = "http://www.eclzz.ink" - - -async def get_bt_info(keyword: str, page: int): - """ - 获取资源信息 - :param keyword: 关键词 - :param page: 页数 - """ - text = (await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=5)).text - if "大约0条结果" in text: - return - soup = BeautifulSoup(text, "lxml") - item_lst = soup.find_all("div", {"class": "search-item"}) - bt_max_num = Config.get_config("bt", "BT_MAX_NUM") or 10 - bt_max_num = bt_max_num if bt_max_num < len(item_lst) else len(item_lst) - for item in item_lst[:bt_max_num]: - divs = item.find_all("div") - title = ( - str(divs[0].find("a").text).replace("", "").replace("", "").strip() - ) - spans = divs[2].find_all("span") - type_ = spans[0].text - create_time = spans[1].find("b").text - file_size = spans[2].find("b").text - link = await get_download_link(divs[0].find("a")["href"]) - yield title, type_, create_time, file_size, link - - -async def get_download_link(_url: str) -> str: - """ - 获取资源下载地址 - :param _url: 链接 - """ - text = (await AsyncHttpx.get(f"{url}{_url}")).text - soup = BeautifulSoup(text, "lxml") - return soup.find("a", {"id": "down-url"})["href"] diff --git a/plugins/check/__init__.py b/plugins/check/__init__.py deleted file mode 100755 index 153512ad..00000000 --- a/plugins/check/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -from nonebot import on_command -from .data_source import Check -from nonebot.rule import to_me -from nonebot.permission import SUPERUSER -from utils.message_builder import image - - -__zx_plugin_name__ = "服务器自我检查 [Superuser]" -__plugin_usage__ = """ -usage: - 查看服务器当前状态 - 指令: - 自检 -""" -__plugin_des__ = "查看服务器当前状态" -__plugin_cmd__ = ["自检/check"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -check = Check() - - -check_ = on_command( - "自检", aliases={"check"}, rule=to_me(), permission=SUPERUSER, block=True, priority=1 -) - - -@check_.handle() -async def _(): - await check_.send(image(b64=await check.show())) diff --git a/plugins/check/data_source.py b/plugins/check/data_source.py deleted file mode 100755 index cf24a4dc..00000000 --- a/plugins/check/data_source.py +++ /dev/null @@ -1,77 +0,0 @@ -import psutil -import time -from datetime import datetime -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from configs.path_config import IMAGE_PATH -import asyncio -from services.log import logger - - -class Check: - def __init__(self): - self.cpu = None - self.memory = None - self.disk = None - self.user = None - self.baidu = 200 - self.google = 200 - - async def check_all(self): - await self.check_network() - await asyncio.sleep(0.1) - self.check_system() - self.check_user() - - def check_system(self): - self.cpu = psutil.cpu_percent() - self.memory = psutil.virtual_memory().percent - self.disk = psutil.disk_usage("/").percent - - async def check_network(self): - try: - await AsyncHttpx.get("https://www.baidu.com/", timeout=5) - except Exception as e: - logger.warning(f"访问BaiDu失败... {type(e)}: {e}") - self.baidu = 404 - try: - await AsyncHttpx.get("https://www.google.com/", timeout=5) - except Exception as e: - logger.warning(f"访问Google失败... {type(e)}: {e}") - self.google = 404 - - def check_user(self): - rst = "" - for user in psutil.users(): - rst += f'[{user.name}] {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user.started))}\n' - self.user = rst[:-1] - - async def show(self): - await self.check_all() - A = BuildImage(0, 0, font_size=24) - rst = ( - f'[Time] {str(datetime.now()).split(".")[0]}\n' - f"-----System-----\n" - f"[CPU] {self.cpu}%\n" - f"[Memory] {self.memory}%\n" - f"[Disk] {self.disk}%\n" - f"-----Network-----\n" - f"[BaiDu] {self.baidu}\n" - f"[Google] {self.google}\n" - ) - if self.user: - rst += "-----User-----\n" + self.user - width = 0 - height = 0 - for x in rst.split('\n'): - w, h = A.getsize(x) - if w > width: - width = w - height += 30 - A = BuildImage(width + 50, height + 10, font_size=24, font="HWZhongSong.ttf") - A.transparent(1) - A.text((10, 10), rst) - _x = max(width, height) - bk = BuildImage(_x + 100, _x + 100, background=IMAGE_PATH / "background" / "check" / "0.jpg") - bk.paste(A, alpha=True, center_type='center') - return bk.pic2bs4() diff --git a/plugins/check_zhenxun_update/__init__.py b/plugins/check_zhenxun_update/__init__.py deleted file mode 100755 index 506ae25d..00000000 --- a/plugins/check_zhenxun_update/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -import os -import platform -from pathlib import Path - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.params import ArgStr -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME, Config -from services.log import logger -from utils.utils import get_bot, scheduler - -from .data_source import check_update, get_latest_version_data - -__zx_plugin_name__ = "自动更新 [Superuser]" -__plugin_usage__ = """ -usage: - 检查更新真寻最新版本,包括了自动更新 - 指令: - 检查更新真寻 - 重启 -""".strip() -__plugin_des__ = "就算是真寻也会成长的" -__plugin_cmd__ = ["检查更新真寻", "重启"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "UPDATE_REMIND": { - "value": True, - "help": "真寻是否检测版本状态", - "default": True, - "type": bool, - }, - "AUTO_UPDATE_ZHENXUN": { - "value": False, - "help": "真寻是否自动检查更新", - "default": False, - "type": bool, - }, -} - -update_zhenxun = on_command("检查更新真寻", permission=SUPERUSER, priority=1, block=True) - -restart = on_command( - "重启", - aliases={"restart"}, - permission=SUPERUSER, - rule=to_me(), - priority=1, - block=True, -) - - -@update_zhenxun.handle() -async def _(bot: Bot, event: MessageEvent): - try: - code, error = await check_update(bot) - if error: - logger.error(f"错误: {error}", "检查更新真寻") - await bot.send_private_msg( - user_id=event.user_id, message=f"更新真寻未知错误 {error}" - ) - except Exception as e: - logger.error(f"更新真寻未知错误", "检查更新真寻", e=e) - await bot.send_private_msg( - user_id=event.user_id, - message=f"更新真寻未知错误 {type(e)}: {e}", - ) - else: - if code == 200: - await bot.send_private_msg(user_id=event.user_id, message=f"更新完毕,请重启真寻....") - - -@restart.got("flag", prompt=f"确定是否重启{NICKNAME}?确定请回复[是|好|确定](重启失败咱们将失去联系,请谨慎!)") -async def _(flag: str = ArgStr("flag")): - if flag.lower() in ["true", "是", "好", "确定", "确定是"]: - await restart.send(f"开始重启{NICKNAME}..请稍等...") - open("is_restart", "w") - if str(platform.system()).lower() == "windows": - import sys - - python = sys.executable - os.execl(python, python, *sys.argv) - else: - os.system("./restart.sh") - else: - await restart.send("已取消操作...") - - -@scheduler.scheduled_job( - "cron", - hour=12, - minute=0, -) -async def _(): - if Config.get_config("check_zhenxun_update", "UPDATE_REMIND"): - _version = "v0.0.0" - _version_file = Path() / "__version__" - if _version_file.exists(): - _version = ( - open(_version_file, "r", encoding="utf8") - .readline() - .split(":")[-1] - .strip() - ) - data = await get_latest_version_data() - if data: - latest_version = data["name"] - if _version.lower() != latest_version.lower(): - bot = get_bot() - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"检测到真寻版本更新\n" f"当前版本:{_version},最新版本:{latest_version}", - ) - if Config.get_config("check_zhenxun_update", "AUTO_UPDATE_ZHENXUN"): - try: - code = await check_update(bot) - except Exception as e: - logger.error(f"更新真寻未知错误 {type(e)}:{e}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"更新真寻未知错误 {type(e)}:{e}\n", - ) - else: - if code == 200: - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"更新完毕,请重启{NICKNAME}....", - ) diff --git a/plugins/check_zhenxun_update/data_source.py b/plugins/check_zhenxun_update/data_source.py deleted file mode 100755 index fbb67555..00000000 --- a/plugins/check_zhenxun_update/data_source.py +++ /dev/null @@ -1,221 +0,0 @@ -import asyncio -import os -import platform -import shutil -import tarfile -from pathlib import Path -from typing import List, Tuple - -import nonebot -import ujson as json -from nonebot.adapters.onebot.v11 import Bot, Message - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.message_builder import image - -# if str(platform.system()).lower() == "windows": -# policy = asyncio.WindowsSelectorEventLoopPolicy() -# asyncio.set_event_loop_policy(policy) - - -driver = nonebot.get_driver() - -release_url = "https://api.github.com/repos/HibiKier/zhenxun_bot/releases/latest" - -_version_file = Path() / "__version__" -zhenxun_latest_tar_gz = Path() / "zhenxun_latest_file.tar.gz" -temp_dir = Path() / "temp" -backup_dir = Path() / "backup" - - -@driver.on_bot_connect -async def remind(bot: Bot): - if str(platform.system()).lower() != "windows": - restart = Path() / "restart.sh" - if not restart.exists(): - with open(restart, "w", encoding="utf8") as f: - f.write( - f"pid=$(netstat -tunlp | grep " - + str(bot.config.port) - + " | awk '{print $7}')\n" - "pid=${pid%/*}\n" - "kill -9 $pid\n" - "sleep 3\n" - "python3 bot.py" - ) - os.system("chmod +x ./restart.sh") - logger.info("已自动生成 restart.sh(重启) 文件,请检查脚本是否与本地指令符合...") - is_restart_file = Path() / "is_restart" - if is_restart_file.exists(): - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"真寻重启完毕...", - ) - is_restart_file.unlink() - - -async def check_update(bot: Bot) -> Tuple[int, str]: - logger.info("开始检查更新真寻酱....") - _version = "v0.0.0" - if _version_file.exists(): - _version = ( - open(_version_file, "r", encoding="utf8").readline().split(":")[-1].strip() - ) - data = await get_latest_version_data() - if data: - latest_version = data["name"] - if _version != latest_version: - tar_gz_url = data["tarball_url"] - logger.info(f"检测真寻已更新,当前版本:{_version},最新版本:{latest_version}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"检测真寻已更新,当前版本:{_version},最新版本:{latest_version}\n" f"开始更新.....", - ) - logger.info(f"开始下载真寻最新版文件....") - tar_gz_url = (await AsyncHttpx.get(tar_gz_url)).headers.get("Location") - if await AsyncHttpx.download_file(tar_gz_url, zhenxun_latest_tar_gz): - logger.info("下载真寻最新版文件完成....") - error = await asyncio.get_event_loop().run_in_executor( - None, _file_handle, latest_version - ) - if error: - return 998, error - logger.info("真寻更新完毕,清理文件完成....") - logger.info("开始获取真寻更新日志.....") - update_info = data["body"] - width = 0 - height = len(update_info.split("\n")) * 24 - A = BuildImage(width, height, font_size=20) - for m in update_info.split("\n"): - w, h = A.getsize(m) - if w > width: - width = w - A = BuildImage(width + 50, height, font_size=20) - A.text((10, 10), update_info) - A.save(f"{IMAGE_PATH}/update_info.png") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=Message( - f"真寻更新完成,版本:{_version} -> {latest_version}\n" - f"更新日期:{data['created_at']}\n" - f"更新日志:\n" - f"{image('update_info.png')}" - ), - ) - return 200, "" - else: - logger.warning(f"下载真寻最新版本失败...版本号:{latest_version}") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"下载真寻最新版本失败...版本号:{latest_version}.", - ) - else: - logger.info(f"自动获取真寻版本成功:{latest_version},当前版本为最新版,无需更新...") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"自动获取真寻版本成功:{latest_version},当前版本为最新版,无需更新...", - ) - else: - logger.warning("自动获取真寻版本失败....") - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), message=f"自动获取真寻版本失败...." - ) - return 999, "" - - -def _file_handle(latest_version: str) -> str: - if not temp_dir.exists(): - temp_dir.mkdir(exist_ok=True, parents=True) - if backup_dir.exists(): - shutil.rmtree(backup_dir) - tf = None - error = "" - # try: - backup_dir.mkdir(exist_ok=True, parents=True) - logger.info("开始解压真寻文件压缩包....") - tf = tarfile.open(zhenxun_latest_tar_gz) - tf.extractall(temp_dir) - logger.info("解压真寻文件压缩包完成....") - zhenxun_latest_file = Path(temp_dir) / os.listdir(temp_dir)[0] - update_info_file = Path(zhenxun_latest_file) / "update_info.json" - update_info = json.load(open(update_info_file, "r", encoding="utf8")) - update_file = update_info["update_file"] - add_file = update_info["add_file"] - delete_file = update_info["delete_file"] - config_file = Path() / "configs" / "config.py" - config_path_file = Path() / "configs" / "path_config.py" - # for file in [config_file.name]: - # tmp = "" - # new_file = Path(zhenxun_latest_file) / "configs" / file - # old_file = Path() / "configs" / file - # new_lines = open(new_file, "r", encoding="utf8").readlines() - # old_lines = open(old_file, "r", encoding="utf8").readlines() - # for nl in new_lines: - # tmp += check_old_lines(old_lines, nl) - # with open(old_file, "w", encoding="utf8") as f: - # f.write(tmp) - for file in delete_file + update_file: - if file != "configs": - file = Path() / file - backup_file = Path(backup_dir) / file - if file.exists(): - backup_file.parent.mkdir(parents=True, exist_ok=True) - if backup_file.exists(): - backup_file.unlink() - if file not in [config_file, config_path_file]: - shutil.move(file.absolute(), backup_file.absolute()) - else: - with open(file, "r", encoding="utf8") as rf: - data = rf.read() - with open(backup_file, "w", encoding="utf8") as wf: - wf.write(data) - logger.info(f"已备份文件:{file}") - for file in add_file + update_file: - new_file = Path(zhenxun_latest_file) / file - old_file = Path() / file - if old_file not in [config_file, config_path_file] and file != "configs": - if not old_file.exists() and new_file.exists(): - shutil.move(new_file.absolute(), old_file.absolute()) - logger.info(f"已更新文件:{file}") - # except Exception as e: - # error = f'{type(e)}:{e}' - if tf: - tf.close() - if temp_dir.exists(): - shutil.rmtree(temp_dir) - if zhenxun_latest_tar_gz.exists(): - zhenxun_latest_tar_gz.unlink() - local_update_info_file = Path() / "update_info.json" - if local_update_info_file.exists(): - local_update_info_file.unlink() - with open(_version_file, "w", encoding="utf8") as f: - f.write(f"__version__: {latest_version}") - os.system(f"poetry run pip install -r {(Path() / 'pyproject.toml').absolute()}") - return error - - -# 获取最新版本号 -async def get_latest_version_data() -> dict: - for _ in range(3): - try: - res = await AsyncHttpx.get(release_url) - if res.status_code == 200: - return res.json() - except TimeoutError: - pass - except Exception as e: - logger.error(f"检查更新真寻获取版本失败 {type(e)}:{e}") - return {} - - -# 逐行检测 -def check_old_lines(lines: List[str], line: str) -> str: - if "=" not in line: - return line - for l in lines: - if "=" in l and l.split("=")[0].strip() == line.split("=")[0].strip(): - return l - return line diff --git a/plugins/coser/__init__.py b/plugins/coser/__init__.py deleted file mode 100755 index edb78e3f..00000000 --- a/plugins/coser/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -import time -from typing import Any, Tuple - -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import RegexGroup - -from configs.config import Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.manager import withdraw_message_manager -from utils.message_builder import image - -__zx_plugin_name__ = "coser" -__plugin_usage__ = """ -usage: - 三次元也不戳,嘿嘿嘿 - 指令: - ?N连cos/coser - 示例:cos - 示例:5连cos (单次请求张数小于9) -""".strip() -__plugin_des__ = "三次元也不戳,嘿嘿嘿" -__plugin_cmd__ = ["cos/coser"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["cos", "coser", "括丝", "COS", "Cos", "cOS", "coS"], -} -__plugin_configs__ = { - "WITHDRAW_COS_MESSAGE": { - "value": (0, 1), - "help": "自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - "default_value": (0, 1), - "type": Tuple[int, int], - }, -} - -coser = on_regex(r"^(\d)?连?(cos|COS|coser|括丝)$", priority=5, block=True) - -# 纯cos,较慢:https://picture.yinux.workers.dev -# 比较杂,有福利姬,较快:https://api.jrsgslb.cn/cos/url.php?return=img -url = "https://picture.yinux.workers.dev" - - -@coser.handle() -async def _(event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - num = reg_group[0] or 1 - for _ in range(int(num)): - path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg" - try: - await AsyncHttpx.download_file(url, path) - msg_id = await coser.send(image(path)) - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("coser", "WITHDRAW_COS_MESSAGE"), - ) - logger.info( - f"发送cos", "cos", event.user_id, getattr(event, "group_id", None) - ) - except Exception as e: - await coser.send("你cos给我看!") - logger.error( - f"coser错误", "cos", event.user_id, getattr(event, "group_id", None), e=e - ) diff --git a/plugins/dialogue/__init__.py b/plugins/dialogue/__init__.py deleted file mode 100755 index e7ccb9b5..00000000 --- a/plugins/dialogue/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, Message, GroupMessageEvent -from nonebot.permission import SUPERUSER -from utils.utils import is_number, get_message_img -from utils.message_builder import image -from utils.message_builder import text as _text -from services.log import logger -from utils.message_builder import at -from nonebot.params import CommandArg - - -__zx_plugin_name__ = "联系管理员" -__plugin_usage__ = """ -usage: - 有什么话想对管理员说嘛? - 指令: - [滴滴滴]/滴滴滴- ?[文本] ?[图片] - 示例:滴滴滴- 我喜欢你 -""".strip() -__plugin_superuser_usage__ = """ -superuser usage: - 管理员对消息的回复 - 指令[以下qq与group均为乱打]: - /t: 查看当前存储的消息 - /t [qq] [group] [文本]: 在group回复指定用户 - /t [qq] [文本]: 私聊用户 - /t -1 [group] [文本]: 在group内发送消息 - /t [id] [文本]: 回复指定id的对话,id在 /t 中获取 - 示例:/t 73747222 32848432 你好啊 - 示例:/t 73747222 你好不好 - 示例:/t -1 32848432 我不太好 - 示例:/t 0 我收到你的话了 -""" -__plugin_des__ = "跨越空间与时间跟管理员对话" -__plugin_cmd__ = [ - "滴滴滴-/[滴滴滴] ?[文本] ?[图片]", - "/t [_superuser]", - "t [qq] [group] [文本] [_superuser]", - "/t [qq] [文本] [_superuser]", - "/t -1 [group] [_superuser]", - "/t [id] [文本] [_superuser]", -] -__plugin_type__ = ("联系管理员",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["滴滴滴-", "滴滴滴"], -} - -dialogue_data = {} - - -dialogue = on_command("[滴滴滴]", aliases={"滴滴滴-"}, priority=5, block=True) -reply = on_command("/t", priority=1, permission=SUPERUSER, block=True) - - -@dialogue.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - text = arg.extract_plain_text().strip() - img_msg = _text("") - for img in get_message_img(event.json()): - img_msg += image(img) - if not text and not img_msg: - await dialogue.send("请发送[滴滴滴]+您要说的内容~", at_sender=True) - else: - group_id = 0 - group_name = "None" - nickname = event.sender.nickname - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - group_name = (await bot.get_group_info(group_id=event.group_id))[ - "group_name" - ] - nickname = event.sender.card or event.sender.nickname - for coffee in bot.config.superusers: - await bot.send_private_msg( - user_id=int(coffee), - message=_text( - f"*****一份交流报告*****\n" - f"昵称:{nickname}({event.user_id})\n" - f"群聊:{group_name}({group_id})\n" - f"消息:{text}" - ) - + img_msg, - ) - await dialogue.send( - _text(f"您的话已发送至管理员!\n======\n{text}") + img_msg, at_sender=True - ) - nickname = event.sender.nickname if event.sender.nickname else event.sender.card - dialogue_data[len(dialogue_data)] = { - "nickname": nickname, - "user_id": event.user_id, - "group_id": group_id, - "group_name": group_name, - "msg": _text(text) + img_msg, - } - # print(dialogue_data) - logger.info(f"Q{event.user_id}@群{group_id} 联系管理员:text:{text}") - - -@reply.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - result = "*****待回复消息总览*****\n" - for key in dialogue_data.keys(): - result += ( - f"id:{key}\n" - f'\t昵称:{dialogue_data[key]["nickname"]}({dialogue_data[key]["user_id"]})\n' - f'\t群群:{dialogue_data[key]["group_name"]}({dialogue_data[key]["group_id"]})\n' - f'\t消息:{dialogue_data[key]["msg"]}' - f"\n--------------------\n" - ) - await reply.finish(Message(result[:-1])) - msg = msg.split() - text = "" - group_id = 0 - user_id = -1 - if is_number(msg[0]): - if len(msg[0]) < 3: - msg[0] = int(msg[0]) - if msg[0] >= 0: - id_ = msg[0] - user_id = dialogue_data[id_]["user_id"] - group_id = dialogue_data[id_]["group_id"] - text = " ".join(msg[1:]) - dialogue_data.pop(id_) - else: - user_id = 0 - if is_number(msg[1]): - group_id = int(msg[1]) - text = " ".join(msg[2:]) - else: - await reply.finish("群号错误...", at_sender=True) - else: - user_id = int(msg[0]) - if is_number(msg[1]) and len(msg[1]) > 5: - group_id = int(msg[1]) - text = " ".join(msg[2:]) - else: - group_id = 0 - text = " ".join(msg[1:]) - else: - await reply.finish("第一参数,请输入数字.....", at_sender=True) - for img in get_message_img(event.json()): - text += image(img) - if group_id: - if user_id: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + "\n管理员回复\n=======\n" + text - ) - else: - await bot.send_group_msg(group_id=group_id, message=text) - await reply.finish("消息发送成功...", at_sender=True) - else: - if user_id in [qq["user_id"] for qq in await bot.get_friend_list()]: - await bot.send_private_msg( - user_id=user_id, message="管理员回复\n=======\n" + text - ) - await reply.finish("发送成功", at_sender=True) - else: - await reply.send( - f"对象不是{list(bot.config.nickname)[0]}的好友...", at_sender=True - ) diff --git a/plugins/draw_card/__init__.py b/plugins/draw_card/__init__.py deleted file mode 100644 index f3864344..00000000 --- a/plugins/draw_card/__init__.py +++ /dev/null @@ -1,305 +0,0 @@ -import asyncio -import traceback -from dataclasses import dataclass -from typing import Any, Optional, Set, Tuple - -import nonebot -from cn2an import cn2an -from configs.config import Config -from nonebot import on_keyword, on_message, on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.log import logger -from nonebot.matcher import Matcher -from nonebot.params import RegexGroup -from nonebot.permission import SUPERUSER -from nonebot.typing import T_Handler -from nonebot_plugin_apscheduler import scheduler - -from .handles.azur_handle import AzurHandle -from .handles.ba_handle import BaHandle -from .handles.base_handle import BaseHandle -from .handles.fgo_handle import FgoHandle -from .handles.genshin_handle import GenshinHandle -from .handles.guardian_handle import GuardianHandle -from .handles.onmyoji_handle import OnmyojiHandle -from .handles.pcr_handle import PcrHandle -from .handles.pretty_handle import PrettyHandle -from .handles.prts_handle import PrtsHandle -from .rule import rule - -__zx_plugin_name__ = "游戏抽卡" -__plugin_usage__ = """ -usage: - 模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡 - 指令: - 原神[1-180]抽: 原神常驻池 - 原神角色[1-180]抽: 原神角色UP池子 - 原神角色2池[1-180]抽: 原神角色UP池子 - 原神武器[1-180]抽: 原神武器UP池子 - 重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率] - 方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池 - 赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池 - 坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池 - pcr/公主连接[1-300]抽: 公主连接卡池 - 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池 - fgo[1-300]抽: fgo卡池 - 阴阳师[1-300]抽: 阴阳师卡池 - ba/碧蓝档案[1-200]抽:碧蓝档案卡池 - * 以上指令可以通过 XX一井 来指定最大抽取数量 * - * 示例:原神一井 * -""".strip() -__plugin_superuser_usage__ = """ -usage: - 卡池方面的更新 - 指令: - 更新方舟信息 - 重载方舟卡池 - 更新原神信息 - 重载原神卡池 - 更新赛马娘信息 - 重载赛马娘卡池 - 更新坎公骑冠剑信息 - 更新碧蓝航线信息 - 更新fgo信息 - 更新阴阳师信息 -""".strip() -__plugin_des__ = "就算是模拟抽卡也不能改变自己是个非酋" -__plugin_cmd__ = [ - "原神[1-180]抽", - "原神角色[1-180]抽", - "原神武器[1-180]抽", - "重置原神抽卡", - "方舟[1-300]抽", - "赛马娘[1-200]抽", - "坎公骑冠剑[1-300]抽", - "pcr/公主连接[1-300]抽", - "fgo[1-300]抽", - "阴阳师[1-300]抽", - "碧蓝档案[1-200]抽", - "更新方舟信息 [_superuser]", - "重载方舟卡池 [_superuser]", - "更新原神信息 [_superuser]", - "重载原神卡池 [_superuser]", - "更新赛马娘信息 [_superuser]", - "重载赛马娘卡池 [_superuser]", - "更新坎公骑冠剑信息 [_superuser]", - "更新碧蓝航线信息 [_superuser]", - "更新fgo信息 [_superuser]", - "更新阴阳师信息 [_superuser]", - "更新碧蓝档案信息 [_superuser]", -] -__plugin_type__ = ("抽卡相关", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["游戏抽卡", "抽卡"], -} - - -x = on_message(rule=lambda: False) - - -@dataclass -class Game: - keywords: Set[str] - handle: BaseHandle - flag: bool - config_name: str - max_count: int = 300 # 一次最大抽卡数 - reload_time: Optional[int] = None # 重载UP池时间(小时) - has_other_pool: bool = False - - -games = ( - Game( - {"azur", "碧蓝航线"}, - AzurHandle(), - Config.get_config("draw_card", "AZUR_FLAG", True), - "AZUR_FLAG", - ), - Game( - {"fgo", "命运冠位指定"}, - FgoHandle(), - Config.get_config("draw_card", "FGO_FLAG", True), - "FGO_FLAG", - ), - Game( - {"genshin", "原神"}, - GenshinHandle(), - Config.get_config("draw_card", "GENSHIN_FLAG", True), - "GENSHIN_FLAG", - max_count=180, - reload_time=18, - has_other_pool=True, - ), - Game( - {"guardian", "坎公骑冠剑"}, - GuardianHandle(), - Config.get_config("draw_card", "GUARDIAN_FLAG", True), - "GUARDIAN_FLAG", - reload_time=4, - ), - Game( - {"onmyoji", "阴阳师"}, - OnmyojiHandle(), - Config.get_config("draw_card", "ONMYOJI_FLAG", True), - "ONMYOJI_FLAG", - ), - Game( - {"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"}, - PcrHandle(), - Config.get_config("draw_card", "PCR_FLAG", True), - "PCR_FLAG", - ), - Game( - {"pretty", "马娘", "赛马娘"}, - PrettyHandle(), - Config.get_config("draw_card", "PRETTY_FLAG", True), - "PRETTY_FLAG", - max_count=200, - reload_time=4, - ), - Game( - {"prts", "方舟", "明日方舟"}, - PrtsHandle(), - Config.get_config("draw_card", "PRTS_FLAG", True), - "PRTS_FLAG", - reload_time=4, - ), - Game( - {"ba", "碧蓝档案"}, - BaHandle(), - Config.get_config("draw_card", "BA_FLAG", True), - "BA_FLAG", - ), -) - - -def create_matchers(): - def draw_handler(game: Game) -> T_Handler: - async def handler( - matcher: Matcher, event: MessageEvent, args: Tuple[Any, ...] = RegexGroup() - ): - pool_name, pool_type_, num, unit = args - if num == "单": - num = 1 - else: - try: - num = int(cn2an(num, mode="smart")) - except ValueError: - await matcher.finish("必!须!是!数!字!") - if unit == "井": - num *= game.max_count - if num < 1: - await matcher.finish("虚空抽卡???") - elif num > game.max_count: - await matcher.finish("一井都满不足不了你嘛!快爬开!") - pool_name = ( - pool_name.replace("池", "") - .replace("武器", "arms") - .replace("角色", "char") - .replace("卡牌", "card") - .replace("卡", "card") - ) - try: - if pool_type_ in ["2池", "二池"]: - pool_name = pool_name + "1" - res = game.handle.draw(num, pool_name=pool_name, user_id=event.user_id) - except: - logger.warning(traceback.format_exc()) - await matcher.finish("出错了...") - await matcher.finish(res, at_sender=True) - - return handler - - def update_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher): - await game.handle.update_info() - await matcher.finish("更新完成!") - - return handler - - def reload_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher): - res = await game.handle.reload_pool() - if res: - await matcher.finish(res) - - return handler - - def reset_handler(game: Game) -> T_Handler: - async def handler(matcher: Matcher, event: MessageEvent): - if game.handle.reset_count(event.user_id): - await matcher.finish("重置成功!") - - return handler - - def scheduled_job(game: Game) -> T_Handler: - async def handler(): - await game.handle.reload_pool() - - return handler - - for game in games: - pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})" - num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})" - unit_pattern = r"([抽|井|连])" - pool_type = "()" - if game.has_other_pool: - pool_type = r"([2二]池)?" - draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format( - "|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern - ) - update_keywords = {f"更新{keyword}信息" for keyword in game.keywords} - reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords} - reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords} - on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler( - draw_handler(game) - ) - on_keyword( - update_keywords, priority=1, block=True, permission=SUPERUSER - ).append_handler(update_handler(game)) - on_keyword( - reload_keywords, priority=1, block=True, permission=SUPERUSER - ).append_handler(reload_handler(game)) - on_keyword(reset_keywords, priority=5, block=True).append_handler( - reset_handler(game) - ) - if game.reload_time: - scheduler.add_job( - scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1 - ) - - -create_matchers() - - -# 更新资源 -@scheduler.scheduled_job( - "cron", - hour=4, - minute=1, -) -async def _(): - tasks = [] - for game in games: - if game.flag: - tasks.append(asyncio.ensure_future(game.handle.update_info())) - await asyncio.gather(*tasks) - - -driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - tasks = [] - for game in games: - if game.flag: - game.handle.init_data() - if not game.handle.data_exists(): - tasks.append(asyncio.ensure_future(game.handle.update_info())) - await asyncio.gather(*tasks) diff --git a/plugins/draw_card/config.py b/plugins/draw_card/config.py deleted file mode 100644 index d809b1fd..00000000 --- a/plugins/draw_card/config.py +++ /dev/null @@ -1,204 +0,0 @@ -from pathlib import Path - -import nonebot -from nonebot.log import logger -from pydantic import BaseModel, Extra, ValidationError - -from configs.config import Config as AConfig -from configs.path_config import DATA_PATH, IMAGE_PATH - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -# 原神 -class GenshinConfig(BaseModel, extra=Extra.ignore): - GENSHIN_FIVE_P: float = 0.006 - GENSHIN_FOUR_P: float = 0.051 - GENSHIN_THREE_P: float = 0.43 - GENSHIN_G_FIVE_P: float = 0.016 - GENSHIN_G_FOUR_P: float = 0.13 - I72_ADD: float = 0.0585 - - -# 明日方舟 -class PrtsConfig(BaseModel, extra=Extra.ignore): - PRTS_SIX_P: float = 0.02 - PRTS_FIVE_P: float = 0.08 - PRTS_FOUR_P: float = 0.48 - PRTS_THREE_P: float = 0.42 - - -# 赛马娘 -class PrettyConfig(BaseModel, extra=Extra.ignore): - PRETTY_THREE_P: float = 0.03 - PRETTY_TWO_P: float = 0.18 - PRETTY_ONE_P: float = 0.79 - - -# 坎公骑冠剑 -class GuardianConfig(BaseModel, extra=Extra.ignore): - GUARDIAN_THREE_CHAR_P: float = 0.0275 - GUARDIAN_TWO_CHAR_P: float = 0.19 - GUARDIAN_ONE_CHAR_P: float = 0.7825 - GUARDIAN_THREE_CHAR_UP_P: float = 0.01375 - GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375 - GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03 - GUARDIAN_FIVE_ARMS_P: float = 0.03 - GUARDIAN_FOUR_ARMS_P: float = 0.09 - GUARDIAN_THREE_ARMS_P: float = 0.27 - GUARDIAN_TWO_ARMS_P: float = 0.58 - GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01 - GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02 - - -# 公主连结 -class PcrConfig(BaseModel, extra=Extra.ignore): - PCR_THREE_P: float = 0.025 - PCR_TWO_P: float = 0.18 - PCR_ONE_P: float = 0.795 - PCR_G_THREE_P: float = 0.025 - PCR_G_TWO_P: float = 0.975 - - -# 碧蓝航线 -class AzurConfig(BaseModel, extra=Extra.ignore): - AZUR_FIVE_P: float = 0.012 - AZUR_FOUR_P: float = 0.07 - AZUR_THREE_P: float = 0.12 - AZUR_TWO_P: float = 0.51 - AZUR_ONE_P: float = 0.3 - - -# 命运-冠位指定 -class FgoConfig(BaseModel, extra=Extra.ignore): - FGO_SERVANT_FIVE_P: float = 0.01 - FGO_SERVANT_FOUR_P: float = 0.03 - FGO_SERVANT_THREE_P: float = 0.4 - FGO_CARD_FIVE_P: float = 0.04 - FGO_CARD_FOUR_P: float = 0.12 - FGO_CARD_THREE_P: float = 0.4 - - -# 阴阳师 -class OnmyojiConfig(BaseModel, extra=Extra.ignore): - ONMYOJI_SP: float = 0.0025 - ONMYOJI_SSR: float = 0.01 - ONMYOJI_SR: float = 0.2 - ONMYOJI_R: float = 0.7875 - - -# 碧蓝档案 -class BaConfig(BaseModel, extra=Extra.ignore): - BA_THREE_P: float = 0.025 - BA_TWO_P: float = 0.185 - BA_ONE_P: float = 0.79 - BA_G_TWO_P: float = 0.975 - - -class Config(BaseModel, extra=Extra.ignore): - # 开关 - PRTS_FLAG: bool = True - GENSHIN_FLAG: bool = True - PRETTY_FLAG: bool = True - GUARDIAN_FLAG: bool = True - PCR_FLAG: bool = True - AZUR_FLAG: bool = True - FGO_FLAG: bool = True - ONMYOJI_FLAG: bool = True - BA_FLAG: bool = True - - # 其他配置 - PCR_TAI: bool = True - SEMAPHORE: int = 5 - - # 抽卡概率 - prts: PrtsConfig = PrtsConfig() - genshin: GenshinConfig = GenshinConfig() - pretty: PrettyConfig = PrettyConfig() - guardian: GuardianConfig = GuardianConfig() - pcr: PcrConfig = PcrConfig() - azur: AzurConfig = AzurConfig() - fgo: FgoConfig = FgoConfig() - onmyoji: OnmyojiConfig = OnmyojiConfig() - ba: BaConfig = BaConfig() - - -driver = nonebot.get_driver() - -# DRAW_PATH = Path("data/draw_card").absolute() -DRAW_PATH = IMAGE_PATH / "draw_card" -# try: -# DRAW_PATH = Path(global_config.draw_path).absolute() -# except: -# pass -config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json" - -draw_config: Config = Config() - -for game_flag, game_name in zip( - [ - "PRTS_FLAG", - "GENSHIN_FLAG", - "PRETTY_FLAG", - "GUARDIAN_FLAG", - "PCR_FLAG", - "AZUR_FLAG", - "FGO_FLAG", - "ONMYOJI_FLAG", - "PCR_TAI", - "BA_FLAG", - ], - [ - "明日方舟", - "原神", - "赛马娘", - "坎公骑冠剑", - "公主连结", - "碧蓝航线", - "命运-冠位指定(FGO)", - "阴阳师", - "pcr台服卡池", - "碧蓝档案", - ], -): - AConfig.add_plugin_config( - "draw_card", - game_flag, - True, - name="游戏抽卡", - help_=f"{game_name} 抽卡开关", - default_value=True, - type=bool, - ) -AConfig.add_plugin_config( - "draw_card", "SEMAPHORE", 5, help_=f"异步数据下载数量限制", default_value=5, type=int -) - - -@driver.on_startup -def check_config(): - global draw_config - - if not config_path.exists(): - config_path.parent.mkdir(parents=True, exist_ok=True) - draw_config = Config() - logger.warning("draw_card:配置文件不存在,已重新生成配置文件.....") - else: - with config_path.open("r", encoding="utf8") as fp: - data = json.load(fp) - try: - draw_config = Config.parse_obj({**data}) - except ValidationError: - draw_config = Config() - logger.warning("draw_card:配置文件格式错误,已重新生成配置文件.....") - - with config_path.open("w", encoding="utf8") as fp: - json.dump( - draw_config.dict(), - fp, - indent=4, - ensure_ascii=False, - ) diff --git a/plugins/draw_card/count_manager.py b/plugins/draw_card/count_manager.py deleted file mode 100644 index 7768b057..00000000 --- a/plugins/draw_card/count_manager.py +++ /dev/null @@ -1,149 +0,0 @@ -from typing import Optional, TypeVar, Generic -from pydantic import BaseModel -from cachetools import TTLCache - - -class BaseUserCount(BaseModel): - count: int = 0 # 当前抽卡次数 - - -TCount = TypeVar("TCount", bound="BaseUserCount") - - -class DrawCountManager(Generic[TCount]): - """ - 抽卡统计保底 - """ - - def __init__( - self, game_draw_count_rule: tuple, star2name: tuple, max_draw_count: int - ): - """ - 初始化保底统计 - - 例如:DrawCountManager((10, 90, 180), ("4", "5", "5")) - - 抽卡保底需要的次数和返回的对应名称,例如星级等 - - :param game_draw_count_rule:抽卡规则 - :param star2name:星级对应的名称 - :param max_draw_count:最大累计抽卡次数,当下次单次抽卡超过该次数时将会清空数据 - - """ - # 只有保底 - # 超过60秒重置抽卡次数 - self._data: TTLCache[int, TCount] = TTLCache(maxsize=1000, ttl=60) - self._guarantee_tuple = game_draw_count_rule - self._star2name = star2name - self._max_draw_count = max_draw_count - - @classmethod - def get_count_class(cls) -> TCount: - raise NotImplementedError - - def _get_count(self, key: int) -> TCount: - if self._data.get(key) is None: - self._data[key] = self.get_count_class() - else: - self._data[key] = self._data[key] - return self._data[key] - - def increase(self, key: int, value: int = 1): - """ - 用户抽卡次数加1 - """ - self._get_count(key).count += value - - def get_max_guarantee(self): - """ - 获取最大保底抽卡次数 - """ - return self._guarantee_tuple[-1] - - def get_user_count(self, key: int) -> int: - """ - 获取当前抽卡次数 - """ - return self._get_count(key).count - - def reset(self, key: int): - """ - 清空记录 - """ - self._data.pop(key, None) - - -class GenshinUserCount(BaseUserCount): - five_index: int = 0 # 获取五星时的抽卡次数 - four_index: int = 0 # 获取四星时的抽卡次数 - is_up: bool = False # 下次五星是否必定为up - - -class GenshinCountManager(DrawCountManager[GenshinUserCount]): - @classmethod - def get_count_class(cls) -> GenshinUserCount: - return GenshinUserCount() - - def set_is_up(self, key: int, value: bool): - """ - 设置下次是否必定up - """ - self._get_count(key).is_up = value - - def is_up(self, key: int) -> bool: - """ - 判断该次保底是否必定为up - """ - return self._get_count(key).is_up - - def get_user_five_index(self, key: int) -> int: - """ - 获取用户上次获取五星的次数 - """ - return self._get_count(key).five_index - - def get_user_four_index(self, key: int) -> int: - """ - 获取用户上次获取四星的次数 - """ - return self._get_count(key).four_index - - def mark_five_index(self, key: int): - """ - 标记用户该次次数为五星 - """ - self._get_count(key).five_index = self._get_count(key).count - - def mark_four_index(self, key: int): - """ - 标记用户该次次数为四星 - """ - self._get_count(key).four_index = self._get_count(key).count - - def check_count(self, key: int, count: int): - """ - 检查用户该次抽卡次数累计是否超过最大限制次数 - """ - if self._get_count(key).count + count > self._max_draw_count: - self._data.pop(key, None) - - def get_user_guarantee_count(self, key: int) -> int: - user = self._get_count(key) - return ( - self.get_max_guarantee() - - (user.count % self.get_max_guarantee() - user.five_index) - ) % self.get_max_guarantee() or self.get_max_guarantee() - - def check(self, key: int) -> Optional[int]: - """ - 是否保底 - """ - # print(self._data) - user = self._get_count(key) - if user.count - user.five_index == 90: - user.five_index = user.count - return 5 - if user.count - user.four_index == 10: - user.four_index = user.count - return 4 - return None diff --git a/plugins/draw_card/handles/azur_handle.py b/plugins/draw_card/handles/azur_handle.py deleted file mode 100644 index 0ae2dade..00000000 --- a/plugins/draw_card/handles/azur_handle.py +++ /dev/null @@ -1,304 +0,0 @@ -import random -import dateparser -from lxml import etree -from typing import List, Optional, Tuple -from PIL import ImageDraw -from urllib.parse import unquote -from pydantic import ValidationError -from nonebot.log import logger -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from utils.message_builder import image -from .base_handle import BaseHandle, BaseData, UpEvent as _UpEvent, UpChar as _UpChar -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -class AzurChar(BaseData): - type_: str # 舰娘类型 - - @property - def star_str(self) -> str: - return ["白", "蓝", "紫", "金"][self.star - 1] - - -class UpChar(_UpChar): - type_: str # 舰娘类型 - - -class UpEvent(_UpEvent): - up_char: List[UpChar] # up对象 - - -class AzurHandle(BaseHandle[AzurChar]): - def __init__(self): - super().__init__("azur", "碧蓝航线") - self.max_star = 4 - self.config = draw_config.azur - self.ALL_CHAR: List[AzurChar] = [] - self.UP_EVENT: Optional[UpEvent] = None - - def get_card(self, pool_name: str, **kwargs) -> AzurChar: - if pool_name == "轻型": - type_ = ["驱逐", "轻巡", "维修"] - elif pool_name == "重型": - type_ = ["重巡", "战列", "战巡", "重炮"] - else: - type_ = ["维修", "潜艇", "重巡", "轻航", "航母"] - up_pool_flag = pool_name == "活动" - # Up - up_ship = ( - [x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else [] - ) - # print(up_ship) - acquire_char = None - if up_ship and up_pool_flag: - up_zoom: List[Tuple[float, float]] = [(0, up_ship[0].zoom / 100)] - # 初始化概率 - cur_ = up_ship[0].zoom / 100 - for i in range(len(up_ship)): - try: - up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100)) - cur_ += up_ship[i + 1].zoom / 100 - except IndexError: - pass - rand = random.random() - # 抽取up - for i, zoom in enumerate(up_zoom): - if zoom[0] <= rand <= zoom[1]: - try: - acquire_char = [ - x for x in self.ALL_CHAR if x.name == up_ship[i].name - ][0] - except IndexError: - pass - # 没有up或者未抽取到up - if not acquire_char: - star = self.get_star( - [4, 3, 2, 1], - [ - self.config.AZUR_FOUR_P, - self.config.AZUR_THREE_P, - self.config.AZUR_TWO_P, - self.config.AZUR_ONE_P, - ], - ) - acquire_char = random.choice( - [ - x - for x in self.ALL_CHAR - if x.star == star and x.type_ in type_ and not x.limited - ] - ) - return acquire_char - - def draw(self, count: int, **kwargs) -> Message: - index2card = self.get_cards(count, **kwargs) - cards = [card[0] for card in index2card] - up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] - result = self.format_result(index2card, **{**kwargs, "up_list": up_list}) - return image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: AzurChar) -> BuildImage: - sep_w = 5 - sep_t = 5 - sep_b = 20 - w = 100 - h = 100 - bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) - frame_path = str(self.img_path / f"{card.star}_star.png") - frame = BuildImage(w, h, background=frame_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(w, h, background=img_path) - # 加圆角 - frame.circle_corner(6) - img.circle_corner(6) - bg.paste(img, (sep_w, sep_t), alpha=True) - bg.paste(frame, (sep_w, sep_t), alpha=True) - # 加名字 - text = card.name[:6] + "..." if len(card.name) > 7 else card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), - text, - font=font, - fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1], - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - AzurChar( - name=value["名称"], - star=int(value["星级"]), - limited="可以建造" not in value["获取途径"], - type_=value["类型"], - ) - for value in self.load_data().values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_EVENT: - data = {"char": json.loads(self.UP_EVENT.json())} - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - info = {} - # 更新图鉴 - url = "https://wiki.biligame.com/blhx/舰娘图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - contents = dom.xpath( - "//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']" - ) - for index, content in enumerate(contents): - char_list = content.xpath("./div[@id='CardSelectTr']/div") - for char in char_list: - try: - name = char.xpath("./div/a/@title")[0] - frame = char.xpath("./div/div/a/img/@alt")[0] - avatar = char.xpath("./div/a/img/@srcset")[0] - except IndexError: - continue - member_dict = { - "名称": remove_prohibited_str(name), - "头像": unquote(str(avatar).split(" ")[-2]), - "星级": self.parse_star(frame), - "类型": char.xpath("./@data-param1")[0].split(",")[1], - } - info[member_dict["名称"]] = member_dict - # 更新额外信息 - for key in info.keys(): - url = f"https://wiki.biligame.com/blhx/{key}" - result = await self.get_url(url) - if not result: - info[key]["获取途径"] = [] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - time = dom.xpath( - "//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()" - )[0] - sources = [] - if "无法建造" in time: - sources.append("无法建造") - elif "活动已关闭" in time: - sources.append("活动限定") - else: - sources.append("可以建造") - info[key]["获取途径"] = sources - except IndexError: - info[key]["获取途径"] = [] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载头像框 - idx = 1 - BLHX_URL = "https://patchwiki.biligame.com/images/blhx" - for url in [ - "/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png", - "/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png", - "/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png", - "/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png", - "/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png", - ]: - await self.download_img(BLHX_URL + url, f"{idx}_star") - idx += 1 - await self.update_up_char() - - @staticmethod - def parse_star(star: str) -> int: - if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]: - return 1 - elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]: - return 2 - elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]: - return 3 - elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]: - return 4 - elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]: - return 5 - elif star in [ - "舰娘头像外框最高方案.png", - "舰娘头像外框决战方案.png", - "舰娘头像外框超稀有META.png", - "舰娘头像外框精锐META.png", - ]: - return 6 - else: - return 6 - - async def update_up_char(self): - url = "https://wiki.biligame.com/blhx/游戏活动表" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取活动表出错") - return - try: - dom = etree.HTML(result, etree.HTMLParser()) - dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0] - url = "https://wiki.biligame.com" + dd.xpath("./@href")[0] - title = dd.xpath("string(.)") - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取活动页面出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - timer = dom.xpath("//span[@class='eventTimer']")[0] - start_time = dateparser.parse(timer.xpath("./@data-start")[0]) - end_time = dateparser.parse(timer.xpath("./@data-end")[0]) - ships = dom.xpath("//table[@class='shipinfo']") - up_chars = [] - for ship in ships: - name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0] - type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型 - try: - p = float(str(ship.xpath(".//sup/text()")[0]).strip("%")) - except (IndexError, ValueError): - p = 0 - star = self.parse_star( - ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0] - ) - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=p, type_=type_) - ) - self.UP_EVENT = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.dump_up_char() - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_EVENT: - return Message(f"重载成功!\n当前活动:{self.UP_EVENT.title}") diff --git a/plugins/draw_card/handles/ba_handle.py b/plugins/draw_card/handles/ba_handle.py deleted file mode 100644 index 5fe00462..00000000 --- a/plugins/draw_card/handles/ba_handle.py +++ /dev/null @@ -1,156 +0,0 @@ -import random -from typing import List, Tuple -from urllib.parse import unquote - -from lxml import etree -from nonebot.log import logger -from PIL import ImageDraw -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage - -from ..config import draw_config -from ..util import cn2py, load_font, remove_prohibited_str -from .base_handle import BaseData, BaseHandle - - -class BaChar(BaseData): - pass - - -class BaHandle(BaseHandle[BaChar]): - def __init__(self): - super().__init__("ba", "碧蓝档案") - self.max_star = 3 - self.config = draw_config.ba - self.ALL_CHAR: List[BaChar] = [] - - def get_card(self, mode: int = 1) -> BaChar: - if mode == 2: - star = self.get_star( - [3, 2], [self.config.BA_THREE_P, self.config.BA_G_TWO_P] - ) - else: - star = self.get_star( - [3, 2, 1], - [self.config.BA_THREE_P, self.config.BA_TWO_P, self.config.BA_ONE_P], - ) - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - return random.choice(chars) - - def get_cards(self, count: int, **kwargs) -> List[Tuple[BaChar, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(2) - card_count = 0 - else: - card = self.get_card(1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def generate_card_img(self, card: BaChar) -> BuildImage: - sep_w = 5 - sep_h = 5 - star_h = 15 - img_w = 90 - img_h = 100 - font_h = 20 - bar_h = 20 - bar_w = 90 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bar = BuildImage(bar_w, bar_h, color="#6495ED") - bg.paste(img, (sep_w, sep_h), alpha=True) - bg.paste(bar, (sep_w, img_h - bar_h + sep_h), alpha=True) - if card.star == 1: - star_path = str(self.img_path / "star-1.png") - star_w = 15 - elif card.star == 2: - star_path = str(self.img_path / "star-2.png") - star_w = 30 - else: - star_path = str(self.img_path / "star-3.png") - star_w = 45 - star = BuildImage(star_w, star_h, background=star_path) - bg.paste( - star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h), alpha=True - ) - text = card.name[:5] + "..." if len(card.name) > 6 else card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - BaChar( - name=value["名称"], - star=int(value["星级"]), - limited=True if "(" in key else False, - ) - for key, value in self.load_data().items() - ] - - def title2star(self, title: int): - if title == "Star-3.png": - return 3 - elif title == "Star-2.png": - return 2 - else: - return 1 - - async def _update_info(self): - info = {} - url = "https://lonqie.github.io/SchaleDB/data/cn/students.min.json?v=49" - result = (await AsyncHttpx.get(url)).json() - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - else: - for char in result: - try: - name = char["Name"] - avatar = ( - "https://github.com/lonqie/SchaleDB/raw/main/images/student/icon/" - + char["CollectionTexture"] - + ".png" - ) - star = char["StarGrade"] - except IndexError: - continue - member_dict = { - "头像": avatar, - "名称": name, - "星级": star, - } - info[member_dict["名称"]] = member_dict - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - await self.download_img( - "https://patchwiki.biligame.com/images/bluearchive/thumb/e/e0/82nj2x9sxko473g7782r14fztd4zyky.png/15px-Star-1.png", - "star-1", - ) - await self.download_img( - "https://patchwiki.biligame.com/images/bluearchive/thumb/0/0b/msaff2g0zk6nlyl1rrn7n1ri4yobcqc.png/30px-Star-2.png", - "star-2", - ) - await self.download_img( - "https://patchwiki.biligame.com/images/bluearchive/thumb/8/8a/577yv79x1rwxk8efdccpblo0lozl158.png/46px-Star-3.png", - "star-3", - ) diff --git a/plugins/draw_card/handles/base_handle.py b/plugins/draw_card/handles/base_handle.py deleted file mode 100644 index ac0ee79e..00000000 --- a/plugins/draw_card/handles/base_handle.py +++ /dev/null @@ -1,294 +0,0 @@ -import math -import anyio -import random -import aiohttp -import asyncio -from PIL import Image -from datetime import datetime -from pydantic import BaseModel, Extra -from asyncio.exceptions import TimeoutError -from typing import Dict, List, Optional, TypeVar, Generic, Tuple -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from configs.path_config import DATA_PATH -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from utils.image_utils import BuildImage -from ..config import DRAW_PATH, draw_config -from ..util import cn2py, circled_number - - -class BaseData(BaseModel, extra=Extra.ignore): - name: str # 名字 - star: int # 星级 - limited: bool # 限定 - - def __eq__(self, other: "BaseData"): - return self.name == other.name - - def __hash__(self): - return hash(self.name) - - @property - def star_str(self) -> str: - return "".join(["★" for _ in range(self.star)]) - - -class UpChar(BaseData): - zoom: float # up提升倍率 - - -class UpEvent(BaseModel): - title: str # up池标题 - pool_img: str # up池封面 - start_time: Optional[datetime] # 开始时间 - end_time: Optional[datetime] # 结束时间 - up_char: List[UpChar] # up对象 - - -TC = TypeVar("TC", bound="BaseData") - - -class BaseHandle(Generic[TC]): - def __init__(self, game_name: str, game_name_cn: str): - self.game_name = game_name - self.game_name_cn = game_name_cn - self.max_star = 1 # 最大星级 - self.game_card_color: str = "#ffffff" - self.data_path = DATA_PATH / "draw_card" - self.img_path = DRAW_PATH / f"{self.game_name}" - self.up_path = DATA_PATH / "draw_card" / "draw_card_up" - self.img_path.mkdir(parents=True, exist_ok=True) - self.up_path.mkdir(parents=True, exist_ok=True) - self.data_files: List[str] = [f"{self.game_name}.json"] - - def draw(self, count: int, **kwargs) -> Message: - index2card = self.get_cards(count, **kwargs) - cards = [card[0] for card in index2card] - result = self.format_result(index2card) - return image(b64=self.generate_img(cards).pic2bs4()) + result - - # 抽取卡池 - def get_card(self, **kwargs) -> TC: - raise NotImplementedError - - def get_cards(self, count: int, **kwargs) -> List[Tuple[TC, int]]: - return [(self.get_card(**kwargs), i) for i in range(count)] - - # 获取星级 - @staticmethod - def get_star(star_list: List[int], probability_list: List[float]) -> int: - return random.choices(star_list, weights=probability_list, k=1)[0] - - def format_result(self, index2card: List[Tuple[TC, int]], **kwargs) -> str: - card_list = [card[0] for card in index2card] - results = [ - self.format_star_result(card_list, **kwargs), - self.format_max_star(index2card, **kwargs), - self.format_max_card(card_list, **kwargs), - ] - results = [rst for rst in results if rst] - return "\n".join(results) - - def format_star_result(self, card_list: List[TC], **kwargs) -> str: - star_dict: Dict[str, int] = {} # 记录星级及其次数 - - card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True) - for card in card_list_sorted: - try: - star_dict[card.star_str] += 1 - except KeyError: - star_dict[card.star_str] = 1 - - rst = "" - for star_str, count in star_dict.items(): - rst += f"[{star_str}×{count}] " - return rst.strip() - - def format_max_star( - self, card_list: List[Tuple[TC, int]], up_list: List[str] = [], **kwargs - ) -> str: - up_list = up_list or kwargs.get("up_list", []) - rst = "" - for card, index in card_list: - if card.star == self.max_star: - if card.name in up_list: - rst += f"第 {index} 抽获取UP {card.name}\n" - else: - rst += f"第 {index} 抽获取 {card.name}\n" - return rst.strip() - - def format_max_card(self, card_list: List[TC], **kwargs) -> str: - card_dict: Dict[TC, int] = {} # 记录卡牌抽取次数 - - for card in card_list: - try: - card_dict[card] += 1 - except KeyError: - card_dict[card] = 1 - - max_count = max(card_dict.values()) - max_card = list(card_dict.keys())[list(card_dict.values()).index(max_count)] - if max_count <= 1: - return "" - return f"抽取到最多的是{max_card.name},共抽取了{max_count}次" - - def generate_img( - self, - cards: List[TC], - num_per_line: int = 5, - max_per_line: Tuple[int, int] = (40, 10), - ) -> BuildImage: - """ - 生成统计图片 - :param cards: 卡牌列表 - :param num_per_line: 单行角色显示数量 - :param max_per_line: 当card_list超过一定数值时,更改单行数量 - """ - if len(cards) > max_per_line[0]: - num_per_line = max_per_line[1] - if len(cards) > 90: - card_dict: Dict[TC, int] = {} # 记录卡牌抽取次数 - for card in cards: - try: - card_dict[card] += 1 - except KeyError: - card_dict[card] = 1 - card_list = list(card_dict) - num_list = list(card_dict.values()) - else: - card_list = cards - num_list = [1] * len(cards) - - card_imgs: List[BuildImage] = [] - for card, num in zip(card_list, num_list): - card_img = self.generate_card_img(card) - # 数量 > 1 时加数字上标 - if num > 1: - label = circled_number(num) - label_w = int(min(card_img.w, card_img.h) / 7) - label = label.resize( - ( - int(label_w * label.width / label.height), - label_w, - ), - Image.ANTIALIAS, - ) - card_img.paste(label, alpha=True) - - card_imgs.append(card_img) - - img_w = card_imgs[0].w - img_h = card_imgs[0].h - if len(card_imgs) < num_per_line: - w = img_w * len(card_imgs) - else: - w = img_w * num_per_line - h = img_h * math.ceil(len(card_imgs) / num_per_line) - img = BuildImage(w, h, img_w, img_h, color=self.game_card_color) - for card_img in card_imgs: - img.paste(card_img) - return img - - def generate_card_img(self, card: TC) -> BuildImage: - img = str(self.img_path / f"{cn2py(card.name)}.png") - return BuildImage(100, 100, background=img) - - def load_data(self, filename: str = "") -> dict: - if not filename: - filename = f"{self.game_name}.json" - filepath = self.data_path / filename - if not filepath.exists(): - return {} - with filepath.open("r", encoding="utf8") as f: - return json.load(f) - - def dump_data(self, data: dict, filename: str = ""): - if not filename: - filename = f"{self.game_name}.json" - filepath = self.data_path / filename - with filepath.open("w", encoding="utf8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - def data_exists(self) -> bool: - for file in self.data_files: - if not (self.data_path / file).exists(): - return False - return True - - def _init_data(self): - raise NotImplementedError - - def init_data(self): - try: - self._init_data() - except Exception as e: - logger.warning(f"{self.game_name_cn} 导入角色数据错误:{type(e)}:{e}") - - async def _update_info(self): - raise NotImplementedError - - def client(self) -> aiohttp.ClientSession: - headers = { - "User-Agent": '"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)"' - } - return aiohttp.ClientSession(headers=headers) - - async def update_info(self): - try: - async with asyncio.Semaphore(draw_config.SEMAPHORE): - async with self.client() as session: - self.session = session - await self._update_info() - except Exception as e: - logger.warning(f"{self.game_name_cn} 更新数据错误:{type(e)}:{e}") - self.init_data() - - async def get_url(self, url: str) -> str: - result = "" - retry = 5 - for i in range(retry): - try: - async with self.session.get(url, timeout=10) as response: - result = await response.text() - break - except TimeoutError: - logger.warning(f"访问 {url} 超时, 重试 {i + 1}/{retry}") - await asyncio.sleep(1) - return result - - async def download_img(self, url: str, name: str) -> bool: - img_path = self.img_path / f"{cn2py(name)}.png" - if img_path.exists(): - return True - try: - async with self.session.get(url, timeout=10) as response: - async with await anyio.open_file(img_path, "wb") as f: - await f.write(await response.read()) - return True - except TimeoutError: - logger.warning(f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}") - return False - except: - logger.warning(f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}") - return False - - async def _reload_pool(self) -> Optional[Message]: - return None - - async def reload_pool(self) -> Optional[Message]: - try: - async with self.client() as session: - self.session = session - return await self._reload_pool() - except Exception as e: - logger.warning(f"{self.game_name_cn} 重载UP池错误:{type(e)}:{e}") - - def reset_count(self, user_id: int) -> bool: - return False diff --git a/plugins/draw_card/handles/fgo_handle.py b/plugins/draw_card/handles/fgo_handle.py deleted file mode 100644 index a491b906..00000000 --- a/plugins/draw_card/handles/fgo_handle.py +++ /dev/null @@ -1,221 +0,0 @@ -import random -from lxml import etree -from typing import List, Tuple -from PIL import ImageDraw -from nonebot.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class FgoData(BaseData): - pass - - -class FgoChar(FgoData): - pass - - -class FgoCard(FgoData): - pass - - -class FgoHandle(BaseHandle[FgoData]): - def __init__(self): - super().__init__("fgo", "命运-冠位指定") - self.data_files.append("fgo_card.json") - self.max_star = 5 - self.config = draw_config.fgo - self.ALL_CHAR: List[FgoChar] = [] - self.ALL_CARD: List[FgoCard] = [] - - def get_card(self, mode: int = 1) -> FgoData: - if mode == 1: - star = self.get_star( - [8, 7, 6, 5, 4, 3], - [ - self.config.FGO_SERVANT_FIVE_P, - self.config.FGO_SERVANT_FOUR_P, - self.config.FGO_SERVANT_THREE_P, - self.config.FGO_CARD_FIVE_P, - self.config.FGO_CARD_FOUR_P, - self.config.FGO_CARD_THREE_P, - ], - ) - elif mode == 2: - star = self.get_star( - [5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P] - ) - else: - star = self.get_star( - [8, 7, 6], - [ - self.config.FGO_SERVANT_FIVE_P, - self.config.FGO_SERVANT_FOUR_P, - self.config.FGO_SERVANT_THREE_P, - ], - ) - if star > 5: - star -= 3 - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - else: - chars = [x for x in self.ALL_CARD if x.star == star and not x.limited] - return random.choice(chars) - - def get_cards(self, count: int, **kwargs) -> List[Tuple[FgoData, int]]: - card_list = [] # 获取所有角色 - servant_count = 0 # 保底计算 - card_count = 0 # 保底计算 - for i in range(count): - servant_count += 1 - card_count += 1 - if card_count == 9: # 四星卡片保底 - mode = 2 - elif servant_count == 10: # 三星从者保底 - mode = 3 - else: # 普通抽 - mode = 1 - card = self.get_card(mode) - if isinstance(card, FgoCard) and card.star > self.max_star - 2: - card_count = 0 - if isinstance(card, FgoChar): - servant_count = 0 - card_list.append((card, i + 1)) - return card_list - - def generate_card_img(self, card: FgoData) -> BuildImage: - sep_w = 5 - sep_t = 5 - sep_b = 20 - w = 128 - h = 140 - bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(w, h, background=img_path) - bg.paste(img, (sep_w, sep_t), alpha=True) - # 加名字 - text = card.name[:6] + "..." if len(card.name) > 7 else card.name - font = load_font(fontsize=16) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - FgoChar( - name=value["名称"], - star=int(value["星级"]), - limited=True - if not ("圣晶石召唤" in value["入手方式"] or "圣晶石召唤(Story卡池)" in value["入手方式"]) - else False, - ) - for value in self.load_data().values() - ] - self.ALL_CARD = [ - FgoCard(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data("fgo_card.json").values() - ] - - async def _update_info(self): - # fgo.json - fgo_info = {} - for i in range(500): - url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} page {i} 出错") - continue - fgo_data = json.loads(result) - if int(fgo_data["nums"]) <= 0: - break - for x in fgo_data["data"]: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "card_id": x["charid"], - "头像": x["icon"], - "名称": remove_prohibited_str(x["name"]), - "职阶": x["classes"], - "星级": int(x["star"]), - "hp": x["lvmax4hp"], - "atk": x["lvmax4atk"], - "card_quick": x["cardquick"], - "card_arts": x["cardarts"], - "card_buster": x["cardbuster"], - "宝具": x["tprop"], - } - fgo_info[name] = member_dict - # 更新额外信息 - for key in fgo_info.keys(): - url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}' - result = await self.get_url(url) - if not result: - fgo_info[key]["入手方式"] = ["圣晶石召唤"] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - obtain = dom.xpath( - "//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()" - )[0] - obtain = str(obtain).strip() - if "限时活动免费获取 活动结束后无法获得" in obtain: - obtain = ["活动获取"] - elif "非限时UP无法获得" in obtain: - obtain = ["限时召唤"] - else: - if "&" in obtain: - obtain = obtain.split("&") - else: - obtain = obtain.split(" ") - obtain = [s.strip() for s in obtain if s.strip()] - fgo_info[key]["入手方式"] = obtain - except IndexError: - fgo_info[key]["入手方式"] = ["圣晶石召唤"] - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(fgo_info) - logger.info(f"{self.game_name_cn} 更新成功") - # fgo_card.json - fgo_card_info = {} - for i in range(500): - url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错") - continue - fgo_data = json.loads(result) - if int(fgo_data["nums"]) <= 0: - break - for x in fgo_data["data"]: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "card_id": x["equipid"], - "头像": x["icon"], - "名称": name, - "星级": int(x["star"]), - "hp": x["lvmax_hp"], - "atk": x["lvmax_atk"], - "skill_e": str(x["skill_e"]).split("
")[:-1], - } - fgo_card_info[name] = member_dict - self.dump_data(fgo_card_info, "fgo_card.json") - logger.info(f"{self.game_name_cn} 卡牌更新成功") - # 下载头像 - for value in fgo_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in fgo_card_info.values(): - await self.download_img(value["头像"], value["名称"]) diff --git a/plugins/draw_card/handles/genshin_handle.py b/plugins/draw_card/handles/genshin_handle.py deleted file mode 100644 index 71e79544..00000000 --- a/plugins/draw_card/handles/genshin_handle.py +++ /dev/null @@ -1,448 +0,0 @@ -import random -import dateparser -from lxml import etree -from PIL import Image, ImageDraw -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from datetime import datetime, timedelta -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..count_manager import GenshinCountManager -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class GenshinData(BaseData): - pass - - -class GenshinChar(GenshinData): - pass - - -class GenshinArms(GenshinData): - pass - - -class GenshinHandle(BaseHandle[GenshinData]): - def __init__(self): - super().__init__("genshin", "原神") - self.data_files.append("genshin_arms.json") - self.max_star = 5 - self.game_card_color = "#ebebeb" - self.config = draw_config.genshin - - self.ALL_CHAR: List[GenshinData] = [] - self.ALL_ARMS: List[GenshinData] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_CHAR_LIST: Optional[UpEvent] = [] - self.UP_ARMS: Optional[UpEvent] = None - - self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180) - - # 抽取卡池 - def get_card( - self, pool_name: str, mode: int = 1, add: float = 0.0, is_up: bool = False, card_index: int = 0 - ): - """ - mode 1:普通抽 2:四星保底 3:五星保底 - """ - if mode == 1: - star = self.get_star( - [5, 4, 3], - [ - self.config.GENSHIN_FIVE_P + add, - self.config.GENSHIN_FOUR_P, - self.config.GENSHIN_THREE_P, - ], - ) - elif mode == 2: - star = self.get_star( - [5, 4], - [self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P], - ) - else: - star = 5 - - if pool_name == "char": - up_event = self.UP_CHAR_LIST[card_index] - all_list = self.ALL_CHAR + [ - x for x in self.ALL_ARMS if x.star == star and x.star < 5 - ] - elif pool_name == "arms": - up_event = self.UP_ARMS - all_list = self.ALL_ARMS + [ - x for x in self.ALL_CHAR if x.star == star and x.star < 5 - ] - else: - up_event = None - all_list = self.ALL_ARMS + self.ALL_CHAR - - acquire_char = None - # 是否UP - if up_event and star > 3: - # 获取up角色列表 - up_list = [x.name for x in up_event.up_char if x.star == star] - # 成功获取up角色 - if random.random() < 0.5 or is_up: - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_list if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - chars = [x for x in all_list if x.star == star and not x.limited] - acquire_char = random.choice(chars) - return acquire_char - - def get_cards( - self, count: int, user_id: int, pool_name: str, card_index: int = 0 - ) -> List[Tuple[GenshinData, int]]: - card_list = [] # 获取角色列表 - add = 0.0 - count_manager = self.count_manager - count_manager.check_count(user_id, count) # 检查次数累计 - pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS - for i in range(count): - count_manager.increase(user_id) - star = count_manager.check(user_id) # 是否有四星或五星保底 - if ( - count_manager.get_user_count(user_id) - - count_manager.get_user_five_index(user_id) - ) % count_manager.get_max_guarantee() >= 72: - add += draw_config.genshin.I72_ADD - if star: - if star == 4: - card = self.get_card(pool_name, 2, add=add, card_index=card_index) - else: - card = self.get_card( - pool_name, 3, add, count_manager.is_up(user_id), card_index=card_index - ) - else: - card = self.get_card(pool_name, 1, add, count_manager.is_up(user_id), card_index=card_index) - # print(f"{count_manager.get_user_count(user_id)}:", - # count_manager.get_user_five_index(user_id), star, card.star, add) - # 四星角色 - if card.star == 4: - count_manager.mark_four_index(user_id) - # 五星角色 - elif card.star == self.max_star: - add = 0 - count_manager.mark_five_index(user_id) # 记录五星保底 - count_manager.mark_four_index(user_id) # 记录四星保底 - if pool and card.name in [ - x.name for x in pool.up_char if x.star == self.max_star - ]: - count_manager.set_is_up(user_id, True) - else: - count_manager.set_is_up(user_id, False) - card_list.append((card, count_manager.get_user_count(user_id))) - return card_list - - def generate_card_img(self, card: GenshinData) -> BuildImage: - sep_w = 10 - sep_h = 5 - frame_w = 112 - frame_h = 132 - img_w = 106 - img_h = 106 - bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB") - frame_path = str(self.img_path / "avatar_frame.png") - frame = Image.open(frame_path) - # 加名字 - text = card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(frame) - draw.text( - ((frame_w - text_w) / 2, frame_h - 15 - text_h / 2), - text, - font=font, - fill="gray", - ) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - if isinstance(card, GenshinArms): - # 武器卡背景不是透明的,切去上方两个圆弧 - r = 12 - circle = Image.new("L", (r * 2, r * 2), 0) - alpha = Image.new("L", img.size, 255) - alpha.paste(circle, (-r - 3, -r - 3)) # 左上角 - alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角 - img.markImg.putalpha(alpha) - star_path = str(self.img_path / f"{card.star}_star.png") - star = Image.open(star_path) - bg.paste(frame, (sep_w, sep_h), alpha=True) - bg.paste(img, (sep_w + 3, sep_h + 3), alpha=True) - bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6), alpha=True) - return bg - - def format_pool_info(self, pool_name: str, card_index: int = 0) -> str: - info = "" - up_event = None - if pool_name == "char": - up_event = self.UP_CHAR_LIST[card_index] - elif pool_name == "arms": - up_event = self.UP_ARMS - if up_event: - star5_list = [x.name for x in up_event.up_char if x.star == 5] - star4_list = [x.name for x in up_event.up_char if x.star == 4] - if star5_list: - info += f"五星UP:{' '.join(star5_list)}\n" - if star4_list: - info += f"四星UP:{' '.join(star4_list)}\n" - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, user_id: int, pool_name: str = "", **kwargs) -> Message: - card_index = 0 - if "1" in pool_name: - card_index = 1 - pool_name = pool_name.replace("1", "") - index2cards = self.get_cards(count, user_id, pool_name, card_index) - cards = [card[0] for card in index2cards] - up_event = None - if pool_name == "char": - if card_index == 1 and len(self.UP_CHAR_LIST) == 1: - return Message("当前没有第二个角色UP池") - up_event = self.UP_CHAR_LIST[card_index] - elif pool_name == "arms": - up_event = self.UP_ARMS - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_star_result(cards) - result += ( - "\n" + max_star_str - if (max_star_str := self.format_max_star(index2cards, up_list=up_list)) - else "" - ) - result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽" - # result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】" - pool_info = self.format_pool_info(pool_name, card_index) - img = self.generate_img(cards) - bk = BuildImage(img.w, img.h + 50, font_size=20, color="#ebebeb") - bk.paste(img) - bk.text((0, img.h + 10), "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】") - return pool_info + image(b64=bk.pic2bs4()) + result - - def _init_data(self): - self.ALL_CHAR = [ - GenshinChar( - name=value["名称"], - star=int(value["星级"]), - limited=value["常驻/限定"] == "限定UP", - ) - for key, value in self.load_data().items() - if "旅行者" not in key - ] - self.ALL_ARMS = [ - GenshinArms( - name=value["名称"], - star=int(value["星级"]), - limited="祈愿" not in value["获取途径"], - ) - for value in self.load_data("genshin_arms.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {}))) - self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {}))) - self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR_LIST and self.UP_ARMS: - data = { - "char": json.loads(self.UP_CHAR_LIST[0].json()), - "arms": json.loads(self.UP_ARMS.json()), - } - if len(self.UP_CHAR_LIST) > 1: - data['char1'] = json.loads(self.UP_CHAR_LIST[1].json()) - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # genshin.json - char_info = {} - url = "https://wiki.biligame.com/ys/角色筛选" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()[:1]), - } - char_info[member_dict["名称"]] = member_dict - # 更新额外信息 - for key in char_info.keys(): - result = await self.get_url(f"https://wiki.biligame.com/ys/{key}") - if not result: - char_info[key]["常驻/限定"] = "未知" - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - limit = dom.xpath( - "//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()" - )[0] - char_info[key]["常驻/限定"] = str(limit).strip() - except IndexError: - char_info[key]["常驻/限定"] = "未知" - logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") - self.dump_data(char_info) - logger.info(f"{self.game_name_cn} 更新成功") - # genshin_arms.json - arms_info = {} - url = "https://wiki.biligame.com/ys/武器图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[4]/img/@alt")[0] - sources = str(char.xpath("./td[5]/text()")[0]).split(",") - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()[:1]), - "获取途径": [s.strip() for s in sources if s.strip()], - } - arms_info[member_dict["名称"]] = member_dict - self.dump_data(arms_info, "genshin_arms.json") - logger.info(f"{self.game_name_cn} 武器更新成功") - # 下载头像 - for value in char_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in arms_info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - idx = 1 - YS_URL = "https://patchwiki.biligame.com/images/ys" - for url in [ - "/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png", - "/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png", - "/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png", - "/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png", - "/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png", - ]: - await self.download_img(YS_URL + url, f"{idx}_star") - idx += 1 - # 下载头像框 - await self.download_img( - YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame" - ) - await self.update_up_char() - - async def update_up_char(self): - self.UP_CHAR_LIST = [] - url = "https://wiki.biligame.com/ys/祈愿" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取祈愿页面出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - tables = dom.xpath( - "//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody" - ) - if not tables or len(tables) < 2: - logger.warning(f"{self.game_name_cn}获取活动祈愿出错") - return - try: - for index, table in enumerate(tables): - title = table.xpath("./tr[1]/th/img/@title")[0] - title = str(title).split("」")[0] + "」" if "」" in title else title - pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2] - time = table.xpath("./tr[2]/td/text()")[0] - star5_list = table.xpath("./tr[3]/td/a/@title") - star4_list = table.xpath("./tr[4]/td/a/@title") - start, end = str(time).split("~") - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - if not start_time and end_time: - start_time = end_time - timedelta(days=20) - if start_time and end_time and start_time <= datetime.now() <= end_time: - up_event = UpEvent( - title=title, - pool_img=pool_img, - start_time=start_time, - end_time=end_time, - up_char=[ - UpChar(name=name, star=5, limited=False, zoom=50) - for name in star5_list - ] - + [ - UpChar(name=name, star=4, limited=False, zoom=50) - for name in star4_list - ], - ) - if '神铸赋形' not in title: - self.UP_CHAR_LIST.append(up_event) - else: - self.UP_ARMS = up_event - if self.UP_CHAR_LIST and self.UP_ARMS: - self.dump_up_char() - char_title = " & ".join([x.title for x in self.UP_CHAR_LIST]) - logger.info( - f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}" - ) - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - def reset_count(self, user_id: int) -> bool: - self.count_manager.reset(user_id) - return True - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR_LIST and self.UP_ARMS: - if len(self.UP_CHAR_LIST) > 1: - return Message( - Message.template("重载成功!\n当前UP池子:{} & {} & {}{:image}{:image}{:image}").format( - self.UP_CHAR_LIST[0].title, - self.UP_CHAR_LIST[1].title, - self.UP_ARMS.title, - self.UP_CHAR_LIST[0].pool_img, - self.UP_CHAR_LIST[1].pool_img, - self.UP_ARMS.pool_img, - ) - ) - return Message( - Message.template("重载成功!\n当前UP池子:{} & {}{:image}{:image}").format( - char_title, - self.UP_ARMS.title, - self.UP_CHAR_LIST[0].pool_img, - self.UP_ARMS.pool_img, - ) - ) diff --git a/plugins/draw_card/handles/guardian_handle.py b/plugins/draw_card/handles/guardian_handle.py deleted file mode 100644 index 33e9449b..00000000 --- a/plugins/draw_card/handles/guardian_handle.py +++ /dev/null @@ -1,400 +0,0 @@ -import re -import random -import dateparser -from lxml import etree -from PIL import ImageDraw -from datetime import datetime -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class GuardianData(BaseData): - pass - - -class GuardianChar(GuardianData): - pass - - -class GuardianArms(GuardianData): - pass - - -class GuardianHandle(BaseHandle[GuardianData]): - def __init__(self): - super().__init__("guardian", "坎公骑冠剑") - self.data_files.append("guardian_arms.json") - self.config = draw_config.guardian - - self.ALL_CHAR: List[GuardianChar] = [] - self.ALL_ARMS: List[GuardianArms] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_ARMS: Optional[UpEvent] = None - - def get_card(self, pool_name: str, mode: int = 1) -> GuardianData: - if pool_name == "char": - if mode == 1: - star = self.get_star( - [3, 2, 1], - [ - self.config.GUARDIAN_THREE_CHAR_P, - self.config.GUARDIAN_TWO_CHAR_P, - self.config.GUARDIAN_ONE_CHAR_P, - ], - ) - else: - star = self.get_star( - [3, 2], - [ - self.config.GUARDIAN_THREE_CHAR_P, - self.config.GUARDIAN_TWO_CHAR_P, - ], - ) - up_event = self.UP_CHAR - self.max_star = 3 - all_data = self.ALL_CHAR - else: - if mode == 1: - star = self.get_star( - [5, 4, 3, 2], - [ - self.config.GUARDIAN_FIVE_ARMS_P, - self.config.GUARDIAN_FOUR_ARMS_P, - self.config.GUARDIAN_THREE_ARMS_P, - self.config.GUARDIAN_TWO_ARMS_P, - ], - ) - else: - star = self.get_star( - [5, 4], - [ - self.config.GUARDIAN_FIVE_ARMS_P, - self.config.GUARDIAN_FOUR_ARMS_P, - ], - ) - up_event = self.UP_ARMS - self.max_star = 5 - all_data = self.ALL_ARMS - - acquire_char = None - # 是否UP - if up_event and star == self.max_star and pool_name: - # 获取up角色列表 - up_list = [x.name for x in up_event.up_char if x.star == star] - # 成功获取up角色 - if random.random() < 0.5: - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_data if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - chars = [x for x in all_data if x.star == star and not x.limited] - acquire_char = random.choice(chars) - return acquire_char - - def get_cards(self, count: int, pool_name: str) -> List[Tuple[GuardianData, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(pool_name, 2) - card_count = 0 - else: - card = self.get_card(pool_name, 1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self, pool_name: str) -> str: - info = "" - up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS - if up_event: - if pool_name == "char": - up_list = [x.name for x in up_event.up_char if x.star == 3] - info += f'三星UP:{" ".join(up_list)}\n' - else: - up_list = [x.name for x in up_event.up_char if x.star == 5] - info += f'五星UP:{" ".join(up_list)}\n' - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, pool_name: str, **kwargs) -> Message: - index2card = self.get_cards(count, pool_name) - cards = [card[0] for card in index2card] - up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info(pool_name) - return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: GuardianData) -> BuildImage: - sep_w = 1 - sep_h = 1 - block_w = 170 - block_h = 90 - img_w = 90 - img_h = 90 - if isinstance(card, GuardianChar): - block_color = "#2e2923" - font_color = "#e2ccad" - star_w = 90 - star_h = 30 - star_name = f"{card.star}_star.png" - frame_path = "" - else: - block_color = "#EEE4D5" - font_color = "#A65400" - star_w = 45 - star_h = 45 - star_name = f"{card.star}_star_rank.png" - frame_path = str(self.img_path / "avatar_frame.png") - bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED") - block = BuildImage(block_w, block_h, color=block_color) - star_path = str(self.img_path / star_name) - star = BuildImage(star_w, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - block.paste(img, (0, 0), alpha=True) - if frame_path: - frame = BuildImage(img_w, img_h, background=frame_path) - block.paste(frame, (0, 0), alpha=True) - block.paste( - star, - (int((block_w + img_w - star_w) / 2), block_h - star_h - 30), - alpha=True, - ) - # 加名字 - text = card.name[:4] + "..." if len(card.name) > 5 else card.name - font = load_font(fontsize=14) - text_w, _ = font.getsize(text) - draw = ImageDraw.Draw(block.markImg) - draw.text( - ((block_w + img_w - text_w) / 2, 55), - text, - font=font, - fill=font_color, - ) - bg.paste(block, (sep_w, sep_h)) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data().values() - ] - self.ALL_ARMS = [ - GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False) - for value in self.load_data("guardian_arms.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) - self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR and self.UP_ARMS: - data = { - "char": json.loads(self.UP_CHAR.json()), - "arms": json.loads(self.UP_ARMS.json()), - } - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # guardian.json - guardian_info = {} - url = "https://wiki.biligame.com/gt/英雄筛选表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - # name = char.xpath("./td[1]/a/@title")[0] - # avatar = char.xpath("./td[1]/a/img/@src")[0] - # star = char.xpath("./td[1]/span/img/@alt")[0] - name = char.xpath("./th[1]/a[1]/@title")[0] - avatar = char.xpath("./th[1]/a/img/@src")[0] - star = char.xpath("./th[1]/span/img/@alt")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).split(" ")[0].replace("Rank", "")), - } - guardian_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_info) - logger.info(f"{self.game_name_cn} 更新成功") - # guardian_arms.json - guardian_arms_info = {} - url = "https://wiki.biligame.com/gt/武器" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 武器出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tabs-container']/div[1]/div/table[2]/tbody/tr" - ) - for char in char_list: - try: - name = char.xpath("./td[2]/a/@title")[0] - avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - guardian_arms_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_arms_info, "guardian_arms.json") - logger.info(f"{self.game_name_cn} 武器更新成功") - url = "https://wiki.biligame.com/gt/盾牌" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 盾牌出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr" - ) - for char in char_list: - try: - name = char.xpath("./td[2]/a/@title")[0] - avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] - star = char.xpath("./td[3]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar)), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - guardian_arms_info[member_dict["名称"]] = member_dict - self.dump_data(guardian_arms_info, "guardian_arms.json") - logger.info(f"{self.game_name_cn} 盾牌更新成功") - # 下载头像 - for value in guardian_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in guardian_arms_info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - idx = 1 - GT_URL = "https://patchwiki.biligame.com/images/gt" - for url in [ - "/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png", - "/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png", - "/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png", - ]: - await self.download_img(GT_URL + url, f"{idx}_star") - idx += 1 - # 另一种星星 - idx = 1 - for url in [ - "/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png", - "/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png", - "/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png", - "/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png", - "/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png", - ]: - await self.download_img(GT_URL + url, f"{idx}_star_rank") - idx += 1 - # 头像框 - await self.download_img( - GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame" - ) - await self.update_up_char() - - async def update_up_char(self): - url = "https://wiki.biligame.com/gt/首页" - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - try: - dom = etree.HTML(result, etree.HTMLParser()) - announcement = dom.xpath( - "//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]" - )[0] - title = announcement.xpath("./font/p/b/text()")[0] - match = re.search(r"从(.*?)开始.*?至(.*?)结束", title) - if not match: - logger.warning(f"{self.game_name_cn}找不到UP时间") - return - start, end = match.groups() - start_time = dateparser.parse(start.replace("月", "/").replace("日", "")) - end_time = dateparser.parse(end.replace("月", "/").replace("日", "")) - if not (start_time and end_time) or not ( - start_time <= datetime.now() <= end_time - ): - return - divs = announcement.xpath("./font/div") - char_index = 0 - arms_index = 0 - for index, div in enumerate(divs): - if div.xpath("string(.)") == "角色": - char_index = index - elif div.xpath("string(.)") == "武器": - arms_index = index - chars = divs[char_index + 1 : arms_index] - arms = divs[arms_index + 1 :] - up_chars = [] - up_arms = [] - for char in chars: - name = char.xpath("./p/a/@title")[0] - up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0)) - for arm in arms: - name = arm.xpath("./p/a/@title")[0] - up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0)) - self.UP_CHAR = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.UP_ARMS = UpEvent( - title=title, - pool_img="", - start_time=start_time, - end_time=end_time, - up_char=up_arms, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR and self.UP_ARMS: - return Message(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}") diff --git a/plugins/draw_card/handles/onmyoji_handle.py b/plugins/draw_card/handles/onmyoji_handle.py deleted file mode 100644 index 5797e830..00000000 --- a/plugins/draw_card/handles/onmyoji_handle.py +++ /dev/null @@ -1,179 +0,0 @@ -import random -from lxml import etree -from typing import List, Tuple -from nonebot.log import logger -from PIL import Image, ImageDraw -from PIL.Image import Image as IMG - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class OnmyojiChar(BaseData): - @property - def star_str(self) -> str: - return ["N", "R", "SR", "SSR", "SP"][self.star - 1] - - -class OnmyojiHandle(BaseHandle[OnmyojiChar]): - def __init__(self): - super().__init__("onmyoji", "阴阳师") - self.max_star = 5 - self.config = draw_config.onmyoji - self.ALL_CHAR: List[OnmyojiChar] = [] - - def get_card(self, **kwargs) -> OnmyojiChar: - star = self.get_star( - [5, 4, 3, 2], - [ - self.config.ONMYOJI_SP, - self.config.ONMYOJI_SSR, - self.config.ONMYOJI_SR, - self.config.ONMYOJI_R, - ], - ) - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - return random.choice(chars) - - def format_max_star(self, card_list: List[Tuple[OnmyojiChar, int]]) -> str: - rst = "" - for card, index in card_list: - if card.star == self.max_star: - rst += f"第 {index} 抽获取SP {card.name}\n" - elif card.star == self.max_star - 1: - rst += f"第 {index} 抽获取SSR {card.name}\n" - return rst.strip() - - @staticmethod - def star_label(star: int) -> IMG: - text, color1, color2 = [ - ("N", "#7E7E82", "#F5F6F7"), - ("R", "#014FA8", "#37C6FD"), - ("SR", "#6E0AA4", "#E94EFD"), - ("SSR", "#E5511D", "#FAF905"), - ("SP", "#FA1F2D", "#FFBBAF"), - ][star - 1] - w = 200 - h = 110 - # 制作渐变色图片 - base = Image.new("RGBA", (w, h), color1) - top = Image.new("RGBA", (w, h), color2) - mask = Image.new("L", (w, h)) - mask_data = [] - for y in range(h): - mask_data.extend([int(255 * (y / h))] * w) - mask.putdata(mask_data) - base.paste(top, (0, 0), mask) - # 透明图层 - font = load_font("gorga.otf", 100) - alpha = Image.new("L", (w, h)) - draw = ImageDraw.Draw(alpha) - draw.text((20, -30), text, fill="white", font=font) - base.putalpha(alpha) - # stroke - bg = Image.new("RGBA", (w, h)) - draw = ImageDraw.Draw(bg) - draw.text( - (20, -30), - text, - font=font, - fill="gray", - stroke_width=3, - stroke_fill="gray", - ) - bg.paste(base, (0, 0), base) - return bg - - def generate_img(self, card_list: List[OnmyojiChar]) -> BuildImage: - return super().generate_img(card_list, num_per_line=10) - - def generate_card_img(self, card: OnmyojiChar) -> BuildImage: - bg = BuildImage(73, 240, color="#F1EFE9") - img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png") - img = BuildImage(0, 0, background=img_path) - img = Image.open(img_path).convert("RGBA") - label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS) - bg.paste(img, (0, 0), alpha=True) - bg.paste(label, (0, 135), alpha=True) - font = load_font("msyh.ttf", 16) - draw = ImageDraw.Draw(bg.markImg) - text = "\n".join([t for t in card.name[:4]]) - _, text_h = font.getsize_multiline(text, spacing=0) - draw.text( - (40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0 - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - OnmyojiChar( - name=value["名称"], - star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1, - limited=True - if key - in [ - "奴良陆生", - "卖药郎", - "鬼灯", - "阿香", - "蜜桃&芥子", - "犬夜叉", - "杀生丸", - "桔梗", - "朽木露琪亚", - "黑崎一护", - "灶门祢豆子", - "灶门炭治郎", - ] - else False, - ) - for key, value in self.load_data().items() - ] - - async def _update_info(self): - info = {} - url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74=" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - data = json.loads(result) - for x in data: - name = remove_prohibited_str(x["name"]) - member_dict = { - "id": x["id"], - "名称": name, - "星级": x["level"], - } - info[name] = member_dict - # logger.info(f"{name} is update...") - # 更新头像 - for key in info.keys(): - url = f'https://yys.163.com/shishen/{info[key]["id"]}.html' - result = await self.get_url(url) - if not result: - info[key]["头像"] = "" - continue - try: - dom = etree.HTML(result, etree.HTMLParser()) - avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0] - avatar = "https:" + avatar - info[key]["头像"] = avatar - except IndexError: - info[key]["头像"] = "" - logger.warning(f"{self.game_name_cn} 获取头像错误 {key}") - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载书签形式的头像 - url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png" - await self.download_img(url, value["名称"] + "_mark_btn") diff --git a/plugins/draw_card/handles/pcr_handle.py b/plugins/draw_card/handles/pcr_handle.py deleted file mode 100644 index d82ee27a..00000000 --- a/plugins/draw_card/handles/pcr_handle.py +++ /dev/null @@ -1,147 +0,0 @@ -import random -from lxml import etree -from typing import List, Tuple -from PIL import ImageDraw -from urllib.parse import unquote -from nonebot.log import logger - -from .base_handle import BaseHandle, BaseData -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class PcrChar(BaseData): - pass - - -class PcrHandle(BaseHandle[PcrChar]): - def __init__(self): - super().__init__("pcr", "公主连结") - self.max_star = 3 - self.config = draw_config.pcr - self.ALL_CHAR: List[PcrChar] = [] - - def get_card(self, mode: int = 1) -> PcrChar: - if mode == 2: - star = self.get_star( - [3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P] - ) - else: - star = self.get_star( - [3, 2, 1], - [self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P], - ) - chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] - return random.choice(chars) - - def get_cards(self, count: int, **kwargs) -> List[Tuple[PcrChar, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(2) - card_count = 0 - else: - card = self.get_card(1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def generate_card_img(self, card: PcrChar) -> BuildImage: - sep_w = 5 - sep_h = 5 - star_h = 15 - img_w = 90 - img_h = 90 - font_h = 20 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, sep_h), alpha=True) - for i in range(card.star): - bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h), alpha=True) - # 加名字 - text = card.name[:5] + "..." if len(card.name) > 6 else card.name - font = load_font(fontsize=14) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - PcrChar( - name=value["名称"], - star=int(value["星级"]), - limited=True if "(" in key else False, - ) - for key, value in self.load_data().items() - ] - - async def _update_info(self): - info = {} - if draw_config.PCR_TAI: - url = "https://wiki.biligame.com/pcr/角色图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath( - "//div[@class='resp-tab-content']/div[@class='unit-icon']" - ) - for char in char_list: - try: - name = char.xpath("./a/@title")[0] - avatar = char.xpath("./a/img/@srcset")[0] - star = len(char.xpath("./div[1]/img")) - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": star, - } - info[member_dict["名称"]] = member_dict - else: - url = "https://wiki.biligame.com/pcr/角色筛选表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = char.xpath("./td[4]/text()")[0] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "星级": int(str(star).strip()), - } - info[member_dict["名称"]] = member_dict - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - await self.download_img( - "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", - "star", - ) diff --git a/plugins/draw_card/handles/pretty_handle.py b/plugins/draw_card/handles/pretty_handle.py deleted file mode 100644 index 5558e268..00000000 --- a/plugins/draw_card/handles/pretty_handle.py +++ /dev/null @@ -1,424 +0,0 @@ -import re -import random -import dateparser -from lxml import etree -from PIL import ImageDraw -from bs4 import BeautifulSoup -from datetime import datetime -from urllib.parse import unquote -from typing import List, Optional, Tuple -from pydantic import ValidationError -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class PrettyData(BaseData): - pass - - -class PrettyChar(PrettyData): - pass - - -class PrettyCard(PrettyData): - @property - def star_str(self) -> str: - return ["R", "SR", "SSR"][self.star - 1] - - -class PrettyHandle(BaseHandle[PrettyData]): - def __init__(self): - super().__init__("pretty", "赛马娘") - self.data_files.append("pretty_card.json") - self.max_star = 3 - self.game_card_color = "#eff2f5" - self.config = draw_config.pretty - - self.ALL_CHAR: List[PrettyChar] = [] - self.ALL_CARD: List[PrettyCard] = [] - self.UP_CHAR: Optional[UpEvent] = None - self.UP_CARD: Optional[UpEvent] = None - - def get_card(self, pool_name: str, mode: int = 1) -> PrettyData: - if mode == 1: - star = self.get_star( - [3, 2, 1], - [ - self.config.PRETTY_THREE_P, - self.config.PRETTY_TWO_P, - self.config.PRETTY_ONE_P, - ], - ) - else: - star = self.get_star( - [3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P] - ) - up_pool = None - if pool_name == "char": - up_pool = self.UP_CHAR - all_list = self.ALL_CHAR - else: - up_pool = self.UP_CARD - all_list = self.ALL_CARD - - all_char = [x for x in all_list if x.star == star and not x.limited] - acquire_char = None - # 有UP池子 - if up_pool and star in [x.star for x in up_pool.up_char]: - up_list = [x.name for x in up_pool.up_char if x.star == star] - # 抽到UP - if random.random() < 1 / len(all_char) * (0.7 / 0.1385): - up_name = random.choice(up_list) - try: - acquire_char = [x for x in all_list if x.name == up_name][0] - except IndexError: - pass - if not acquire_char: - acquire_char = random.choice(all_char) - return acquire_char - - def get_cards(self, count: int, pool_name: str) -> List[Tuple[PrettyData, int]]: - card_list = [] - card_count = 0 # 保底计算 - for i in range(count): - card_count += 1 - # 十连保底 - if card_count == 10: - card = self.get_card(pool_name, 2) - card_count = 0 - else: - card = self.get_card(pool_name, 1) - if card.star > self.max_star - 2: - card_count = 0 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self, pool_name: str) -> str: - info = "" - up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD - if up_event: - star3_list = [x.name for x in up_event.up_char if x.star == 3] - star2_list = [x.name for x in up_event.up_char if x.star == 2] - star1_list = [x.name for x in up_event.up_char if x.star == 1] - if star3_list: - if pool_name == "char": - info += f'三星UP:{" ".join(star3_list)}\n' - else: - info += f'SSR UP:{" ".join(star3_list)}\n' - if star2_list: - if pool_name == "char": - info += f'二星UP:{" ".join(star2_list)}\n' - else: - info += f'SR UP:{" ".join(star2_list)}\n' - if star1_list: - if pool_name == "char": - info += f'一星UP:{" ".join(star1_list)}\n' - else: - info += f'R UP:{" ".join(star1_list)}\n' - info = f"当前up池:{up_event.title}\n{info}" - return info.strip() - - def draw(self, count: int, pool_name: str, **kwargs) -> Message: - pool_name = "char" if not pool_name else pool_name - index2card = self.get_cards(count, pool_name) - cards = [card[0] for card in index2card] - up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD - up_list = [x.name for x in up_event.up_char] if up_event else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info(pool_name) - return pool_info + image(b64=self.generate_img(cards).pic2bs4()) + result - - def generate_card_img(self, card: PrettyData) -> BuildImage: - if isinstance(card, PrettyChar): - star_h = 30 - img_w = 200 - img_h = 219 - font_h = 50 - bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - star_w = star_h * card.star - for i in range(card.star): - bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0), alpha=True) - bg.paste(img, (0, 0), alpha=True) - # 加名字 - text = card.name[:5] + "..." if len(card.name) > 6 else card.name - font = load_font(fontsize=30) - text_w, _ = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - ((img_w - text_w) / 2, img_h), - text, - font=font, - fill="gray", - ) - return bg - else: - sep_w = 10 - img_w = 200 - img_h = 267 - font_h = 75 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5") - label_path = str(self.img_path / f"{card.star}_label.png") - label = BuildImage(40, 40, background=label_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, 0), alpha=True) - bg.paste(label, (30, 3), alpha=True) - # 加名字 - text = "" - texts = [] - font = load_font(fontsize=25) - for t in card.name: - if font.getsize(text + t)[0] > 190: - texts.append(text) - text = "" - if len(texts) >= 2: - texts[-1] += "..." - break - else: - text += t - if text: - texts.append(text) - text = "\n".join(texts) - text_w, _ = font.getsize_multiline(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - ((img_w - text_w) / 2, img_h), - text, - font=font, - align="center", - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_CHAR = [ - PrettyChar( - name=value["名称"], - star=int(value["初始星级"]), - limited=False, - ) - for value in self.load_data().values() - ] - self.ALL_CARD = [ - PrettyCard( - name=value["中文名"], - star=["R", "SR", "SSR"].index(value["稀有度"]) + 1, - limited=True if "卡池" not in value["获取方式"] else False, - ) - for value in self.load_data("pretty_card.json").values() - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) - self.UP_CARD = UpEvent.parse_obj(data.get("card", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_CHAR and self.UP_CARD: - data = { - "char": json.loads(self.UP_CHAR.json()), - "card": json.loads(self.UP_CARD.json()), - } - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - # pretty.json - pretty_info = {} - url = "https://wiki.biligame.com/umamusume/赛马娘图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/a/@title")[0] - avatar = char.xpath("./td[1]/a/img/@srcset")[0] - star = len(char.xpath("./td[3]/img")) - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "初始星级": star, - } - pretty_info[member_dict["名称"]] = member_dict - self.dump_data(pretty_info) - logger.info(f"{self.game_name_cn} 更新成功") - # pretty_card.json - pretty_card_info = {} - url = "https://wiki.biligame.com/umamusume/支援卡图鉴" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 卡牌出错") - else: - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - name = char.xpath("./td[1]/div/a/@title")[0] - name_cn = char.xpath("./td[3]/a/text()")[0] - avatar = char.xpath("./td[1]/div/a/img/@srcset")[0] - star = str(char.xpath("./td[5]/text()")[0]).strip() - sources = str(char.xpath("./td[7]/text()")[0]).strip() - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(name), - "中文名": remove_prohibited_str(name_cn), - "稀有度": star, - "获取方式": [sources] if sources else [], - } - pretty_card_info[member_dict["中文名"]] = member_dict - self.dump_data(pretty_card_info, "pretty_card.json") - logger.info(f"{self.game_name_cn} 卡牌更新成功") - # 下载头像 - for value in pretty_info.values(): - await self.download_img(value["头像"], value["名称"]) - for value in pretty_card_info.values(): - await self.download_img(value["头像"], value["中文名"]) - # 下载星星 - PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume" - await self.download_img( - PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star" - ) - # 下载稀有度标志 - idx = 1 - for url in [ - "/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png", - "/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png", - "/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png", - ]: - await self.download_img(PRETTY_URL + url, f"{idx}_label") - idx += 1 - await self.update_up_char() - - async def update_up_char(self): - announcement_url = "https://wiki.biligame.com/umamusume/公告" - result = await self.get_url(announcement_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a") - title = "" - url = "" - for announcement in announcements: - try: - title = announcement.xpath("./@title")[0] - url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0] - if re.match(r".*?\d{8}$", title) or re.match( - r"^\d{1,2}月\d{1,2}日.*?", title - ): - break - except IndexError: - continue - if not title: - logger.warning(f"{self.game_name_cn}未找到新UP公告") - return - result = await self.get_url(url) - if not result: - logger.warning(f"{self.game_name_cn}获取UP公告出错") - return - try: - start_time = None - end_time = None - char_img = "" - card_img = "" - up_chars = [] - up_cards = [] - soup = BeautifulSoup(result, "lxml") - heads = soup.find_all("span", {"class": "mw-headline"}) - for head in heads: - if "时间" in head.text: - time = head.find_next("p").text.split("\n")[0] - if "~" in time: - start, end = time.split("~") - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - elif "赛马娘" in head.text: - char_img = head.find_next("a", {"class": "image"}).find("img")[ - "src" - ] - lines = str(head.find_next("p").text).split("\n") - chars = [ - line - for line in lines - if "★" in line and "(" in line and ")" in line - ] - for char in chars: - star = char.count("★") - name = re.split(r"[()]", char)[-2].strip() - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=70) - ) - elif "支援卡" in head.text: - card_img = head.find_next("a", {"class": "image"}).find("img")[ - "src" - ] - lines = str(head.find_next("p").text).split("\n") - cards = [ - line - for line in lines - if "R" in line and "(" in line and ")" in line - ] - for card in cards: - star = 3 if "SSR" in card else 2 if "SR" in card else 1 - name = re.split(r"[()]", card)[-2].strip() - up_cards.append( - UpChar(name=name, star=star, limited=False, zoom=70) - ) - if start_time and end_time: - if start_time <= datetime.now() <= end_time: - self.UP_CHAR = UpEvent( - title=title, - pool_img=char_img, - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.UP_CARD = UpEvent( - title=title, - pool_img=card_img, - start_time=start_time, - end_time=end_time, - up_char=up_cards, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - except Exception as e: - logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_CHAR and self.UP_CARD: - return Message( - Message.template("重载成功!\n当前UP池子:{}{:image}{:image}").format( - self.UP_CHAR.title, - self.UP_CHAR.pool_img, - self.UP_CARD.pool_img, - ) - ) diff --git a/plugins/draw_card/handles/prts_handle.py b/plugins/draw_card/handles/prts_handle.py deleted file mode 100644 index af0c5be2..00000000 --- a/plugins/draw_card/handles/prts_handle.py +++ /dev/null @@ -1,325 +0,0 @@ -import random -import re -from datetime import datetime -from typing import List, Optional, Tuple -from urllib.parse import unquote - -import dateparser -from PIL import ImageDraw -from lxml import etree -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.log import logger -from pydantic import ValidationError - -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -from .base_handle import BaseHandle, BaseData, UpChar, UpEvent -from ..config import draw_config -from ..util import remove_prohibited_str, cn2py, load_font -from utils.image_utils import BuildImage - - -class Operator(BaseData): - recruit_only: bool # 公招限定 - event_only: bool # 活动获得干员 - core_only:bool #中坚干员 - # special_only: bool # 升变/异格干员 - - -class PrtsHandle(BaseHandle[Operator]): - def __init__(self): - super().__init__(game_name="prts", game_name_cn="明日方舟") - self.max_star = 6 - self.game_card_color = "#eff2f5" - self.config = draw_config.prts - - self.ALL_OPERATOR: List[Operator] = [] - self.UP_EVENT: Optional[UpEvent] = None - - def get_card(self, add: float) -> Operator: - star = self.get_star( - star_list=[6, 5, 4, 3], - probability_list=[ - self.config.PRTS_SIX_P + add, - self.config.PRTS_FIVE_P, - self.config.PRTS_FOUR_P, - self.config.PRTS_THREE_P, - ], - ) - - all_operators = [ - x - for x in self.ALL_OPERATOR - if x.star == star and not any([x.limited, x.recruit_only, x.event_only,x.core_only]) - ] - acquire_operator = None - - if self.UP_EVENT: - up_operators = [x for x in self.UP_EVENT.up_char if x.star == star] - # UPs - try: - zooms = [x.zoom for x in up_operators] - zoom_sum = sum(zooms) - if random.random() < zoom_sum: - up_name = random.choices(up_operators, weights=zooms, k=1)[0].name - acquire_operator = [ - x for x in self.ALL_OPERATOR if x.name == up_name - ][0] - except IndexError: - pass - if not acquire_operator: - acquire_operator = random.choice(all_operators) - return acquire_operator - - def get_cards(self, count: int, **kwargs) -> List[Tuple[Operator, int]]: - card_list = [] # 获取所有角色 - add = 0.0 - count_idx = 0 - for i in range(count): - count_idx += 1 - card = self.get_card(add) - if card.star == self.max_star: - add = 0.0 - count_idx = 0 - elif count_idx > 50: - add += 0.02 - card_list.append((card, i + 1)) - return card_list - - def format_pool_info(self) -> str: - info = "" - if self.UP_EVENT: - star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6] - star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5] - star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4] - if star6_list: - info += f"六星UP:{' '.join(star6_list)}\n" - if star5_list: - info += f"五星UP:{' '.join(star5_list)}\n" - if star4_list: - info += f"四星UP:{' '.join(star4_list)}\n" - info = f"当前up池: {self.UP_EVENT.title}\n{info}" - return info.strip() - - def draw(self, count: int, **kwargs) -> Message: - index2card = self.get_cards(count) - """这里cards修复了抽卡图文不符的bug""" - cards = [card[0] for card in index2card] - up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] - result = self.format_result(index2card, up_list=up_list) - pool_info = self.format_pool_info() - return ( - pool_info - + image(b64=self.generate_img(cards).pic2bs4()) - + result - ) - - def generate_card_img(self, card: Operator) -> BuildImage: - sep_w = 5 - sep_h = 5 - star_h = 15 - img_w = 120 - img_h = 120 - font_h = 20 - bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - star_path = str(self.img_path / "star.png") - star = BuildImage(star_h, star_h, background=star_path) - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) - bg.paste(img, (sep_w, sep_h), alpha=True) - for i in range(card.star): - bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h), alpha=True) - # 加名字 - text = card.name[:7] + "..." if len(card.name) > 8 else card.name - font = load_font(fontsize=16) - text_w, text_h = font.getsize(text) - draw = ImageDraw.Draw(bg.markImg) - draw.text( - (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), - text, - font=font, - fill="gray", - ) - return bg - - def _init_data(self): - self.ALL_OPERATOR = [ - Operator( - name=value["名称"], - star=int(value["星级"]), - limited="标准寻访" not in value["获取途径"] and "中坚寻访" not in value["获取途径"], - recruit_only=True - if "标准寻访" not in value["获取途径"] and "中坚寻访" not in value["获取途径"] and "公开招募" in value["获取途径"] - else False, - event_only=True - if "活动获取" in value["获取途径"] - else False, - core_only=True - if "标准寻访" not in value["获取途径"] and "中坚寻访" in value["获取途径"] - else False, - ) - for key, value in self.load_data().items() - if "阿米娅" not in key - ] - self.load_up_char() - - def load_up_char(self): - try: - data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") - """这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下""" - if not data: - logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在") - else: - self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) - except ValidationError: - logger.warning(f"{self.game_name}_up_char 解析出错") - - def dump_up_char(self): - if self.UP_EVENT: - data = {"char": json.loads(self.UP_EVENT.json())} - self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") - - async def _update_info(self): - """更新信息""" - info = {} - url = "https://wiki.biligame.com/arknights/干员数据表" - result = await self.get_url(url) - if not result: - logger.warning(f"更新 {self.game_name_cn} 出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") - for char in char_list: - try: - avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0] - name = char.xpath("./td[2]/a/text()")[0] - star = char.xpath("./td[5]/text()")[0] - """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因""" - sources = [_.strip('\n') for _ in char.xpath("./td[8]/text()")] - except IndexError: - continue - member_dict = { - "头像": unquote(str(avatar).split(" ")[-2]), - "名称": remove_prohibited_str(str(name).strip()), - "星级": int(str(star).strip()), - "获取途径": sources, - } - info[member_dict["名称"]] = member_dict - self.dump_data(info) - logger.info(f"{self.game_name_cn} 更新成功") - # 下载头像 - for value in info.values(): - await self.download_img(value["头像"], value["名称"]) - # 下载星星 - await self.download_img( - "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", - "star", - ) - await self.update_up_char() - - async def update_up_char(self): - """重载卡池""" - announcement_url = "https://ak.hypergryph.com/news.html" - result = await self.get_url(announcement_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告出错") - return - dom = etree.HTML(result, etree.HTMLParser()) - activity_urls = dom.xpath( - "//ol[@class='articleList' and @data-category-key='ACTIVITY']/li/a/@href" - ) - start_time = None - end_time = None - up_chars = [] - pool_img = "" - for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了 - activity_url = f"https://ak.hypergryph.com{activity_url}" - result = await self.get_url(activity_url) - if not result: - logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错") - continue - - """因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败""" - dom = etree.HTML(result, etree.HTMLParser()) - contents = dom.xpath( - "//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src" - ) - title = "" - time = "" - chars: List[str] = [] - for index, content in enumerate(contents): - if re.search("(.*)(寻访|复刻).*?开启", content): - title = re.split(r"[【】]", content) - title = "".join(title[1:-1]) if "-" in title else title[1] - lines = [contents[index-2+_] for _ in range(8)] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前 - lines.append("") # 防止IndexError,加个空字符串 - for idx, line in enumerate(lines): - match = re.search( - r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line - ) - if match: - time = match.group(1) - """因为

的诡异排版,所以有了下面的一段""" - if ("★★" in line and "%" in line) or ("★★" in line and "%" in lines[idx + 1]): - chars.append(line) if ("★★" in line and "%" in line) else chars.append(line + lines[idx + 1]) - if not time: - continue - start, end = time.replace("月", "/").replace("日", " ").split("-")[:2] # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题 - start_time = dateparser.parse(start) - end_time = dateparser.parse(end) - pool_img = contents[index-2] - r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)""" - for char in chars: - star = char.split("(")[0].count("★") - name = re.split(r"[:(]", char)[1] if "★(" not in char else re.split("):", char)[1] # 有的括号在前面有的在后面 - dual_up = False - if "\\" in name: - names = name.split("\\") - dual_up = True - elif "/" in name: - names = name.split("/") - dual_up = True - else: - names = [name] # 既有用/分割的,又有用\分割的 - - names = [name.replace("[限定]", "").strip() for name in names] - zoom = 1 - if "权值" in char: - zoom = 0.03 - else: - match = re.search(r"(占.*?的.*?(\d+).*?%)", char) - if dual_up == True: - zoom = float(match.group(1))/2 - else: - zoom = float(match.group(1)) - zoom = zoom / 100 if zoom > 1 else zoom - for name in names: - up_chars.append( - UpChar(name=name, star=star, limited=False, zoom=zoom) - ) - break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决 - if title and start_time and end_time: - if start_time <= datetime.now() <= end_time: - self.UP_EVENT = UpEvent( - title=title, - pool_img=pool_img, - start_time=start_time, - end_time=end_time, - up_char=up_chars, - ) - self.dump_up_char() - logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") - break - - async def _reload_pool(self) -> Optional[Message]: - await self.update_up_char() - self.load_up_char() - if self.UP_EVENT: - return f"重载成功!\n当前UP池子:{self.UP_EVENT.title}" + MessageSegment.image( - self.UP_EVENT.pool_img - ) diff --git a/plugins/draw_card/rule.py b/plugins/draw_card/rule.py deleted file mode 100644 index fbb42096..00000000 --- a/plugins/draw_card/rule.py +++ /dev/null @@ -1,11 +0,0 @@ -from nonebot.internal.rule import Rule - -from configs.config import Config - - -def rule(game) -> Rule: - async def _rule() -> bool: - return Config.get_config("draw_card", game.config_name, True) - - return Rule(_rule) - diff --git a/plugins/draw_card/util.py b/plugins/draw_card/util.py deleted file mode 100644 index 860e35eb..00000000 --- a/plugins/draw_card/util.py +++ /dev/null @@ -1,60 +0,0 @@ -import platform -import pypinyin -from pathlib import Path -from PIL.ImageFont import FreeTypeFont -from PIL import Image, ImageDraw, ImageFont -from PIL.Image import Image as IMG - -from configs.path_config import FONT_PATH - -dir_path = Path(__file__).parent.absolute() - - -def cn2py(word) -> str: - """保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题""" - temp = "" - for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3): - temp += "".join(i) - return temp - - -# 移除windows和linux下特殊字符 -def remove_prohibited_str(name: str) -> str: - if platform.system().lower() == "windows": - tmp = "" - for i in name: - if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]: - tmp += i - name = tmp - else: - name = name.replace("/", "\\") - return name - - -def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont: - return ImageFont.truetype( - str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8" - ) - - -def circled_number(num: int) -> IMG: - font = load_font(fontsize=450) - text = str(num) - text_w = font.getsize(text)[0] - w = 240 + text_w - w = w if w >= 500 else 500 - img = Image.new("RGBA", (w, 500)) - draw = ImageDraw.Draw(img) - draw.ellipse(((0, 0), (500, 500)), fill="red") - draw.ellipse(((w - 500, 0), (w, 500)), fill="red") - draw.rectangle(((250, 0), (w - 250, 500)), fill="red") - draw.text( - (120, -60), - text, - font=font, - fill="white", - stroke_width=10, - stroke_fill="white", - ) - return img - diff --git a/plugins/epic/__init__.py b/plugins/epic/__init__.py deleted file mode 100755 index e201a123..00000000 --- a/plugins/epic/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.manager import group_manager -from utils.utils import get_bot, scheduler - -from .data_source import get_epic_free - -__zx_plugin_name__ = "epic免费游戏" -__plugin_usage__ = """ -usage: - 可以不玩,不能没有,每日白嫖 - 指令: - epic -""".strip() -__plugin_des__ = "可以不玩,不能没有,每日白嫖" -__plugin_cmd__ = ["epic"] -__plugin_version__ = 0.1 -__plugin_author__ = "AkashiCoin" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["epic"], -} -__plugin_task__ = {"epic_free_game": "epic免费游戏"} -Config.add_plugin_config( - "_task", - "DEFAULT_EPIC_FREE_GAME", - True, - help_="被动 epic免费游戏 进群默认开关状态", - default_value=True, - type=bool, -) - -epic = on_regex("^epic$", priority=5, block=True) - - -@epic.handle() -async def handle(bot: Bot, event: MessageEvent, state: T_State): - Type_Event = "Private" - if isinstance(event, GroupMessageEvent): - Type_Event = "Group" - msg_list, code = await get_epic_free(bot, Type_Event) - if code == 404: - await epic.send(msg_list) - elif isinstance(event, GroupMessageEvent): - await bot.send_group_forward_msg(group_id=event.group_id, messages=msg_list) - else: - for msg in msg_list: - await bot.send_private_msg(user_id=event.user_id, message=msg) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 获取epic免费游戏" - ) - - -# epic免费游戏 -@scheduler.scheduled_job( - "cron", - hour=12, - minute=1, -) -async def _(): - bot = get_bot() - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - msg_list, code = await get_epic_free(bot, "Group") - for g in gl: - if group_manager.check_task_status("epic_free_game", str(g)): - try: - if msg_list and code == 200: - await bot.send_group_forward_msg(group_id=g, messages=msg_list) - else: - await bot.send_group_msg(group_id=g) - except Exception as e: - logger.error(f"GROUP {g} epic免费游戏推送错误 {type(e)}: {e}") diff --git a/plugins/epic/data_source.py b/plugins/epic/data_source.py deleted file mode 100755 index f9b5f4ea..00000000 --- a/plugins/epic/data_source.py +++ /dev/null @@ -1,196 +0,0 @@ -from datetime import datetime -from nonebot.log import logger -from nonebot.adapters.onebot.v11 import Bot -from configs.config import NICKNAME -from utils.http_utils import AsyncHttpx - - -# 获取所有 Epic Game Store 促销游戏 -# 方法参考:RSSHub /epicgames 路由 -# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js -async def get_epic_game(): - epic_url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN" - headers = { - "Referer": "https://www.epicgames.com/store/zh-CN/", - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", - } - try: - res = await AsyncHttpx.get(epic_url, headers=headers, timeout=10) - res_json = res.json() - games = res_json["data"]["Catalog"]["searchStore"]["elements"] - return games - except Exception as e: - logger.error(f"Epic 访问接口错误 {type(e)}:{e}") - return None - -# 此处用于获取游戏简介 -async def get_epic_game_desp(name): - desp_url = "https://store-content-ipv4.ak.epicgames.com/api/zh-CN/content/products/" + str(name) - headers = { - "Referer": "https://store.epicgames.com/zh-CN/p/" + str(name), - "Content-Type": "application/json; charset=utf-8", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", - } - try: - res = await AsyncHttpx.get(desp_url, headers=headers, timeout=10) - res_json = res.json() - gamesDesp = res_json["pages"][0]["data"]["about"] - return gamesDesp - except Exception as e: - logger.error(f"Epic 访问接口错误 {type(e)}:{e}") - return None - -# 获取 Epic Game Store 免费游戏信息 -# 处理免费游戏的信息方法借鉴 pip 包 epicstore_api 示例 -# https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py -async def get_epic_free(bot: Bot, type_event: str): - games = await get_epic_game() - if not games: - return "Epic 可能又抽风啦,请稍后再试(", 404 - else: - msg_list = [] - for game in games: - game_name = game["title"] - game_corp = game["seller"]["name"] - game_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - # 赋初值以避免 local variable referenced before assignment - game_thumbnail, game_dev, game_pub = None, game_corp, game_corp - try: - game_promotions = game["promotions"]["promotionalOffers"] - upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"] - if not game_promotions and upcoming_promotions: - # 促销暂未上线,但即将上线 - promotion_data = upcoming_promotions[0]["promotionalOffers"][0] - start_date_iso, end_date_iso = ( - promotion_data["startDate"][:-1], - promotion_data["endDate"][:-1], - ) - # 删除字符串中最后一个 "Z" 使 Python datetime 可处理此时间 - start_date = datetime.fromisoformat(start_date_iso).strftime( - "%b.%d %H:%M" - ) - end_date = datetime.fromisoformat(end_date_iso).strftime( - "%b.%d %H:%M" - ) - if type_event == "Group": - _message = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( - game_corp, game_name, game_price, start_date, end_date - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) - else: - msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( - game_corp, game_name, game_price, start_date, end_date - ) - msg_list.append(msg) - else: - for image in game["keyImages"]: - if ( - image.get("url") - and not game_thumbnail - and image["type"] - in [ - "Thumbnail", - "VaultOpened", - "DieselStoreFrontWide", - "OfferImageWide", - ] - ): - game_thumbnail = image["url"] - break - for pair in game["customAttributes"]: - if pair["key"] == "developerName": - game_dev = pair["value"] - if pair["key"] == "publisherName": - game_pub = pair["value"] - if game.get("productSlug"): - gamesDesp = await get_epic_game_desp(game["productSlug"]) - try: - #是否存在简短的介绍 - if "shortDescription" in gamesDesp: - game_desp = gamesDesp["shortDescription"] - except KeyError: - game_desp = gamesDesp["description"] - else: - game_desp = game["description"] - try: - end_date_iso = game["promotions"]["promotionalOffers"][0][ - "promotionalOffers" - ][0]["endDate"][:-1] - end_date = datetime.fromisoformat(end_date_iso).strftime( - "%b.%d %H:%M" - ) - except IndexError: - end_date = '未知' - # API 返回不包含游戏商店 URL,此处自行拼接,可能出现少数游戏 404 请反馈 - if game.get("productSlug"): - game_url = "https://store.epicgames.com/zh-CN/p/{}".format( - game["productSlug"].replace("/home", "") - ) - elif game.get("url"): - game_url = game["url"] - else: - slugs = ( - [ - x["pageSlug"] - for x in game.get("offerMappings", []) - if x.get("pageType") == "productHome" - ] - + [ - x["pageSlug"] - for x in game.get("catalogNs", {}).get("mappings", []) - if x.get("pageType") == "productHome" - ] - + [ - x["value"] - for x in game.get("customAttributes", []) - if "productSlug" in x.get("key") - ] - ) - game_url = "https://store.epicgames.com/zh-CN{}".format( - f"/p/{slugs[0]}" if len(slugs) else "" - ) - if type_event == "Group": - _message = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( - game_thumbnail, - game_name, - game_price, - game_desp, - game_dev, - game_pub, - end_date, - game_url, - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) - else: - msg = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( - game_thumbnail, - game_name, - game_price, - game_desp, - game_dev, - game_pub, - end_date, - game_url, - ) - msg_list.append(msg) - except TypeError as e: - # logger.info(str(e)) - pass - return msg_list, 200 diff --git a/plugins/fake_msg.py b/plugins/fake_msg.py deleted file mode 100755 index 44d16314..00000000 --- a/plugins/fake_msg.py +++ /dev/null @@ -1,57 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from nonebot import on_command -from utils.utils import get_message_img -from utils.message_builder import share -from services.log import logger - - -__zx_plugin_name__ = "构造分享消息" -__plugin_usage__ = """ -usage: - 自定义的分享消息构造 - 指令: - 假消息 [网址] [标题] ?[内容] ?[图片] - 示例:假消息 www.4399.com 我喜欢萝莉 为什么我喜欢... [图片] -""".strip() -__plugin_des__ = "自定义的分享消息构造" -__plugin_cmd__ = ["假消息 [网址] [标题] ?[内容] ?[图片]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["假消息"], -} - - -fake_msg = on_command("假消息", priority=5, block=True) - - -@fake_msg.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - img = get_message_img(event.json()) - if len(msg) > 1: - if len(msg) == 2: - url = msg[0] - title = msg[1] - content = "" - else: - url = msg[0] - title = msg[1] - content = msg[2] - if img: - img = img[0] - else: - img = "" - if "http" not in url: - url = "http://" + url - await fake_msg.send(share(url, title, content, img)) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 构造假消息 url {url}, title {title}, content {content}" - ) - else: - await fake_msg.finish("消息格式错误:\n网址 标题 内容(可省略) 图片(可省略)") diff --git a/plugins/fudu.py b/plugins/fudu.py deleted file mode 100755 index 972f5f24..00000000 --- a/plugins/fudu.py +++ /dev/null @@ -1,138 +0,0 @@ -import random - -from nonebot import on_message -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from configs.config import NICKNAME, Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.message_builder import image -from utils.utils import get_message_img, get_message_text - -__zx_plugin_name__ = "复读" -__plugin_usage__ = """ -usage: - 重复3次相同的消息时会复读 -""".strip() -__plugin_des__ = "群友的本质是什么?是复读机哒!" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"fudu": "复读"} -__plugin_configs__ = { - "FUDU_PROBABILITY": { - "value": 0.7, - "help": "复读概率", - "default_value": 0.7, - "type": float, - } -} -Config.add_plugin_config( - "_task", "DEFAULT_FUDU", True, help_="被动 复读 进群默认开关状态", default_value=True, type=bool -) - - -class Fudu: - def __init__(self): - self.data = {} - - def append(self, key, content): - self._create(key) - self.data[key]["data"].append(content) - - def clear(self, key): - self._create(key) - self.data[key]["data"] = [] - self.data[key]["is_repeater"] = False - - def size(self, key) -> int: - self._create(key) - return len(self.data[key]["data"]) - - def check(self, key, content) -> bool: - self._create(key) - return self.data[key]["data"][0] == content - - def get(self, key): - self._create(key) - return self.data[key]["data"][0] - - def is_repeater(self, key): - self._create(key) - return self.data[key]["is_repeater"] - - def set_repeater(self, key): - self._create(key) - self.data[key]["is_repeater"] = True - - def _create(self, key): - if self.data.get(key) is None: - self.data[key] = {"is_repeater": False, "data": []} - - -_fudu_list = Fudu() - - -fudu = on_message(permission=GROUP, priority=999) - - -@fudu.handle() -async def _(event: GroupMessageEvent): - if event.is_tome(): - return - if msg := get_message_text(event.json()): - if msg.startswith(f"@可爱的{NICKNAME}"): - await fudu.finish("复制粘贴的虚空艾特?", at_sender=True) - img = get_message_img(event.json()) - msg = get_message_text(event.json()) - if not img and not msg: - return - if img: - img_hash = await get_fudu_img_hash(img[0], event.group_id) - else: - img_hash = "" - add_msg = msg + "|-|" + img_hash - if _fudu_list.size(event.group_id) == 0: - _fudu_list.append(event.group_id, add_msg) - elif _fudu_list.check(event.group_id, add_msg): - _fudu_list.append(event.group_id, add_msg) - else: - _fudu_list.clear(event.group_id) - _fudu_list.append(event.group_id, add_msg) - if _fudu_list.size(event.group_id) > 2: - if random.random() < Config.get_config( - "fudu", "FUDU_PROBABILITY" - ) and not _fudu_list.is_repeater(event.group_id): - if random.random() < 0.2: - if msg.endswith("打断施法!"): - await fudu.finish("[[_task|fudu]]打断" + msg) - else: - await fudu.finish("[[_task|fudu]]打断施法!") - _fudu_list.set_repeater(event.group_id) - if img and msg: - rst = msg + image(TEMP_PATH / f"compare_{event.group_id}_img.jpg") - elif img: - rst = image(TEMP_PATH / f"compare_{event.group_id}_img.jpg") - elif msg: - rst = msg - else: - rst = "" - if rst: - await fudu.finish("[[_task|fudu]]" + rst) - - -async def get_fudu_img_hash(url, group_id): - try: - if await AsyncHttpx.download_file( - url, TEMP_PATH / f"compare_{group_id}_img.jpg" - ): - img_hash = get_img_hash(TEMP_PATH / f"compare_{group_id}_img.jpg") - return str(img_hash) - else: - logger.warning(f"复读下载图片失败...") - except Exception as e: - logger.warning(f"复读读取图片Hash出错 {type(e)}:{e}") - return "" diff --git a/plugins/genshin/almanac/__init__.py b/plugins/genshin/almanac/__init__.py deleted file mode 100755 index ecbe8e75..00000000 --- a/plugins/genshin/almanac/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.manager import group_manager -from utils.message_builder import image -from utils.utils import get_bot, scheduler - -from ._data_source import build_alc_image - -__zx_plugin_name__ = "原神老黄历" -__plugin_usage__ = """ -usage: - 有时候也该迷信一回!特别是运气方面 - 指令: - 原神黄历 -""".strip() -__plugin_des__ = "有时候也该迷信一回!特别是运气方面" -__plugin_cmd__ = ["原神黄历"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神黄历", "原神老黄历"], -} -__plugin_task__ = {"genshin_alc": "原神黄历提醒"} - -Config.add_plugin_config( - "_task", - "DEFAULT_GENSHIN_ALC", - True, - help_="被动 原神黄历提醒 进群默认开关状态", - default_value=True, - type=bool, -) - -almanac = on_command("原神黄历", priority=5, block=True) - - -@almanac.handle() -async def _(event: MessageEvent): - await almanac.send(image(b64=await build_alc_image())) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送查看原神黄历" - ) - - -@scheduler.scheduled_job( - "cron", - hour=10, - minute=25, -) -async def _(): - # 每日提醒 - bot = get_bot() - if bot: - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - alc_img = image(b64=await build_alc_image()) - if alc_img: - mes = "[[_task|genshin_alc]]" + alc_img - for gid in gl: - if group_manager.check_group_task_status(gid, "genshin_alc"): - await bot.send_group_msg(group_id=int(gid), message=mes) diff --git a/plugins/genshin/almanac/_data_source.py b/plugins/genshin/almanac/_data_source.py deleted file mode 100644 index 07538d7e..00000000 --- a/plugins/genshin/almanac/_data_source.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import random -from dataclasses import dataclass -from datetime import datetime -from typing import List, Tuple, Union - -import ujson as json -from configs.path_config import DATA_PATH, IMAGE_PATH -from utils.image_utils import BuildImage - -CONFIG_PATH = DATA_PATH / "genshin_alc" / "config.json" - -ALC_PATH = IMAGE_PATH / "genshin" / "alc" - -ALC_PATH.mkdir(exist_ok=True, parents=True) - -BACKGROUND_PATH = ALC_PATH / "back.png" - -chinese = { - "0": "十", - "1": "一", - "2": "二", - "3": "三", - "4": "四", - "5": "五", - "6": "六", - "7": "七", - "8": "八", - "9": "九", -} - - -@dataclass -class Fortune: - title: str - desc: str - - -def random_fortune() -> Tuple[List[Fortune], List[Fortune]]: - """ - 说明: - 随机运势 - """ - data = json.load(CONFIG_PATH.open("r", encoding="utf8")) - fortune_data = {} - good_fortune = [] - bad_fortune = [] - while len(fortune_data) < 6: - r = random.choice(list(data.keys())) - if r not in fortune_data: - fortune_data[r] = data[r] - for i, k in enumerate(fortune_data): - if i < 3: - good_fortune.append( - Fortune(title=k, desc=random.choice(fortune_data[k]["buff"])) - ) - else: - bad_fortune.append( - Fortune(title=k, desc=random.choice(fortune_data[k]["debuff"])) - ) - return good_fortune, bad_fortune - - -def int2cn(v: Union[str, int]): - """ - 说明: - 数字转中文 - 参数: - :param v: str - """ - return "".join([chinese[x] for x in str(v)]) - - -async def build_alc_image() -> str: - """ - 说明: - 构造今日运势图片 - """ - for file in os.listdir(ALC_PATH): - if file not in ["back.png", f"{datetime.now().date()}.png"]: - (ALC_PATH / file).unlink() - path = ALC_PATH / f"{datetime.now().date()}.png" - if path.exists(): - return BuildImage(0, 0, background=path).pic2bs4() - good_fortune, bad_fortune = random_fortune() - background = BuildImage( - 0, 0, background=BACKGROUND_PATH, font="HYWenHei-85W.ttf", font_size=30 - ) - now = datetime.now() - await background.atext((78, 145), str(now.year), fill="#8d7650ff") - month = str(now.month) - month_w = 358 - if now.month < 10: - month_w = 373 - elif now.month != 10: - month = "0" + month[-1] - await background.atext((month_w, 145), f"{int2cn(month)}月", fill="#8d7650ff") - day = str(now.day) - if now.day > 10 and day[-1] != "0": - day = day[0] + "0" + day[-1] - day_str = f"{int2cn(day)}日" - day_w = 193 - if (n := len(day_str)) == 3: - day_w = 207 - elif n == 2: - day_w = 228 - await background.atext( - (day_w, 145), f"{int2cn(day)}日", fill="#f7f8f2ff", font_size=35 - ) - fortune_h = 230 - for fortune in good_fortune: - await background.atext( - (150, fortune_h), fortune.title, fill="#756141ff", font_size=25 - ) - await background.atext( - (150, fortune_h + 28), fortune.desc, fill="#b5b3acff", font_size=19 - ) - fortune_h += 55 - fortune_h += 4 - for fortune in bad_fortune: - await background.atext( - (150, fortune_h), fortune.title, fill="#756141ff", font_size=25 - ) - await background.atext( - (150, fortune_h + 28), fortune.desc, fill="#b5b3acff", font_size=19 - ) - fortune_h += 55 - await background.asave(path) - return background.pic2bs4() diff --git a/plugins/genshin/material_remind/__init__.py b/plugins/genshin/material_remind/__init__.py deleted file mode 100755 index 795f8911..00000000 --- a/plugins/genshin/material_remind/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -import asyncio -import os -import time -from datetime import datetime, timedelta -from typing import List - -import nonebot -from nonebot import Driver, on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.permission import SUPERUSER - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.browser import get_browser -from utils.image_utils import BuildImage -from utils.message_builder import image - -__zx_plugin_name__ = "今日素材" -__plugin_usage__ = """ -usage: - 看看原神今天要刷什么 - 指令: - 今日素材/今天素材 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新原神今日素材 - 指令: - 更新原神今日素材 -""".strip() -__plugin_des__ = "看看原神今天要刷什么" -__plugin_cmd__ = ["今日素材/今天素材", "更新原神今日素材 [_superuser]"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["今日素材", "今天素材"], -} - -driver: Driver = nonebot.get_driver() - -material = on_command("今日素材", aliases={"今日材料", "今天素材", "今天材料"}, priority=5, block=True) - -super_cmd = on_command("更新原神今日素材", permission=SUPERUSER, priority=1, block=True) - - -@material.handle() -async def _(event: MessageEvent): - if time.strftime("%w") == "0": - await material.send("今天是周日,所有材料副本都开放了。") - return - file_name = str((datetime.now() - timedelta(hours=4)).date()) - if not (IMAGE_PATH / "genshin" / "material" / f"{file_name}.png").exists(): - await update_image() - await material.send( - Message( - image(IMAGE_PATH / "genshin" / "material" / f"{file_name}.png") - + "\n※ 每日素材数据来源于米游社" - ) - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送查看今日素材" - ) - - -@super_cmd.handle() -async def _(): - if await update_image(): - await super_cmd.send("更新成功...") - logger.info(f"更新每日天赋素材成功...") - else: - await super_cmd.send(f"更新失败...") - - -async def update_image(): - page = None - try: - if not os.path.exists(f"{IMAGE_PATH}/genshin/material"): - os.mkdir(f"{IMAGE_PATH}/genshin/material") - for file in os.listdir(f"{IMAGE_PATH}/genshin/material"): - os.remove(f"{IMAGE_PATH}/genshin/material/{file}") - browser = get_browser() - if not browser: - logger.warning("获取 browser 失败,请部署至 linux 环境....") - return False - # url = "https://genshin.pub/daily" - url = "https://bbs.mihoyo.com/ys/obc/channel/map/193" - page = await browser.new_page(viewport={"width": 860, "height": 3000}) - await page.goto(url) - await page.wait_for_timeout(3000) - file_name = str((datetime.now() - timedelta(hours=4)).date()) - # background_img.save(f"{IMAGE_PATH}/genshin/material/{file_name}.png") - await page.locator( - '//*[@id="__layout"]/div/div[2]/div[2]/div/div[1]/div[2]/div/div' - ).screenshot(path=f"{IMAGE_PATH}/genshin/material/{file_name}.png") - await page.close() - return True - except Exception as e: - logger.error(f"原神每日素材更新出错... {type(e)}: {e}") - if page: - await page.close() - return False diff --git a/plugins/genshin/query_resource_points/__init__.py b/plugins/genshin/query_resource_points/__init__.py deleted file mode 100755 index b17edf72..00000000 --- a/plugins/genshin/query_resource_points/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -from nonebot import on_command, on_regex -from .query_resource import get_resource_type_list, query_resource, init, check_resource_exists -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, Message -from utils.utils import scheduler -from services.log import logger -from configs.config import NICKNAME -from nonebot.permission import SUPERUSER -from nonebot.params import CommandArg -import re - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "原神资源查询" -__plugin_usage__ = """ -usage: - 不需要打开网页,就能帮你生成资源图片 - 指令: - 原神资源查询 [资源名称] - 原神资源列表 - [资源名称]在哪 - 哪有[资源名称] -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新原神资源信息 - 指令: - 更新原神资源信息 -""".strip() -__plugin_des__ = "原神大地图资源速速查看" -__plugin_cmd__ = ["原神资源查询 [资源名称]", "原神资源列表", "[资源名称]在哪/哪有[资源名称]", "更新原神资源信息 [_superuser]"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神资源查询", "原神资源列表"], -} -__plugin_block_limit__ = { - "rst": "您有资源正在查询!" -} - -qr = on_command("原神资源查询", aliases={"原神资源查找"}, priority=5, block=True) -qr_lst = on_command("原神资源列表", priority=5, block=True) -rex_qr = on_regex(".*?(在哪|在哪里|哪有|哪里有).*?", priority=5, block=True) -update_info = on_regex("^更新原神资源信息$", permission=SUPERUSER, priority=1, block=True) - - -@qr.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - resource_name = arg.extract_plain_text().strip() - if check_resource_exists(resource_name): - await qr.send("正在生成位置....") - resource = await query_resource(resource_name) - await qr.send(Message(resource), at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询原神材料:" + resource_name - ) - else: - await qr.send(f"未查找到 {resource_name} 资源,可通过 “原神资源列表” 获取全部资源名称..") - - -@rex_qr.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if "在哪" in msg: - rs = re.search("(.*)在哪.*?", msg) - resource_name = rs.group(1) if rs else "" - else: - rs = re.search(".*?(哪有|哪里有)(.*)", msg) - resource_name = rs.group(2) if rs else "" - if check_resource_exists(resource_name): - await qr.send("正在生成位置....") - resource = await query_resource(resource_name) - if resource: - await rex_qr.send(Message(resource), at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询原神材料:" + resource_name - ) - - -@qr_lst.handle() -async def _(bot: Bot, event: MessageEvent): - txt = get_resource_type_list() - txt_list = txt.split("\n") - if isinstance(event, GroupMessageEvent): - mes_list = [] - for txt in txt_list: - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": txt, - }, - } - mes_list.append(data) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - rst = "" - for i in range(len(txt_list)): - rst += txt_list[i] + "\n" - if i % 5 == 0: - if rst: - await qr_lst.send(rst) - rst = "" - - -@update_info.handle() -async def _(): - await init(True) - await update_info.send("更新原神资源信息完成...") - - -@scheduler.scheduled_job( - "cron", - hour=5, - minute=1, -) -async def _(): - try: - await init() - logger.info(f"每日更新原神材料信息成功!") - except Exception as e: - logger.error(f"每日更新原神材料信息错误:{e}") diff --git a/plugins/genshin/query_resource_points/map.py b/plugins/genshin/query_resource_points/map.py deleted file mode 100755 index f974f812..00000000 --- a/plugins/genshin/query_resource_points/map.py +++ /dev/null @@ -1,265 +0,0 @@ -from configs.path_config import IMAGE_PATH, TEXT_PATH, TEMP_PATH -from utils.image_utils import BuildImage -from typing import Tuple, List -from math import sqrt, pow -import random - -try: - import ujson as json -except ModuleNotFoundError: - import json - -icon_path = IMAGE_PATH / "genshin" / "genshin_icon" -map_path = IMAGE_PATH / "genshin" / "map" / "map.png" -resource_label_file = TEXT_PATH / "genshin" / "resource_label_file.json" -resource_point_file = TEXT_PATH / "genshin" / "resource_point_file.json" - - -class Map: - """ - 原神资源生成类 - """ - - def __init__( - self, - resource_name: str, - center_point: Tuple[int, int], - deviation: Tuple[int, int] = (25, 51), - padding: int = 100, - planning_route: bool = False, - ratio: float = 1, - ): - """ - 参数: - :param resource_name: 资源名称 - :param center_point: 中心点 - :param deviation: 坐标误差 - :param padding: 截图外边距 - :param planning_route: 是否规划最佳线路 - :param ratio: 压缩比率 - """ - self.map = BuildImage(0, 0, background=map_path) - self.resource_name = resource_name - self.center_x = center_point[0] - self.center_y = center_point[1] - self.deviation = deviation - self.padding = int(padding * ratio) - self.planning_route = planning_route - self.ratio = ratio - - self.deviation = ( - int(self.deviation[0] * ratio), - int(self.deviation[1] * ratio), - ) - - data = json.load(open(resource_label_file, "r", encoding="utf8")) - # 资源 id - self.resource_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == resource_name - ][0] - # 传送锚点 id - self.teleport_anchor_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == "传送锚点" - ][0] - # 神像 id - self.teleport_god_id = [ - data[x]["id"] - for x in data - if x != "CENTER_POINT" and data[x]["name"] == "七天神像" - ][0] - # 资源坐标 - data = json.load(open(resource_point_file, "r", encoding="utf8")) - self.resource_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.resource_id - ] - # 传送锚点坐标 - self.teleport_anchor_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.teleport_anchor_id - ] - # 神像坐标 - self.teleport_god_point = [ - Resources( - int((self.center_x + data[x]["x_pos"]) * ratio), - int((self.center_y + data[x]["y_pos"]) * ratio), - ) - for x in data - if x != "CENTER_POINT" and data[x]["label_id"] == self.teleport_god_id - ] - - # 将地图上生成资源图标 - def generate_resource_icon_in_map(self) -> int: - x_list = [x.x for x in self.resource_point] - y_list = [x.y for x in self.resource_point] - min_width = min(x_list) - self.padding - max_width = max(x_list) + self.padding - min_height = min(y_list) - self.padding - max_height = max(y_list) + self.padding - self._generate_transfer_icon((min_width, min_height, max_width, max_height)) - for res in self.resource_point: - icon = self._get_icon_image(self.resource_id) - self.map.paste( - icon, (res.x - self.deviation[0], res.y - self.deviation[1]), True - ) - if self.planning_route: - self._generate_best_route() - self.map.crop((min_width, min_height, max_width, max_height)) - rand = random.randint(1, 10000) - self.map.save(f"{TEMP_PATH}/genshin_map_{rand}.png") - return rand - - # 资源数量 - def get_resource_count(self) -> int: - return len(self.resource_point) - - # 生成传送锚点和神像 - def _generate_transfer_icon(self, box: Tuple[int, int, int, int]): - min_width, min_height, max_width, max_height = box - for resources in [self.teleport_anchor_point, self.teleport_god_point]: - id_ = ( - self.teleport_anchor_id - if resources == self.teleport_anchor_point - else self.teleport_god_id - ) - for res in resources: - if min_width < res.x < max_width and min_height < res.y < max_height: - icon = self._get_icon_image(id_) - self.map.paste( - icon, - (res.x - self.deviation[0], res.y - self.deviation[1]), - True, - ) - - # 生成最优路线(说是最优其实就是直线最短) - def _generate_best_route(self): - line_points = [] - teleport_list = self.teleport_anchor_point + self.teleport_god_point - for teleport in teleport_list: - current_res, res_min_distance = teleport.get_resource_distance(self.resource_point) - current_teleport, teleport_min_distance = current_res.get_resource_distance(teleport_list) - if current_teleport == teleport: - self.map.line( - (current_teleport.x, current_teleport.y, current_res.x, current_res.y), (255, 0, 0), width=1 - ) - is_used_res_points = [] - for res in self.resource_point: - if res in is_used_res_points: - continue - current_teleport, teleport_min_distance = res.get_resource_distance(teleport_list) - current_res, res_min_distance = res.get_resource_distance(self.resource_point) - if teleport_min_distance < res_min_distance: - self.map.line( - (current_teleport.x, current_teleport.y, res.x, res.y), (255, 0, 0), width=1 - ) - else: - is_used_res_points.append(current_res) - self.map.line( - (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - ) - res_cp = self.resource_point[:] - res_cp.remove(current_res) - # for _ in res_cp: - current_teleport_, teleport_min_distance = res.get_resource_distance(teleport_list) - current_res, res_min_distance = res.get_resource_distance(res_cp) - if teleport_min_distance < res_min_distance: - self.map.line( - (current_teleport.x, current_teleport.y, res.x, res.y), (255, 0, 0), width=1 - ) - else: - self.map.line( - (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - ) - is_used_res_points.append(current_res) - is_used_res_points.append(res) - - # resources_route = [] - # # 先连上最近的资源路径 - # for res in self.resource_point: - # # 拿到最近的资源 - # current_res, _ = res.get_resource_distance( - # self.resource_point - # + self.teleport_anchor_point - # + self.teleport_god_point - # ) - # self.map.line( - # (current_res.x, current_res.y, res.x, res.y), (255, 0, 0), width=1 - # ) - # resources_route.append((current_res, res)) - # teleport_list = self.teleport_anchor_point + self.teleport_god_point - # for res1, res2 in resources_route: - # point_list = [x for x in resources_route if res1 in x or res2 in x] - # if not list(set(point_list).intersection(set(teleport_list))): - # if res1 not in teleport_list and res2 not in teleport_list: - # # while True: - # # tmp = [x for x in point_list] - # # break - # teleport1, distance1 = res1.get_resource_distance(teleport_list) - # teleport2, distance2 = res2.get_resource_distance(teleport_list) - # if distance1 > distance2: - # self.map.line( - # (teleport1.x, teleport1.y, res1.x, res1.y), - # (255, 0, 0), - # width=1, - # ) - # else: - # self.map.line( - # (teleport2.x, teleport2.y, res2.x, res2.y), - # (255, 0, 0), - # width=1, - # ) - - # self.map.line(xy, (255, 0, 0), width=3) - - # 获取资源图标 - def _get_icon_image(self, id_: int) -> "BuildImage": - icon = icon_path / f"{id_}.png" - if icon.exists(): - return BuildImage( - int(50 * self.ratio), int(50 * self.ratio), background=icon - ) - return BuildImage( - int(50 * self.ratio), - int(50 * self.ratio), - background=f"{icon_path}/box.png", - ) - - # def _get_shortest_path(self, res: 'Resources', res_2: 'Resources'): - - -# 资源类 -class Resources: - def __init__(self, x: int, y: int): - self.x = x - self.y = y - - def get_distance(self, x: int, y: int): - return int(sqrt(pow(abs(self.x - x), 2) + pow(abs(self.y - y), 2))) - - # 拿到资源在该列表中的最短路径 - def get_resource_distance(self, resources: List["Resources"]) -> "Resources, int": - current_res = None - min_distance = 999999 - for res in resources: - distance = self.get_distance(res.x, res.y) - if distance < min_distance and res != self: - current_res = res - min_distance = distance - return current_res, min_distance - - - - - diff --git a/plugins/genshin/query_resource_points/query_resource.py b/plugins/genshin/query_resource_points/query_resource.py deleted file mode 100755 index 53c2beaf..00000000 --- a/plugins/genshin/query_resource_points/query_resource.py +++ /dev/null @@ -1,272 +0,0 @@ -from pathlib import Path -from typing import Tuple, Optional, List -from configs.path_config import IMAGE_PATH, TEXT_PATH, TEMP_PATH -from utils.message_builder import image -from services.log import logger -from utils.image_utils import BuildImage -from asyncio.exceptions import TimeoutError -from asyncio import Semaphore -from utils.image_utils import is_valid -from utils.http_utils import AsyncHttpx -from httpx import ConnectTimeout -from .map import Map -import asyncio -import nonebot -import os - -try: - import ujson as json -except ModuleNotFoundError: - import json - -driver: nonebot.Driver = nonebot.get_driver() - -LABEL_URL = "https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/label/tree?app_sn=ys_obc" -POINT_LIST_URL = "https://api-static.mihoyo.com/common/blackboard/ys_obc/v1/map/point/list?map_id=2&app_sn=ys_obc" -MAP_URL = "https://api-static.mihoyo.com/common/map_user/ys_obc/v1/map/info?map_id=2&app_sn=ys_obc&lang=zh-cn" - -icon_path = IMAGE_PATH / "genshin" / "genshin_icon" -map_path = IMAGE_PATH / "genshin" / "map" -resource_label_file = TEXT_PATH / "genshin" / "resource_label_file.json" -resource_point_file = TEXT_PATH / "genshin" / "resource_point_file.json" -resource_type_file = TEXT_PATH / "genshin" / "resource_type_file.json" - -# 地图中心坐标 -CENTER_POINT: Optional[Tuple[int, int]] = None - -resource_name_list: List[str] = [] - -MAP_RATIO = 0.5 - - -# 查找资源 -async def query_resource(resource_name: str) -> str: - global CENTER_POINT - planning_route: bool = False - if resource_name and resource_name[-2:] in ["路径", "路线"]: - resource_name = resource_name[:-2].strip() - planning_route = True - if not resource_name or resource_name not in resource_name_list: - # return f"未查找到 {resource_name} 资源,可通过 “原神资源列表” 获取全部资源名称.." - return "" - map_ = Map( - resource_name, CENTER_POINT, planning_route=planning_route, ratio=MAP_RATIO - ) - count = map_.get_resource_count() - rand = await asyncio.get_event_loop().run_in_executor( - None, map_.generate_resource_icon_in_map - ) - return ( - f"{image(TEMP_PATH / f'genshin_map_{rand}.png')}" - f"\n\n※ {resource_name} 一共找到 {count} 个位置点\n※ 数据来源于米游社wiki" - ) - - -# 原神资源列表 -def get_resource_type_list(): - with open(resource_type_file, "r", encoding="utf8") as f: - data = json.load(f) - temp = {} - for id_ in data.keys(): - temp[data[id_]["name"]] = [] - for x in data[id_]["children"]: - temp[data[id_]["name"]].append(x["name"]) - - mes = "当前资源列表如下:\n" - - for resource_type in temp.keys(): - mes += f"{resource_type}:{','.join(temp[resource_type])}\n" - return mes - - -def check_resource_exists(resource: str) -> bool: - """ - 检查资源是否存在 - :param resource: 资源名称 - """ - resource = resource.replace("路径", "").replace("路线", "") - return resource in resource_name_list - - -@driver.on_startup -async def init(flag: bool = False): - global CENTER_POINT, resource_name_list - try: - semaphore = asyncio.Semaphore(10) - await download_map_init(semaphore, flag) - await download_resource_data(semaphore) - await download_resource_type() - if not CENTER_POINT: - if resource_label_file.exists(): - CENTER_POINT = json.load( - open(resource_label_file, "r", encoding="utf8") - )["CENTER_POINT"] - if resource_label_file.exists(): - with open(resource_type_file, "r", encoding="utf8") as f: - data = json.load(f) - for id_ in data: - for x in data[id_]["children"]: - resource_name_list.append(x["name"]) - except TimeoutError: - logger.warning("原神资源查询信息初始化超时....") - - -# 图标及位置资源 -async def download_resource_data(semaphore: Semaphore): - icon_path.mkdir(parents=True, exist_ok=True) - resource_label_file.parent.mkdir(parents=True, exist_ok=True) - try: - response = await AsyncHttpx.get(POINT_LIST_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = data["data"] - for lst in ["label_list", "point_list"]: - resource_data = {"CENTER_POINT": CENTER_POINT} - tasks = [] - file = ( - resource_label_file - if lst == "label_list" - else resource_point_file - ) - for x in data[lst]: - id_ = x["id"] - if lst == "label_list": - img_url = x["icon"] - tasks.append( - asyncio.ensure_future( - download_image( - img_url, - icon_path / f"{id_}.png", - semaphore, - True, - ) - ) - ) - resource_data[id_] = x - await asyncio.gather(*tasks) - with open(file, "w", encoding="utf8") as f: - json.dump(resource_data, f, ensure_ascii=False, indent=4) - else: - logger.warning(f'获取原神资源失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神资源失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("获取原神资源数据超时...已再次尝试...") - await download_resource_data(semaphore) - except Exception as e: - logger.error(f"获取原神资源数据未知错误 {type(e)}:{e}") - - -# 下载原神地图并拼图 -async def download_map_init(semaphore: Semaphore, flag: bool = False): - global CENTER_POINT, MAP_RATIO - map_path.mkdir(exist_ok=True, parents=True) - _map = map_path / "map.png" - if _map.exists() and os.path.getsize(_map) > 1024 * 1024 * 30: - _map.unlink() - try: - response = await AsyncHttpx.get(MAP_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = json.loads(data["data"]["info"]["detail"]) - CENTER_POINT = (data["origin"][0], data["origin"][1]) - if not _map.exists() or flag: - data = data["slices"] - idx = 0 - w_len = len(data[0]) - h_len = len(data) - for _map_data in data: - for _map in _map_data: - map_url = _map["url"] - await download_image( - map_url, - map_path / f"{idx}.png", - semaphore, - force_flag=flag, - ) - BuildImage( - 0, 0, background=f"{map_path}/{idx}.png", ratio=MAP_RATIO - ).save() - idx += 1 - w, h = BuildImage(0, 0, background=f"{map_path}/0.png").size - map_file = BuildImage(w * w_len, h * h_len, w, h, ratio=MAP_RATIO) - for i in range(idx): - img = BuildImage(0, 0, background=f"{map_path}/{i}.png") - await map_file.apaste(img) - map_file.save(f"{map_path}/map.png") - else: - logger.warning(f'获取原神地图失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神地图失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("下载原神地图数据超时....") - except Exception as e: - logger.error(f"下载原神地图数据失败 {type(e)}:{e}") - - -# 下载资源类型数据 -async def download_resource_type(): - resource_type_file.parent.mkdir(parents=True, exist_ok=True) - try: - response = await AsyncHttpx.get(LABEL_URL, timeout=10) - if response.status_code == 200: - data = response.json() - if data["message"] == "OK": - data = data["data"]["tree"] - resource_data = {} - for x in data: - id_ = x["id"] - resource_data[id_] = x - with open(resource_type_file, "w", encoding="utf8") as f: - json.dump(resource_data, f, ensure_ascii=False, indent=4) - logger.info(f"更新原神资源类型成功...") - else: - logger.warning(f'获取原神资源类型失败 msg: {data["message"]}') - else: - logger.warning(f"获取原神资源类型失败 code:{response.status_code}") - except (TimeoutError, ConnectTimeout): - logger.warning("下载原神资源类型数据超时....") - except Exception as e: - logger.error(f"载原神资源类型数据错误 {type(e)}:{e}") - - -# 初始化资源图标 -def gen_icon(icon: Path): - A = BuildImage(0, 0, background=f"{icon_path}/box.png") - B = BuildImage(0, 0, background=f"{icon_path}/box_alpha.png") - icon_img = BuildImage(115, 115, background=icon) - icon_img.circle() - B.paste(icon_img, (17, 10), True) - B.paste(A, alpha=True) - B.save(icon) - logger.info(f"生成图片成功 file:{str(icon)}") - - -# 下载图片 -async def download_image( - img_url: str, - path: Path, - semaphore: Semaphore, - gen_flag: bool = False, - force_flag: bool = False, -): - async with semaphore: - try: - if not path.exists() or not is_valid(path) or force_flag: - if await AsyncHttpx.download_file(img_url, path, timeout=10): - logger.info(f"下载原神资源图标:{img_url}") - if gen_flag: - gen_icon(path) - else: - logger.info(f"下载原神资源图标:{img_url} 失败,等待下次更新...") - except Exception as e: - logger.warning(f"原神图片错误..已删除,等待下次更新... file: {path} {type(e)}:{e}") - if os.path.exists(path): - os.remove(path) - - -# -# def _get_point_ratio(): -# diff --git a/plugins/genshin/query_user/__init__.py b/plugins/genshin/query_user/__init__.py deleted file mode 100644 index 49b8a858..00000000 --- a/plugins/genshin/query_user/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -from pathlib import Path - -from configs.config import Config -import nonebot - - -Config.add_plugin_config( - "genshin", - "mhyVersion", - "2.11.1" -) - -Config.add_plugin_config( - "genshin", - "salt", - "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs" -) - -Config.add_plugin_config( - "genshin", - "n", - "h8w582wxwgqvahcdkpvdhbh2w9casgfl" -) - -Config.add_plugin_config( - "genshin", - "client_type", - "5" -) - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) - - - - diff --git a/plugins/genshin/query_user/_models/__init__.py b/plugins/genshin/query_user/_models/__init__.py deleted file mode 100644 index dacb029e..00000000 --- a/plugins/genshin/query_user/_models/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -import random -from datetime import datetime, timedelta -from typing import Optional - -import pytz -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class Genshin(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - uid = fields.BigIntField() - """uid""" - mys_id: int = fields.BigIntField(null=True) - """米游社id""" - cookie: str = fields.TextField(default="") - """米游社cookie""" - auto_sign = fields.BooleanField(default=False) - """是否自动签到""" - today_query_uid = fields.TextField(default="") - """cookie今日查询uid""" - auto_sign_time = fields.DatetimeField(null=True) - """签到日期时间""" - resin_remind = fields.BooleanField(default=False) - """树脂提醒""" - resin_recovery_time = fields.DatetimeField(null=True) - """满树脂提醒日期""" - bind_group: int = fields.BigIntField(null=True) - """发送提示 绑定群聊""" - login_ticket = fields.TextField(default="") - """login_ticket""" - stuid: str = fields.TextField(default="") - """stuid""" - stoken: str = fields.TextField(default="") - """stoken""" - - class Meta: - table = "genshin" - table_description = "原神数据表" - unique_together = ("user_id", "uid") - - @classmethod - async def random_sign_time(cls, uid: int) -> Optional[datetime]: - """ - 说明: - 随机签到时间 - 说明: - :param uid: uid - """ - user = await cls.get_or_none(uid=uid) - if user and user.cookie: - if user.auto_sign_time and user.auto_sign_time.astimezone( - pytz.timezone("Asia/Shanghai") - ) - timedelta(seconds=2) >= datetime.now(pytz.timezone("Asia/Shanghai")): - return user.auto_sign_time.astimezone(pytz.timezone("Asia/Shanghai")) - hours = int(str(datetime.now()).split()[1].split(":")[0]) - minutes = int(str(datetime.now()).split()[1].split(":")[1]) - date = ( - datetime.now() - + timedelta(days=1) - - timedelta(hours=hours) - - timedelta(minutes=minutes - 1) - ) - random_hours = random.randint(0, 22) - random_minutes = random.randint(1, 59) - date += timedelta(hours=random_hours) + timedelta(minutes=random_minutes) - user.auto_sign_time = date - await user.save(update_fields=["auto_sign_time"]) - return date - return None - - @classmethod - async def random_cookie(cls, uid: int) -> Optional[str]: - """ - 说明: - 随机获取查询角色信息cookie - 参数: - :param uid: 原神uid - """ - # 查找用户今日是否已经查找过,防止重复 - user = await cls.get_or_none(today_query_uid__contains=str(uid)) - if user: - return user.cookie - for user in await cls.filter(cookie__not="").annotate(rand=Random()).all(): - if not user.today_query_uid or len(user.today_query_uid[:-1].split()) < 30: - user.today_query_uid = user.today_query_uid + f"{uid} " - await user.save(update_fields=["today_query_uid"]) - return user.cookie - return None - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE genshin ADD auto_sign_time timestamp with time zone;", - "ALTER TABLE genshin ADD resin_remind boolean DEFAULT False;", - "ALTER TABLE genshin ADD resin_recovery_time timestamp with time zone;", - "ALTER TABLE genshin ADD login_ticket VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin ADD stuid VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin ADD stoken VARCHAR(255) DEFAULT '';", - "ALTER TABLE genshin RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE genshin ALTER COLUMN user_id TYPE character varying(255);", - ] diff --git a/plugins/genshin/query_user/_utils/__init__.py b/plugins/genshin/query_user/_utils/__init__.py deleted file mode 100644 index 8dddb2c9..00000000 --- a/plugins/genshin/query_user/_utils/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -import hashlib -import json -import random -import string -import time - -from configs.config import Config - - -def _md5(text): - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -def get_old_ds() -> str: - n = Config.get_config("genshin", "n") - i = str(int(time.time())) - r = "".join(random.sample(string.ascii_lowercase + string.digits, 6)) - c = _md5("salt=" + n + "&t=" + i + "&r=" + r) - return i + "," + r + "," + c - - -def get_ds(q: str = "", b: dict = None) -> str: - if b: - br = json.dumps(b) - else: - br = "" - s = Config.get_config("genshin", "salt") - t = str(int(time.time())) - r = str(random.randint(100000, 200000)) - c = _md5("salt=" + s + "&t=" + t + "&r=" + r + "&b=" + br + "&q=" + q) - return t + "," + r + "," + c - - -def random_hex(length: int) -> str: - result = hex(random.randint(0, 16**length)).replace("0x", "").upper() - if len(result) < length: - result = "0" * (length - len(result)) + result - return result - - -element_mastery = { - "anemo": "风", - "pyro": "火", - "geo": "岩", - "electro": "雷", - "cryo": "冰", - "hydro": "水", - "dendro": "草", - "none": "无", -} diff --git a/plugins/genshin/query_user/bind/__init__.py b/plugins/genshin/query_user/bind/__init__.py deleted file mode 100644 index 86da4281..00000000 --- a/plugins/genshin/query_user/bind/__init__.py +++ /dev/null @@ -1,153 +0,0 @@ -import json -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg - -from services.log import logger -from utils.depends import OneCommand -from utils.http_utils import AsyncHttpx -from utils.utils import is_number - -from .._models import Genshin - -__zx_plugin_name__ = "原神绑定" -__plugin_usage__ = """ -usage: - 绑定原神uid等数据,cookie极为重要,请谨慎绑定 - ** 如果对拥有者不熟悉,并不建议添加cookie ** - 该项目只会对cookie用于”米游社签到“,“原神玩家查询”,“原神便笺查询” - 指令: - 原神绑定uid [uid] - 原神绑定米游社id [mys_id] - 原神绑定cookie [cookie] # 该绑定请私聊 - 原神解绑 - 示例:原神绑定uid 92342233 - 如果不明白怎么获取cookie请输入“原神绑定cookie”。 -""".strip() -__plugin_des__ = "绑定自己的原神uid等" -__plugin_cmd__ = ["原神绑定uid [uid]", "原神绑定米游社id [mys_id]", "原神绑定cookie [cookie]", "原神解绑"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神绑定"], -} - -bind = on_command( - "原神绑定uid", aliases={"原神绑定米游社id", "原神绑定cookie"}, priority=5, block=True -) - -unbind = on_command("原神解绑", priority=5, block=True) - -web_Api = "https://api-takumi.mihoyo.com" -bbs_Cookie_url = "https://webapi.account.mihoyo.com/Api/cookie_accountinfo_by_loginticket?login_ticket={}" -bbs_Cookie_url2 = ( - web_Api - + "/auth/api/getMultiTokenByLoginTicket?login_ticket={}&token_types=3&uid={}" -) - - -@bind.handle() -async def _(event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if cmd in ["原神绑定uid", "原神绑定米游社id"]: - if not is_number(msg): - await bind.finish("uid/id必须为纯数字!", at_senders=True) - msg = int(msg) - if cmd == "原神绑定uid": - if user: - await bind.finish(f"您已绑定过uid:{user.uid},如果希望更换uid,请先发送原神解绑") - if await Genshin.get_or_none(user_id=str(event.user_id), uid=msg): - await bind.finish("添加失败,该uid可能已存在...") - user = await Genshin.create(user_id=str(event.user_id), uid=msg) - _x = f"已成功添加原神uid:{msg}" - elif cmd == "原神绑定米游社id": - if not user: - await bind.finish("请先绑定原神uid..") - user.mys_id = int(msg) - _x = f"已成功为uid:{user.uid} 设置米游社id:{msg}" - else: - if not msg: - await bind.finish( - """私聊发送!! - 1.以无痕模式打开浏览器(Edge请新建InPrivate窗口) - 2.打开http://bbs.mihoyo.com/ys/ 并登陆 - 3.登陆后打开http://user.mihoyo.com/进行登陆 - 4.按下F12,打开控制台,输入以下命令: - var cookie=document.cookie;var ask=confirm('Cookie:'+cookie+'\\n\\nDo you want to copy the cookie to the clipboard?');if(ask==true){copy(cookie);msg=cookie}else{msg='Cancel'} - 5.私聊发送:原神绑定cookie 刚刚复制的cookie""" - ) - if isinstance(event, GroupMessageEvent): - await bind.finish("请立即撤回你的消息并私聊发送!") - if not user: - await bind.finish("请先绑定原神uid..") - if msg.startswith('"') or msg.startswith("'"): - msg = msg[1:] - if msg.endswith('"') or msg.endswith("'"): - msg = msg[:-1] - cookie = msg - # 用: 代替=, ,代替; - cookie = '{"' + cookie.replace("=", '": "').replace("; ", '","') + '"}' - # print(cookie) - cookie_json = json.loads(cookie) - # print(cookie_json) - if "login_ticket" not in cookie_json: - await bind.finish("请发送正确完整的cookie!") - user.cookie = str(msg) - login_ticket = cookie_json["login_ticket"] - # try: - res = await AsyncHttpx.get(url=bbs_Cookie_url.format(login_ticket)) - res.encoding = "utf-8" - data = json.loads(res.text) - # print(data) - if "成功" in data["data"]["msg"]: - stuid = str(data["data"]["cookie_info"]["account_id"]) - res = await AsyncHttpx.get(url=bbs_Cookie_url2.format(login_ticket, stuid)) - res.encoding = "utf-8" - data = json.loads(res.text) - stoken = data["data"]["list"][0]["token"] - # await Genshin.set_cookie(uid, cookie) - user.stoken = stoken - user.stuid = stuid - user.login_ticket = login_ticket - # except Exception as e: - # await bind.finish("获取登陆信息失败,请检查cookie是否正确或更新cookie") - elif data["data"]["msg"] == "登录信息已失效,请重新登录": - await bind.finish("登录信息失效,请重新获取最新cookie进行绑定") - _x = f"已成功为uid:{user.uid} 设置cookie" - if isinstance(event, GroupMessageEvent): - user.bind_group = event.group_id - if user: - await user.save( - update_fields=[ - "mys_id", - "cookie", - "stoken", - "stuid", - "login_ticket", - "bind_group", - ] - ) - await bind.send(_x) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" {cmd}:{msg}" - ) - - -@unbind.handle() -async def _(event: MessageEvent): - await Genshin.filter(user_id=str(event.user_id)).delete() - await unbind.send("用户数据删除成功...") - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f"原神解绑" - ) diff --git a/plugins/genshin/query_user/genshin_sign/__init__.py b/plugins/genshin/query_user/genshin_sign/__init__.py deleted file mode 100644 index 732f0f44..00000000 --- a/plugins/genshin/query_user/genshin_sign/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import Tuple - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.params import Command - -from services.log import logger -from utils.depends import OneCommand - -from .._models import Genshin -from ..mihoyobbs_sign import mihoyobbs_sign -from .data_source import genshin_sign, get_sign_reward_list -from .init_task import _sign, add_job, scheduler - -__zx_plugin_name__ = "原神自动签到" -__plugin_usage__ = """ -usage: - 米游社原神签到,需要uid以及cookie - 且在第二天自动排序签到时间 - # 不听,就要手动签到!(使用命令 “原神我硬签 - 指令: - 开/关原神自动签到 - 原神我硬签 -""".strip() -__plugin_des__ = "原神懒人签到" -__plugin_cmd__ = ["开启/关闭原神自动签到", "原神我硬签", "查看我的cookie"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神签到"], -} - - -genshin_matcher = on_command( - "开原神自动签到", aliases={"关原神自动签到", "原神我硬签", "查看我的cookie"}, priority=5, block=True -) - - -@genshin_matcher.handle() -async def _(event: MessageEvent, cmd: str = OneCommand()): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user: - await genshin_matcher.finish("请先绑定user.uid...") - if cmd == "查看我的cookie": - if isinstance(event, GroupMessageEvent): - await genshin_matcher.finish("请私聊查看您的cookie!") - await genshin_matcher.finish("您的cookie为" + user.cookie) - if not user.uid or not user.cookie: - await genshin_matcher.finish("请先绑定user.uid和cookie!") - # if "account_id" not in await Genshin.get_user_cookie(user.uid, True): - # await genshin_matcher.finish("请更新cookie!") - if cmd == "原神我硬签": - try: - await genshin_matcher.send("正在进行签到...", at_sender=True) - msg = await genshin_sign(user.uid) - return_data = await mihoyobbs_sign(event.user_id) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) UID:{user.uid} 原神签到" - ) - logger.info(msg) - # 硬签,移除定时任务 - try: - for i in range(3): - scheduler.remove_job( - f"genshin_auto_sign_{user.uid}_{event.user_id}_{i}", - ) - except JobLookupError: - pass - if user.auto_sign: - user.auto_sign_time = None - next_date = await Genshin.random_sign_time(user.uid) - add_job(event.user_id, user.uid, next_date) - msg += f"\n{return_data}\n因开启自动签到\n下一次签到时间为:{next_date.replace(microsecond=0)}" - except Exception as e: - msg = "原神签到失败..请尝试检查cookie或报告至管理员!" - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) UID:{user.uid} 原神签到发生错误 " - f"{type(e)}:{e}" - ) - msg = msg or "请检查cookie是否更新!" - await genshin_matcher.send(msg, at_sender=True) - else: - for i in range(3): - try: - scheduler.remove_job( - f"genshin_auto_sign_{user.uid}_{event.user_id}_{i}" - ) - except JobLookupError: - pass - if cmd[0] == "开": - next_date = await Genshin.random_sign_time(user.uid) - user.auto_sign = True - user.auto_sign_time = next_date - add_job(event.user_id, user.uid, next_date) - await genshin_matcher.send( - f"已开启原神自动签到!\n下一次签到时间为:{next_date.replace(microsecond=0)}", - at_sender=True, - ) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 开启原神自动签到" - ) - else: - user.auto_sign = False - user.auto_sign_time = None - await genshin_matcher.send(f"已关闭原神自动签到!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 关闭原神自动签到" - ) - if user: - await user.save(update_fields=["auto_sign_time", "auto_sign"]) diff --git a/plugins/genshin/query_user/genshin_sign/data_source.py b/plugins/genshin/query_user/genshin_sign/data_source.py deleted file mode 100644 index 1dbaac4c..00000000 --- a/plugins/genshin/query_user/genshin_sign/data_source.py +++ /dev/null @@ -1,153 +0,0 @@ -import hashlib -import random -import string -import time -import uuid -from typing import Dict, Optional - -from configs.config import Config -from services.log import logger -from utils.http_utils import AsyncHttpx - -from .._models import Genshin -from ..mihoyobbs_sign.setting import * - - -async def genshin_sign(uid: int) -> Optional[str]: - """ - 原神签到信息 - :param uid: uid - """ - data = await _sign(uid) - if not data: - return "签到失败..." - status = data["message"] - if status == "OK": - try: - sign_info = await _get_sign_info(uid) - if sign_info: - sign_info = sign_info["data"] - sign_list = await get_sign_reward_list() - get_reward = sign_list["data"]["awards"][ - int(sign_info["total_sign_day"]) - 1 - ]["name"] - reward_num = sign_list["data"]["awards"][ - int(sign_info["total_sign_day"]) - 1 - ]["cnt"] - get_im = f"本次签到获得:{get_reward}x{reward_num}" - logger.info("get_im:" + get_im + "\nsign_info:" + str(sign_info)) - if status == "OK" and sign_info["is_sign"]: - return f"原神签到成功!\n{get_im}\n本月漏签次数:{sign_info['sign_cnt_missed']}" - except Exception as e: - logger.error(f"原神签到发生错误 UID:{str(data)}") - return f"原神签到发生错误: {str(data)}" - else: - return status - if data["data"]["risk_code"] == 375: - return "原神签到失败\n账号可能被风控,请前往米游社手动签到!" - return str(data) - - -# 获取请求Header里的DS 当web为true则生成网页端的DS -def get_ds(web: bool) -> str: - if web: - n = mihoyobbs_Salt_web - else: - n = mihoyobbs_Salt - i = str(timestamp()) - r = random_text(6) - c = md5("salt=" + n + "&t=" + i + "&r=" + r) - return f"{i},{r},{c}" - - -# 时间戳 -def timestamp() -> int: - return int(time.time()) - - -def random_text(num: int) -> str: - return "".join(random.sample(string.ascii_lowercase + string.digits, num)) - - -def md5(text: str) -> str: - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -# 生成一个device id -def get_device_id(cookie) -> str: - return str(uuid.uuid3(uuid.NAMESPACE_URL, cookie)).replace("-", "").upper() - - -async def _sign(uid: int, server_id: str = "cn_gf01") -> Optional[Dict[str, str]]: - """ - 米游社签到 - :param uid: uid - :param server_id: 服务器id - """ - if str(uid)[0] == "5": - server_id = "cn_qd01" - try: - if user := await Genshin.get_or_none(uid=uid): - headers["DS"] = get_ds(web=True) - headers["Referer"] = ( - "https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?bbs_auth_required=true" - f"&act_id={genshin_Act_id}&utm_source=bbs&utm_medium=mys&utm_campaign=icon" - ) - headers["Cookie"] = user.cookie - headers["x-rpc-device_id"] = get_device_id(user.cookie) - req = await AsyncHttpx.post( - url=genshin_Signurl, - headers=headers, - json={"act_id": genshin_Act_id, "uid": uid, "region": server_id}, - ) - return req.json() - except Exception as e: - logger.error(f"米游社签到发生错误 UID:{uid} {type(e)}:{e}") - return None - - -async def get_sign_reward_list(): - """ - 获取签到奖励列表 - """ - try: - req = await AsyncHttpx.get( - url="https://api-takumi.mihoyo.com/event/bbs_sign_reward/home?act_id=e202009291139501", - headers={ - "x-rpc-app_version": str(Config.get_config("genshin", "mhyVersion")), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": str(Config.get_config("genshin", "client_type")), - "Referer": "https://webstatic.mihoyo.com/", - }, - ) - return req.json() - except Exception as e: - logger.error(f"获取签到奖励列表发生错误 {type(e)}:{e}") - return None - - -async def _get_sign_info(uid: int, server_id: str = "cn_gf01"): - if str(uid)[0] == "5": - server_id = "cn_qd01" - try: - if user := await Genshin.get_or_none(uid=uid): - req = await AsyncHttpx.get( - url=f"https://api-takumi.mihoyo.com/event/bbs_sign_reward/info?act_id=e202009291139501®ion={server_id}&uid={uid}", - headers={ - "x-rpc-app_version": str( - Config.get_config("genshin", "mhyVersion") - ), - "Cookie": user.cookie, - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": str( - Config.get_config("genshin", "client_type") - ), - "Referer": "https://webstatic.mihoyo.com/", - }, - ) - return req.json() - except Exception as e: - logger.error(f"获取签到信息发生错误 UID:{uid} {type(e)}:{e}") - return None diff --git a/plugins/genshin/query_user/genshin_sign/init_task.py b/plugins/genshin/query_user/genshin_sign/init_task.py deleted file mode 100644 index 828727e1..00000000 --- a/plugins/genshin/query_user/genshin_sign/init_task.py +++ /dev/null @@ -1,124 +0,0 @@ -import random -from datetime import datetime, timedelta - -import nonebot -import pytz -from apscheduler.jobstores.base import ConflictingIdError -from nonebot import Driver - -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.message_builder import at -from utils.utils import get_bot, scheduler - -from .._models import Genshin -from ..mihoyobbs_sign import mihoyobbs_sign -from .data_source import genshin_sign - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 启动时分配定时任务 - """ - g_list = await Genshin.filter(auto_sign=True).all() - for u in g_list: - if u.auto_sign_time: - if date := await Genshin.random_sign_time(u.uid): - scheduler.add_job( - _sign, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_auto_sign_{u.uid}_{u.user_id}_0", - args=[u.user_id, u.uid, 0], - ) - logger.info( - f"genshin_sign add_job:USER:{u.user_id} UID:{u.uid} " - f"{date} 原神自动签到" - ) - - -def add_job(user_id: int, uid: int, date: datetime): - try: - scheduler.add_job( - _sign, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_0", - args=[user_id, uid, 0], - ) - logger.debug(f"genshin_sign add_job:{date.replace(microsecond=0)} 原神自动签到") - except ConflictingIdError: - pass - - -async def _sign(user_id: int, uid: int, count: int): - """ - 执行签到任务 - :param user_id: 用户id - :param uid: uid - :param count: 执行次数 - """ - try: - return_data = await mihoyobbs_sign(user_id) - except Exception as e: - logger.error(f"mihoyobbs_sign error:{e}") - return_data = "米游社签到失败,请尝试发送'米游社签到'进行手动签到" - if count < 3: - try: - msg = await genshin_sign(uid) - next_time = await Genshin.random_sign_time(uid) - msg += f"\n下一次签到时间为:{next_time.replace(microsecond=0)}" - logger.info(f"USER:{user_id} UID:{uid} 原神自动签到任务发生成功...") - try: - scheduler.add_job( - _sign, - "date", - run_date=next_time.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_0", - args=[user_id, uid, 0], - ) - except ConflictingIdError: - msg += "\n定时任务设定失败..." - except Exception as e: - logger.error(f"USER:{user_id} UID:{uid} 原神自动签到任务发生错误 {type(e)}:{e}") - msg = None - if not msg: - now = datetime.now(pytz.timezone("Asia/Shanghai")) - if now.hour < 23: - random_hours = random.randint(1, 23 - now.hour) - next_time = now + timedelta(hours=random_hours) - scheduler.add_job( - _sign, - "date", - run_date=next_time.replace(microsecond=0), - id=f"genshin_auto_sign_{uid}_{user_id}_{count}", - args=[user_id, uid, count + 1], - ) - msg = ( - f"{now.replace(microsecond=0)} 原神" - f"签到失败,将在 {next_time.replace(microsecond=0)} 时重试!" - ) - else: - msg = "今日原神签到失败,请手动签到..." - logger.debug(f"USER:{user_id} UID:{uid} 原神今日签到失败...") - else: - msg = "今日原神自动签到重试次数已达到3次,请手动签到。" - logger.debug(f"USER:{user_id} UID:{uid} 原神今日签到失败次数打到 3 次...") - bot = get_bot() - if bot: - if user_id in [x["user_id"] for x in await bot.get_friend_list()]: - await bot.send_private_msg(user_id=user_id, message=return_data) - await bot.send_private_msg(user_id=user_id, message=msg) - else: - if user := await Genshin.get_or_none(uid=uid): - group_id = user.bind_group - if not group_id: - if group_list := await GroupInfoUser.get_user_all_group(user_id): - group_id = group_list[0] - if group_id: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + msg - ) diff --git a/plugins/genshin/query_user/mihoyobbs_sign/__init__.py b/plugins/genshin/query_user/mihoyobbs_sign/__init__.py deleted file mode 100644 index 5bd3267c..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import Command - -from services.log import logger - -# from .init_task import add_job, scheduler, _sign -# from apscheduler.jobstores.base import JobLookupError -from .._models import Genshin -from .mihoyobbs import * - -__zx_plugin_name__ = "米游社自动签到" -__plugin_usage__ = """ -usage: - 发送'米游社签到'或绑定原神自动签到 - 即可手动/自动进行米游社签到 - (若启用了原神自动签到会在签到原神同时完成米游币领取) - --> 每天白嫖90-110米游币不香吗 - 注:需要重新绑定原神cookie!!! - 遇到问题请提issue或@作者 -""".strip() -__plugin_des__ = "米游社自动签到任务" -__plugin_cmd__ = ["米游社签到", "米游社我硬签"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HDU_Nbsp" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["米游社签到"], -} - -mihoyobbs_matcher = on_command("米游社签到", aliases={"米游社我硬签"}, priority=5, block=True) - - -@mihoyobbs_matcher.handle() -async def _(event: MessageEvent, cmd: Tuple[str, ...] = Command()): - await mihoyobbs_matcher.send("提交米游社签到申请", at_sender=True) - return_data = await mihoyobbs_sign(event.user_id) - if return_data: - await mihoyobbs_matcher.finish(return_data, at_sender=True) - else: - await mihoyobbs_matcher.finish("米游社签到失败,请查看控制台输出", at_sender=True) - - -async def mihoyobbs_sign(user_id): - user = await Genshin.get_or_none(user_id=str(user_id)) - if not user or not user.uid or not user.cookie: - await mihoyobbs_matcher.finish("请先绑定uid和cookie!", at_sender=True) - bbs = mihoyobbs.Mihoyobbs(stuid=user.stuid, stoken=user.stoken, cookie=user.cookie) - await bbs.init() - return_data = "" - if ( - bbs.Task_do["bbs_Sign"] - and bbs.Task_do["bbs_Read_posts"] - and bbs.Task_do["bbs_Like_posts"] - and bbs.Task_do["bbs_Share"] - ): - return_data += ( - f"今天的米游社签到任务已经全部完成了!\n" - f"一共获得{mihoyobbs.today_have_get_coins}个米游币\n目前有{mihoyobbs.Have_coins}个米游币" - ) - logger.info( - f"今天已经全部完成了!一共获得{mihoyobbs.today_have_get_coins}个米游币,目前有{mihoyobbs.Have_coins}个米游币" - ) - else: - i = 0 - # print("开始签到") - # print(mihoyobbs.today_have_get_coins) - while mihoyobbs.today_get_coins != 0 and i < 3: - # if i > 0: - await bbs.refresh_list() - await bbs.signing() - await bbs.read_posts() - await bbs.like_posts() - await bbs.share_post() - await bbs.get_tasks_list() - i += 1 - return_data += ( - "\n" + f"今天已经获得{mihoyobbs.today_have_get_coins}个米游币\n" - f"还能获得{mihoyobbs.today_get_coins}个米游币\n目前有{mihoyobbs.Have_coins}个米游币" - ) - logger.info( - f"今天已经获得{mihoyobbs.today_have_get_coins}个米游币," - f"还能获得{mihoyobbs.today_get_coins}个米游币,目前有{mihoyobbs.Have_coins}个米游币" - ) - return return_data diff --git a/plugins/genshin/query_user/mihoyobbs_sign/error.py b/plugins/genshin/query_user/mihoyobbs_sign/error.py deleted file mode 100644 index b5e6ff00..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/error.py +++ /dev/null @@ -1,6 +0,0 @@ -class CookieError(Exception): - def __init__(self, info): - self.info = info - - def __str__(self): - return repr(self.info) diff --git a/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py b/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py deleted file mode 100644 index e8d67a8f..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/mihoyobbs.py +++ /dev/null @@ -1,193 +0,0 @@ -from services.log import logger -from .error import CookieError -from utils.http_utils import AsyncHttpx -from .setting import * -from .tools import * -import json - -today_get_coins = 0 -today_have_get_coins = 0 # 这个变量以后可能会用上,先留着了 -Have_coins = 0 - - -class Mihoyobbs: - def __init__(self, stuid: str, stoken: str, cookie: str) -> None: - self.postsList = None - self.headers = { - "DS": get_ds(web=False), - "cookie": f'stuid={stuid};stoken={stoken}', - "x-rpc-client_type": mihoyobbs_Client_type, - "x-rpc-app_version": mihoyobbs_Version, - "x-rpc-sys_version": "6.0.1", - "x-rpc-channel": "miyousheluodi", - "x-rpc-device_id": get_device_id(cookie=cookie), - "x-rpc-device_name": random_text(random.randint(1, 10)), - "x-rpc-device_model": "Mi 10", - "Referer": "https://app.mihoyo.com", - "Host": "bbs-api.mihoyo.com", - "User-Agent": "okhttp/4.8.0" - } - self.Task_do = { - "bbs_Sign": False, - "bbs_Read_posts": False, - "bbs_Read_posts_num": 3, - "bbs_Like_posts": False, - "bbs_Like_posts_num": 5, - "bbs_Share": False - } - - async def init(self): - await self.get_tasks_list() - # 如果这三个任务都做了就没必要获取帖子了 - if self.Task_do["bbs_Read_posts"] and self.Task_do["bbs_Like_posts"] and self.Task_do["bbs_Share"]: - pass - else: - self.postsList = await self.get_list() - - async def refresh_list(self) -> None: - self.postsList = await self.get_list() - - # 获取任务列表,用来判断做了哪些任务 - async def get_tasks_list(self): - global today_get_coins - global today_have_get_coins - global Have_coins - logger.info("正在获取任务列表") - req = await AsyncHttpx.get(url=bbs_Tasks_list, headers=self.headers) - data = req.json() - if "err" in data["message"] or data["retcode"] == -100: - logger.error("获取任务列表失败,你的cookie可能已过期,请重新设置cookie。") - raise CookieError('Cookie expires') - else: - today_get_coins = data["data"]["can_get_points"] - today_have_get_coins = data["data"]["already_received_points"] - Have_coins = data["data"]["total_points"] - # 如果当日可获取米游币数量为0直接判断全部任务都完成了 - if today_get_coins == 0: - self.Task_do["bbs_Sign"] = True - self.Task_do["bbs_Read_posts"] = True - self.Task_do["bbs_Like_posts"] = True - self.Task_do["bbs_Share"] = True - else: - # 如果第0个大于或等于62则直接判定任务没做 - if data["data"]["states"][0]["mission_id"] >= 62: - logger.info(f"今天可以获得{today_get_coins}个米游币") - pass - else: - logger.info(f"还有任务未完成,今天还能获得{today_get_coins}米游币") - for i in data["data"]["states"]: - # 58是讨论区签到 - if i["mission_id"] == 58: - if i["is_get_award"]: - self.Task_do["bbs_Sign"] = True - # 59是看帖子 - elif i["mission_id"] == 59: - if i["is_get_award"]: - self.Task_do["bbs_Read_posts"] = True - else: - self.Task_do["bbs_Read_posts_num"] -= i["happened_times"] - # 60是给帖子点赞 - elif i["mission_id"] == 60: - if i["is_get_award"]: - self.Task_do["bbs_Like_posts"] = True - else: - self.Task_do["bbs_Like_posts_num"] -= i["happened_times"] - # 61是分享帖子 - elif i["mission_id"] == 61: - if i["is_get_award"]: - self.Task_do["bbs_Share"] = True - # 分享帖子,是最后一个任务,到这里了下面都是一次性任务,直接跳出循环 - break - - # 获取要帖子列表 - async def get_list(self) -> list: - temp_list = [] - logger.info("正在获取帖子列表......") - req = await AsyncHttpx.get(url=bbs_List_url.format(mihoyobbs_List_Use[0]["forumId"]), - headers=self.headers) - data = req.json()["data"]["list"] - for n in range(5): - r_l = random.choice(data) - while r_l["post"]["subject"] in str(temp_list): - r_l = random.choice(data) - temp_list.append([r_l["post"]["post_id"], r_l["post"]["subject"]]) - # temp_list.append([data["data"]["list"][n]["post"]["post_id"], data["data"]["list"][n]["post"]["subject"]]) - - logger.info("已获取{}个帖子".format(len(temp_list))) - return temp_list - - # 进行签到操作 - async def signing(self): - if self.Task_do["bbs_Sign"]: - logger.info("讨论区任务已经完成过了~") - else: - logger.info("正在签到......") - header = {} - header.update(self.headers) - for i in mihoyobbs_List_Use: - header["DS"] = get_ds2("", json.dumps({"gids": i["id"]})) - req = await AsyncHttpx.post(url=bbs_Sign_url, json={"gids": i["id"]}, headers=header) - data = req.json() - if "err" not in data["message"]: - logger.info(str(i["name"] + data["message"])) - time.sleep(random.randint(2, 8)) - else: - logger.error("签到失败,你的cookie可能已过期,请重新设置cookie。") - raise CookieError('Cookie expires') - - # 看帖子 - async def read_posts(self): - if self.Task_do["bbs_Read_posts"]: - logger.info("看帖任务已经完成过了~") - else: - logger.info("正在看帖......") - for i in range(self.Task_do["bbs_Read_posts_num"]): - req = await AsyncHttpx.get(url=bbs_Detail_url.format(self.postsList[i][0]), headers=self.headers) - data = req.json() - if data["message"] == "OK": - logger.debug("看帖:{} 成功".format(self.postsList[i][1])) - time.sleep(random.randint(2, 8)) - - # 点赞 - async def like_posts(self): - if self.Task_do["bbs_Like_posts"]: - logger.info("点赞任务已经完成过了~") - else: - logger.info("正在点赞......") - for i in range(self.Task_do["bbs_Like_posts_num"]): - req = await AsyncHttpx.post(url=bbs_Like_url, headers=self.headers, - json={"post_id": self.postsList[i][0], "is_cancel": False}) - data = req.json() - if data["message"] == "OK": - logger.debug("点赞:{} 成功".format(self.postsList[i][1])) - # 判断取消点赞是否打开 - # if config.config["mihoyobbs"]["un_like"] : - # time.sleep(random.randint(2, 8)) - # req = httpx.post(url=bbs_Like_url, headers=self.headers, - # json={"post_id": self.postsList[i][0], "is_cancel": True}) - # data = req.json() - # if data["message"] == "OK": - # logger.debug("取消点赞:{} 成功".format(self.postsList[i][1])) - time.sleep(random.randint(2, 8)) - - # 分享操作 - - async def share_post(self): - if self.Task_do["bbs_Share"]: - logger.info("分享任务已经完成过了~") - else: - logger.info("正在执行分享任务......") - for i in range(3): - req = await AsyncHttpx.get(url=bbs_Share_url.format(self.postsList[0][0]), headers=self.headers) - data = req.json() - if data["message"] == "OK": - logger.debug("分享:{} 成功".format(self.postsList[0][1])) - logger.info("分享任务执行成功......") - break - else: - logger.debug(f"分享任务执行失败,正在执行第{i + 2}次,共3次") - time.sleep(random.randint(2, 8)) - time.sleep(random.randint(2, 8)) - - - diff --git a/plugins/genshin/query_user/mihoyobbs_sign/setting.py b/plugins/genshin/query_user/mihoyobbs_sign/setting.py deleted file mode 100644 index e6b7e538..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/setting.py +++ /dev/null @@ -1,124 +0,0 @@ -# 米游社的Salt -mihoyobbs_Salt = "z8DRIUjNDT7IT5IZXvrUAxyupA1peND9" -mihoyobbs_Salt2 = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v" -mihoyobbs_Salt_web = "9nQiU3AV0rJSIBWgdynfoGMGKaklfbM7" -# 米游社的版本 -mihoyobbs_Version = "2.34.1" # Slat和Version相互对应 -# 米游社的客户端类型 -mihoyobbs_Client_type = "2" # 1为ios 2为安卓 -mihoyobbs_Client_type_web = "5" # 4为pc web 5为mobile web -# 米游社的分区列表 -mihoyobbs_List = [{ - "id": "1", - "forumId": "1", - "name": "崩坏3", - "url": "https://bbs.mihoyo.com/bh3/" -}, { - "id": "2", - "forumId": "26", - "name": "原神", - "url": "https://bbs.mihoyo.com/ys/" -}, { - "id": "3", - "forumId": "30", - "name": "崩坏2", - "url": "https://bbs.mihoyo.com/bh2/" -}, { - "id": "4", - "forumId": "37", - "name": "未定事件簿", - "url": "https://bbs.mihoyo.com/wd/" -}, { - "id": "5", - "forumId": "34", - "name": "大别野", - "url": "https://bbs.mihoyo.com/dby/" -}, { - "id": "6", - "forumId": "52", - "name": "崩坏:星穹铁道", - "url": "https://bbs.mihoyo.com/sr/" -}, { - "id": "8", - "forumId": "57", - "name": "绝区零", - "url": "https://bbs.mihoyo.com/zzz/" -}] - -game_id2name = { - "bh2_cn": "崩坏2", - "bh3_cn": "崩坏3", - "nxx_cn": "未定事件簿", - "hk4e_cn": "原神", -} -# Config Load之后run里面进行列表的选择 -mihoyobbs_List_Use = [{ - "id": "2", - "forumId": "26", - "name": "原神", - "url": "https://bbs.mihoyo.com/ys/" -}, - # 不玩原神可以把签到讨论区换为大别墅 - # { - # "id": "5", - # "forumId": "34", - # "name": "大别野", - # "url": "https://bbs.mihoyo.com/dby/" - # } -] - -# 游戏签到的请求头 -headers = { - 'Accept': 'application/json, text/plain, */*', - 'DS': "", - 'Origin': 'https://webstatic.mihoyo.com', - 'x-rpc-app_version': mihoyobbs_Version, - 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) ' - f'Version/4.0 Chrome/103.0.5060.129 Mobile Safari/537.36 miHoYoBBS/{mihoyobbs_Version}', - 'x-rpc-client_type': mihoyobbs_Client_type_web, - 'Referer': '', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'zh-CN,en-US;q=0.8', - 'X-Requested-With': 'com.mihoyo.hyperion', - "Cookie": "", - 'x-rpc-device_id': "" -} - -# 通用设置 -bbs_Api = "https://bbs-api.mihoyo.com" -web_Api = "https://api-takumi.mihoyo.com" -account_Info_url = web_Api + "/binding/api/getUserGameRolesByCookie?game_biz=" - -# 米游社的API列表 -bbs_Cookie_url = "https://webapi.account.mihoyo.com/Api/cookie_accountinfo_by_loginticket?login_ticket={}" -bbs_Cookie_url2 = web_Api + "/auth/api/getMultiTokenByLoginTicket?login_ticket={}&token_types=3&uid={}" -bbs_Tasks_list = bbs_Api + "/apihub/sapi/getUserMissionsState" # 获取任务列表 -bbs_Sign_url = bbs_Api + "/apihub/app/api/signIn" # post -bbs_List_url = bbs_Api + "/post/api/getForumPostList?forum_id={}&is_good=false&is_hot=false&page_size=20&sort_type=1" -bbs_Detail_url = bbs_Api + "/post/api/getPostFull?post_id={}" -bbs_Share_url = bbs_Api + "/apihub/api/getShareConf?entity_id={}&entity_type=1" -bbs_Like_url = bbs_Api + "/apihub/sapi/upvotePost" # post json - -# 崩坏2自动签到相关的相关设置 -honkai2_Act_id = "e202203291431091" -honkai2_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={honkai2_Act_id}' -honkai2_Is_signurl = web_Api + "/event/luna/info?lang=zh-cn&act_id={}®ion={}&uid={}" -honkai2_Sign_url = web_Api + "/event/luna/sign" - -# 崩坏3自动签到相关的设置 -honkai3rd_Act_id = "e202207181446311" -honkai3rd_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={honkai3rd_Act_id}' -honkai3rd_Is_signurl = web_Api + "/event/luna/info?lang=zh-cn&act_id={}®ion={}&uid={}" -honkai3rd_Sign_url = web_Api + "/event/luna/sign" - -# 未定事件簿自动签到相关设置 -tearsofthemis_Act_id = "e202202251749321" -tearsofthemis_checkin_rewards = f'{web_Api}/event/luna/home?lang=zh-cn&act_id={tearsofthemis_Act_id}' -tearsofthemis_Is_signurl = honkai2_Is_signurl -tearsofthemis_Sign_url = honkai2_Sign_url # 和二崩完全一致 - -# 原神自动签到相关的设置 -genshin_Act_id = "e202009291139501" -genshin_checkin_rewards = f'{web_Api}/event/bbs_sign_reward/home?act_id={genshin_Act_id}' -genshin_Is_signurl = web_Api + "/event/bbs_sign_reward/info?act_id={}®ion={}&uid={}" -genshin_Signurl = web_Api + "/event/bbs_sign_reward/sign" diff --git a/plugins/genshin/query_user/mihoyobbs_sign/tools.py b/plugins/genshin/query_user/mihoyobbs_sign/tools.py deleted file mode 100644 index 2552330e..00000000 --- a/plugins/genshin/query_user/mihoyobbs_sign/tools.py +++ /dev/null @@ -1,65 +0,0 @@ -import uuid -import time -import random -import string -import hashlib -from .setting import * - - -# md5计算 -def md5(text: str) -> str: - md5 = hashlib.md5() - md5.update(text.encode()) - return md5.hexdigest() - - -# 随机文本 -def random_text(num: int) -> str: - return ''.join(random.sample(string.ascii_lowercase + string.digits, num)) - - -# 时间戳 -def timestamp() -> int: - return int(time.time()) - - -# 获取请求Header里的DS 当web为true则生成网页端的DS -def get_ds(web: bool) -> str: - if web: - n = mihoyobbs_Salt_web - else: - n = mihoyobbs_Salt - i = str(timestamp()) - r = random_text(6) - c = md5("salt=" + n + "&t=" + i + "&r=" + r) - return f"{i},{r},{c}" - - -# 获取请求Header里的DS(版本2) 这个版本ds之前见到都是查询接口里的 -def get_ds2(q: str, b: str) -> str: - n = mihoyobbs_Salt2 - i = str(timestamp()) - r = str(random.randint(100001, 200000)) - add = f'&b={b}&q={q}' - c = md5("salt=" + n + "&t=" + i + "&r=" + r + add) - return f"{i},{r},{c}" - - -# 生成一个device id -def get_device_id(cookie) -> str: - return str(uuid.uuid3(uuid.NAMESPACE_URL, cookie)) - - -# 获取签到的奖励名称 -def get_item(raw_data: dict) -> str: - temp_name = raw_data["name"] - temp_cnt = raw_data["cnt"] - return f"{temp_name}x{temp_cnt}" - - -# 获取明天早晨0点的时间戳 -def next_day() -> int: - now_time = int(time.time()) - next_day_time = now_time - now_time % 86400 + time.timezone + 86400 - return next_day_time - diff --git a/plugins/genshin/query_user/query_memo/__init__.py b/plugins/genshin/query_user/query_memo/__init__.py deleted file mode 100644 index 090c2a7f..00000000 --- a/plugins/genshin/query_user/query_memo/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent - -from services.log import logger - -from .._models import Genshin -from .data_source import get_memo, get_user_memo - -__zx_plugin_name__ = "原神便笺查询" -__plugin_usage__ = """ -usage: - 通过指定cookie和uid查询事实数据 - 指令: - 原神便笺查询/yss - 示例:原神便笺查询 92342233 -""".strip() -__plugin_des__ = "不能浪费丝毫体力" -__plugin_cmd__ = ["原神便笺查询/yss"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神便笺查询"], -} -__plugin_block_limit__ = {} - - -query_memo_matcher = on_command( - "原神便签查询", aliases={"原神便笺查询", "yss"}, priority=5, block=True -) - - -@query_memo_matcher.handle() -async def _(event: MessageEvent): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user or not user.uid or not user.cookie: - await query_memo_matcher.finish("请先绑定uid和cookie!") - if isinstance(event, GroupMessageEvent): - uname = event.sender.card or event.sender.nickname - else: - uname = event.sender.nickname - data = await get_user_memo(event.user_id, user.uid, uname) - if data: - await query_memo_matcher.send(data) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) " - f"使用原神便笺查询 uid:{user.uid}" - ) - else: - await query_memo_matcher.send("未查询到数据...") diff --git a/plugins/genshin/query_user/query_memo/data_source.py b/plugins/genshin/query_user/query_memo/data_source.py deleted file mode 100644 index f7fe592f..00000000 --- a/plugins/genshin/query_user/query_memo/data_source.py +++ /dev/null @@ -1,280 +0,0 @@ -import asyncio -from asyncio.exceptions import TimeoutError -from io import BytesIO -from typing import Optional, Tuple, Union - -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar - -from .._models import Genshin -from .._utils import get_ds - -driver: Driver = nonebot.get_driver() - - -memo_path = IMAGE_PATH / "genshin" / "genshin_memo" -memo_path.mkdir(exist_ok=True, parents=True) - - -@driver.on_startup -async def _(): - for name, url in zip( - ["resin.png", "task.png", "resin_discount.png", "chengehu.png", "zhibian.png"], - [ - "https://upload-bbs.mihoyo.com/upload/2021/09/29/8819732/54266243c7d15ba31690c8f5d63cc3c6_71491376413333325" - "20.png?x-oss-process=image//resize,s_600/quality,q_80/auto-orient,0/interlace,1/format,png", - "https://patchwiki.biligame.com/images/ys/thumb/c/cc/6k6kuj1kte6m1n7hexqfrn92z6h4yhh.png/60px-委托任务logo.png", - "https://patchwiki.biligame.com/images/ys/d/d9/t1hv6wpucbwucgkhjntmzroh90nmcdv.png", - "https://s3.bmp.ovh/imgs/2022/08/21/3a3b2e6c22e305ff.png", - "https://s3.bmp.ovh/imgs/2022/08/21/c2d7ace21e1d46cf.png", - ], - ): - file = memo_path / name - if not file.exists(): - await AsyncHttpx.download_file(url, file) - logger.info(f"已下载原神便签资源 -> {file}...") - - -async def get_user_memo( - user_id: int, uid: int, uname: str -) -> Optional[Union[str, MessageSegment]]: - uid = str(uid) - if uid[0] in ["1", "2"]: - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return None - return await parse_data_and_draw(user_id, uid, server_id, uname) - - -async def get_memo(uid: str, server_id: str) -> Tuple[Union[str, dict], int]: - try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/dailyNote?server={server_id}&role_id={uid}", - headers={ - "DS": get_ds(f"role_id={uid}&server={server_id}"), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": Config.get_config("genshin", "client_type"), - "Referer": "https://webstatic.mihoyo.com/", - "Cookie": await Genshin.random_cookie(uid), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"], 200 - return data["message"], 999 - except TimeoutError: - return "访问超时,请稍后再试", 997 - except Exception as e: - logger.error(f"便签查询获取失败未知错误 {e}:{e}") - return "发生了一些错误,请稍后再试", 998 - - -def create_border( - image_name: str, content: str, notice_text: str, value: str -) -> BuildImage: - border = BuildImage(500, 75, color="#E0D9D1", font="HYWenHei-85W.ttf", font_size=20) - text_bk = BuildImage( - 350, 75, color="#F5F1EB", font_size=23, font="HYWenHei-85W.ttf" - ) - _x = 70 if image_name == "resin.png" else 50 - _px = 10 if image_name == "resin.png" else 20 - text_bk.paste( - BuildImage(_x, _x, background=memo_path / image_name), - (_px, 0), - True, - center_type="by_height", - ) - text_bk.text((87, 15), content) - text_bk.paste( - BuildImage( - 0, - 0, - plain_text=notice_text, - font_color=(203, 189, 175), - font="HYWenHei-85W.ttf", - font_size=17, - ), - (87, 45), - True, - ) - font_width, _ = border.getsize(value) - border.text((350 + 76 - int(font_width / 2), 0), value, center_type="by_height") - border.paste(text_bk, (2, 0), center_type="by_height") - return border - - -async def parse_data_and_draw( - user_id: int, uid: str, server_id: str, uname: str -) -> Union[str, MessageSegment]: - data, code = await get_memo(uid, server_id) - if code != 200: - return data - user_avatar = BytesIO(await get_user_avatar(user_id)) - for x in data["expeditions"]: - file_name = x["avatar_side_icon"].split("_")[-1] - role_avatar = memo_path / "role_avatar" / file_name - if not role_avatar.exists(): - await AsyncHttpx.download_file(x["avatar_side_icon"], role_avatar) - return await asyncio.get_event_loop().run_in_executor( - None, _parse_data_and_draw, data, user_avatar, uid, uname - ) - - -def _parse_data_and_draw( - data: dict, user_avatar: BytesIO, uid: int, uname: str -) -> Union[str, MessageSegment]: - current_resin = data["current_resin"] # 当前树脂 - max_resin = data["max_resin"] # 最大树脂 - resin_recovery_time = data["resin_recovery_time"] # 树脂全部回复时间 - finished_task_num = data["finished_task_num"] # 完成的每日任务 - total_task_num = data["total_task_num"] # 每日任务总数 - remain_resin_discount_num = data["remain_resin_discount_num"] # 值得铭记的强敌总数 - resin_discount_num_limit = data["resin_discount_num_limit"] # 剩余值得铭记的强敌 - current_expedition_num = data["current_expedition_num"] # 当前挖矿人数 - max_expedition_num = data["max_expedition_num"] # 每日挖矿最大人数 - expeditions = data["expeditions"] # 挖矿详情 - current_coin = data["current_home_coin"] # 当前宝钱 - max_coin = data["max_home_coin"] # 最大宝钱 - coin_recovery_time = data["home_coin_recovery_time"] # 宝钱全部回复时间 - transformer_available = data["transformer"]["obtained"] # 参量质变仪可获取 - transformer_state = data["transformer"]["recovery_time"]["reached"] # 参量质变仪状态 - transformer_recovery_time = data["transformer"]["recovery_time"]["Day"] # 参量质变仪回复时间 - transformer_recovery_hour = data["transformer"]["recovery_time"][ - "Hour" - ] # 参量质变仪回复时间 - coin_minute, coin_second = divmod(int(coin_recovery_time), 60) - coin_hour, coin_minute = divmod(coin_minute, 60) - # print(data) - minute, second = divmod(int(resin_recovery_time), 60) - hour, minute = divmod(minute, 60) - - A = BuildImage(1030, 570, color="#f1e9e1", font_size=15, font="HYWenHei-85W.ttf") - A.text((10, 15), "原神便笺 | Create By ZhenXun", (198, 186, 177)) - ava = BuildImage(100, 100, background=user_avatar) - ava.circle() - A.paste(ava, (40, 40), True) - A.paste( - BuildImage(0, 0, plain_text=uname, font_size=20, font="HYWenHei-85W.ttf"), - (160, 62), - True, - ) - A.paste( - BuildImage( - 0, - 0, - plain_text=f"UID:{uid}", - font_size=15, - font="HYWenHei-85W.ttf", - font_color=(21, 167, 89), - ), - (160, 92), - True, - ) - border = create_border( - "resin.png", - "原粹树脂", - "将在{:0>2d}:{:0>2d}:{:0>2d}秒后全部恢复".format(hour, minute, second), - f"{current_resin}/{max_resin}", - ) - - A.paste(border, (10, 155)) - border = create_border( - "task.png", - "每日委托", - "今日委托已全部完成" if finished_task_num == total_task_num else "今日委托完成数量不足", - f"{finished_task_num}/{total_task_num}", - ) - A.paste(border, (10, 235)) - border = create_border( - "resin_discount.png", - "值得铭记的强敌", - "本周剩余消耗减半次数", - f"{remain_resin_discount_num}/{resin_discount_num_limit}", - ) - A.paste(border, (10, 315)) - border = create_border( - "chengehu.png", - "洞天财翁-洞天宝钱", - "洞天财翁已达到存储上限" - if current_coin == max_coin - else f"{coin_hour}小时{coin_minute}分钟后存满", - f"{current_coin}/{max_coin}", - ) - A.paste(border, (10, 395)) - border = create_border( - "zhibian.png", - "参量质变仪", - "不存在" - if not transformer_available - else "已准备完成 " - if transformer_state - else f"{transformer_recovery_hour}小时后可使用" - if not transformer_recovery_time - else f"{transformer_recovery_time}天后可使用", - "不存在" if not transformer_available else "可使用" if transformer_state else "冷却中", - ) - A.paste(border, (10, 475)) - - expeditions_border = BuildImage( - 470, 510, color="#E0D9D1", font="HYWenHei-85W.ttf", font_size=20 - ) - expeditions_text = BuildImage( - 466, 506, color="#F5F1EB", font_size=23, font="HYWenHei-85W.ttf" - ) - expeditions_text.text( - (5, 5), f"探索派遣限制{current_expedition_num}/{max_expedition_num}", (100, 100, 98) - ) - h = 45 - for x in expeditions: - _bk = BuildImage( - 400, 82, color="#ECE3D8", font="HYWenHei-85W.ttf", font_size=21 - ) - file_name = x["avatar_side_icon"].split("_")[-1] - role_avatar = memo_path / "role_avatar" / file_name - _ava_img = BuildImage(75, 75, background=role_avatar) - # _ava_img.circle() - if x["status"] == "Finished": - msg = "探索完成" - font_color = (146, 188, 63) - _circle_color = (146, 188, 63) - else: - minute, second = divmod(int(x["remained_time"]), 60) - hour, minute = divmod(minute, 60) - font_color = (193, 180, 167) - msg = "还剩{:0>2d}小时{:0>2d}分钟{:0>2d}秒".format(hour, minute, second) - _circle_color = "#DE9C58" - - _circle_bk = BuildImage(60, 60) - _circle_bk.circle() - a_circle = BuildImage(55, 55, color=_circle_color) - a_circle.circle() - b_circle = BuildImage(47, 47) - b_circle.circle() - a_circle.paste(b_circle, (4, 4), True) - _circle_bk.paste(a_circle, (4, 4), True) - - _bk.paste(_circle_bk, (25, 0), True, "by_height") - _bk.paste(_ava_img, (19, -13), True) - _bk.text((100, 0), msg, font_color, "by_height") - _bk.circle_corner(20) - - expeditions_text.paste(_bk, (25, h), True) - h += 75 + 16 - - expeditions_border.paste(expeditions_text, center_type="center") - - A.paste(expeditions_border, (550, 45)) - - return image(b64=A.pic2bs4()) diff --git a/plugins/genshin/query_user/query_role/__init__.py b/plugins/genshin/query_user/query_role/__init__.py deleted file mode 100644 index c203439f..00000000 --- a/plugins/genshin/query_user/query_role/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -from httpx import ConnectError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from services.log import logger -from utils.utils import is_number - -from .._models import Genshin -from .data_source import query_role_data - -__zx_plugin_name__ = "原神玩家查询" -__plugin_usage__ = """ -usage: - 通过uid查询原神玩家信息 - 指令: - 原神玩家查询/ys ?[uid] - 示例:原神玩家查询 92342233 -""".strip() -__plugin_des__ = "请问你们有几个肝?" -__plugin_cmd__ = ["原神玩家查询/ys"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神玩家查询"], -} -__plugin_block_limit__ = {} - - -query_role_info_matcher = on_command( - "原神玩家查询", aliases={"原神玩家查找", "ys"}, priority=5, block=True -) - - -@query_role_info_matcher.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - if not is_number(msg): - await query_role_info_matcher.finish("查询uid必须为数字!") - msg = int(msg) - uid = None - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not msg and user: - uid = user.uid - else: - uid = msg - if not uid: # or not await Genshin.get_user_cookie(uid): - await query_role_info_matcher.finish("请先绑定uid和cookie!") - nickname = event.sender.card or event.sender.nickname - mys_id = user.mys_id if user else None - try: - data = await query_role_data(event.user_id, uid, mys_id, nickname) - if data: - await query_role_info_matcher.send(data) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 使用原神玩家查询 uid:{uid}" - ) - else: - await query_role_info_matcher.send("查询失败..") - except ConnectError: - await query_role_info_matcher.send("网络出小差啦~") diff --git a/plugins/genshin/query_user/query_role/data_source.py b/plugins/genshin/query_user/query_role/data_source.py deleted file mode 100644 index 58f0ffe9..00000000 --- a/plugins/genshin/query_user/query_role/data_source.py +++ /dev/null @@ -1,254 +0,0 @@ -from typing import Dict, List, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import Config -from services.log import logger -from utils.http_utils import AsyncHttpx - -from .._models import Genshin -from .._utils import element_mastery, get_ds -from .draw_image import get_genshin_image, init_image - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -async def query_role_data( - user_id: int, uid: int, mys_id: Optional[str] = None, nickname: Optional[str] = None -) -> Optional[Union[MessageSegment, str]]: - uid = str(uid) - if uid[0] == "1" or uid[0] == "2": - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return None - return await get_image(user_id, uid, server_id, mys_id, nickname) - - -async def get_image( - user_id: int, - uid: str, - server_id: str, - mys_id: Optional[str] = None, - nickname: Optional[str] = None, -) -> Optional[Union[MessageSegment, str]]: - """ - 生成图片 - :param user_id:用户qq - :param uid: 用户uid - :param server_id: 服务器 - :param mys_id: 米游社id - :param nickname: QQ昵称 - :return: - """ - data, code = await get_info(uid, server_id) - if code != 200: - return data - if data: - char_data_list, role_data, world_data_dict, home_data_list = parsed_data(data) - mys_data = await get_mys_data(uid, mys_id) - if mys_data: - nickname = None - if char_data_list: - char_detailed_data = await get_character( - uid, [x["id"] for x in char_data_list], server_id - ) - _x = {} - if char_detailed_data: - for char in char_detailed_data["avatars"]: - _x[char["name"]] = { - "weapon": char["weapon"]["name"], - "weapon_image": char["weapon"]["icon"], - "level": char["weapon"]["level"], - "affix_level": char["weapon"]["affix_level"], - } - - await init_image(world_data_dict, char_data_list, _x, home_data_list) - return await get_genshin_image( - user_id, - uid, - char_data_list, - role_data, - world_data_dict, - home_data_list, - _x, - mys_data, - nickname, - ) - return "未找到用户数据..." - - -# Github-@lulu666lulu https://github.com/Azure99/GenshinPlayerQuery/issues/20 -""" -{body:"",query:{"action_ticket": undefined, "game_biz": "hk4e_cn”}} -对应 https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz=hk4e_cn //查询米哈游账号下绑定的游戏(game_biz可留空) -{body:"",query:{"uid": 12345(被查询账号米哈游uid)}} -对应 https://api-takumi.mihoyo.com/game_record/app/card/wapi/getGameRecordCard?uid= -{body:"",query:{'role_id': '查询账号的uid(游戏里的)' ,'server': '游戏服务器'}} -对应 https://api-takumi.mihoyo.com/game_record/app/genshin/api/index?server= server信息 &role_id= 游戏uid -{body:"",query:{'role_id': '查询账号的uid(游戏里的)' , 'schedule_type': 1(我这边只看到出现过1和2), 'server': 'cn_gf01'}} -对应 https://api-takumi.mihoyo.com/game_record/app/genshin/api/spiralAbyss?schedule_type=1&server= server信息 &role_id= 游戏uid -{body:"",query:{game_id: 2(目前我知道有崩坏3是1原神是2)}} -对应 https://api-takumi.mihoyo.com/game_record/app/card/wapi/getAnnouncement?game_id= 这个是公告api -b=body q=query -其中b只在post的时候有内容,q只在get的时候有内容 -""" - - -async def get_info(uid_: str, server_id: str) -> Tuple[Optional[Union[dict, str]], int]: - # try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/index?server={server_id}&role_id={uid_}", - headers={ - "Accept": "application/json, text/plain, */*", - "DS": get_ds(f"role_id={uid_}&server={server_id}"), - "Origin": "https://webstatic.mihoyo.com", - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (Linux; Android 9; Unspecified Device) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/39.0.0.0 Mobile Safari/537.36 miHoYoBBS/2.2.0", - "x-rpc-client_type": Config.get_config("genshin", "client_type"), - "Referer": "https://webstatic.mihoyo.com/app/community-game-records/index.html?v=6", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN,en-US;q=0.8", - "X-Requested-With": "com.mihoyo.hyperion", - "Cookie": await Genshin.random_cookie(uid_), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"], 200 - return data["message"], 999 - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None, -1 - - -async def get_character( - uid: str, character_ids: List[str], server_id="cn_gf01" -) -> Optional[dict]: - # try: - req = await AsyncHttpx.post( - url="https://api-takumi-record.mihoyo.com/game_record/app/genshin/api/character", - headers={ - "Accept": "application/json, text/plain, */*", - "DS": get_ds( - "", - { - "character_ids": character_ids, - "role_id": uid, - "server": server_id, - }, - ), - "Origin": "https://webstatic.mihoyo.com", - "Cookie": await Genshin.random_cookie(uid), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": "5", - "Referer": "https://webstatic.mihoyo.com/", - "Accept-Encoding": "gzip, deflate", - "Accept-Language": "zh-CN,en-US;q=0.8", - "X-Requested-With": "com.mihoyo.hyperion", - }, - json={"character_ids": character_ids, "role_id": uid, "server": server_id}, - ) - data = req.json() - if data["message"] == "OK": - return data["data"] - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None - - -def parsed_data( - data: dict, -) -> "Optional[List[Dict[str, str]]], Dict[str, str], Optional[List[Dict[str, str]]], Optional[List[Dict[str, str]]]": - """ - 解析数据 - :param data: 数据 - """ - char_data_list = [] - for char in data["avatars"]: - _x = { - "id": char["id"], - "image": char["image"], - "name": char["name"], - "element": element_mastery[char["element"].lower()], - "fetter": char["fetter"], - "level": char["level"], - "rarity": char["rarity"], - "actived_constellation_num": char["actived_constellation_num"], - } - char_data_list.append(_x) - role_data = { - "active_day_number": data["stats"]["active_day_number"], # 活跃天数 - "achievement_number": data["stats"]["achievement_number"], # 达成成就数量 - # "win_rate": data["stats"]["win_rate"], - "anemoculus_number": data["stats"]["anemoculus_number"], # 风神瞳已收集 - "geoculus_number": data["stats"]["geoculus_number"], # 岩神瞳已收集 - "avatar_number": data["stats"]["avatar_number"], # 获得角色数量 - "way_point_number": data["stats"]["way_point_number"], # 传送点已解锁 - "domain_number": data["stats"]["domain_number"], # 秘境解锁数量 - "spiral_abyss": data["stats"]["spiral_abyss"], # 深渊当期进度 - "precious_chest_number": data["stats"]["precious_chest_number"], # 珍贵宝箱 - "luxurious_chest_number": data["stats"]["luxurious_chest_number"], # 华丽宝箱 - "exquisite_chest_number": data["stats"]["exquisite_chest_number"], # 精致宝箱 - "magic_chest_number": data["stats"]["magic_chest_number"], # 奇馈宝箱 - "common_chest_number": data["stats"]["common_chest_number"], # 普通宝箱 - "electroculus_number": data["stats"]["electroculus_number"], # 雷神瞳已收集 - "dendroculus_number": data["stats"]["dendroculus_number"], # 草神瞳已收集 - } - world_data_dict = {} - for world in data["world_explorations"]: - _x = { - "level": world["level"], # 声望等级 - "exploration_percentage": world["exploration_percentage"], # 探索进度 - "image": world["icon"], - "name": world["name"], - "offerings": world["offerings"], - "icon": world["icon"], - } - world_data_dict[world["name"]] = _x - home_data_list = [] - for home in data["homes"]: - _x = { - "level": home["level"], # 最大信任等级 - "visit_num": home["visit_num"], # 最高历史访客数 - "comfort_num": home["comfort_num"], # 最高洞天仙力 - "item_num": home["item_num"], # 已获得摆件数量 - "name": home["name"], - "icon": home["icon"], - "comfort_level_name": home["comfort_level_name"], - "comfort_level_icon": home["comfort_level_icon"], - } - home_data_list.append(_x) - return char_data_list, role_data, world_data_dict, home_data_list - - -async def get_mys_data(uid: str, mys_id: Optional[str]) -> Optional[List[Dict]]: - """ - 获取用户米游社数据 - :param uid: 原神uid - :param mys_id: 米游社id - """ - if mys_id: - # try: - req = await AsyncHttpx.get( - url=f"https://api-takumi-record.mihoyo.com/game_record/card/wapi/getGameRecordCard?uid={mys_id}", - headers={ - "DS": get_ds(f"uid={mys_id}"), - "x-rpc-app_version": Config.get_config("genshin", "mhyVersion"), - "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.11.1", - "x-rpc-client_type": "5", - "Referer": "https://webstatic.mihoyo.com/", - "Cookie": await Genshin.random_cookie(uid), - }, - ) - data = req.json() - if data["message"] == "OK": - return data["data"]["list"] - # except Exception as e: - # logger.error(f"访问失败,请重试! {type(e)}: {e}") - return None diff --git a/plugins/genshin/query_user/query_role/draw_image.py b/plugins/genshin/query_user/query_role/draw_image.py deleted file mode 100644 index fbb2eb5d..00000000 --- a/plugins/genshin/query_user/query_role/draw_image.py +++ /dev/null @@ -1,595 +0,0 @@ -from configs.path_config import IMAGE_PATH, TEMP_PATH -from utils.image_utils import BuildImage -from typing import List, Dict, Optional -from utils.message_builder import image -from nonebot.adapters.onebot.v11 import MessageSegment -from utils.http_utils import AsyncHttpx -from utils.utils import get_user_avatar -from io import BytesIO -import random -import asyncio -import os - - -image_path = IMAGE_PATH / "genshin" / "genshin_card" - - -async def get_genshin_image( - user_id: int, - uid: str, - char_data_list: List[Dict], - role_data: Dict, - world_data_dict: Dict, - home_data_list: List[Dict], - char_detailed_dict: dict = None, - mys_data: Optional[List[Dict]] = None, - nickname: Optional[str] = None, -) -> MessageSegment: - """ - 生成图片数据 - :param user_id:用户qq - :param uid: 原神uid - :param char_data_list: 角色列表 - :param role_data: 玩家数据 - :param world_data_dict: 国家数据字典 - :param home_data_list: 家园列表 - :param char_detailed_dict: 角色武器字典 - :param mys_data: 用户米游社数据 - :param nickname: 用户昵称 - """ - user_ava = BytesIO(await get_user_avatar(user_id)) - return await asyncio.get_event_loop().run_in_executor( - None, - _get_genshin_image, - uid, - char_data_list, - role_data, - world_data_dict, - home_data_list, - char_detailed_dict, - mys_data, - nickname, - user_ava, - ) - - -def _get_genshin_image( - uid: str, - char_data_list: List[Dict], - role_data: Dict, - world_data_dict: Dict, - home_data_list: List[Dict], - char_detailed_dict: dict = None, - mys_data: Optional[Dict] = None, - nickname: Optional[str] = None, - user_ava: Optional[BytesIO] = None, -) -> MessageSegment: - """ - 生成图片数据 - :param uid: 原神uid - :param char_data_list: 角色列表 - :param role_data: 玩家数据 - :param world_data_dict: 国家数据字典 - :param home_data_list: 家园列表 - :param char_detailed_dict: 角色武器字典 - :param mys_data: 用户米游社数据 - :param nickname: 用户昵称 - :param user_ava:用户头像 - """ - user_image = get_user_data_image(uid, role_data, mys_data, nickname, user_ava) - home_image = get_home_data_image(home_data_list) - country_image = get_country_data_image(world_data_dict) - char_image = get_char_data_image(char_data_list, char_detailed_dict) - top_bk = BuildImage(user_image.w, user_image.h + max([home_image.h, country_image.h]) + 100, color="#F9F6F2") - top_bk.paste(user_image, alpha=True) - top_bk.paste(home_image, (0, user_image.h + 50), alpha=True) - top_bk.paste(country_image, (home_image.w + 100, user_image.h + 50), alpha=True) - bar = BuildImage(1600, 200, font_size=50, color="#F9F6F2", font="HYWenHei-85W.ttf") - bar.text((50, 10), "角色背包", (104, 103, 101)) - bar.line((50, 90, 1550, 90), (227, 219, 209), width=10) - - foot = BuildImage(1700, 87, background=image_path / "head.png") - head = BuildImage(1700, 87, background=image_path / "head.png") - head.rotate(180) - middle = BuildImage( - 1700, top_bk.h + bar.h + char_image.h, background=image_path / "middle.png" - ) - A = BuildImage(middle.w, middle.h + foot.h + head.h) - A.paste(head, (-5, 0), True) - A.paste(middle, (0, head.h), True) - A.paste(foot, (0, head.h + middle.h), True) - A.crop((0, 0, A.w - 5, A.h)) - if A.h - top_bk.h - bar.h - char_image.h > 200: - _h = A.h - top_bk.h - bar.h - char_image.h - 200 - A.crop((0, 0, A.w, A.h - _h)) - A.paste(foot, (0, A.h - 87)) - A.paste(top_bk, (0, 100), center_type="by_width") - A.paste(bar, (50, top_bk.h + 80)) - A.paste(char_image, (0, top_bk.h + bar.h + 10), center_type="by_width") - rand = random.randint(1, 10000) - A.resize(0.8) - A.save(TEMP_PATH / f"genshin_user_card_{rand}.png") - return image(TEMP_PATH / f"genshin_user_card_{rand}.png") - - -def get_user_data_image( - uid: str, - role_data: Dict, - mys_data: Optional[Dict] = None, - nickname: Optional[str] = None, - user_ava: Optional[BytesIO] = None, -) -> BuildImage: - """ - 画出玩家基本数据 - :param uid: 原神uid - :param role_data: 玩家数据 - :param mys_data: 玩家米游社数据 - :param nickname: 用户昵称 - :param user_ava:用户头像 - """ - if mys_data: - nickname = [x["nickname"] for x in mys_data if x["game_id"] == 2][0] - region = BuildImage(1440, 560, color="#E3DBD1", font="HYWenHei-85W.ttf") - region.circle_corner(30) - uname_img = BuildImage( - 0, - 0, - plain_text=nickname, - font_size=40, - color=(255, 255, 255, 0), - font="HYWenHei-85W.ttf", - ) - uid_img = BuildImage( - 0, - 0, - plain_text=f"UID: {uid}", - font_size=25, - color=(255, 255, 255, 0), - font="HYWenHei-85W.ttf", - font_color=(21, 167, 89), - ) - ava_bk = BuildImage(270, 270, background=image_path / "cover.png") - # 用户头像 - if user_ava: - ava_img = BuildImage(200, 200, background=user_ava) - ava_img.circle() - ava_bk.paste(ava_img, alpha=True, center_type="center") - else: - ava_img = BuildImage( - 245, - 245, - background=image_path - / "chars_ava" - / random.choice(os.listdir(image_path / "chars_ava")), - ) - ava_bk.paste(ava_img, (12, 16), alpha=True) - region.paste(uname_img, (int(170 + uid_img.w / 2 - uname_img.w / 2), 365), True) - region.paste(uid_img, (170, 415), True) - region.paste(ava_bk, (int(550 / 2 - ava_bk.w / 2), 100), True) - data_img = BuildImage( - 800, 510, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - _height = 0 - keys = [ - ["活跃天数", "成就达成", "获得角色", "解锁传送"], - ["风神瞳", "岩神瞳", "雷神瞳", "草神瞳"], - ["解锁秘境", "深境螺旋", "华丽宝箱", "珍贵宝箱"], - ["精致宝箱", "普通宝箱", "奇馈宝箱",], - ] - values = [ - [ - role_data["active_day_number"], - role_data["achievement_number"], - role_data["avatar_number"], - role_data["way_point_number"], - ], - [ - role_data["anemoculus_number"], - role_data["geoculus_number"], - role_data["electroculus_number"], - role_data["dendroculus_number"], - ], - [ - role_data["domain_number"], - role_data["spiral_abyss"], - role_data["luxurious_chest_number"], - role_data["precious_chest_number"], - ], - [ - role_data["exquisite_chest_number"], - role_data["common_chest_number"], - role_data["magic_chest_number"], - ], - ] - for key, value in zip(keys, values): - _tmp_data_img = BuildImage( - 800, 200, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - _width = 10 - for k, v in zip(key, value): - t_ = BuildImage( - 0, - 0, - plain_text=k, - color=(255, 255, 255, 0), - font_color=(138, 143, 143), - font="HYWenHei-85W.ttf", - font_size=30, - ) - tmp_ = BuildImage( - t_.w, t_.h + 70, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - tmp_.text((0, 0), str(v), center_type="by_width") - tmp_.paste(t_, (0, 50), True, "by_width") - _tmp_data_img.paste(tmp_, ((_width + 15) if keys.index(key) == 1 else _width, 0)) - _width += 200 - data_img.paste(_tmp_data_img, (0, _height)) - _height += _tmp_data_img.h - 70 - region.paste(data_img, (510, 50)) - return region - - -def get_home_data_image(home_data_list: List[Dict]) -> BuildImage: - """ - 画出家园数据 - :param home_data_list: 家园列表 - """ - homes = os.listdir(image_path / "homes") - homes.remove("lock.png") - homes.sort() - h = 130 + 340 * len(homes) - region = BuildImage( - 550, h, color="#E3DBD1", font="HYWenHei-85W.ttf", font_size=40 - ) - try: - region.text( - (0, 30), f'尘歌壶 Lv.{home_data_list[0]["level"]}', center_type="by_width" - ) - region.text( - (0, region.h - 70), f'仙力: {home_data_list[0]["comfort_num"]}', center_type="by_width" - ) - except (IndexError, KeyError): - region.text((0, 30), f"尘歌壶 Lv.0", center_type="by_width") - region.text((0, region.h - 70), f"仙力: 0", center_type="by_width") - region.circle_corner(30) - height = 100 - unlock_home = [x["name"] for x in home_data_list] - for i, file in enumerate(homes): - home_img = image_path / "homes" / file - x = BuildImage(500, 250, background=home_img) - if file.split(".")[0] not in unlock_home: - black_img = BuildImage(500, 250, color="black") - lock_img = BuildImage(0, 0, background=image_path / "homes" / "lock.png") - black_img.circle_corner(50) - black_img.transparent(1) - black_img.paste(lock_img, alpha=True, center_type="center") - x.paste(black_img, alpha=True) - else: - black_img = BuildImage( - 500, 150, color="black", font="HYWenHei-85W.ttf", font_size=40 - ) - black_img.text((55, 55), file.split(".")[0], fill=(226, 211, 146)) - black_img.transparent(1) - text_img = BuildImage( - 0, - 0, - plain_text="洞天等级", - font="HYWenHei-85W.ttf", - font_color=(203, 200, 184), - font_size=35, - color=(255, 255, 255, 0), - ) - level_img = BuildImage( - 0, - 0, - plain_text=f'{home_data_list[0]["comfort_level_name"]}', - font="HYWenHei-85W.ttf", - font_color=(211, 213, 207), - font_size=30, - color=(255, 255, 255, 0), - ) - black_img.paste(text_img, (270, 25), True) - black_img.paste(level_img, (278, 85), True) - x.paste(black_img, alpha=True, center_type="center") - x.circle_corner(50) - region.paste(x, (0, height), True, "by_width") - height += 340 - return region - - -def get_country_data_image(world_data_dict: Dict) -> BuildImage: - """ - 画出国家探索供奉等图像 - :param world_data_dict: 国家数据字典 - """ - # 层岩巨渊 和 地下矿区 算一个 - region = BuildImage(790, 267 * ((len(world_data_dict) - 1) if world_data_dict.get("层岩巨渊·地下矿区") else len(world_data_dict)), color="#F9F6F2") - height = 0 - for country in ["蒙德", "龙脊雪山", "璃月", "层岩巨渊", "稻妻", "渊下宫", "须弥"]: - if not world_data_dict.get(country): - continue - x = BuildImage(790, 250, color="#3A4467") - logo = BuildImage(180, 180, background=image_path / "logo" / f"{country}.png") - tmp_bk = BuildImage(770, 230, color="#606779") - tmp_bk.circle_corner(10) - content_bk = BuildImage( - 755, 215, color="#3A4467", font_size=40, font="HYWenHei-85W.ttf" - ) - content_bk.paste(logo, (50, 0), True, "by_height") - if country in ["蒙德", "璃月"]: - content_bk.text((300, 40), "蒙德探索" if country == "蒙德" else "璃月探索", fill=(239, 211, 114)) - content_bk.text( - (500, 40), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 120), "蒙德声望" if country == "蒙德" else "璃月声望", fill=(239, 211, 114)) - content_bk.text( - (500, 120), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - elif country in ["层岩巨渊"]: - content_bk.text((300, 20), "层岩巨渊探索", fill=(239, 211, 114)) - content_bk.text( - (570, 20), - f"{world_data_dict['层岩巨渊']['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - if world_data_dict.get('层岩巨渊·地下矿区'): - content_bk.text((300, 85), "地下矿区探索", fill=(239, 211, 114)) - content_bk.text( - (570, 85), - f"{world_data_dict['层岩巨渊·地下矿区']['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "流明石触媒", fill=(239, 211, 114)) - content_bk.text( - (570, 150), - f"LV.{world_data_dict['层岩巨渊·地下矿区']['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["龙脊雪山"]: - content_bk.text((300, 40), "雪山探索", fill=(239, 211, 114)) - content_bk.text( - (500, 40), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 120), "忍冬之树", fill=(239, 211, 114)) - content_bk.text( - (500, 120), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["稻妻"]: - content_bk.text((300, 20), "稻妻探索", fill=(239, 211, 114)) - content_bk.text( - (500, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 85), "稻妻声望", fill=(239, 211, 114)) - content_bk.text( - (500, 85), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "神樱眷顾", fill=(239, 211, 114)) - content_bk.text( - (500, 150), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - elif country in ["渊下宫"]: - content_bk.text((300, 0), "渊下宫探索", fill=(239, 211, 114), center_type="by_height") - content_bk.text( - (530, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - center_type="by_height", - ) - elif country in ["须弥"]: - content_bk.text((300, 20), "须弥探索", fill=(239, 211, 114)) - content_bk.text( - (500, 20), - f"{world_data_dict[country]['exploration_percentage'] / 10}%", - fill=(255, 255, 255), - ) - content_bk.text((300, 85), "须弥声望", fill=(239, 211, 114)) - content_bk.text( - (500, 85), - f"Lv.{world_data_dict[country]['level']}", - fill=(255, 255, 255), - ) - content_bk.text((300, 150), "梦之树", fill=(239, 211, 114)) - content_bk.text( - (500, 150), - f"Lv.{world_data_dict[country]['offerings'][0]['level']}", - fill=(255, 255, 255), - ) - - x.paste(tmp_bk, alpha=True, center_type="center") - x.paste(content_bk, alpha=True, center_type="center") - x.circle_corner(20) - region.paste(x, (0, height), center_type="by_width") - height += 267 - return region - - -def get_char_data_image( - char_data_list: List[Dict], char_detailed_dict: dict -) -> "BuildImage, int": - """ - 画出角色列表 - :param char_data_list: 角色列表 - :param char_detailed_dict: 角色武器 - """ - lens = len(char_data_list) / 7 if len(char_data_list) % 7 == 0 else len(char_data_list) / 7 + 1 - x = 500 - _h = int(x * lens) - region = BuildImage( - 1600, - _h, - color="#F9F6F2", - ) - width = 120 - height = 0 - idx = 0 - for char in char_data_list: - if width + 230 > 1550: - width = 120 - height += 420 - idx += 1 - char_img = image_path / "chars" / f'{char["name"]}.png' - char_bk = BuildImage( - 270, - 500, - background=image_path / "element.png", - font="HYWenHei-85W.ttf", - font_size=35, - ) - char_img = BuildImage(0, 0, background=char_img) - actived_constellation_num = BuildImage( - 0, - 0, - plain_text=f"命之座: {char['actived_constellation_num']}层", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - ) - level = BuildImage( - 0, - 0, - plain_text=f"Lv.{char['level']}", - font="HYWenHei-85W.ttf", - font_size=30, - color=(255, 255, 255, 0), - font_color=(21, 167, 89), - ) - love_log = BuildImage( - 0, - 0, - plain_text="♥", - font="HWZhongSong.ttf", - font_size=40, - color=(255, 255, 255, 0), - font_color=(232, 31, 168), - ) - fetter = BuildImage( - 0, - 0, - plain_text=f'{char["fetter"]}', - font="HYWenHei-85W.ttf", - font_size=30, - color=(255, 255, 255, 0), - font_color=(232, 31, 168), - ) - if char_detailed_dict.get(char["name"]): - weapon = BuildImage( - 100, - 100, - background=image_path - / "weapons" - / f'{char_detailed_dict[char["name"]]["weapon"]}.png', - ) - weapon_name = BuildImage( - 0, - 0, - plain_text=f"{char_detailed_dict[char['name']]['weapon']}", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - ) - weapon_affix_level = BuildImage( - 0, - 0, - plain_text=f"精炼: {char_detailed_dict[char['name']]['affix_level']}", - font="HYWenHei-85W.ttf", - font_size=20, - color=(255, 255, 255, 0), - ) - weapon_level = BuildImage( - 0, - 0, - plain_text=f"Lv.{char_detailed_dict[char['name']]['level']}", - font="HYWenHei-85W.ttf", - font_size=25, - color=(255, 255, 255, 0), - font_color=(21, 167, 89), - ) - char_bk.paste(weapon, (20, 380), True) - char_bk.paste( - weapon_name, - (100 + int((char_bk.w - 22 - weapon.w - weapon_name.w) / 2 - 10), 390), - True, - ) - char_bk.paste( - weapon_affix_level, - ( - ( - 100 - + int( - (char_bk.w - 10 - weapon.w - weapon_affix_level.w) / 2 - 10 - ), - 420, - ) - ), - True, - ) - char_bk.paste( - weapon_level, - ( - ( - 100 - + int((char_bk.w - 10 - weapon.w - weapon_level.w) / 2 - 10), - 450, - ) - ), - True, - ) - char_bk.paste(char_img, (0, 5), alpha=True, center_type="by_width") - char_bk.text((0, 270), char["name"], center_type="by_width") - char_bk.paste(actived_constellation_num, (0, 310), True, "by_width") - char_bk.paste(level, (60, 340), True) - char_bk.paste(love_log, (155, 330), True) - char_bk.paste(fetter, (180, 340), True) - char_bk.resize(0.8) - region.paste(char_bk, (width, height), True) - width += 230 - region.crop((0, 0, region.w, height + 430)) - return region - - -async def init_image(world_data_dict: Dict[str, Dict[str, str]], char_data_list: List[Dict[str, str]], char_detailed_dict: dict, home_data_list: List[Dict]): - """ - 下载头像 - :param world_data_dict: 地图标志 - :param char_data_list: 角色列表 - :param char_detailed_dict: 角色武器 - :param home_data_list: 家园列表 - """ - for world in world_data_dict: - file = image_path / "logo" / f'{world_data_dict[world]["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file(world_data_dict[world]["icon"], file) - for char in char_data_list: - file = image_path / "chars" / f'{char["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file(char["image"], file) - for char in char_detailed_dict.keys(): - file = image_path / "weapons" / f'{char_detailed_dict[char]["weapon"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file( - char_detailed_dict[char]["weapon_image"], file - ) - for home in home_data_list: - file = image_path / "homes" / f'{home["name"]}.png' - file.parent.mkdir(parents=True, exist_ok=True) - if not file.exists(): - await AsyncHttpx.download_file( - home["icon"], file - ) diff --git a/plugins/genshin/query_user/reset_today_query_user_data/__init__.py b/plugins/genshin/query_user/reset_today_query_user_data/__init__.py deleted file mode 100644 index 667ba90a..00000000 --- a/plugins/genshin/query_user/reset_today_query_user_data/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from services.log import logger -from utils.utils import scheduler - -from .._models import Genshin - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - try: - await Genshin.all().update(today_query_uid="") - logger.warning(f"重置原神查询记录成功..") - except Exception as e: - logger.error(f"重置原神查询记录失败. {type(e)}:{e}") - diff --git a/plugins/genshin/query_user/resin_remind/__init__.py b/plugins/genshin/query_user/resin_remind/__init__.py deleted file mode 100644 index 7d1400a8..00000000 --- a/plugins/genshin/query_user/resin_remind/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Tuple - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.params import Command - -from services.log import logger -from utils.depends import OneCommand - -from .._models import Genshin -from .init_task import add_job, scheduler - -__zx_plugin_name__ = "原神树脂提醒" -__plugin_usage__ = """ -usage: - 即将满树脂的提醒 - 会在 120-140 140-160 160 以及溢出指定部分时提醒, - 共提醒3-4次 - 指令: - 开原神树脂提醒 - 关原神树脂提醒 -""".strip() -__plugin_des__ = "时时刻刻警醒你!" -__plugin_cmd__ = ["开原神树脂提醒", "关原神树脂提醒"] -__plugin_type__ = ("原神相关",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["原神树脂提醒", "关原神树脂提醒", "开原神树脂提醒"], -} -__plugin_configs__ = { - "AUTO_CLOSE_QUERY_FAIL_RESIN_REMIND": { - "value": True, - "help": "当请求连续三次失败时,关闭用户的树脂提醒", - "default_value": True, - "type": bool, - }, - "CUSTOM_RESIN_OVERFLOW_REMIND": { - "value": 20, - "help": "自定义树脂溢出指定数量时的提醒,空值是为关闭", - "default_value": None, - "type": int, - }, -} - -resin_remind = on_command("开原神树脂提醒", aliases={"关原神树脂提醒"}, priority=5, block=True) - - -@resin_remind.handle() -async def _(event: MessageEvent, cmd: str = OneCommand()): - user = await Genshin.get_or_none(user_id=str(event.user_id)) - if not user or not user.uid or not user.cookie: - await resin_remind.finish("请先绑定uid和cookie!") - try: - scheduler.remove_job(f"genshin_resin_remind_{user.uid}_{event.user_id}") - except JobLookupError: - pass - if cmd[0] == "开": - if user.resin_remind: - await resin_remind.finish("原神树脂提醒已经是开启状态,请勿重复开启!", at_sender=True) - user.resin_remind = True - add_job(event.user_id, user.uid) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 开启原神体力提醒" - ) - await resin_remind.send("开启原神树脂提醒成功!", at_sender=True) - else: - if not user.resin_remind: - await resin_remind.finish("原神树脂提醒已经是开启状态,请勿重复开启!", at_sender=True) - user.resin_remind = False - user.resin_recovery_time = None - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 关闭原神体力提醒" - ) - await resin_remind.send("已关闭原神树脂提醒..", at_sender=True) - if user: - await user.save(update_fields=["resin_remind", "resin_recovery_time"]) diff --git a/plugins/genshin/query_user/resin_remind/init_task.py b/plugins/genshin/query_user/resin_remind/init_task.py deleted file mode 100644 index fefabc17..00000000 --- a/plugins/genshin/query_user/resin_remind/init_task.py +++ /dev/null @@ -1,217 +0,0 @@ -import random -from datetime import datetime, timedelta - -import nonebot -import pytz -from apscheduler.jobstores.base import ConflictingIdError, JobLookupError -from nonebot import Driver -from nonebot.adapters.onebot.v11 import ActionFailed -from nonebot.plugin import require - -from configs.config import Config -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.message_builder import at -from utils.utils import get_bot, scheduler - -from .._models import Genshin - -driver: Driver = nonebot.get_driver() - - -require("query_memo") - -from ..query_memo import get_memo - -global_map = {} - - -class UserManager: - def __init__(self, max_error_count: int = 3): - self._data = [] - self._overflow_data = [] - self._error_count = {} - self.max_error_count = max_error_count - - def append(self, o: str): - if o not in self._data: - self._data.append(o) - - def remove(self, o: str): - if o in self._data: - self._data.remove(o) - - def exists(self, o: str): - return o in self._data - - def add_error_count(self, uid: str): - if uid in self._error_count.keys(): - self._error_count[uid] += 1 - else: - self._error_count[uid] = 1 - - def check(self, uid: str) -> bool: - if uid in self._error_count.keys(): - return self._error_count[uid] == self.max_error_count - return False - - def remove_error_count(self, uid): - if uid in self._error_count.keys(): - del self._error_count[uid] - - def add_overflow(self, uid: str): - if uid not in self._overflow_data: - self._overflow_data.append(uid) - - def remove_overflow(self, uid: str): - if uid in self._overflow_data: - self._overflow_data.remove(uid) - - def is_overflow(self, uid: str) -> bool: - return uid in self._overflow_data - - -user_manager = UserManager() - - -@driver.on_startup -async def _(): - """ - 启动时分配定时任务 - """ - g_list = await Genshin.filter(resin_remind=True).all() - update_list = [] - date = datetime.now(pytz.timezone("Asia/Shanghai")) + timedelta(seconds=30) - for u in g_list: - if u.resin_remind: - if u.resin_recovery_time: - if u.resin_recovery_time and u.resin_recovery_time > datetime.now( - pytz.timezone("Asia/Shanghai") - ): - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - else: - u.resin_recovery_time = None # type: ignore - update_list.append(u) - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job CHECK:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - else: - add_job(u.user_id, u.uid) - logger.info( - f"genshin_resin_remind add_job CHECK:USER:{u.user_id} UID:{u.uid}启动原神树脂提醒 " - ) - if update_list: - await Genshin.bulk_update(update_list, ["resin_recovery_time"]) - - -def add_job(user_id: str, uid: int): - # 移除 - try: - scheduler.remove_job(f"genshin_resin_remind_{uid}_{user_id}") - except JobLookupError: - pass - date = datetime.now(pytz.timezone("Asia/Shanghai")) + timedelta(seconds=30) - try: - scheduler.add_job( - _remind, - "date", - run_date=date.replace(microsecond=0), - id=f"genshin_resin_remind_{uid}_{user_id}", - args=[user_id, uid], - ) - except ConflictingIdError: - pass - - -async def _remind(user_id: int, uid: str): - user = await Genshin.get_or_none(user_id=str(user_id), uid=int(uid)) - uid = str(uid) - if uid[0] in ["1", "2"]: - server_id = "cn_gf01" - elif uid[0] == "5": - server_id = "cn_qd01" - else: - return - data, code = await get_memo(uid, server_id) - now = datetime.now(pytz.timezone("Asia/Shanghai")) - next_time = None - if code == 200: - current_resin = int(data["current_resin"]) # 当前树脂 - max_resin = int(data["max_resin"]) # 最大树脂 - msg = f"你的已经存了 {current_resin} 个树脂了!不要忘记刷掉!" - # resin_recovery_time = data["resin_recovery_time"] # 树脂全部回复时间 - if current_resin < max_resin: - user_manager.remove(uid) - user_manager.remove_overflow(uid) - if current_resin < max_resin - 40: - next_time = now + timedelta(minutes=(max_resin - 40 - current_resin) * 8) - elif max_resin - 40 <= current_resin < max_resin - 20: - next_time = now + timedelta(minutes=(max_resin - 20 - current_resin) * 8) - elif max_resin - 20 <= current_resin < max_resin: - next_time = now + timedelta(minutes=(max_resin - current_resin) * 8) - elif current_resin == max_resin: - custom_overflow_resin = Config.get_config( - "resin_remind", "CUSTOM_RESIN_OVERFLOW_REMIND" - ) - if user_manager.is_overflow(uid) and custom_overflow_resin: - next_time = now + timedelta(minutes=custom_overflow_resin * 8) - user_manager.add_overflow(uid) - user_manager.remove(uid) - msg = f"你的树脂都溢出 {custom_overflow_resin} 个了!浪费可耻!" - else: - next_time = now + timedelta(minutes=40 * 8 + random.randint(5, 50)) - - if not user_manager.exists(uid) and current_resin >= max_resin - 40: - if current_resin == max_resin: - user_manager.append(uid) - bot = get_bot() - if bot: - if user_id in [x["user_id"] for x in await bot.get_friend_list()]: - await bot.send_private_msg( - user_id=user_id, - message=msg, - ) - else: - if user: - group_id = user.bind_group - if not group_id: - if group_list := await GroupInfoUser.get_user_all_group( - user_id - ): - group_id = group_list[0] - try: - await bot.send_group_msg( - group_id=group_id, message=at(user_id) + msg - ) - except ActionFailed as e: - logger.error(f"树脂提醒推送发生错误 {type(e)}:{e}") - - if not next_time: - if user_manager.check(uid) and Config.get_config( - "resin_remind", "AUTO_CLOSE_QUERY_FAIL_RESIN_REMIND" - ): - if user: - user.resin_remind = False - user.resin_recovery_time = None - await user.save(update_fields=["resin_recovery_time", "resin_remind"]) - next_time = now + timedelta(minutes=(20 + random.randint(5, 20)) * 8) - user_manager.add_error_count(uid) - else: - user_manager.remove_error_count(uid) - if user: - user.resin_recovery_time = next_time - await user.save(update_fields=["resin_recovery_time", "resin_remind"]) - scheduler.add_job( - _remind, - "date", - run_date=next_time, - id=f"genshin_resin_remind_{uid}_{user_id}", - args=[user_id, uid], - ) - logger.info( - f"genshin_resin_remind add_job:USER:{user_id} UID:{uid} " f"{next_time} 原神树脂提醒" - ) diff --git a/plugins/gold_redbag/__init__.py b/plugins/gold_redbag/__init__.py deleted file mode 100755 index 1cf9733c..00000000 --- a/plugins/gold_redbag/__init__.py +++ /dev/null @@ -1,373 +0,0 @@ -import random -import re -import time -from datetime import datetime, timedelta -from typing import Dict, List, Optional - -from apscheduler.jobstores.base import JobLookupError -from nonebot import on_command, on_notice -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - GroupMessageEvent, - Message, - PokeNotifyEvent, -) -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.matcher import Matcher -from nonebot.message import IgnoredException, run_preprocessor -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import AtList, GetConfig -from utils.message_builder import at, image -from utils.utils import is_number, scheduler - -from .config import FESTIVE_KEY, GroupRedBag, RedBag -from .data_source import ( - build_open_result_image, - check_gold, - end_festive_red_bag, - random_red_bag_background, -) - -__zx_plugin_name__ = "金币红包" -__plugin_usage__ = """ -usage: - 在群内发送指定金额的红包,拼手气项目 - 指令: - 塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包 - 开/抢/*戳一戳*: 打开红包 - 退回: 退回未开完的红包,必须在一分钟后使用 - 示例:塞红包 1000 - 示例:塞红包 1000 10 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 节日全群红包指令 - 指令: - 节日红包 [金额] [数量] ?[祝福语] ?[指定群] -""".strip() -__plugin_des__ = "运气项目又来了" -__plugin_cmd__ = [ - "塞红包 [金币数] ?[红包数=5] ?[at指定人]", - "开/抢", - "退回", - "节日红包 [金额] [数量] ?[祝福语] ?[指定群] [_superuser]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["金币红包", "塞红包"], -} -__plugin_cd_limit__ = {"rst": "急什么急什么,待会再发!"} -__plugin_configs__ = { - "DEFAULT_TIMEOUT": { - "value": 600, - "help": "普通红包默认超时时间", - "default_value": 600, - "type": int, - }, - "DEFAULT_INTERVAL": { - "value": 60, - "help": "用户发送普通红包最小间隔时间", - "default_value": 60, - "type": int, - }, - "RANK_NUM": { - "value": 10, - "help": "结算排行显示前N位", - "default_value": 10, - "type": int, - }, -} -# __plugin_resources__ = {"prts": IMAGE_PATH} - - -async def rule(event: GroupMessageEvent) -> bool: - return check_on_gold_red(event) - - -gold_red_bag = on_command( - "塞红包", aliases={"金币红包"}, priority=5, block=True, permission=GROUP -) - -open_ = on_command( - "开", aliases={"抢"}, priority=5, block=True, permission=GROUP, rule=rule -) - -poke_ = on_notice(priority=6, block=False) - -return_ = on_command("退回", aliases={"退还"}, priority=5, block=True, permission=GROUP) - -festive_redbag = on_command( - "节日红包", priority=5, block=True, permission=SUPERUSER, rule=to_me() -) - -GROUP_DATA: Dict[int, GroupRedBag] = {} - -PATTERN = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~,。;‘、""" - - -# 阻断其他poke -# @run_preprocessor -# async def _( -# matcher: Matcher, -# event: PokeNotifyEvent, -# ): -# try: -# if matcher.type == "notice" and event.self_id == event.target_id: -# flag = check_on_gold_red(event) -# if flag: -# if matcher.plugin_name == "poke": -# raise IgnoredException("目前正在抢红包...") -# else: -# if matcher.plugin_name == "gold_red_bag": -# raise IgnoredException("目前没有红包...") -# except AttributeError: -# pass - - -@gold_red_bag.handle() -async def _( - bot: Bot, - event: GroupMessageEvent, - arg: Message = CommandArg(), - at_list: List[int] = AtList(), - default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), -): - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) - if not group_red_bag: - group_red_bag = GroupRedBag(event.group_id) - GROUP_DATA[event.group_id] = group_red_bag - # 剩余过期时间 - time_remaining = group_red_bag.check_timeout(event.user_id) - if time_remaining != -1: - # 判断用户红包是否存在且是否过时覆盖 - if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): - now = time.time() - if now < user_red_bag.start_time + default_interval: - await gold_red_bag.finish( - f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." - f"(或等待{time_remaining}秒红包cd)" - ) - msg = arg.extract_plain_text().strip().split() - if not msg: - await gold_red_bag.finish("不塞钱发什么红包!") - amount = msg[0] - if len(msg) == 1: - flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) - if not flag: - await gold_red_bag.finish(tip, at_sender=True) - num = 5 - else: - num = msg[1] - if not is_number(num) or int(num) < 1: - await gold_red_bag.finish("红包个数给我输正确啊!", at_sender=True) - flag, tip = await check_gold(str(event.user_id), str(event.group_id), amount) - if not flag: - await gold_red_bag.finish(tip, at_sender=True) - group_member_num = (await bot.get_group_info(group_id=event.group_id))[ - "member_count" - ] - num = int(num) - if num > group_member_num: - await gold_red_bag.send("你发的红包数量也太多了,已经为你修改成与本群人数相同的红包数量...") - num = group_member_num - nickname = event.sender.card or event.sender.nickname - await group_red_bag.add_red_bag( - f"{nickname}的红包", - int(amount), - 1 if at_list else num, - nickname or "", - str(event.user_id), - assigner=str(at_list[0]) if at_list else None, - ) - await gold_red_bag.send( - f"{nickname}发起了金币红包\n金额: {amount}\n数量: {num}\n" - + image(await random_red_bag_background(event.user_id)) - ) - logger.info(f"塞入 {num} 个红包,共 {amount} 金币", "金币红包", event.user_id, event.group_id) - - -@open_.handle() -async def _( - event: GroupMessageEvent, - arg: Message = CommandArg(), - rank_num: int = GetConfig(config="RANK_NUM"), -): - if msg := arg.extract_plain_text().strip(): - msg = re.sub(PATTERN, "", msg) - if "红包" not in msg: - return - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(event.group_id) - if group_red_bag: - open_data, settlement_list = await group_red_bag.open(event.user_id) - send_msg = "" - for _, item in open_data.items(): - amount, red_bag = item - result_image = await build_open_result_image(red_bag, event.user_id, amount) - send_msg += ( - f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" - + image(result_image) - + "\n" - ) - logger.info( - f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", - "开红包", - event.user_id, - event.group_id, - ) - send_msg = send_msg[:-1] if send_msg else "没有红包给你开!" - await open_.send(send_msg, at_sender=True) - if settlement_list: - for red_bag in settlement_list: - await open_.send( - f"{red_bag.name}已结算\n" - + image(await red_bag.build_amount_rank(rank_num)) - ) - - -# @poke_.handle() -# async def _poke_(event: PokeNotifyEvent): -# group_id = getattr(event, "group_id", None) -# if event.self_id == event.target_id and group_id: -# is_open = check_on_gold_red(event) -# if not is_open: -# return -# group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(group_id) -# if group_red_bag: -# open_data, settlement_list = await group_red_bag.open(event.user_id) -# send_msg = "" -# for _, item in open_data.items(): -# amount, red_bag = item -# result_image = await build_open_result_image( -# red_bag, event.user_id, amount -# ) -# send_msg += ( -# f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n" -# + image(result_image) -# + "\n" -# ) -# logger.info( -# f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", -# "开红包", -# event.user_id, -# event.group_id, -# ) -# if send_msg: -# await open_.send(send_msg, at_sender=True) -# if settlement_list: -# for red_bag in settlement_list: -# await open_.send( -# f"{red_bag.name}已结算\n" -# + image(await red_bag.build_amount_rank()) -# ) - - -@return_.handle() -async def _( - event: GroupMessageEvent, - default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), - rank_num: int = GetConfig(config="RANK_NUM"), -): - group_red_bag: GroupRedBag = GROUP_DATA[event.group_id] - if group_red_bag: - if user_red_bag := group_red_bag.get_user_red_bag(event.user_id): - now = time.time() - if now - user_red_bag.start_time < default_interval: - await return_.finish( - f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " - f"秒后可以退回...", - at_sender=True, - ) - user_red_bag = group_red_bag.get_user_red_bag(event.user_id) - if user_red_bag and ( - return_amount := await group_red_bag.settlement(event.user_id) - ): - logger.info( - f"退回了红包 {return_amount} 金币", "红包退回", event.user_id, event.group_id - ) - await return_.send( - f"已成功退还了 " - f"{return_amount} 金币\n" - + image(await user_red_bag.build_amount_rank(rank_num)), - at_sender=True, - ) - await return_.send("目前没有红包可以退回...", at_sender=True) - - -@festive_redbag.handle() -async def _(bot: Bot, arg: Message = CommandArg()): - global redbag_data - msg = arg.extract_plain_text().strip() - if msg: - msg = msg.split() - amount = 0 - num = 0 - greetings = "恭喜发财 大吉大利" - gl = [] - if (lens := len(msg)) < 2: - await festive_redbag.finish("参数不足,格式:节日红包 [金额] [数量] [祝福语](可省) [指定群](可省)") - if lens > 1: - if not is_number(msg[0]): - await festive_redbag.finish("金额必须要是数字!", at_sender=True) - amount = int(msg[0]) - if not is_number(msg[1]): - await festive_redbag.finish("数量必须要是数字!", at_sender=True) - num = int(msg[1]) - if lens > 2: - greetings = msg[2] - if lens > 3: - for i in range(3, lens): - if not is_number(msg[i]): - await festive_redbag.finish("指定的群号必须要是数字啊!", at_sender=True) - gl.append(int(msg[i])) - if not gl: - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - for g in gl: - group_red_bag: Optional[GroupRedBag] = GROUP_DATA.get(g) - if not group_red_bag: - group_red_bag = GroupRedBag(g) - GROUP_DATA[g] = group_red_bag - try: - scheduler.remove_job(f"{FESTIVE_KEY}_{g}") - await end_festive_red_bag(bot, group_red_bag) - except JobLookupError: - pass - await group_red_bag.add_red_bag( - f"{NICKNAME}的红包", int(amount), num, NICKNAME, FESTIVE_KEY, True - ) - scheduler.add_job( - end_festive_red_bag, - "date", - # run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), - run_date=(datetime.now() + timedelta(seconds=30)).replace( - microsecond=0 - ), - id=f"{FESTIVE_KEY}_{g}", - args=[bot, group_red_bag], - ) - try: - await bot.send_group_msg( - group_id=g, - message=f"{NICKNAME}发起了金币红包\n金额:{amount}\n数量:{num}\n" - + image(await random_red_bag_background(bot.self_id, greetings)), - ) - logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) - except ActionFailed: - logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) - - -def check_on_gold_red(event) -> bool: - if group_red_bag := GROUP_DATA.get(event.group_id): - return group_red_bag.check_open(event.user_id) - return False diff --git a/plugins/gold_redbag/config.py b/plugins/gold_redbag/config.py deleted file mode 100644 index a2fe4c55..00000000 --- a/plugins/gold_redbag/config.py +++ /dev/null @@ -1,311 +0,0 @@ -import random -import time -from datetime import datetime -from io import BytesIO -from typing import Dict, List, Optional, Tuple, Union, overload - -from pydantic import BaseModel - -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from plugins.gold_redbag.model import RedbagUser -from utils.image_utils import BuildImage -from utils.utils import get_user_avatar - -FESTIVE_KEY = "FESTIVE" -"""节日红包KEY""" - - -class RedBag(BaseModel): - - """ - 红包 - """ - - group_id: str - """所属群聊""" - name: str - """红包名称""" - amount: int - """总金币""" - num: int - """红包数量""" - promoter: str - """发起人昵称""" - promoter_id: str - """发起人id""" - is_festival: bool - """是否为节日红包""" - timeout: int - """过期时间""" - assigner: Optional[str] = None - """指定人id""" - start_time: float - """红包发起时间""" - open_user: Dict[str, int] = {} - """开启用户""" - red_bag_list: List[int] - - async def build_amount_rank(self, num: int = 10) -> BuildImage: - """生成结算红包图片 - - 参数: - num: 查看的排名数量. - - 返回: - BuildImage: 结算红包图片 - """ - user_image_list = [] - if self.open_user: - sort_data = sorted( - self.open_user.items(), key=lambda item: item[1], reverse=True - ) - num = num if num < len(self.open_user) else len(self.open_user) - user_id_list = [sort_data[i][0] for i in range(num)] - group_user_list = await GroupInfoUser.filter( - group_id=self.group_id, user_id__in=user_id_list - ).all() - for i in range(num): - user_background = BuildImage(600, 100, font_size=30) - user_id, amount = sort_data[i] - user_ava_bytes = await get_user_avatar(user_id) - user_ava = None - if user_ava_bytes: - user_ava = BuildImage(80, 80, background=BytesIO(user_ava_bytes)) - else: - user_ava = BuildImage(80, 80) - await user_ava.acircle_corner(10) - await user_background.apaste(user_ava, (130, 10), True) - no_image = BuildImage(100, 100, font_size=65, font="CJGaoDeGuo.otf") - await no_image.atext((0, 0), f"{i+1}", center_type="center") - await no_image.aline((99, 10, 99, 90), "#b9b9b9") - await user_background.apaste(no_image) - name = [ - user.user_name - for user in group_user_list - if user_id == user.user_id - ] - await user_background.atext((225, 15), name[0] if name else "") - amount_image = BuildImage( - 0, 0, plain_text=f"{amount} 元", font_size=30, font_color="#cdac72" - ) - await user_background.apaste( - amount_image, (user_background.w - amount_image.w - 20, 50), True - ) - await user_background.aline((225, 99, 590, 99), "#b9b9b9") - user_image_list.append(user_background) - background = BuildImage(600, 150 + len(user_image_list) * 100) - top = BuildImage(600, 100, color="#f55545", font_size=30) - promoter_ava_bytes = await get_user_avatar(self.promoter_id) - promoter_ava = None - if promoter_ava_bytes: - promoter_ava = BuildImage(60, 60, background=BytesIO(promoter_ava_bytes)) - else: - promoter_ava = BuildImage(60, 60) - await promoter_ava.acircle() - await top.apaste(promoter_ava, (10, 0), True, "by_height") - await top.atext((80, 33), self.name, (255, 255, 255)) - right_text = BuildImage(150, 100, color="#f55545", font_size=30) - await right_text.atext((10, 33), "结算排行", (255, 255, 255)) - await right_text.aline((4, 10, 4, 90), (255, 255, 255), 2) - await top.apaste(right_text, (460, 0)) - await background.apaste(top) - cur_h = 110 - for user_image in user_image_list: - await background.apaste(user_image, (0, cur_h)) - cur_h += user_image.h - return background - - -class GroupRedBag: - - """ - 群组红包管理 - """ - - def __init__(self, group_id: Union[int, str]): - self.group_id = str(group_id) - self._data: Dict[str, RedBag] = {} - """红包列表""" - - def get_user_red_bag(self, user_id: Union[str, int]) -> Optional[RedBag]: - """获取用户塞红包数据 - - 参数: - user_id: 用户id - - 返回: - Optional[RedBag]: RedBag - """ - return self._data.get(str(user_id)) - - def check_open(self, user_id: Union[str, int]) -> bool: - """检查是否有可开启的红包 - - 参数: - user_id: 用户id - - 返回: - bool: 是否有可开启的红包 - """ - user_id = str(user_id) - for _, red_bag in self._data.items(): - if red_bag.assigner: - if red_bag.assigner == user_id: - return True - else: - if user_id not in red_bag.open_user: - return True - return False - - def check_timeout(self, user_id: Union[int, str]) -> int: - """判断用户红包是否过期 - - 参数: - user_id: 用户id - - 返回: - int: 距离过期时间 - """ - user_id = str(user_id) - if user_id in self._data: - reg_bag = self._data[user_id] - now = time.time() - if now < reg_bag.timeout + reg_bag.start_time: - return int(reg_bag.timeout + reg_bag.start_time - now) - return -1 - - async def open( - self, user_id: Union[int, str] - ) -> Tuple[Dict[str, Tuple[int, RedBag]], List[RedBag]]: - """开启红包 - - 参数: - user_id: 用户id - - 返回: - Dict[str, Tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag - List[RedBag]: 开完的红包 - """ - user_id = str(user_id) - open_data = {} - settlement_list: List[RedBag] = [] - for _, red_bag in self._data.items(): - if red_bag.num > len(red_bag.open_user): - is_open = False - if red_bag.assigner: - is_open = red_bag.assigner == user_id - else: - is_open = user_id not in red_bag.open_user - if is_open: - random_amount = red_bag.red_bag_list.pop() - await RedbagUser.add_redbag_data( - user_id, self.group_id, "get", random_amount - ) - await BagUser.add_gold(user_id, self.group_id, random_amount) - red_bag.open_user[user_id] = random_amount - open_data[red_bag.promoter_id] = (random_amount, red_bag) - if red_bag.num == len(red_bag.open_user): - # 红包开完,结算 - settlement_list.append(red_bag) - if settlement_list: - for uid in [red_bag.promoter_id for red_bag in settlement_list]: - if uid in self._data: - del self._data[uid] - return open_data, settlement_list - - def festive_red_bag_expire(self) -> Optional[RedBag]: - """节日红包过期 - - 返回: - Optional[RedBag]: 过期的节日红包 - """ - if FESTIVE_KEY in self._data: - red_bag = self._data[FESTIVE_KEY] - del self._data[FESTIVE_KEY] - return red_bag - return None - - async def settlement( - self, user_id: Optional[Union[int, str]] = None - ) -> Optional[int]: - """红包退回 - - 参数: - user_id: 用户id, 指定id时结算指定用户红包. - - 返回: - int: 退回金币 - """ - user_id = str(user_id) - if user_id: - if red_bag := self._data.get(user_id): - del self._data[user_id] - if red_bag.red_bag_list: - # 退还剩余金币 - if amount := sum(red_bag.red_bag_list): - await BagUser.add_gold(user_id, self.group_id, amount) - return amount - return None - - async def add_red_bag( - self, - name: str, - amount: int, - num: int, - promoter: str, - promoter_id: str, - is_festival: bool = False, - timeout: int = 60, - assigner: Optional[str] = None, - ): - """添加红包 - - 参数: - name: 红包名称 - amount: 金币数量 - num: 红包数量 - promoter: 发起人昵称 - promoter_id: 发起人id - is_festival: 是否为节日红包. - timeout: 超时时间. - assigner: 指定人. - """ - user_gold = await BagUser.get_gold(promoter_id, self.group_id) - if not is_festival and (amount < 1 or user_gold < amount): - raise ValueError("红包金币不足或用户金币不足") - red_bag_list = self._random_red_bag(amount, num) - if not is_festival: - await BagUser.spend_gold(promoter_id, self.group_id, amount) - await RedbagUser.add_redbag_data(promoter_id, self.group_id, "send", amount) - self._data[promoter_id] = RedBag( - group_id=self.group_id, - name=name, - amount=amount, - num=num, - promoter=promoter, - promoter_id=promoter_id, - is_festival=is_festival, - timeout=timeout, - start_time=time.time(), - assigner=assigner, - red_bag_list=red_bag_list, - ) - - def _random_red_bag(self, amount: int, num: int) -> List[int]: - """初始化红包金币 - - 参数: - amount: 金币数量 - num: 红包数量 - - 返回: - List[int]: 红包列表 - """ - red_bag_list = [] - for _ in range(num - 1): - tmp = int(amount / random.choice(range(3, num + 3))) - red_bag_list.append(tmp) - amount -= tmp - red_bag_list.append(amount) - return red_bag_list diff --git a/plugins/gold_redbag/data_source.py b/plugins/gold_redbag/data_source.py deleted file mode 100755 index e2e02bc7..00000000 --- a/plugins/gold_redbag/data_source.py +++ /dev/null @@ -1,154 +0,0 @@ -import os -import random -from io import BytesIO -from typing import Tuple, Union - -from nonebot.adapters.onebot.v11 import Bot - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.bag_user import BagUser -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar, is_number - -from .config import FESTIVE_KEY, GroupRedBag, RedBag - - -async def end_festive_red_bag(bot: Bot, group_red_bag: GroupRedBag): - """结算节日红包 - - 参数: - bot: Bot - group_red_bag: GroupRedBag - """ - if festive_red_bag := group_red_bag.festive_red_bag_expire(): - rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 - rank_image = await festive_red_bag.build_amount_rank(rank_num) - message = ( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{len(festive_red_bag.open_user)}" - f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" + image(rank_image) - ) - await bot.send_group_msg(group_id=int(group_red_bag.group_id), message=message) - - -async def check_gold( - user_id: str, group_id: str, amount: Union[str, int] -) -> Tuple[bool, str]: - """检查金币数量是否合法 - - 参数: - user_id: 用户id - group_id: 群聊id - amount: 金币数量 - - 返回: - Tuple[bool, str]: 是否合法以及提示语 - """ - if is_number(amount): - amount = int(amount) - user_gold = await BagUser.get_gold(user_id, group_id) - if amount < 1: - return False, "小气鬼,要别人倒贴金币给你嘛!" - if user_gold < amount: - return False, "没有金币的话请不要发红包..." - return True, "" - else: - return False, "给我好好的输入红包里金币的数量啊喂!" - - -async def random_red_bag_background( - user_id: Union[str, int], msg="恭喜发财 大吉大利" -) -> BuildImage: - """构造发送红包图片 - - 参数: - user_id: 用户id - msg: 红包消息. - - 异常: - ValueError: 图片背景列表为空 - - 返回: - BuildImage: 构造后的图片 - """ - background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2") - if not background_list: - raise ValueError("prts/redbag_1 背景图列表为空...") - random_redbag = random.choice(background_list) - redbag = BuildImage( - 0, 0, font_size=38, background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag - ) - ava_byte = await get_user_avatar(user_id) - ava = None - if ava_byte: - ava = BuildImage(65, 65, background=BytesIO(ava_byte)) - else: - ava = BuildImage(65, 65, color=(0, 0, 0), is_alpha=True) - await ava.acircle() - await redbag.atext( - (int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210), msg, (240, 218, 164) - ) - await redbag.apaste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130), True) - return redbag - - -async def build_open_result_image( - red_bag: RedBag, user_id: Union[int, str], amount: int -) -> BuildImage: - """构造红包开启图片 - - 参数: - red_bag: RedBag - user_id: 开启红包用户id - amount: 开启红包获取的金额 - - 异常: - ValueError: 图片背景列表为空 - - 返回: - BuildImage: 构造后的图片 - """ - background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1") - if not background_list: - raise ValueError("prts/redbag_1 背景图列表为空...") - random_redbag = random.choice(background_list) - head = BuildImage( - 1000, - 980, - font_size=30, - background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag, - ) - size = BuildImage(0, 0, font_size=50).getsize(red_bag.name) - ava_bk = BuildImage(100 + size[0], 66, is_alpha=True, font_size=50) - - ava_byte = await get_user_avatar(user_id) - ava = None - if ava_byte: - ava = BuildImage(66, 66, is_alpha=True, background=BytesIO(ava_byte)) - else: - ava = BuildImage(66, 66, color=(0, 0, 0), is_alpha=True) - await ava_bk.apaste(ava) - ava_bk.text((100, 7), red_bag.name) - ava_bk_w, ava_bk_h = ava_bk.size - await head.apaste(ava_bk, (int((1000 - ava_bk_w) / 2), 300), alpha=True) - size = BuildImage(0, 0, font_size=150).getsize(amount) - amount_image = BuildImage(size[0], size[1], is_alpha=True, font_size=150) - await amount_image.atext((0, 0), str(amount), fill=(209, 171, 108)) - # 金币中文 - await head.apaste(amount_image, (int((1000 - size[0]) / 2) - 50, 460), alpha=True) - await head.atext( - (int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70), - "金币", - fill=(209, 171, 108), - ) - # 剩余数量和金额 - text = ( - f"已领取" - f"{red_bag.num - len(red_bag.open_user)}" - f"/{red_bag.num}个," - f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币" - ) - await head.atext((350, 900), text, (198, 198, 198)) - return head diff --git a/plugins/gold_redbag/model.py b/plugins/gold_redbag/model.py deleted file mode 100755 index 76755b0d..00000000 --- a/plugins/gold_redbag/model.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import List - -from tortoise import fields - -from services.db_context import Model - - -class RedbagUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - send_redbag_count = fields.IntField(default=0) - """发送红包次数""" - get_redbag_count = fields.IntField(default=0) - """开启红包次数""" - spend_gold = fields.IntField(default=0) - """发送红包花费金额""" - get_gold = fields.IntField(default=0) - """开启红包获取金额""" - - class Meta: - table = "redbag_users" - table_description = "红包统计数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def add_redbag_data( - cls, user_id: str, group_id: str, i_type: str, money: int - ): - """ - 说明: - 添加收发红包数据 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param i_type: 收或发 - :param money: 金钱数量 - """ - - user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) - if i_type == "get": - user.get_redbag_count = user.get_redbag_count + 1 - user.get_gold = user.get_gold + money - else: - user.send_redbag_count = user.send_redbag_count + 1 - user.spend_gold = user.spend_gold + money - await user.save( - update_fields=[ - "get_redbag_count", - "get_gold", - "send_redbag_count", - "spend_gold", - ] - ) - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE redbag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE redbag_users ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE redbag_users ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/group_welcome_msg.py b/plugins/group_welcome_msg.py deleted file mode 100755 index 27c22c01..00000000 --- a/plugins/group_welcome_msg.py +++ /dev/null @@ -1,53 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from configs.path_config import DATA_PATH -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "查看群欢迎消息" -__plugin_usage__ = """ -usage: - 查看当前的群欢迎消息 - 指令: - 查看群欢迎消息 -""".strip() -__plugin_des__ = "查看群欢迎消息" -__plugin_cmd__ = ["查看群欢迎消息"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查看群欢迎消息"], -} - -view_custom_welcome = on_command( - "群欢迎消息", aliases={"查看群欢迎消息", "查看当前群欢迎消息"}, permission=GROUP, priority=5, block=True -) - - -@view_custom_welcome.handle() -async def _(event: GroupMessageEvent): - img = "" - msg = "" - if (DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg").exists(): - img = image(DATA_PATH / "custom_welcome_msg" / f"{event.group_id}.jpg") - custom_welcome_msg_json = ( - DATA_PATH / "custom_welcome_msg" / "custom_welcome_msg.json" - ) - if custom_welcome_msg_json.exists(): - data = json.load(open(custom_welcome_msg_json, "r")) - if data.get(str(event.group_id)): - msg = data[str(event.group_id)] - if msg.find("[at]") != -1: - msg = msg.replace("[at]", "") - if img or msg: - await view_custom_welcome.finish(msg + img, at_sender=True) - else: - await view_custom_welcome.finish("当前还没有自定义群欢迎消息哦", at_sender=True) diff --git a/plugins/image_management/__init__.py b/plugins/image_management/__init__.py deleted file mode 100755 index 16a9738f..00000000 --- a/plugins/image_management/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ -from pathlib import Path -from typing import List, Tuple - -import nonebot - -from configs.config import Config -from configs.path_config import IMAGE_PATH - -Config.add_plugin_config( - "image_management", - "IMAGE_DIR_LIST", - ["美图", "萝莉", "壁纸"], - name="图库操作", - help_="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]", - default_value=[], - type=List[str], -) - -Config.add_plugin_config( - "image_management", - "WITHDRAW_IMAGE_MESSAGE", - (0, 1), - name="图库操作", - help_="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - default_value=(0, 1), - type=Tuple[int, int], -) - -Config.add_plugin_config( - "image_management:delete_image", - "DELETE_IMAGE_LEVEL [LEVEL]", - 7, - help_="删除图库图片需要的管理员等级", - default_value=7, - type=int, -) - -Config.add_plugin_config( - "image_management:move_image", - "MOVE_IMAGE_LEVEL [LEVEL]", - 7, - help_="移动图库图片需要的管理员等级", - default_value=7, - type=int, -) - -Config.add_plugin_config( - "image_management:upload_image", - "UPLOAD_IMAGE_LEVEL [LEVEL]", - 6, - help_="上传图库图片需要的管理员等级", - default_value=6, - type=int, -) - -Config.add_plugin_config( - "image_management", - "SHOW_ID", - True, - help_="是否消息显示图片下标id", - default_value=True, - type=bool, -) - - -(IMAGE_PATH / "image_management").mkdir(parents=True, exist_ok=True) - - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/image_management/delete_image/__init__.py b/plugins/image_management/delete_image/__init__.py deleted file mode 100755 index 9f7d5d22..00000000 --- a/plugins/image_management/delete_image/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.message_builder import image -from utils.utils import cn2py, is_number - -__zx_plugin_name__ = "删除图片 [Admin]" -__plugin_usage__ = """ -usage: - 删除图库指定图片 - 指令: - 删除图片 [图库] [id] - 查看图库 - 示例:删除图片 美图 666 -""".strip() -__plugin_des__ = "不好看的图片删掉删掉!" -__plugin_cmd__ = ["删除图片 [图库] [id]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "DELETE_IMAGE_LEVEL") -} - - -delete_img = on_command("删除图片", priority=5, rule=to_me(), block=True) - - -_path = IMAGE_PATH / "image_management" - - -@delete_img.handle() -async def _(state: T_State, arg: Message = CommandArg()): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - args = arg.extract_plain_text().strip().split() - if args: - if args[0] in image_dir_list: - state["path"] = args[0] - if len(args) > 1 and is_number(args[1]): - state["id"] = args[1] - - -@delete_img.got("path", prompt="请输入要删除的目标图库?") -@delete_img.got("id", prompt="请输入要删除的图片id?") -async def arg_handle( - event: MessageEvent, - state: T_State, - path_: str = ArgStr("path"), - img_id: str = ArgStr("id"), -): - if path_ in ["取消", "算了"] or img_id in ["取消", "算了"]: - await delete_img.finish("已取消操作...") - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path_ not in image_dir_list: - await delete_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not is_number(img_id): - await delete_img.reject_arg("id", "id不正确!请重新输入数字...") - path = _path / cn2py(path_) - if not path.exists() and (path.parent.parent / cn2py(state["path"])).exists(): - path = path.parent.parent / cn2py(state["path"]) - max_id = len(os.listdir(path)) - 1 - if int(img_id) > max_id or int(img_id) < 0: - await delete_img.finish(f"Id超过上下限,上限:{max_id}", at_sender=True) - try: - if (TEMP_PATH / f"{event.user_id}_delete.jpg").exists(): - (TEMP_PATH / f"{event.user_id}_delete.jpg").unlink() - logger.info(f"删除{cn2py(state['path'])}图片 {img_id}.jpg 成功") - except Exception as e: - logger.warning(f"删除图片 delete.jpg 失败 e{e}") - try: - os.rename(path / f"{img_id}.jpg", TEMP_PATH / f"{event.user_id}_delete.jpg") - logger.info(f"移动 {path}/{img_id}.jpg 移动成功") - except Exception as e: - logger.warning(f"{path}/{img_id}.jpg --> 移动失败 e:{e}") - if not os.path.exists(path / f"{img_id}.jpg"): - try: - if int(img_id) != max_id: - os.rename(path / f"{max_id}.jpg", path / f"{img_id}.jpg") - except FileExistsError as e: - logger.error(f"{path}/{max_id}.jpg 替换 {path}/{img_id}.jpg 失败 e:{e}") - logger.info(f"{path}/{max_id}.jpg 替换 {path}/{img_id}.jpg 成功") - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" -> id: {img_id} 删除成功" - ) - await delete_img.finish( - f"id: {img_id} 删除成功" - + image( - TEMP_PATH / f"{event.user_id}_delete.jpg", - ), - at_sender=True, - ) - await delete_img.finish(f"id: {img_id} 删除失败!") diff --git a/plugins/image_management/move_image/__init__.py b/plugins/image_management/move_image/__init__.py deleted file mode 100755 index d8d9c039..00000000 --- a/plugins/image_management/move_image/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -from services.log import logger -from nonebot import on_command -from nonebot.rule import to_me -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from configs.config import Config -from utils.utils import is_number, cn2py -from configs.path_config import IMAGE_PATH -from nonebot.params import CommandArg, ArgStr -import os - -__zx_plugin_name__ = "移动图片 [Admin]" -__plugin_usage__ = """ -usage: - 图库间的图片移动操作 - 指令: - 移动图片 [源图库] [目标图库] [id] - 查看图库 - 示例:移动图片 萝莉 美图 234 -""".strip() -__plugin_des__ = "图库间的图片移动操作" -__plugin_cmd__ = ["移动图片 [源图库] [目标图库] [id]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "MOVE_IMAGE_LEVEL") -} - - -move_img = on_command("移动图片", priority=5, rule=to_me(), block=True) - - -_path = IMAGE_PATH / "image_management" - - -@move_img.handle() -async def _(state: T_State, arg: Message = CommandArg()): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - args = arg.extract_plain_text().strip().split() - if args: - if n := len(args): - if args[0] in image_dir_list: - state["source_path"] = args[0] - if n > 1: - if args[1] in image_dir_list: - state["destination_path"] = args[1] - if n > 2 and is_number(args[2]): - state["id"] = args[2] - - -@move_img.got("source_path", prompt="要从哪个图库移出?") -@move_img.got("destination_path", prompt="要移动到哪个图库?") -@move_img.got("id", prompt="要移动的图片id是?") -async def _( - event: MessageEvent, - source_path_: str = ArgStr("source_path"), - destination_path_: str = ArgStr("destination_path"), - img_id: str = ArgStr("id"), -): - if ( - source_path_ in ["取消", "算了"] - or img_id in ["取消", "算了"] - or destination_path_ in ["取消", "算了"] - ): - await move_img.finish("已取消操作...") - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if source_path_ not in image_dir_list: - await move_img.reject_arg("source_path", "移除目录不正确,请重新输入!") - if destination_path_ not in image_dir_list: - await move_img.reject_arg("destination_path", "移入目录不正确,请重新输入!") - if not is_number(img_id): - await move_img.reject_arg("id", "id不正确!请重新输入数字...") - source_path = _path / cn2py(source_path_) - destination_path = _path / cn2py(destination_path_) - if not source_path.exists(): - if (source_path.parent.parent / cn2py(source_path.name)).exists(): - source_path = source_path.parent.parent / cn2py(source_path.name) - if not destination_path.exists(): - if (destination_path.parent.parent / cn2py(destination_path.name)).exists(): - source_path = destination_path.parent.parent / cn2py(destination_path.name) - source_path.mkdir(exist_ok=True, parents=True) - destination_path.mkdir(exist_ok=True, parents=True) - if not len(os.listdir(source_path)): - await move_img.finish(f"{source_path}图库中没有任何图片,移动失败。") - max_id = len(os.listdir(source_path)) - 1 - des_max_id = len(os.listdir(destination_path)) - if int(img_id) > max_id or int(img_id) < 0: - await move_img.finish(f"Id超过上下限,上限:{max_id}", at_sender=True) - try: - move_file = source_path / f"{img_id}.jpg" - move_file.rename(destination_path / f"{des_max_id}.jpg") - logger.info( - f"移动 {source_path}/{img_id}.jpg ---> {destination_path}/{des_max_id} 移动成功" - ) - except Exception as e: - logger.warning( - f"移动 {source_path}/{img_id}.jpg ---> {destination_path}/{des_max_id} 移动失败 e:{e}" - ) - await move_img.finish(f"移动图片id:{img_id} 失败了...", at_sender=True) - if max_id > 0: - try: - rep_file = source_path / f"{max_id}.jpg" - rep_file.rename(source_path / f"{img_id}.jpg") - logger.info(f"{source_path}/{max_id}.jpg 替换 {source_path}/{img_id}.jpg 成功") - except Exception as e: - logger.warning( - f"{source_path}/{max_id}.jpg 替换 {source_path}/{img_id}.jpg 失败 e:{e}" - ) - await move_img.finish(f"替换图片id:{max_id} -> {img_id} 失败了...", at_sender=True) - logger.info( - f"USER {event.user_id} GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'} ->" - f" {source_path} --> {destination_path} (id:{img_id}) 移动图片成功" - ) - await move_img.finish(f"移动图片 id:{img_id} --> id:{des_max_id}成功", at_sender=True) diff --git a/plugins/image_management/send_image/__init__.py b/plugins/image_management/send_image/__init__.py deleted file mode 100755 index d7458939..00000000 --- a/plugins/image_management/send_image/__init__.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import random -from typing import List - -from nonebot import on_message, on_regex -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from nonebot.params import CommandArg - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import GetConfig -from utils.manager import withdraw_message_manager -from utils.message_builder import image -from utils.utils import FreqLimiter, cn2py, get_message_text, is_number - -from .anti import pix_random_change_file -from .rule import rule - -__zx_plugin_name__ = "本地图库" -__plugin_usage__ = f""" -usage: - 发送指定图库下的随机或指定id图片genshin_memo - 指令: - {Config.get_config("image_management", "IMAGE_DIR_LIST")} ?[id] - 示例:美图 - 示例: 萝莉 2 -""".strip() -__plugin_des__ = "让看看我的私藏,指[图片]" -__plugin_cmd__ = Config.get_config("image_management", "IMAGE_DIR_LIST") -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["发送图片"] + (Config.get_config("image_management", "IMAGE_DIR_LIST") or []), -} -__plugin_resources__ = {"pa": IMAGE_PATH / "pa"} - -Config.add_plugin_config( - "_task", "DEFAULT_PA", True, help_="被动 爬 进群默认开关状态", default_value=True, type=int -) - -_flmt = FreqLimiter(1) - - -send_img = on_message(priority=5, rule=rule, block=True) -pa_reg = on_regex("^(爬|丢人爬|爪巴)$", priority=5, block=True) - - -_path = IMAGE_PATH / "image_management" - - -@send_img.handle() -async def _( - event: MessageEvent, - image_dir_list: List[str] = GetConfig("image_management", "IMAGE_DIR_LIST", []), -): - msg = get_message_text(event.message).split() - gallery = msg[0] - if gallery not in image_dir_list: - return - img_id = None - if len(msg) > 1: - img_id = msg[1] - path = _path / cn2py(gallery) - if gallery in image_dir_list: - if not path.exists() and (path.parent.parent / cn2py(gallery)).exists(): - path = IMAGE_PATH / cn2py(gallery) - else: - path.mkdir(parents=True, exist_ok=True) - length = len(os.listdir(path)) - if not length: - logger.warning(f"图库 {cn2py(gallery)} 为空,调用取消!") - await send_img.finish("该图库中没有图片噢...") - index = img_id if img_id else str(random.randint(0, length - 1)) - if not is_number(index): - return - if int(index) > length - 1 or int(index) < 0: - await send_img.finish(f"超过当前图库的上下限了哦!({length - 1})") - result = image(pix_random_change_file(path / f"{index}.jpg")) - if result: - logger.info( - f"发送{cn2py(gallery)}:" + str(path / f"{index}.jpg"), - "发送图片", - event.user_id, - getattr(event, "group_id", None), - ) - msg_id = await send_img.send( - f"id:{index}" + result - if Config.get_config("image_management", "SHOW_ID") - else "" + result - ) - withdraw_message_manager.withdraw_message( - event, - msg_id, - Config.get_config("image_management", "WITHDRAW_IMAGE_MESSAGE"), - ) - else: - logger.info( - f"发送 {cn2py(gallery)} 不存在", - "发送图片", - event.user_id, - getattr(event, "group_id", None), - ) - await send_img.finish(f"不想给你看Ov|") - - -@pa_reg.handle() -async def _(event: MessageEvent): - if _flmt.check(event.user_id): - _flmt.start_cd(event.user_id) - path = IMAGE_PATH / "pa" - if not path.exists() or not os.listdir(IMAGE_PATH / "pa"): - await pa_reg.finish("该图库中没有图片噢...") - await pa_reg.finish( - image(IMAGE_PATH / "pa" / random.choice(os.listdir(IMAGE_PATH / "pa"))) - ) diff --git a/plugins/image_management/send_image/anti.py b/plugins/image_management/send_image/anti.py deleted file mode 100644 index c3c23b53..00000000 --- a/plugins/image_management/send_image/anti.py +++ /dev/null @@ -1,26 +0,0 @@ -import random -import warnings -from pathlib import Path - -import cv2 -import numpy as np -from PIL import Image - - -def pix_random_change(img): - # Image转cv2 - img = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) - img[0, 0, 0] = random.randint(0, 0xFFFFFFF) - # cv2转Image - img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) - return img - - -def pix_random_change_file(path: Path): - # 注意:cv2.imread()不支持路径中文 - str_path = str(path.absolute()) - warnings.filterwarnings("ignore", category=Warning) - img = cv2.imread(str_path) - img[0, 0, 0] = random.randint(0, 0xFFFFFFF) - cv2.imwrite(str_path, img) - return str_path diff --git a/plugins/image_management/send_image/rule.py b/plugins/image_management/send_image/rule.py deleted file mode 100644 index 1ce46829..00000000 --- a/plugins/image_management/send_image/rule.py +++ /dev/null @@ -1,18 +0,0 @@ -from nonebot.adapters.onebot.v11 import Bot, Event -from nonebot.typing import T_State -from utils.utils import get_message_text -from configs.config import Config - - -def rule(bot: Bot, event: Event, state: T_State) -> bool: - """ - 检测文本是否是关闭功能命令 - :param bot: pass - :param event: pass - :param state: pass - """ - msg = get_message_text(event.json()) - for x in Config.get_config("image_management", "IMAGE_DIR_LIST"): - if msg.startswith(x): - return True - return False diff --git a/plugins/image_management/upload_image/__init__.py b/plugins/image_management/upload_image/__init__.py deleted file mode 100755 index 7e7c25e0..00000000 --- a/plugins/image_management/upload_image/__init__.py +++ /dev/null @@ -1,134 +0,0 @@ -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.config import Config -from utils.depends import ImageList -from utils.utils import get_message_img - -from .data_source import upload_image_to_local - -__zx_plugin_name__ = "上传图片 [Admin]" -__plugin_usage__ = """ -usage: - 上传图片至指定图库 - 指令: - 查看图库 - 上传图片 [图库] [图片] - 连续上传图片 [图库] - 示例:上传图片 美图 [图片] - * 连续上传图片可以通过发送 “stop” 表示停止收集发送的图片,可以开始上传 * -""".strip() -__plugin_des__ = "指定图库图片上传" -__plugin_cmd__ = ["上传图片 [图库] [图片]", "连续上传图片 [图库]", "查看公开图库"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": Config.get_config("image_management", "UPLOAD_IMAGE_LEVEL") -} - -upload_img = on_command("上传图片", rule=to_me(), priority=5, block=True) - -continuous_upload_img = on_command("连续上传图片", rule=to_me(), priority=5, block=True) - -show_gallery = on_command("查看公开图库", priority=1, block=True) - - -@show_gallery.handle() -async def _(): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") - if not image_dir_list: - await show_gallery.finish("未发现任何图库") - x = "公开图库列表:\n" - for i, e in enumerate(image_dir_list): - x += f"\t{i+1}.{e}\n" - await show_gallery.send(x[:-1]) - - -@upload_img.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") - if not image_dir_list: - await show_gallery.finish("未发现任何图库") - args = arg.extract_plain_text().strip() - if args: - if args in image_dir_list: - state["path"] = args - if img_list: - state["img_list"] = arg - state["dir_list"] = "\n-".join(image_dir_list) - - -@upload_img.got( - "path", - prompt=Message.template("请选择要上传的图库\n-{dir_list}"), -) -@upload_img.got("img_list", prompt="图呢图呢图呢图呢!GKD!") -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - path: str = ArgStr("path"), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path not in image_dir_list: - await upload_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not img_list: - await upload_img.reject_arg("img_list", "图呢图呢图呢图呢!GKD!") - group_id = 0 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - await upload_img.send( - await upload_image_to_local(img_list, path, event.user_id, group_id) - ) - - -@continuous_upload_img.handle() -async def _( - event: MessageEvent, - state: T_State, - arg: Message = CommandArg(), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - path = arg.extract_plain_text().strip() - if path in image_dir_list: - state["path"] = path - state["img_list"] = [] - state["dir_list"] = "\n-".join(image_dir_list) - - -@continuous_upload_img.got("path", prompt=Message.template("请选择要上传的图库\n-{dir_list}")) -@continuous_upload_img.got("img", prompt="图呢图呢图呢图呢!GKD!【发送‘stop’为停止】") -async def _( - event: MessageEvent, - state: T_State, - collect_img_list: List[str] = Arg("img_list"), - path: str = ArgStr("path"), - img: Message = Arg("img"), - img_list: List[str] = ImageList(), -): - image_dir_list = Config.get_config("image_management", "IMAGE_DIR_LIST") or [] - if path not in image_dir_list: - await upload_img.reject_arg("path", "此目录不正确,请重新输入目录!") - if not img.extract_plain_text() == "stop": - if img_list: - for i in img_list: - collect_img_list.append(i) - await upload_img.reject_arg("img", "图再来!!【发送‘stop’为停止】") - group_id = 0 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - await continuous_upload_img.send( - await upload_image_to_local(collect_img_list, path, event.user_id, group_id) - ) diff --git a/plugins/image_management/upload_image/data_source.py b/plugins/image_management/upload_image/data_source.py deleted file mode 100755 index b04b3e77..00000000 --- a/plugins/image_management/upload_image/data_source.py +++ /dev/null @@ -1,48 +0,0 @@ -import os -from typing import List - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.utils import cn2py - -_path = IMAGE_PATH / "image_management" - - -async def upload_image_to_local( - img_list: List[str], path_: str, user_id: int, group_id: int = 0 -) -> str: - _path_name = path_ - path = _path / cn2py(path_) - if not path.exists() and (path.parent.parent / cn2py(path_)).exists(): - path = path.parent.parent / cn2py(path_) - path.mkdir(parents=True, exist_ok=True) - img_id = len(os.listdir(path)) - failed_list = [] - success_id = "" - for img_url in img_list: - if await AsyncHttpx.download_file(img_url, path / f"{img_id}.jpg"): - success_id += str(img_id) + "," - img_id += 1 - else: - failed_list.append(img_url) - failed_result = "" - for img in failed_list: - failed_result += str(img) + "\n" - logger.info( - f"上传图片至 {_path_name} 共 {len(img_list)} 张,失败 {len(failed_list)} 张,id={success_id[:-1]}", - "上传图片", - user_id, - group_id, - ) - if failed_list: - return ( - f"这次一共为 {_path_name}库 添加了 {len(img_list) - len(failed_list)} 张图片\n" - f"依次的Id为:{success_id[:-1]}\n上传失败:{failed_result[:-1]}\n{NICKNAME}感谢您对图库的扩充!WW" - ) - else: - return ( - f"这次一共为 {_path_name}库 添加了 {len(img_list)} 张图片\n依次的Id为:" - f"{success_id[:-1]}\n{NICKNAME}感谢您对图库的扩充!WW" - ) diff --git a/plugins/luxun/__init__.py b/plugins/luxun/__init__.py deleted file mode 100755 index 38c2ee85..00000000 --- a/plugins/luxun/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -from configs.path_config import IMAGE_PATH -from nonebot import on_command -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from utils.message_builder import image -from services.log import logger -from utils.image_utils import BuildImage -from nonebot.params import CommandArg - -__zx_plugin_name__ = "鲁迅说" -__plugin_usage__ = """ -usage: - 鲁迅说了啥? - 指令: - 鲁迅说 [文本] -""".strip() -__plugin_des__ = "鲁迅说他没说过这话!" -__plugin_cmd__ = ["鲁迅说"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["鲁迅说"], -} -__plugin_block_limit__ = { - "rst": "你的鲁迅正在说,等会" -} - -luxun = on_command("鲁迅说过", aliases={"鲁迅说"}, priority=5, block=True) - - -luxun_author = BuildImage(0, 0, plain_text="--鲁迅", font_size=30, font='msyh.ttf', font_color=(255, 255, 255)) - - -@luxun.handle() -async def handle(state: T_State, arg: Message = CommandArg()): - args = arg.extract_plain_text().strip() - if args: - state["content"] = args if args else "烦了,不说了" - - -@luxun.got("content", prompt="你让鲁迅说点啥?") -async def handle_event(event: MessageEvent, state: T_State): - content = state["content"].strip() - if content.startswith(",") or content.startswith(","): - content = content[1:] - A = BuildImage(0, 0, font_size=37, background=f'{IMAGE_PATH}/other/luxun.jpg', font='msyh.ttf') - x = "" - if len(content) > 40: - await luxun.finish('太长了,鲁迅说不完...') - while A.getsize(content)[0] > A.w - 50: - n = int(len(content) / 2) - x += content[:n] + '\n' - content = content[n:] - x += content - if len(x.split('\n')) > 2: - await luxun.finish('太长了,鲁迅说不完...') - A.text((int((480 - A.getsize(x.split("\n")[0])[0]) / 2), 300), x, (255, 255, 255)) - A.paste(luxun_author, (320, 400), True) - await luxun.send(image(b64=A.pic2bs4())) - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 鲁迅说过 {content}" - ) diff --git a/plugins/music/__init__.py b/plugins/music/__init__.py deleted file mode 100644 index 109b215c..00000000 --- a/plugins/music/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -from .music_163 import get_song_id, get_song_info -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from nonebot.typing import T_State -from services.log import logger -from nonebot import on_command -from utils.message_builder import music - - -__zx_plugin_name__ = "点歌" -__plugin_usage__ = """ -usage: - 在线点歌 - 指令: - 点歌 [歌名] -""".strip() -__plugin_des__ = "为你点播了一首曾经的歌" -__plugin_cmd__ = ["点歌 [歌名]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["点歌"], -} - - -music_handler = on_command("点歌", priority=5, block=True) - - -@music_handler.handle() -async def handle_first_receive(state: T_State, arg: Message = CommandArg()): - if args := arg.extract_plain_text().strip(): - state["song_name"] = args - - -@music_handler.got("song_name", prompt="歌名是?") -async def _(bot: Bot, event: MessageEvent, state: T_State): - song = state["song_name"] - song_id = await get_song_id(song) - if not song_id: - await music_handler.finish("没有找到这首歌!", at_sender=True) - await music_handler.send(music("163", song_id)) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 点歌 :{song}" - ) - - - - diff --git a/plugins/music/music_163.py b/plugins/music/music_163.py deleted file mode 100644 index 00f24df2..00000000 --- a/plugins/music/music_163.py +++ /dev/null @@ -1,41 +0,0 @@ -from utils.http_utils import AsyncHttpx -import json - - -headers = {"referer": "http://music.163.com"} -cookies = {"appver": "2.0.2"} - - -async def search_song(song_name: str): - """ - 搜索歌曲 - :param song_name: 歌名 - """ - r = await AsyncHttpx.post( - f"http://music.163.com/api/search/get/", - data={"s": song_name, "limit": 1, "type": 1, "offset": 0}, - ) - if r.status_code != 200: - return None - return json.loads(r.text) - - -async def get_song_id(song_name: str) -> int: - """ """ - r = await search_song(song_name) - try: - return r["result"]["songs"][0]["id"] - except KeyError: - return 0 - - -async def get_song_info(songId: int): - """ - 获取歌曲信息 - """ - r = await AsyncHttpx.post( - f"http://music.163.com/api/song/detail/?id={songId}&ids=%5B{songId}%5D", - ) - if r.status_code != 200: - return None - return json.loads(r.text) diff --git a/plugins/mute.py b/plugins/mute.py deleted file mode 100755 index b4e200e9..00000000 --- a/plugins/mute.py +++ /dev/null @@ -1,188 +0,0 @@ -import time -from io import BytesIO -from typing import Any, Dict, Tuple - -import imagehash -from nonebot import on_command, on_message -from nonebot.adapters.onebot.v11 import ActionFailed, Bot, GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import Command, CommandArg -from PIL import Image - -from configs.config import NICKNAME, Config -from configs.path_config import DATA_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.utils import get_message_img, get_message_text, is_number - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "刷屏禁言 [Admin]" -__plugin_usage__ = f""" -usage: - 刷屏禁言相关操作,需要 {NICKNAME} 有群管理员权限 - 指令: - 设置刷屏检测时间 [秒] - 设置刷屏检测次数 [次数] - 设置刷屏禁言时长 [分钟] - 刷屏检测设置: 查看当前的刷屏检测设置 - * 即 X 秒内发送同样消息 N 次,禁言 M 分钟 * -""".strip() -__plugin_des__ = "刷屏禁言相关操作" -__plugin_cmd__ = ["设置刷屏检测时间 [秒]", "设置刷屏检测次数 [次数]", "设置刷屏禁言时长 [分钟]", "刷屏检测设置"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = {"admin_level": Config.get_config("mute", "MUTE_LEVEL")} -__plugin_configs__ = { - "MUTE_LEVEL [LEVEL]": { - "value": 5, - "help": "更改禁言设置的管理权限", - "default_value": 5, - "type": int, - }, - "MUTE_DEFAULT_COUNT": { - "value": 10, - "help": "刷屏禁言默认检测次数", - "default_value": 10, - "type": int, - }, - "MUTE_DEFAULT_TIME": { - "value": 7, - "help": "刷屏检测默认规定时间", - "default_value": 7, - "type": int, - }, - "MUTE_DEFAULT_DURATION": { - "value": 10, - "help": "刷屏检测默禁言时长(分钟)", - "default_value": 10, - "type": int, - }, -} - - -mute = on_message(priority=1, block=False) -mute_setting = on_command( - "mute_setting", - aliases={"设置刷屏检测时间", "设置刷屏检测次数", "设置刷屏禁言时长", "刷屏检测设置"}, - permission=GROUP, - block=True, - priority=5, -) - - -def get_data() -> Dict[Any, Any]: - try: - with open(DATA_PATH / "group_mute_data.json", "r", encoding="utf8") as f: - data = json.load(f) - except (ValueError, FileNotFoundError): - data = {} - return data - - -def save_data(): - global mute_data - with open(DATA_PATH / "group_mute_data.json", "w", encoding="utf8") as f: - json.dump(mute_data, f, indent=4) - - -async def download_img_and_hash(url) -> str: - return str( - imagehash.average_hash(Image.open(BytesIO((await AsyncHttpx.get(url)).content))) - ) - - -mute_dict = {} -mute_data = get_data() - - -@mute.handle() -async def _(bot: Bot, event: GroupMessageEvent): - group_id = str(event.group_id) - msg = get_message_text(event.json()) - img_list = get_message_img(event.json()) - img_hash = "" - for img in img_list: - img_hash += await download_img_and_hash(img) - msg += img_hash - if not mute_data.get(group_id): - mute_data[group_id] = { - "count": Config.get_config("mute", "MUTE_DEFAULT_COUNT"), - "time": Config.get_config("mute", "MUTE_DEFAULT_TIME"), - "duration": Config.get_config("mute", "MUTE_DEFAULT_DURATION"), - } - if not mute_dict.get(event.user_id): - mute_dict[event.user_id] = {"time": time.time(), "count": 1, "msg": msg} - else: - if msg and msg.find(mute_dict[event.user_id]["msg"]) != -1: - mute_dict[event.user_id]["count"] += 1 - else: - mute_dict[event.user_id]["time"] = time.time() - mute_dict[event.user_id]["count"] = 1 - mute_dict[event.user_id]["msg"] = msg - if time.time() - mute_dict[event.user_id]["time"] > mute_data[group_id]["time"]: - mute_dict[event.user_id]["time"] = time.time() - mute_dict[event.user_id]["count"] = 1 - if ( - mute_dict[event.user_id]["count"] > mute_data[group_id]["count"] - and time.time() - mute_dict[event.user_id]["time"] - < mute_data[group_id]["time"] - ): - try: - if mute_data[group_id]["duration"] != 0: - await bot.set_group_ban( - group_id=event.group_id, - user_id=event.user_id, - duration=mute_data[group_id]["duration"] * 60, - ) - await mute.send(f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!", at_sender=True) - mute_dict[event.user_id]["count"] = 0 - logger.info( - f"USER {event.user_id} GROUP {event.group_id} " - f'检测刷屏 被禁言 {mute_data[group_id]["duration"] / 60} 分钟' - ) - except ActionFailed: - pass - - -@mute_setting.handle() -async def _( - event: GroupMessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - global mute_data - group_id = str(event.group_id) - if not mute_data.get(group_id): - mute_data[group_id] = { - "count": Config.get_config("mute", "MUTE_DEFAULT_COUNT"), - "time": Config.get_config("mute", "MUTE_DEFAULT_TIME"), - "duration": Config.get_config("mute", "MUTE_DEFAULT_DURATION"), - } - msg = arg.extract_plain_text().strip() - if cmd[0] == "刷屏检测设置": - await mute_setting.finish( - f'最大次数:{mute_data[group_id]["count"]} 次\n' - f'规定时间:{mute_data[group_id]["time"]} 秒\n' - f'禁言时长:{mute_data[group_id]["duration"]:.2f} 分钟\n' - f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】" - ) - if not is_number(msg): - await mute.finish("设置的参数必须是数字啊!", at_sender=True) - if cmd[0] == "设置刷屏检测时间": - mute_data[group_id]["time"] = int(msg) - msg += "秒" - if cmd[0] == "设置刷屏检测次数": - mute_data[group_id]["count"] = int(msg) - msg += " 次" - if cmd[0] == "设置刷屏禁言时长": - mute_data[group_id]["duration"] = int(msg) - msg += " 分钟" - await mute_setting.send(f"刷屏检测:{cmd[0]}为 {msg}") - logger.info(f"USER {event.user_id} GROUP {group_id} {cmd[0]}:{msg}") - save_data() diff --git a/plugins/my_info/__init__.py b/plugins/my_info/__init__.py deleted file mode 100644 index 94ea437b..00000000 --- a/plugins/my_info/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import timedelta - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser - -__zx_plugin_name__ = "个人信息权限查看" -__plugin_usage__ = """ -usage: - 个人信息权限查看 - 指令: - 我的信息 - 我的权限 -""".strip() -__plugin_des__ = "我们还记得你和你的权利" -__plugin_cmd__ = ["我的信息", "我的权限"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -get_my_group_info = on_command("我的信息", permission=GROUP, priority=1, block=True) -my_level = on_command("我的权限", permission=GROUP, priority=5, block=True) - - -@get_my_group_info.handle() -async def _(event: GroupMessageEvent): - result = await get_member_info(str(event.user_id), str(event.group_id)) - await get_my_group_info.finish(result) - - -async def get_member_info(user_id: str, group_id: str) -> str: - if user := await GroupInfoUser.get_or_none(user_id=user_id, group_id=group_id): - result = "" - result += "昵称:" + user.user_name + "\n" - result += "加群时间:" + str(user.user_join_time.date()) - return result - else: - return "该群员不在列表中,请更新群成员信息" - - -@my_level.handle() -async def _(event: GroupMessageEvent): - if (level := await LevelUser.get_user_level(event.user_id, event.group_id)) == -1: - await my_level.finish("您目前没有任何权限了,硬要说的话就是0吧~", at_sender=True) - await my_level.finish(f"您目前的权限等级:{level}", at_sender=True) diff --git a/plugins/nbnhhsh.py b/plugins/nbnhhsh.py deleted file mode 100755 index bbbb3f3b..00000000 --- a/plugins/nbnhhsh.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from utils.http_utils import AsyncHttpx -from services.log import logger -import ujson as json - - -__zx_plugin_name__ = "能不能好好说话" -__plugin_usage__ = """ -usage: - 说人话 - 指令: - nbnhhsh [文本] -""".strip() -__plugin_des__ = "能不能好好说话,说人话" -__plugin_cmd__ = ["nbnhhsh [文本]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["能不能好好说话", "nbnhhsh"], -} - -HHSH_GUESS_URL = "https://lab.magiconch.com/api/nbnhhsh/guess" - -nbnhhsh = on_command("nbnhhsh", aliases={"能不能好好说话"}, priority=5, block=True) - - -@nbnhhsh.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await nbnhhsh.finish("没话说就别说话!") - response = await AsyncHttpx.post( - HHSH_GUESS_URL, - data=json.dumps({"text": msg}), - timeout=5, - headers={"content-type": "application/json"}, - ) - try: - data = response.json() - tmp = "" - rst = "" - for x in data: - trans = "" - if x.get("trans"): - trans = x["trans"][0] - elif x.get("inputting"): - trans = ",".join(x["inputting"]) - tmp += f'{x["name"]} -> {trans}\n' - rst += trans - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送能不能好好说话: {msg} -> {rst}" - ) - await nbnhhsh.send(f"{tmp}={rst}", at_sender=True) - except (IndexError, KeyError): - await nbnhhsh.finish("没有找到对应的翻译....") diff --git a/plugins/one_friend/__init__.py b/plugins/one_friend/__init__.py deleted file mode 100755 index 2c6add40..00000000 --- a/plugins/one_friend/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -from io import BytesIO -from random import choice -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message -from utils.utils import get_message_at, get_user_avatar, get_message_text -from utils.message_builder import image -from utils.image_utils import BuildImage -from nonebot.params import RegexGroup -from typing import Tuple, Any - -__zx_plugin_name__ = "我有一个朋友" -__plugin_usage__ = """ -usage: - 我有一个朋友他...,不知道是不是你 - 指令: - 我有一个朋友想问问 [文本] ?[at]: 当at时你的朋友就是艾特对象 -""".strip() -__plugin_des__ = "我有一个朋友想问问..." -__plugin_cmd__ = ["我有一个朋友想问问[文本] ?[at]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["我有一个朋友想问问", "我有一个朋友"], -} - -one_friend = on_regex( - "^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(.{0,30})$", - priority=4, - block=True, -) - - -@one_friend.handle() -async def _(bot: Bot, event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - qq = get_message_at(event.json()) - if not qq: - qq = choice( - [ - x["user_id"] - for x in await bot.get_group_member_list( - group_id=event.group_id - ) - ] - ) - user_name = "朋友" - else: - qq = qq[0] - at_user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) - user_name = at_user["card"] or at_user["nickname"] - msg = get_message_text(Message(reg_group[0])).strip() - if not msg: - msg = "都不知道问什么" - msg = msg.replace("他", "我").replace("她", "我").replace("它", "我") - x = await get_user_avatar(qq) - if x: - ava = BuildImage(200, 100, background=BytesIO(x)) - else: - ava = BuildImage(200, 100, color=(0, 0, 0)) - ava.circle() - text = BuildImage(400, 30, font_size=30) - text.text((0, 0), user_name) - A = BuildImage(700, 150, font_size=25, color="white") - await A.apaste(ava, (30, 25), True) - await A.apaste(text, (150, 38)) - await A.atext((150, 85), msg, (125, 125, 125)) - - await one_friend.send(image(b64=A.pic2bs4())) diff --git a/plugins/open_cases/__init__.py b/plugins/open_cases/__init__.py deleted file mode 100755 index 4c1415a0..00000000 --- a/plugins/open_cases/__init__.py +++ /dev/null @@ -1,326 +0,0 @@ -import asyncio -import random -from datetime import datetime, timedelta -from typing import Any, List, Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup -from nonebot.permission import SUPERUSER -from nonebot.plugin import MatcherGroup -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.depends import OneCommand -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import CN2NUM, is_number, scheduler - -from .open_cases_c import ( - auto_update, - get_my_knifes, - group_statistics, - open_case, - open_multiple_case, - total_open_statistics, -) -from .utils import ( - CASE2ID, - KNIFE2ID, - CaseManager, - build_case_image, - download_image, - get_skin_case, - init_skin_trends, - reset_count_daily, - update_skin_data, -) - -__zx_plugin_name__ = "开箱" -__plugin_usage__ = """ -usage: - 看看你的人品罢了 - 模拟开箱,完美公布的真实概率,只想看看替你省了多少钱 - 指令: - 开箱 ?[武器箱] - [1-30]连开箱 ?[武器箱] - 我的开箱 - 我的金色 - 群开箱统计 - 查看武器箱?[武器箱] - * 不包含[武器箱]时随机开箱 * - 示例: 查看武器箱 - 示例: 查看武器箱英勇 -""".strip() -__plugin_superuser_usage__ = """ -usage: - 更新皮肤指令 - 重置开箱: 重置今日开箱所有次数 - 指令: - 更新武器箱 ?[武器箱/ALL] - 更新皮肤 ?[名称/ALL1] - 更新皮肤 ?[名称/ALL1] -S: (必定更新罕见皮肤所属箱子) - 更新武器箱图片 - * 不指定武器箱时则全部更新 * - * 过多的爬取会导致账号API被封 * -""".strip() -__plugin_des__ = "csgo模拟开箱[戒赌]" -__plugin_cmd__ = [ - "开箱 ?[武器箱]", - "[1-30]连开箱 ?[武器箱]", - "我的开箱", - "我的金色", - "群开箱统计", - "查看武器箱?[武器箱]", - "更新武器箱 ?[武器箱] [_superuser]", - "更新皮肤 ?[刀具名称] [_superuser]", - "更新武器箱图片 [_superuser]", -] -__plugin_type__ = ("抽卡相关", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["csgo开箱", "开箱"], -} -__plugin_task__ = {"open_case_reset_remind": "每日开箱重置提醒"} -__plugin_cd_limit__ = {"rst": "着什么急啊,慢慢来!"} -__plugin_resources__ = {f"cases": IMAGE_PATH} -__plugin_configs__ = { - "INITIAL_OPEN_CASE_COUNT": { - "value": 20, - "help": "初始每日开箱次数", - "default_value": 20, - "type": int, - }, - "EACH_IMPRESSION_ADD_COUNT": { - "value": 3, - "help": "每 * 点好感度额外增加开箱次数", - "default_value": 3, - "type": int, - }, - "COOKIE": {"value": None, "help": "BUFF的cookie", "type": str}, - "BUFF_PROXY": {"value": None, "help": "使用代理访问BUFF"}, - "DAILY_UPDATE": { - "value": None, - "help": "每日自动更新的武器箱,存在'ALL'时则更新全部武器箱", - "type": List[str], - }, -} - -Config.add_plugin_config( - "_task", - "DEFAULT_OPEN_CASE_RESET_REMIND", - True, - help_="被动 每日开箱重置提醒 进群默认开关状态", - default_value=True, - type=bool, -) - - -cases_matcher_group = MatcherGroup(priority=5, permission=GROUP, block=True) - - -k_open_case = cases_matcher_group.on_command("开箱") -reload_count = cases_matcher_group.on_command("重置开箱", permission=SUPERUSER) -total_case_data = cases_matcher_group.on_command( - "我的开箱", aliases={"开箱统计", "开箱查询", "查询开箱"} -) -group_open_case_statistics = cases_matcher_group.on_command("群开箱统计") -open_multiple = cases_matcher_group.on_regex("(.*)连开箱(.*)?") -update_case = on_command( - "更新武器箱", aliases={"更新皮肤"}, priority=1, permission=SUPERUSER, block=True -) -update_case_image = on_command("更新武器箱图片", priority=1, permission=SUPERUSER, block=True) -show_case = on_command("查看武器箱", priority=5, block=True) -my_knifes = on_command("我的金色", priority=1, permission=GROUP, block=True) -show_skin = on_command("查看皮肤", priority=5, block=True) -price_trends = on_command("价格趋势", priority=5, block=True) - - -@price_trends.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().replace("武器箱", "").strip() - if not msg: - await price_trends.finish("未指定皮肤") - msg_split = msg.split() - if len(msg_split) < 3: - await price_trends.finish("参数不足, [类型名称] [皮肤名称] [磨损程度] ?[天数=7]") - abrasion = msg_split[2] - day = 7 - if len(msg_split) > 3: - if not is_number(msg_split[3]): - await price_trends.finish("天数必须为数字") - day = int(msg_split[3]) - if day <= 0 or day > 180: - await price_trends.finish("天数必须大于0且小于180") - result = await init_skin_trends(msg_split[0], msg_split[1], msg_split[2], day) - if not result: - await price_trends.finish("未查询到数据") - await price_trends.send( - image(await init_skin_trends(msg_split[0], msg_split[1], msg_split[2], day)) - ) - - -@reload_count.handle() -async def _(event: GroupMessageEvent): - await reset_count_daily() - - -@k_open_case.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - case_name = arg.extract_plain_text().strip() - case_name = case_name.replace("武器箱", "").strip() - result = await open_case(event.user_id, event.group_id, case_name) - await k_open_case.finish(result, at_sender=True) - - -@total_case_data.handle() -async def _(event: GroupMessageEvent): - await total_case_data.finish( - await total_open_statistics(event.user_id, event.group_id), - at_sender=True, - ) - - -@group_open_case_statistics.handle() -async def _(event: GroupMessageEvent): - await group_open_case_statistics.finish(await group_statistics(event.group_id)) - - -@my_knifes.handle() -async def _(event: GroupMessageEvent): - await my_knifes.finish( - await get_my_knifes(event.user_id, event.group_id), at_sender=True - ) - - -@open_multiple.handle() -async def _( - event: GroupMessageEvent, state: T_State, reg_group: Tuple[Any, ...] = RegexGroup() -): - num, case_name = reg_group - if is_number(num) or CN2NUM.get(num): - try: - num = CN2NUM[num] - except KeyError: - num = int(num) - if num > 30: - await open_multiple.finish("开箱次数不要超过30啊笨蛋!", at_sender=True) - if num < 0: - await open_multiple.finish("再负开箱就扣你明天开箱数了!", at_sender=True) - else: - await open_multiple.finish("必须要是数字切不要超过30啊笨蛋!中文也可!", at_sender=True) - case_name = case_name.replace("武器箱", "").strip() - await open_multiple.finish( - await open_multiple_case(event.user_id, event.group_id, case_name, num), - at_sender=True, - ) - - -@update_case.handle() -async def _(event: MessageEvent, arg: Message = CommandArg(), cmd: str = OneCommand()): - msg = arg.extract_plain_text().strip() - is_update_case_name = "-S" in msg - msg = msg.replace("-S", "").strip() - if not msg: - case_list = [] - skin_list = [] - for i, case_name in enumerate(CASE2ID): - if case_name in CaseManager.CURRENT_CASES: - case_list.append(f"{i+1}.{case_name} [已更新]") - else: - case_list.append(f"{i+1}.{case_name}") - for skin_name in KNIFE2ID: - skin_list.append(f"{skin_name}") - text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list) - await update_case.finish( - "未指定武器箱, 当前已包含武器箱/皮肤\n" - + image(await text2image(text, padding=20, color="#f9f6f2")) - ) - if msg in ["ALL", "ALL1"]: - if msg == "ALL": - case_list = list(CASE2ID.keys()) - type_ = "武器箱" - else: - case_list = list(KNIFE2ID.keys()) - type_ = "罕见皮肤" - await update_case.send(f"即将更新所有{type_}, 请稍等") - for i, case_name in enumerate(case_list): - try: - info = await update_skin_data(case_name, is_update_case_name) - if "请先登录" in info: - await update_case.send(f"未登录, 已停止更新, 请配置BUFF token...") - return - rand = random.randint(300, 500) - result = f"更新全部{type_}完成" - if i < len(case_list) - 1: - next_case = case_list[i + 1] - result = f"将在 {rand} 秒后更新下一{type_}: {next_case}" - await update_case.send(f"{info}, {result}") - logger.info(f"info, {result}", "更新武器箱") - await asyncio.sleep(rand) - except Exception as e: - logger.error(f"更新{type_}: {case_name}", e=e) - await update_case.send(f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}") - await update_case.send(f"更新全部{type_}完成") - else: - await update_case.send(f"开始{cmd}: {msg}, 请稍等") - try: - await update_case.send( - await update_skin_data(msg, is_update_case_name), at_sender=True - ) - except Exception as e: - logger.error(f"{cmd}: {msg}", e=e) - await update_case.send(f"成功{cmd}: {msg} 发生错误: {type(e)}: {e}") - - -@show_case.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - result = await build_case_image(msg) - if isinstance(result, str): - await show_case.send(result) - else: - await show_case.send(image(result)) - - -@update_case_image.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - await update_case_image.send("开始更新图片...") - await download_image(msg) - await update_case_image.send("更新图片完成...", at_sender=True) - - -# 重置开箱 -@scheduler.scheduled_job( - "cron", - hour=0, - minute=1, -) -async def _(): - await reset_count_daily() - - -@scheduler.scheduled_job( - "cron", - hour=0, - minute=10, -) -async def _(): - now = datetime.now() - hour = random.choice([0, 1, 2, 3]) - date = now + timedelta(hours=hour) - logger.debug(f"将在 {date} 时自动更新武器箱...", "更新武器箱") - scheduler.add_job( - auto_update, - "date", - run_date=date.replace(microsecond=0), - id=f"auto_update_csgo_cases", - ) diff --git a/plugins/open_cases/build_image.py b/plugins/open_cases/build_image.py deleted file mode 100644 index 972eb45f..00000000 --- a/plugins/open_cases/build_image.py +++ /dev/null @@ -1,156 +0,0 @@ -from datetime import timedelta, timezone -from typing import Optional - -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.image_utils import BuildImage -from utils.utils import cn2py - -from .config import COLOR2COLOR, COLOR2NAME -from .models.buff_skin import BuffSkin - -BASE_PATH = IMAGE_PATH / "csgo_cases" - -ICON_PATH = IMAGE_PATH / "_icon" - - -async def draw_card(skin: BuffSkin, rand: str) -> BuildImage: - """构造抽取图片 - - Args: - skin (BuffSkin): BuffSkin - rand (str): 磨损 - - Returns: - BuildImage: BuildImage - """ - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" - if not file_path.exists(): - logger.warning(f"皮肤图片: {name} 不存在", "开箱") - skin_bk = BuildImage( - 460, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_image = BuildImage(205, 153, background=file_path) - await skin_bk.apaste(skin_image, (10, 30), alpha=True) - await skin_bk.aline((220, 10, 220, 180)) - await skin_bk.atext((10, 10), skin.name, (255, 255, 255)) - name_icon = BuildImage(20, 20, background=ICON_PATH / "name_white.png") - await skin_bk.apaste(name_icon, (240, 13), True) - await skin_bk.atext((265, 15), f"名称:", (255, 255, 255), font_size=20) - await skin_bk.atext( - (300, 9), - f"{skin.skin_name + ('(St)' if skin.is_stattrak else '')}", - (255, 255, 255), - ) - tone_icon = BuildImage(20, 20, background=ICON_PATH / "tone_white.png") - await skin_bk.apaste(tone_icon, (240, 45), True) - await skin_bk.atext((265, 45), "品质:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 40), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color]) - type_icon = BuildImage(20, 20, background=ICON_PATH / "type_white.png") - await skin_bk.apaste(type_icon, (240, 73), True) - await skin_bk.atext((265, 75), "类型:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 70), skin.weapon_type, (255, 255, 255)) - price_icon = BuildImage(20, 20, background=ICON_PATH / "price_white.png") - await skin_bk.apaste(price_icon, (240, 103), True) - await skin_bk.atext((265, 105), "价格:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 102), str(skin.sell_min_price), (0, 255, 98)) - abrasion_icon = BuildImage(20, 20, background=ICON_PATH / "abrasion_white.png") - await skin_bk.apaste(abrasion_icon, (240, 133), True) - await skin_bk.atext((265, 135), "磨损:", (255, 255, 255), font_size=20) - await skin_bk.atext((300, 130), skin.abrasion, (255, 255, 255)) - await skin_bk.atext((228, 165), f"({rand})", (255, 255, 255)) - return skin_bk - - -async def generate_skin(skin: BuffSkin, update_count: int) -> Optional[BuildImage]: - """构造皮肤图片 - - Args: - skin (BuffSkin): BuffSkin - - Returns: - Optional[BuildImage]: 图片 - """ - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" - if not file_path.exists(): - logger.warning(f"皮肤图片: {name} 不存在", "查看武器箱") - if skin.color == "CASE": - case_bk = BuildImage( - 700, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_img = BuildImage(200, 200, background=file_path) - await case_bk.apaste(skin_img, (10, 10), True) - await case_bk.aline((250, 10, 250, 190)) - await case_bk.aline((280, 160, 660, 160)) - name_icon = BuildImage(30, 30, background=ICON_PATH / "box_white.png") - await case_bk.apaste(name_icon, (260, 25), True) - await case_bk.atext((295, 30), "名称:", (255, 255, 255)) - await case_bk.atext((345, 25), skin.case_name, (255, 0, 38), font_size=30) - - type_icon = BuildImage(30, 30, background=ICON_PATH / "type_white.png") - await case_bk.apaste(type_icon, (260, 70), True) - await case_bk.atext((295, 75), "类型:", (255, 255, 255)) - await case_bk.atext((345, 72), "武器箱", (0, 157, 255), font_size=30) - - price_icon = BuildImage(30, 30, background=ICON_PATH / "price_white.png") - await case_bk.apaste(price_icon, (260, 114), True) - await case_bk.atext((295, 120), "单价:", (255, 255, 255)) - await case_bk.atext( - (340, 116), str(skin.sell_min_price), (0, 255, 98), font_size=30 - ) - - update_count_icon = BuildImage( - 40, 40, background=ICON_PATH / "reload_white.png" - ) - await case_bk.apaste(update_count_icon, (575, 10), True) - await case_bk.atext((625, 12), str(update_count), (255, 255, 255), font_size=45) - - num_icon = BuildImage(30, 30, background=ICON_PATH / "num_white.png") - await case_bk.apaste(num_icon, (455, 70), True) - await case_bk.atext((490, 75), "在售:", (255, 255, 255)) - await case_bk.atext((535, 72), str(skin.sell_num), (144, 0, 255), font_size=30) - - want_buy_icon = BuildImage(30, 30, background=ICON_PATH / "want_buy_white.png") - await case_bk.apaste(want_buy_icon, (455, 114), True) - await case_bk.atext((490, 120), "求购:", (255, 255, 255)) - await case_bk.atext((535, 116), str(skin.buy_num), (144, 0, 255), font_size=30) - - await case_bk.atext((275, 165), "更新时间", (255, 255, 255), font_size=22) - date = str( - skin.update_time.replace(microsecond=0).astimezone( - timezone(timedelta(hours=8)) - ) - ).split("+")[0] - await case_bk.atext( - (344, 170), - date, - (255, 255, 255), - font_size=30, - ) - return case_bk - else: - skin_bk = BuildImage( - 235, 250, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" - ) - if file_path.exists(): - skin_image = BuildImage(205, 153, background=file_path) - await skin_bk.apaste(skin_image, (10, 30), alpha=True) - update_count_icon = BuildImage( - 35, 35, background=ICON_PATH / "reload_white.png" - ) - await skin_bk.aline((10, 180, 220, 180)) - await skin_bk.atext((10, 10), skin.name, (255, 255, 255)) - await skin_bk.apaste(update_count_icon, (140, 10), True) - await skin_bk.atext((175, 15), str(update_count), (255, 255, 255)) - await skin_bk.atext((10, 185), f"{skin.skin_name}", (255, 255, 255), "by_width") - await skin_bk.atext((10, 218), "品质:", (255, 255, 255)) - await skin_bk.atext( - (55, 218), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color] - ) - await skin_bk.atext((100, 218), "类型:", (255, 255, 255)) - await skin_bk.atext((145, 218), skin.weapon_type, (255, 255, 255)) - return skin_bk diff --git a/plugins/open_cases/config.py b/plugins/open_cases/config.py deleted file mode 100755 index 43afbecb..00000000 --- a/plugins/open_cases/config.py +++ /dev/null @@ -1,255 +0,0 @@ -import random -from enum import Enum -from typing import List, Tuple - -from configs.path_config import IMAGE_PATH -from services.log import logger - -from .models.buff_skin import BuffSkin - -BLUE = 0.7981 -BLUE_ST = 0.0699 -PURPLE = 0.1626 -PURPLE_ST = 0.0164 -PINK = 0.0315 -PINK_ST = 0.0048 -RED = 0.0057 -RED_ST = 0.00021 -KNIFE = 0.0021 -KNIFE_ST = 0.000041 - -# 崭新 -FACTORY_NEW_S = 0 -FACTORY_NEW_E = 0.0699999 -# 略磨 -MINIMAL_WEAR_S = 0.07 -MINIMAL_WEAR_E = 0.14999 -# 久经 -FIELD_TESTED_S = 0.15 -FIELD_TESTED_E = 0.37999 -# 破损 -WELL_WORN_S = 0.38 -WELL_WORN_E = 0.44999 -# 战痕 -BATTLE_SCARED_S = 0.45 -BATTLE_SCARED_E = 0.99999 - - -class UpdateType(Enum): - - """ - 更新类型 - """ - - CASE = "case" - WEAPON_TYPE = "weapon_type" - - -NAME2COLOR = { - "消费级": "WHITE", - "工业级": "LIGHTBLUE", - "军规级": "BLUE", - "受限": "PURPLE", - "保密": "PINK", - "隐秘": "RED", - "非凡": "KNIFE", -} - -COLOR2NAME = { - "WHITE": "消费级", - "LIGHTBLUE": "工业级", - "BLUE": "军规级", - "PURPLE": "受限", - "PINK": "保密", - "RED": "隐秘", - "KNIFE": "非凡", -} - -COLOR2COLOR = { - "WHITE": (255, 255, 255), - "LIGHTBLUE": (0, 179, 255), - "BLUE": (0, 85, 255), - "PURPLE": (149, 0, 255), - "PINK": (255, 0, 162), - "RED": (255, 34, 0), - "KNIFE": (255, 225, 0), -} - -ABRASION_SORT = ["崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战横累累"] - -CASE_BACKGROUND = IMAGE_PATH / "csgo_cases" / "_background" / "shu" - -# 刀 -KNIFE2ID = { - "鲍伊猎刀": "weapon_knife_survival_bowie", - "蝴蝶刀": "weapon_knife_butterfly", - "弯刀": "weapon_knife_falchion", - "折叠刀": "weapon_knife_flip", - "穿肠刀": "weapon_knife_gut", - "猎杀者匕首": "weapon_knife_tactical", - "M9刺刀": "weapon_knife_m9_bayonet", - "刺刀": "weapon_bayonet", - "爪子刀": "weapon_knife_karambit", - "暗影双匕": "weapon_knife_push", - "短剑": "weapon_knife_stiletto", - "熊刀": "weapon_knife_ursus", - "折刀": "weapon_knife_gypsy_jackknife", - "锯齿爪刀": "weapon_knife_widowmaker", - "海豹短刀": "weapon_knife_css", - "系绳匕首": "weapon_knife_cord", - "求生匕首": "weapon_knife_canis", - "流浪者匕首": "weapon_knife_outdoor", - "骷髅匕首": "weapon_knife_skeleton", - "血猎手套": "weapon_bloodhound_gloves", - "驾驶手套": "weapon_driver_gloves", - "手部束带": "weapon_hand_wraps", - "摩托手套": "weapon_moto_gloves", - "专业手套": "weapon_specialist_gloves", - "运动手套": "weapon_sport_gloves", - "九头蛇手套": "weapon_hydra_gloves", - "狂牙手套": "weapon_brokenfang_gloves", -} - -WEAPON2ID = {} - -# 武器箱 -CASE2ID = { - "变革": "set_community_32", - "反冲": "set_community_31", - "梦魇": "set_community_30", - "激流": "set_community_29", - "蛇噬": "set_community_28", - "狂牙大行动": "set_community_27", - "裂空": "set_community_26", - "棱彩2号": "set_community_25", - "CS20": "set_community_24", - "裂网大行动": "set_community_23", - "棱彩": "set_community_22", - "头号特训": "set_community_21", - "地平线": "set_community_20", - "命悬一线": "set_community_19", - "光谱2号": "set_community_18", - "九头蛇大行动": "set_community_17", - "光谱": "set_community_16", - "手套": "set_community_15", - "伽玛2号": "set_gamma_2", - "伽玛": "set_community_13", - "幻彩3号": "set_community_12", - "野火大行动": "set_community_11", - "左轮": "set_community_10", - "暗影": "set_community_9", - "弯曲猎手": "set_community_8", - "幻彩2号": "set_community_7", - "幻彩": "set_community_6", - "先锋": "set_community_5", - "电竞2014夏季": "set_esports_iii", - "突围大行动": "set_community_4", - "猎杀者": "set_community_3", - "凤凰": "set_community_2", - "电竞2013冬季": "set_esports_ii", - "冬季攻势": "set_community_1", - "军火交易3号": "set_weapons_iii", - "英勇": "set_bravo_i", - "电竞2013": "set_esports", - "军火交易2号": "set_weapons_ii", - "军火交易": "set_weapons_i", -} - - -def get_wear(rand: float) -> str: - """判断磨损度 - - Args: - rand (float): 随机rand - - Returns: - str: 磨损名称 - """ - if rand <= FACTORY_NEW_E: - return "崭新出厂" - if MINIMAL_WEAR_S <= rand <= MINIMAL_WEAR_E: - return "略有磨损" - if FIELD_TESTED_S <= rand <= FIELD_TESTED_E: - return "久经沙场" - if WELL_WORN_S <= rand <= WELL_WORN_E: - return "破损不堪" - return "战痕累累" - - -def random_color_and_st(rand: float) -> Tuple[str, bool]: - """获取皮肤品质及是否暗金 - - Args: - rand (float): 随机rand - - Returns: - Tuple[str, bool]: 品质,是否暗金 - """ - if rand <= KNIFE: - if random.random() <= KNIFE_ST: - return ("KNIFE", True) - return ("KNIFE", False) - elif KNIFE < rand <= RED: - if random.random() <= RED_ST: - return ("RED", True) - return ("RED", False) - elif RED < rand <= PINK: - if random.random() <= PINK_ST: - return ("PINK", True) - return ("PINK", False) - elif PINK < rand <= PURPLE: - if random.random() <= PURPLE_ST: - return ("PURPLE", True) - return ("PURPLE", False) - else: - if random.random() <= BLUE_ST: - return ("BLUE", True) - return ("BLUE", False) - - -async def random_skin(num: int, case_name: str) -> List[Tuple[BuffSkin, float]]: - """ - 随机抽取皮肤 - """ - case_name = case_name.replace("武器箱", "").replace(" ", "") - color_map = {} - for _ in range(num): - rand = random.random() - # 尝试降低磨损 - if rand > MINIMAL_WEAR_E: - for _ in range(2): - if random.random() < 0.5: - logger.debug(f"[START]开箱随机磨损触发降低磨损条件: {rand}") - if random.random() < 0.2: - rand /= 3 - else: - rand /= 2 - logger.debug(f"[END]开箱随机磨损触发降低磨损条件: {rand}") - break - abrasion = get_wear(rand) - logger.debug(f"开箱随机磨损: {rand} | {abrasion}") - color, is_stattrak = random_color_and_st(rand) - if not color_map.get(color): - color_map[color] = {} - if is_stattrak: - if not color_map[color].get(f"{abrasion}_st"): - color_map[color][f"{abrasion}_st"] = [] - color_map[color][f"{abrasion}_st"].append(rand) - else: - if not color_map[color].get(abrasion): - color_map[color][f"{abrasion}"] = [] - color_map[color][f"{abrasion}"].append(rand) - skin_list = [] - for color in color_map: - for abrasion in color_map[color]: - rand_list = color_map[color][abrasion] - is_stattrak = "_st" in abrasion - abrasion = abrasion.replace("_st", "") - skin_list_ = await BuffSkin.random_skin( - len(rand_list), color, abrasion, is_stattrak, case_name - ) - skin_list += [(skin, rand) for skin, rand in zip(skin_list_, rand_list)] - return skin_list - - -# M249(StatTrak™) | 等高线 diff --git a/plugins/open_cases/models/__init__.py b/plugins/open_cases/models/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/plugins/open_cases/models/buff_prices.py b/plugins/open_cases/models/buff_prices.py deleted file mode 100755 index 5402d5e0..00000000 --- a/plugins/open_cases/models/buff_prices.py +++ /dev/null @@ -1,23 +0,0 @@ - -from tortoise import fields - -from services.db_context import Model - -# 1.狂牙武器箱 - - -class BuffPrice(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - case_id = fields.IntField() - """箱子id""" - skin_name = fields.CharField(255, unique=True) - """皮肤名称""" - skin_price = fields.FloatField() - """皮肤价格""" - update_date = fields.DatetimeField() - - class Meta: - table = "buff_prices" - table_description = "Buff价格数据表" diff --git a/plugins/open_cases/models/buff_skin.py b/plugins/open_cases/models/buff_skin.py deleted file mode 100644 index 279f082e..00000000 --- a/plugins/open_cases/models/buff_skin.py +++ /dev/null @@ -1,104 +0,0 @@ -from datetime import datetime -from typing import List, Optional - -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class BuffSkin(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - case_name: str = fields.CharField(255) # type: ignore - """箱子名称""" - name: str = fields.CharField(255) # type: ignore - """武器/手套/刀名称""" - skin_name: str = fields.CharField(255) # type: ignore - """皮肤名称""" - is_stattrak = fields.BooleanField(default=False) - """是否暗金(计数)""" - abrasion = fields.CharField(255) - """磨损度""" - color = fields.CharField(255) - """颜色(品质)""" - skin_id = fields.CharField(255, null=True, unique=True) - """皮肤id""" - - img_url = fields.CharField(255) - """图片url""" - steam_price: float = fields.FloatField(default=0) - """steam价格""" - weapon_type = fields.CharField(255) - """枪械类型""" - buy_max_price: float = fields.FloatField(default=0) - """最大求购价格""" - buy_num: int = fields.IntField(default=0) - """求购数量""" - sell_min_price: float = fields.FloatField(default=0) - """售卖最低价格""" - sell_num: int = fields.IntField(default=0) - """出售个数""" - sell_reference_price: float = fields.FloatField(default=0) - """参考价格""" - - create_time: datetime = fields.DatetimeField(auto_add_now=True) - """创建日期""" - update_time: datetime = fields.DatetimeField(auto_add=True) - """更新日期""" - - class Meta: - table = "buff_skin" - table_description = "Buff皮肤数据表" - # unique_together = ("case_name", "name", "skin_name", "abrasion", "is_stattrak") - - def __eq__(self, other: "BuffSkin"): - - return self.skin_id == other.skin_id - - def __hash__(self): - - return hash(self.case_name + self.name + self.skin_name + str(self.is_stattrak)) - - @classmethod - async def random_skin( - cls, - num: int, - color: str, - abrasion: str, - is_stattrak: bool = False, - case_name: Optional[str] = None, - ) -> List["BuffSkin"]: # type: ignore - query = cls - if case_name: - query = query.filter(case_name__contains=case_name) - query = query.filter(abrasion=abrasion, is_stattrak=is_stattrak, color=color) - skin_list = await query.annotate(rand=Random()).limit(num) # type:ignore - num_ = num - cnt = 0 - while len(skin_list) < num: - cnt += 1 - num_ = num - len(skin_list) - skin_list += await query.annotate(rand=Random()).limit(num_) - if cnt > 10: - break - return skin_list # type: ignore - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE buff_skin ADD img_url varchar(255);", # 新增img_url - "ALTER TABLE buff_skin ADD skin_id varchar(255);", # 新增skin_id - "ALTER TABLE buff_skin ADD steam_price float DEFAULT 0;", # 新增steam_price - "ALTER TABLE buff_skin ADD weapon_type varchar(255);", # 新增type - "ALTER TABLE buff_skin ADD buy_max_price float DEFAULT 0;", # 新增buy_max_price - "ALTER TABLE buff_skin ADD buy_num Integer DEFAULT 0;", # 新增buy_max_price - "ALTER TABLE buff_skin ADD sell_min_price float DEFAULT 0;", # 新增sell_min_price - "ALTER TABLE buff_skin ADD sell_num Integer DEFAULT 0;", # 新增sell_num - "ALTER TABLE buff_skin ADD sell_reference_price float DEFAULT 0;", # 新增sell_reference_price - "ALTER TABLE buff_skin DROP COLUMN skin_price;", # 删除skin_price - "alter table buff_skin drop constraint if EXISTS uid_buff_skin_case_na_c35c93;", # 删除唯一约束 - "UPDATE buff_skin set case_name='手套' where case_name='手套武器箱'", - "UPDATE buff_skin set case_name='左轮' where case_name='左轮武器箱'", - ] diff --git a/plugins/open_cases/models/buff_skin_log.py b/plugins/open_cases/models/buff_skin_log.py deleted file mode 100644 index dd67e4c8..00000000 --- a/plugins/open_cases/models/buff_skin_log.py +++ /dev/null @@ -1,51 +0,0 @@ -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class BuffSkinLog(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - case_name = fields.CharField(255) - """箱子名称""" - name = fields.CharField(255) - """武器/手套/刀名称""" - skin_name = fields.CharField(255) - """皮肤名称""" - is_stattrak = fields.BooleanField(default=False) - """是否暗金(计数)""" - abrasion = fields.CharField(255) - """磨损度""" - color = fields.CharField(255) - """颜色(品质)""" - - steam_price = fields.FloatField(default=0) - """steam价格""" - weapon_type = fields.CharField(255) - """枪械类型""" - buy_max_price = fields.FloatField(default=0) - """最大求购价格""" - buy_num = fields.IntField(default=0) - """求购数量""" - sell_min_price = fields.FloatField(default=0) - """售卖最低价格""" - sell_num = fields.IntField(default=0) - """出售个数""" - sell_reference_price = fields.FloatField(default=0) - """参考价格""" - - create_time = fields.DatetimeField(auto_add_now=True) - """创建日期""" - - class Meta: - table = "buff_skin_log" - table_description = "Buff皮肤更新日志表" - - @classmethod - async def _run_script(cls): - return [ - "UPDATE buff_skin_log set case_name='手套' where case_name='手套武器箱'", - "UPDATE buff_skin_log set case_name='左轮' where case_name='左轮武器箱'", - ] diff --git a/plugins/open_cases/models/open_cases_log.py b/plugins/open_cases/models/open_cases_log.py deleted file mode 100644 index ebcaf5bc..00000000 --- a/plugins/open_cases/models/open_cases_log.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import List, Optional - -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class OpenCasesLog(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - case_name = fields.CharField(255) - """箱子名称""" - name = fields.CharField(255) - """武器/手套/刀名称""" - skin_name = fields.CharField(255) - """皮肤名称""" - is_stattrak = fields.BooleanField(default=False) - """是否暗金(计数)""" - abrasion = fields.CharField(255) - """磨损度""" - abrasion_value = fields.FloatField() - """磨损数值""" - color = fields.CharField(255) - """颜色(品质)""" - price = fields.FloatField(default=0) - """价格""" - create_time = fields.DatetimeField(auto_add_now=True) - """创建日期""" - - class Meta: - table = "open_cases_log" - table_description = "开箱日志表" - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE open_cases_log RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE open_cases_log ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE open_cases_log ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/open_cases/models/open_cases_user.py b/plugins/open_cases/models/open_cases_user.py deleted file mode 100755 index 3b488717..00000000 --- a/plugins/open_cases/models/open_cases_user.py +++ /dev/null @@ -1,62 +0,0 @@ -from datetime import datetime - -from tortoise import fields - -from services.db_context import Model - - -class OpenCasesUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - total_count: int = fields.IntField(default=0) - """总开启次数""" - blue_count: int = fields.IntField(default=0) - """蓝色""" - blue_st_count: int = fields.IntField(default=0) - """蓝色暗金""" - purple_count: int = fields.IntField(default=0) - """紫色""" - purple_st_count: int = fields.IntField(default=0) - """紫色暗金""" - pink_count: int = fields.IntField(default=0) - """粉色""" - pink_st_count: int = fields.IntField(default=0) - """粉色暗金""" - red_count: int = fields.IntField(default=0) - """紫色""" - red_st_count: int = fields.IntField(default=0) - """紫色暗金""" - knife_count: int = fields.IntField(default=0) - """金色""" - knife_st_count: int = fields.IntField(default=0) - """金色暗金""" - spend_money: float = fields.IntField(default=0) - """花费金币""" - make_money: float = fields.FloatField(default=0) - """赚取金币""" - today_open_total: int = fields.IntField(default=0) - """今日开箱数量""" - open_cases_time_last: datetime = fields.DatetimeField() - """最后开箱日期""" - knifes_name: str = fields.TextField(default="") - """已获取金色""" - - class Meta: - table = "open_cases_users" - table_description = "开箱统计数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def _run_script(cls): - return [ - "alter table open_cases_users alter COLUMN make_money type float;", # 将make_money字段改为float - "alter table open_cases_users alter COLUMN spend_money type float;", # 将spend_money字段改为float - "ALTER TABLE open_cases_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE open_cases_users ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE open_cases_users ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/open_cases/open_cases_c.py b/plugins/open_cases/open_cases_c.py deleted file mode 100755 index 1f9409d3..00000000 --- a/plugins/open_cases/open_cases_c.py +++ /dev/null @@ -1,461 +0,0 @@ -import asyncio -import random -import re -from datetime import datetime -from typing import Union - -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import cn2py - -from .build_image import draw_card -from .config import * -from .models.open_cases_log import OpenCasesLog -from .models.open_cases_user import OpenCasesUser -from .utils import CaseManager, update_skin_data - -RESULT_MESSAGE = { - "BLUE": ["这样看着才舒服", "是自己人,大伙把刀收好", "非常舒适~"], - "PURPLE": ["还行吧,勉强接受一下下", "居然不是蓝色,太假了", "运气-1-1-1-1-1..."], - "PINK": ["开始不适....", "你妈妈买菜必涨价!涨三倍!", "你最近不适合出门,真的"], - "RED": ["已经非常不适", "好兄弟你开的什么箱子啊,一般箱子不是只有蓝色的吗", "开始拿阳寿开箱子了?"], - "KNIFE": ["你的好运我收到了,你可以去喂鲨鱼了", "最近该吃啥就迟点啥吧,哎,好好的一个人怎么就....哎", "众所周知,欧皇寿命极短."], -} - -COLOR2NAME = {"BLUE": "军规", "PURPLE": "受限", "PINK": "保密", "RED": "隐秘", "KNIFE": "罕见"} - -COLOR2CN = {"BLUE": "蓝", "PURPLE": "紫", "PINK": "粉", "RED": "红", "KNIFE": "金"} - - -def add_count(user: OpenCasesUser, skin: BuffSkin, case_price: float): - if skin.color == "BLUE": - if skin.is_stattrak: - user.blue_st_count += 1 - else: - user.blue_count += 1 - elif skin.color == "PURPLE": - if skin.is_stattrak: - user.purple_st_count += 1 - else: - user.purple_count += 1 - elif skin.color == "PINK": - if skin.is_stattrak: - user.pink_st_count += 1 - else: - user.pink_count += 1 - elif skin.color == "RED": - if skin.is_stattrak: - user.red_st_count += 1 - else: - user.red_count += 1 - elif skin.color == "KNIFE": - if skin.is_stattrak: - user.knife_st_count += 1 - else: - user.knife_count += 1 - user.make_money += skin.sell_min_price - user.spend_money += 17 + case_price - - -async def get_user_max_count( - user_id: Union[int, str], group_id: Union[str, int] -) -> int: - """获取用户每日最大开箱次数 - - Args: - user_id (str): 用户id - group_id (int): 群号 - - Returns: - int: 最大开箱次数 - """ - user, _ = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group_id) - ) - impression = int(user.impression) - initial_open_case_count = Config.get_config("open_cases", "INITIAL_OPEN_CASE_COUNT") - each_impression_add_count = Config.get_config( - "open_cases", "EACH_IMPRESSION_ADD_COUNT" - ) - return int(initial_open_case_count + impression / each_impression_add_count) # type: ignore - - -async def open_case( - user_id: Union[int, str], group_id: Union[int, str], case_name: str -) -> Union[str, Message]: - """开箱 - - Args: - user_id (str): 用户id - group_id (int): 群号 - case_name (str, optional): 武器箱名称. Defaults to "狂牙大行动". - - Returns: - Union[str, Message]: 回复消息 - """ - user_id = str(user_id) - group_id = str(group_id) - if not CaseManager.CURRENT_CASES: - return "未收录任何武器箱" - if not case_name: - case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore - if case_name not in CaseManager.CURRENT_CASES: - return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore - logger.debug(f"尝试开启武器箱: {case_name}", "开箱", user_id, group_id) - case = cn2py(case_name) - user = await OpenCasesUser.get_or_none(user_id=user_id, group_id=group_id) - if not user: - user = await OpenCasesUser.create( - user_id=user_id, group_id=group_id, open_cases_time_last=datetime.now() - ) - max_count = await get_user_max_count(user_id, group_id) - # 一天次数上限 - if user.today_open_total >= max_count: - return _handle_is_MAX_COUNT() - skin_list = await random_skin(1, case_name) - if not skin_list: - return "未抽取到任何皮肤..." - skin, rand = skin_list[0] - rand = str(rand)[:11] - case_price = 0 - if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): - case_price = case_skin.sell_min_price - user.today_open_total += 1 - user.total_count += 1 - user.open_cases_time_last = datetime.now() - await user.save( - update_fields=["today_open_total", "total_count", "open_cases_time_last"] - ) - add_count(user, skin, case_price) - ridicule_result = random.choice(RESULT_MESSAGE[skin.color]) - price_result = skin.sell_min_price - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - img_path = IMAGE_PATH / "csgo_cases" / case / f"{cn2py(name)}.jpg" - logger.info( - f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand}] 价格: {skin.sell_min_price}", - "开箱", - user_id, - group_id, - ) - await user.save() - await OpenCasesLog.create( - user_id=user_id, - group_id=group_id, - case_name=case_name, - name=skin.name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - price=skin.sell_min_price, - abrasion_value=rand, - create_time=datetime.now(), - ) - logger.debug(f"添加 1 条开箱日志", "开箱", user_id, group_id) - over_count = max_count - user.today_open_total - img = await draw_card(skin, rand) - return ( - f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n" - + image(img) - + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}" - ) - - -async def open_multiple_case( - user_id: Union[int, str], group_id: Union[str, int], case_name: str, num: int = 10 -): - """多连开箱 - - Args: - user_id (int): 用户id - group_id (int): 群号 - case_name (str): 箱子名称 - num (int, optional): 数量. Defaults to 10. - - Returns: - _type_: _description_ - """ - user_id = str(user_id) - group_id = str(group_id) - if not CaseManager.CURRENT_CASES: - return "未收录任何武器箱" - if not case_name: - case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore - if case_name not in CaseManager.CURRENT_CASES: - return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore - user, _ = await OpenCasesUser.get_or_create( - user_id=user_id, - group_id=group_id, - defaults={"open_cases_time_last": datetime.now()}, - ) - max_count = await get_user_max_count(user_id, group_id) - if user.today_open_total >= max_count: - return _handle_is_MAX_COUNT() - if max_count - user.today_open_total < num: - return ( - f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" - f"\n剩余开箱次数:{max_count - user.today_open_total}" - ) - logger.debug(f"尝试开启武器箱: {case_name}", "开箱", user_id, group_id) - case = cn2py(case_name) - skin_count = {} - img_list = [] - skin_list = await random_skin(num, case_name) - if not skin_list: - return "未抽取到任何皮肤..." - total_price = 0 - log_list = [] - now = datetime.now() - user.today_open_total += num - user.total_count += num - user.open_cases_time_last = datetime.now() - await user.save( - update_fields=["today_open_total", "total_count", "open_cases_time_last"] - ) - case_price = 0 - if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): - case_price = case_skin.sell_min_price - img_w, img_h = 0, 0 - for skin, rand in skin_list: - img = await draw_card(skin, str(rand)[:11]) - img_w, img_h = img.size - total_price += skin.sell_min_price - color_name = COLOR2CN[skin.color] - if not skin_count.get(color_name): - skin_count[color_name] = 0 - skin_count[color_name] += 1 - add_count(user, skin, case_price) - img_list.append(img) - logger.info( - f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand:.11f}] 价格: {skin.sell_min_price}", - "开箱", - user_id, - group_id, - ) - log_list.append( - OpenCasesLog( - user_id=user_id, - group_id=group_id, - case_name=case_name, - name=skin.name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - price=skin.sell_min_price, - abrasion_value=rand, - create_time=now, - ) - ) - await user.save() - if log_list: - await OpenCasesLog.bulk_create(log_list, 10) - logger.debug(f"添加 {len(log_list)} 条开箱日志", "开箱", user_id, group_id) - img_w += 10 - img_h += 10 - w = img_w * 5 - if num < 5: - h = img_h - 10 - w = img_w * num - elif not num % 5: - h = img_h * int(num / 5) - else: - h = img_h * int(num / 5) + img_h - markImg = BuildImage( - w - 10, h - 10, img_w - 10, img_h - 10, 10, color=(255, 255, 255) - ) - for img in img_list: - markImg.paste(img, alpha=True) - over_count = max_count - user.today_open_total - result = "" - for color_name in skin_count: - result += f"[{color_name}:{skin_count[color_name]}] " - return ( - f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n" - + image(markImg) - + "\n" - + result[:-1] - + f"\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" - ) - - -def _handle_is_MAX_COUNT() -> str: - return f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" - - -async def total_open_statistics( - user_id: Union[str, int], group_id: Union[str, int] -) -> str: - user, _ = await OpenCasesUser.get_or_create( - user_id=str(user_id), group_id=str(group_id) - ) - return ( - f"开箱总数:{user.total_count}\n" - f"今日开箱:{user.today_open_total}\n" - f"蓝色军规:{user.blue_count}\n" - f"蓝色暗金:{user.blue_st_count}\n" - f"紫色受限:{user.purple_count}\n" - f"紫色暗金:{user.purple_st_count}\n" - f"粉色保密:{user.pink_count}\n" - f"粉色暗金:{user.pink_st_count}\n" - f"红色隐秘:{user.red_count}\n" - f"红色暗金:{user.red_st_count}\n" - f"金色罕见:{user.knife_count}\n" - f"金色暗金:{user.knife_st_count}\n" - f"花费金额:{user.spend_money}\n" - f"获取金额:{user.make_money:.2f}\n" - f"最后开箱日期:{user.open_cases_time_last.date()}" - ) - - -async def group_statistics(group_id: Union[int, str]): - user_list = await OpenCasesUser.filter(group_id=str(group_id)).all() - # lan zi fen hong jin pricei - uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0] - for user in user_list: - uplist[0] += user.blue_count - uplist[1] += user.blue_st_count - uplist[2] += user.purple_count - uplist[3] += user.purple_st_count - uplist[4] += user.pink_count - uplist[5] += user.pink_st_count - uplist[6] += user.red_count - uplist[7] += user.red_st_count - uplist[8] += user.knife_count - uplist[9] += user.knife_st_count - uplist[10] += user.make_money - uplist[11] += user.total_count - uplist[12] += user.today_open_total - return ( - f"群开箱总数:{uplist[11]}\n" - f"群今日开箱:{uplist[12]}\n" - f"蓝色军规:{uplist[0]}\n" - f"蓝色暗金:{uplist[1]}\n" - f"紫色受限:{uplist[2]}\n" - f"紫色暗金:{uplist[3]}\n" - f"粉色保密:{uplist[4]}\n" - f"粉色暗金:{uplist[5]}\n" - f"红色隐秘:{uplist[6]}\n" - f"红色暗金:{uplist[7]}\n" - f"金色罕见:{uplist[8]}\n" - f"金色暗金:{uplist[9]}\n" - f"花费金额:{uplist[11] * 17}\n" - f"获取金额:{uplist[10]:.2f}" - ) - - -async def get_my_knifes( - user_id: Union[str, int], group_id: Union[str, int] -) -> Union[str, MessageSegment]: - """获取我的金色 - - Args: - user_id (str): 用户id - group_id (str): 群号 - - Returns: - Union[str, MessageSegment]: 回复消息或图片 - """ - data_list = await get_old_knife(str(user_id), str(group_id)) - data_list += await OpenCasesLog.filter( - user_id=user_id, group_id=group_id, color="KNIFE" - ).all() - if not data_list: - return "您木有开出金色级别的皮肤喔" - length = len(data_list) - if length < 5: - h = 600 - w = length * 540 - elif length % 5 == 0: - h = 600 * int(length / 5) - w = 540 * 5 - else: - h = 600 * int(length / 5) + 600 - w = 540 * 5 - A = BuildImage(w, h, 540, 600) - for skin in data_list: - name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - img_path = ( - IMAGE_PATH / "csgo_cases" / cn2py(skin.case_name) / f"{cn2py(name)}.jpg" - ) - knife_img = BuildImage(470, 600, 470, 470, font_size=20) - await knife_img.apaste( - BuildImage(470, 470, background=img_path if img_path.exists() else None), - (0, 0), - True, - ) - await knife_img.atext( - (5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})" - ) - await knife_img.atext((5, 530), f"\t磨损:{skin.abrasion_value}") - await knife_img.atext((5, 560), f"\t价格:{skin.price}") - await A.apaste(knife_img) - return image(A) - - -async def get_old_knife(user_id: str, group_id: str) -> List[OpenCasesLog]: - """获取旧数据字段 - - Args: - user_id (str): 用户id - group_id (str): 群号 - - Returns: - List[OpenCasesLog]: 旧数据兼容 - """ - user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) - knifes_name = user.knifes_name - data_list = [] - if knifes_name: - knifes_list = knifes_name[:-1].split(",") - for knife in knifes_list: - try: - if r := re.search( - "(.*)\|\|(.*) \| (.*)\((.*)\) 磨损:(.*), 价格:(.*)", knife - ): - case_name_py = r.group(1) - name = r.group(2) - skin_name = r.group(3) - abrasion = r.group(4) - abrasion_value = r.group(5) - price = r.group(6) - name = name.replace("(StatTrak™)", "") - data_list.append( - OpenCasesLog( - user_id=user_id, - group_id=group_id, - name=name.strip(), - case_name=case_name_py.strip(), - skin_name=skin_name.strip(), - abrasion=abrasion.strip(), - abrasion_value=abrasion_value, - price=price, - ) - ) - except Exception as e: - logger.error(f"获取兼容旧数据错误: {knife}", "我的金色", user_id, group_id, e=e) - return data_list - - -async def auto_update(): - """自动更新武器箱""" - if case_list := Config.get_config("open_cases", "DAILY_UPDATE"): - logger.debug("尝试自动更新武器箱", "更新武器箱") - if "ALL" in case_list: - case_list = CASE2ID.keys() - logger.debug(f"预计自动更新武器箱 {len(case_list)} 个", "更新武器箱") - for case_name in case_list: - logger.debug(f"开始自动更新武器箱: {case_name}", "更新武器箱") - try: - await update_skin_data(case_name) - rand = random.randint(300, 500) - logger.info(f"成功自动更新武器箱: {case_name}, 将在 {rand} 秒后再次更新下一武器箱", "更新武器箱") - await asyncio.sleep(rand) - except Exception as e: - logger.error(f"自动更新武器箱: {case_name}", e=e) diff --git a/plugins/open_cases/utils.py b/plugins/open_cases/utils.py deleted file mode 100755 index d5e783e8..00000000 --- a/plugins/open_cases/utils.py +++ /dev/null @@ -1,643 +0,0 @@ -import asyncio -import os -import random -import re -import time -from datetime import datetime, timedelta -from typing import List, Optional, Tuple, Union - -import nonebot -from tortoise.functions import Count - -from configs.config import Config -from configs.path_config import IMAGE_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, BuildMat -from utils.utils import broadcast_group, cn2py - -from .build_image import generate_skin -from .config import ( - CASE2ID, - CASE_BACKGROUND, - COLOR2NAME, - KNIFE2ID, - NAME2COLOR, - UpdateType, -) -from .models.buff_skin import BuffSkin -from .models.buff_skin_log import BuffSkinLog -from .models.open_cases_user import OpenCasesUser - -URL = "https://buff.163.com/api/market/goods" - -SELL_URL = "https://buff.163.com/goods" - - -driver = nonebot.get_driver() - -BASE_PATH = IMAGE_PATH / "csgo_cases" - - -class CaseManager: - - CURRENT_CASES = [] - - @classmethod - async def reload(cls): - cls.CURRENT_CASES = [] - case_list = await BuffSkin.filter(color="CASE").values_list( - "case_name", flat=True - ) - for case_name in ( - await BuffSkin.filter(case_name__not="未知武器箱") - .annotate() - .distinct() - .values_list("case_name", flat=True) - ): - for name in case_name.split(","): # type: ignore - if name not in cls.CURRENT_CASES and name in case_list: - cls.CURRENT_CASES.append(name) - - -async def update_skin_data(name: str, is_update_case_name: bool = False) -> str: - """更新箱子内皮肤数据 - - Args: - name (str): 箱子名称 - is_update_case_name (bool): 是否必定更新所属箱子 - - Returns: - _type_: _description_ - """ - type_ = None - if name in CASE2ID: - type_ = UpdateType.CASE - if name in KNIFE2ID: - type_ = UpdateType.WEAPON_TYPE - if not type_: - return "未在指定武器箱或指定武器类型内" - session = Config.get_config("open_cases", "COOKIE") - if not session: - return "BUFF COOKIE为空捏!" - weapon2case = {} - if type_ == UpdateType.WEAPON_TYPE: - db_data = await BuffSkin.filter(name__contains=name).all() - weapon2case = { - item.name + item.skin_name: item.case_name - for item in db_data - if item.case_name != "未知武器箱" - } - data_list, total = await search_skin_page(name, 1, type_) - if isinstance(data_list, str): - return data_list - for page in range(2, total + 1): - rand_time = random.randint(20, 50) - logger.debug(f"访问随机等待时间: {rand_time}", "开箱更新") - await asyncio.sleep(rand_time) - data_list_, total = await search_skin_page(name, page, type_) - if isinstance(data_list_, list): - data_list += data_list_ - create_list: List[BuffSkin] = [] - update_list: List[BuffSkin] = [] - log_list = [] - now = datetime.now() - exists_id_list = [] - new_weapon2case = {} - for skin in data_list: - if skin.skin_id in exists_id_list: - continue - if skin.case_name: - skin.case_name = ( - skin.case_name.replace("”", "") - .replace("“", "") - .replace("武器箱", "") - .replace(" ", "") - ) - skin.name = skin.name.replace("(★ StatTrak™)", "").replace("(★)", "") - exists_id_list.append(skin.skin_id) - key = skin.name + skin.skin_name - name_ = skin.name + skin.skin_name + skin.abrasion - skin.create_time = now - skin.update_time = now - if UpdateType.WEAPON_TYPE and not skin.case_name: - if is_update_case_name: - case_name = new_weapon2case.get(key) - else: - case_name = weapon2case.get(key) - if not case_name: - if case_list := await get_skin_case(skin.skin_id): - case_name = ",".join(case_list) - rand = random.randint(10, 20) - logger.debug( - f"获取 {skin.name} | {skin.skin_name} 皮肤所属武器箱: {case_name}, 访问随机等待时间: {rand}", - "开箱更新", - ) - await asyncio.sleep(rand) - if not case_name: - case_name = "未知武器箱" - else: - weapon2case[key] = case_name - new_weapon2case[key] = case_name - if skin.case_name == "反恐精英20周年": - skin.case_name = "CS20" - skin.case_name = case_name - if await BuffSkin.exists(skin_id=skin.skin_id): - update_list.append(skin) - else: - create_list.append(skin) - log_list.append( - BuffSkinLog( - name=skin.name, - case_name=skin.case_name, - skin_name=skin.skin_name, - is_stattrak=skin.is_stattrak, - abrasion=skin.abrasion, - color=skin.color, - steam_price=skin.steam_price, - weapon_type=skin.weapon_type, - buy_max_price=skin.buy_max_price, - buy_num=skin.buy_num, - sell_min_price=skin.sell_min_price, - sell_num=skin.sell_num, - sell_reference_price=skin.sell_reference_price, - create_time=now, - ) - ) - name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - for c_name_ in skin.case_name.split(","): - file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" - if not file_path.exists(): - logger.debug(f"下载皮肤 {name} 图片: {skin.img_url}...", "开箱更新") - await AsyncHttpx.download_file(skin.img_url, file_path) - rand_time = random.randint(1, 10) - await asyncio.sleep(rand_time) - logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱更新") - else: - logger.debug(f"皮肤 {name_} 图片已存在...", "开箱更新") - if create_list: - logger.debug(f"更新武器箱/皮肤: [{name}], 创建 {len(create_list)} 个皮肤!") - await BuffSkin.bulk_create(set(create_list), 10) - if update_list: - abrasion_list = [] - name_list = [] - skin_name_list = [] - for skin in update_list: - if skin.abrasion not in abrasion_list: - abrasion_list.append(skin.abrasion) - if skin.name not in name_list: - name_list.append(skin.name) - if skin.skin_name not in skin_name_list: - skin_name_list.append(skin.skin_name) - db_data = await BuffSkin.filter( - case_name__contains=name, - skin_name__in=skin_name_list, - name__in=name_list, - abrasion__in=abrasion_list, - ).all() - _update_list = [] - for data in db_data: - for skin in update_list: - if ( - data.name == skin.name - and data.skin_name == skin.skin_name - and data.abrasion == skin.abrasion - ): - data.steam_price = skin.steam_price - data.buy_max_price = skin.buy_max_price - data.buy_num = skin.buy_num - data.sell_min_price = skin.sell_min_price - data.sell_num = skin.sell_num - data.sell_reference_price = skin.sell_reference_price - data.update_time = skin.update_time - _update_list.append(data) - logger.debug(f"更新武器箱/皮肤: [{name}], 更新 {len(create_list)} 个皮肤!") - await BuffSkin.bulk_update( - _update_list, - [ - "steam_price", - "buy_max_price", - "buy_num", - "sell_min_price", - "sell_num", - "sell_reference_price", - "update_time", - ], - 10, - ) - if log_list: - logger.debug(f"更新武器箱/皮肤: [{name}], 新增 {len(log_list)} 条皮肤日志!") - await BuffSkinLog.bulk_create(log_list) - if name not in CaseManager.CURRENT_CASES: - CaseManager.CURRENT_CASES.append(name) # type: ignore - return f"更新武器箱/皮肤: [{name}] 成功, 共更新 {len(update_list)} 个皮肤, 新创建 {len(create_list)} 个皮肤!" - - -async def search_skin_page( - s_name: str, page_index: int, type_: UpdateType -) -> Tuple[Union[List[BuffSkin], str], int]: - """查询箱子皮肤 - - Args: - s_name (str): 箱子/皮肤名称 - page_index (int): 页数 - - Returns: - Union[List[BuffSkin], str]: BuffSkin - """ - logger.debug( - f"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]", "开箱更新" - ) - cookie = {"session": Config.get_config("open_cases", "COOKIE")} - params = { - "game": "csgo", - "page_num": page_index, - "page_size": 80, - "_": time.time(), - "use_suggestio": 0, - } - if type_ == UpdateType.CASE: - params["itemset"] = CASE2ID[s_name] - elif type_ == UpdateType.WEAPON_TYPE: - params["category"] = KNIFE2ID[s_name] - proxy = None - if ip := Config.get_config("open_cases", "BUFF_PROXY"): - proxy = {"http://": ip, "https://": ip} - response = None - error = "" - for i in range(3): - try: - response = await AsyncHttpx.get( - URL, - proxy=proxy, - params=params, - cookies=cookie, # type: ignore - ) - if response.status_code == 200: - break - rand = random.randint(3, 7) - logger.debug( - f"尝试访问武器箱/皮肤第 {i+1} 次访问异常, code: {response.status_code}", "开箱更新" - ) - await asyncio.sleep(rand) - except Exception as e: - logger.debug(f"尝试访问武器箱/皮肤第 {i+1} 次访问发生错误 {type(e)}: {e}", "开箱更新") - error = f"{type(e)}: {e}" - if not response: - return f"访问发生异常: {error}", -1 - if response.status_code == 200: - # logger.debug(f"访问BUFF API: {response.text}", "开箱更新") - json_data = response.json() - update_data = [] - if json_data["code"] == "OK": - data_list = json_data["data"]["items"] - for data in data_list: - obj = {} - if type_ == UpdateType.CASE: - obj["case_name"] = s_name - name = data["name"] - try: - logger.debug( - f"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...", - "开箱更新", - ) - obj["skin_id"] = str(data["id"]) - obj["buy_max_price"] = data["buy_max_price"] # 求购最大金额 - obj["buy_num"] = data["buy_num"] # 当前求购 - goods_info = data["goods_info"] - info = goods_info["info"] - tags = info["tags"] - obj["weapon_type"] = tags["type"]["localized_name"] # 枪械类型 - if obj["weapon_type"] in ["音乐盒", "印花", "探员"]: - continue - elif obj["weapon_type"] in ["匕首", "手套"]: - obj["color"] = "KNIFE" - obj["name"] = data["short_name"].split("(")[0].strip() # 名称 - elif obj["weapon_type"] in ["武器箱"]: - obj["color"] = "CASE" - obj["name"] = data["short_name"] - else: - obj["color"] = NAME2COLOR[tags["rarity"]["localized_name"]] - obj["name"] = tags["weapon"]["localized_name"] # 名称 - if obj["weapon_type"] not in ["武器箱"]: - obj["abrasion"] = tags["exterior"]["localized_name"] # 磨损 - obj["is_stattrak"] = "StatTrak" in tags["quality"]["localized_name"] # type: ignore # 是否暗金 - if not obj["color"]: - obj["color"] = NAME2COLOR[ - tags["rarity"]["localized_name"] - ] # 品质颜色 - else: - obj["abrasion"] = "CASE" - obj["skin_name"] = data["short_name"].split("|")[-1].strip() # 皮肤名称 - obj["img_url"] = goods_info["original_icon_url"] # 图片url - obj["steam_price"] = goods_info["steam_price_cny"] # steam价格 - obj["sell_min_price"] = data["sell_min_price"] # 售卖最低价格 - obj["sell_num"] = data["sell_num"] # 售卖数量 - obj["sell_reference_price"] = data["sell_reference_price"] # 参考价格 - update_data.append(BuffSkin(**obj)) - except Exception as e: - logger.error( - f"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误", - e=e, - ) - logger.debug( - f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成", - "开箱更新", - ) - return update_data, json_data["data"]["total_page"] - else: - logger.warning(f'访问BUFF失败: {json_data["error"]}') - return f'访问失败: {json_data["error"]}', -1 - return f"访问失败, 状态码: {response.status_code}", -1 - - -async def build_case_image(case_name: str) -> Union[BuildImage, str]: - """构造武器箱图片 - - Args: - case_name (str): 名称 - - Returns: - Union[BuildImage, str]: 图片 - """ - background = random.choice(os.listdir(CASE_BACKGROUND)) - background_img = BuildImage(0, 0, background=CASE_BACKGROUND / background) - if case_name: - log_list = ( - await BuffSkinLog.filter(case_name__contains=case_name) - .annotate(count=Count("id")) - .group_by("skin_name") - .values_list("skin_name", "count") - ) - skin_list_ = await BuffSkin.filter(case_name__contains=case_name).all() - skin2count = {item[0]: item[1] for item in log_list} - case = None - skin_list: List[BuffSkin] = [] - exists_name = [] - for skin in skin_list_: - if skin.color == "CASE": - case = skin - else: - name = skin.name + skin.skin_name - if name not in exists_name: - skin_list.append(skin) - exists_name.append(name) - generate_img = {} - for skin in skin_list: - skin_img = await generate_skin(skin, skin2count.get(skin.skin_name, 0)) - if skin_img: - if not generate_img.get(skin.color): - generate_img[skin.color] = [] - generate_img[skin.color].append(skin_img) - skin_image_list = [] - for color in COLOR2NAME: - if generate_img.get(color): - skin_image_list = skin_image_list + generate_img[color] - img = skin_image_list[0] - img_w, img_h = img.size - total_size = (img_w + 25) * (img_h + 10) * len(skin_image_list) # 总面积 - new_size = get_bk_image_size(total_size, background_img.size, img.size, 250) - A = BuildImage( - new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background - ) - await A.afilter("GaussianBlur", 2) - if case: - case_img = await generate_skin(case, skin2count.get(f"{case_name}武器箱", 0)) - if case_img: - A.paste(case_img, (25, 25), True) - w = 25 - h = 230 - skin_image_list.reverse() - for image in skin_image_list: - A.paste(image, (w, h), True) - w += image.w + 20 - if w + image.w - 25 > A.w: - h += image.h + 10 - w = 25 - if h + img_h + 100 < A.h: - await A.acrop((0, 0, A.w, h + img_h + 100)) - return A - else: - log_list = ( - await BuffSkinLog.filter(color="CASE") - .annotate(count=Count("id")) - .group_by("case_name") - .values_list("case_name", "count") - ) - name2count = {item[0]: item[1] for item in log_list} - skin_list = await BuffSkin.filter(color="CASE").all() - image_list: List[BuildImage] = [] - for skin in skin_list: - if img := await generate_skin(skin, name2count[skin.case_name]): - image_list.append(img) - if not image_list: - return "未收录武器箱" - w = 25 - h = 150 - img = image_list[0] - img_w, img_h = img.size - total_size = (img_w + 25) * (img_h + 10) * len(image_list) # 总面积 - - new_size = get_bk_image_size(total_size, background_img.size, img.size, 155) - A = BuildImage( - new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background - ) - await A.afilter("GaussianBlur", 2) - bk_img = BuildImage( - img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf" - ) - await bk_img.atext( - (0, 0), f"已收录 {len(image_list)} 个武器箱", (255, 255, 255), center_type="center" - ) - await A.apaste(bk_img, (10, 10), True, "by_width") - for image in image_list: - A.paste(image, (w, h), True) - w += image.w + 20 - if w + image.w - 25 > A.w: - h += image.h + 10 - w = 25 - if h + img_h + 100 < A.h: - await A.acrop((0, 0, A.w, h + img_h + 100)) - return A - - -def get_bk_image_size( - total_size: int, - base_size: Tuple[int, int], - img_size: Tuple[int, int], - extra_height: int = 0, -): - """获取所需背景大小且不改变图片长宽比 - - Args: - total_size (int): 总面积 - base_size (Tuple[int, int]): 初始背景大小 - img_size (Tuple[int, int]): 贴图大小 - - Returns: - _type_: 满足所有贴图大小 - """ - bk_w, bk_h = base_size - img_w, img_h = img_size - is_add_title_size = False - left_dis = 0 - right_dis = 0 - old_size = (0, 0) - new_size = (0, 0) - ratio = 1.1 - while 1: - w_ = int(ratio * bk_w) - h_ = int(ratio * bk_h) - size = w_ * h_ - if size < total_size: - left_dis = size - else: - right_dis = size - r = w_ / (img_w + 25) - if right_dis and r - int(r) < 0.1: - if not is_add_title_size and extra_height: - total_size = int(total_size + w_ * extra_height) - is_add_title_size = True - right_dis = 0 - continue - if total_size - left_dis > right_dis - total_size: - new_size = (w_, h_) - else: - new_size = old_size - break - old_size = (w_, h_) - ratio += 0.1 - return new_size - - -async def get_skin_case(id_: str) -> Optional[List[str]]: - """获取皮肤所在箱子 - - Args: - id_ (str): 皮肤id - - Returns: - Optional[str]: 武器箱名称 - """ - url = f"{SELL_URL}/{id_}" - proxy = None - if ip := Config.get_config("open_cases", "BUFF_PROXY"): - proxy = {"http://": ip, "https://": ip} - response = await AsyncHttpx.get( - url, - proxy=proxy, - ) - if response.status_code == 200: - text = response.text - if r := re.search('', text): - case_list = [] - for s in r.group(1).split(","): - if "武器箱" in s: - case_list.append( - s.replace("”", "") - .replace("“", "") - .replace('"', "") - .replace("'", "") - .replace("武器箱", "") - .replace(" ", "") - ) - return case_list - else: - logger.debug(f"访问皮肤所属武器箱异常 url: {url} code: {response.status_code}") - return None - - -async def init_skin_trends( - name: str, skin: str, abrasion: str, day: int = 7 -) -> Optional[BuildMat]: - date = datetime.now() - timedelta(days=day) - log_list = ( - await BuffSkinLog.filter( - name__contains=name.upper(), - skin_name=skin, - abrasion__contains=abrasion, - create_time__gt=date, - is_stattrak=False, - ) - .order_by("create_time") - .limit(day * 5) - .all() - ) - if not log_list: - return None - date_list = [] - price_list = [] - for log in log_list: - date = str(log.create_time.date()) - if date not in date_list: - date_list.append(date) - price_list.append(log.sell_min_price) - bar_graph = BuildMat( - y=price_list, - mat_type="line", - title=f"{name}({skin})价格趋势({day})", - x_index=date_list, - x_min_spacing=90, - display_num=True, - x_rotate=30, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - return bar_graph - - -async def reset_count_daily(): - """ - 重置每日开箱 - """ - try: - await OpenCasesUser.all().update(today_open_total=0) - await broadcast_group( - "[[_task|open_case_reset_remind]]今日开箱次数重置成功", log_cmd="开箱重置提醒" - ) - except Exception as e: - logger.error(f"开箱重置错误", e=e) - - -async def download_image(case_name: Optional[str] = None): - """下载皮肤图片 - - 参数: - case_name: 箱子名称. - """ - skin_list = ( - await BuffSkin.filter(case_name=case_name).all() - if case_name - else await BuffSkin.all() - ) - for skin in skin_list: - name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion - for c_name_ in skin.case_name.split(","): - try: - file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" - if not file_path.exists(): - logger.debug( - f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}...", - "开箱图片更新", - ) - await AsyncHttpx.download_file(skin.img_url, file_path) - rand_time = random.randint(1, 5) - await asyncio.sleep(rand_time) - logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱图片更新") - else: - logger.debug(f"皮肤 {c_name_}/{skin.name} 图片已存在...", "开箱图片更新") - except Exception as e: - logger.error( - f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}", - "开箱图片更新", - e=e, - ) - - -@driver.on_startup -async def _(): - await CaseManager.reload() diff --git a/plugins/parse_bilibili_json.py b/plugins/parse_bilibili_json.py deleted file mode 100755 index 4a9a91a3..00000000 --- a/plugins/parse_bilibili_json.py +++ /dev/null @@ -1,174 +0,0 @@ -import asyncio -import time - -import aiohttp -import ujson as json -from bilireq import video -from nonebot import on_message -from nonebot.adapters.onebot.v11 import ActionFailed, GroupMessageEvent -from nonebot.adapters.onebot.v11.permission import GROUP - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.browser import get_browser -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.manager import group_manager -from utils.message_builder import image -from utils.user_agent import get_user_agent -from utils.utils import get_local_proxy, get_message_json, get_message_text, is_number - -__zx_plugin_name__ = "B站转发解析" -__plugin_usage__ = """ -usage: - B站转发解析,解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv,且5分钟内不解析相同url -""".strip() -__plugin_des__ = "B站转发解析" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_task__ = {"bilibili_parse": "b站转发解析"} -Config.add_plugin_config( - "_task", - "DEFAULT_BILIBILI_PARSE", - True, - help_="被动 B站转发解析 进群默认开关状态", - default_value=True, - type=bool -) - - -async def plugin_on_checker(event: GroupMessageEvent) -> bool: - return group_manager.get_plugin_status("parse_bilibili_json", event.group_id) - - -parse_bilibili_json = on_message( - priority=1, permission=GROUP, block=False, rule=plugin_on_checker -) - -_tmp = {} - - -@parse_bilibili_json.handle() -async def _(event: GroupMessageEvent): - vd_info = None - url = None - if get_message_json(event.json()): - try: - data = json.loads(get_message_json(event.json())[0]["data"]) - except (IndexError, KeyError): - data = None - if data: - # 转发视频 - if data.get("desc") == "哔哩哔哩" or "哔哩哔哩" in data.get("prompt"): - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get( - data["meta"]["detail_1"]["qqdocurl"], - timeout=7, - ) as response: - url = str(response.url).split("?")[0] - if url[-1] == "/": - url = url[:-1] - bvid = url.split("/")[-1] - vd_info = await video.get_video_base_info(bvid) - # 转发专栏 - if ( - data.get("meta") - and data["meta"].get("news") - and data["meta"]["news"].get("desc") == "哔哩哔哩专栏" - ): - url = data["meta"]["news"]["jumpUrl"] - page = None - try: - browser = await get_browser() - if not browser: - return - page = await browser.new_page( - user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - " (KHTML, like Gecko) Chrome/93.0.4530.0 Safari/537.36" - ) - await page.goto(url, wait_until="networkidle", timeout=10000) - await page.set_viewport_size({"width": 2560, "height": 1080}) - await page.click("#app > div") - div = await page.query_selector("#app > div") - await div.screenshot( - path=f"{IMAGE_PATH}/temp/cv_{event.user_id}.png", - timeout=100000, - ) - await asyncio.get_event_loop().run_in_executor( - None, resize, TEMP_PATH / f"cv_{event.user_id}.png" - ) - await parse_bilibili_json.send( - "[[_task|bilibili_parse]]" - + image(TEMP_PATH / f"cv_{event.user_id}.png") - ) - await page.close() - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" - ) - except Exception as e: - logger.error(f"尝试解析bilibili专栏 {url} 失败 {type(e)}:{e}") - if page: - await page.close() - return - # BV - if msg := get_message_text(event.json()): - if "BV" in msg: - index = msg.find("BV") - if len(msg[index + 2 :]) >= 10: - msg = msg[index : index + 12] - url = f"https://www.bilibili.com/video/{msg}" - vd_info = await video.get_video_base_info(msg) - elif "av" in msg: - index = msg.find("av") - if len(msg[index + 2 :]) >= 1: - msg = msg[index + 2 : index + 11] - if is_number(msg): - url = f"https://www.bilibili.com/video/av{msg}" - vd_info = await video.get_video_base_info("av" + msg) - elif "https://b23.tv" in msg: - url = "https://" + msg[msg.find("b23.tv") : msg.find("b23.tv") + 14] - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get( - url, - timeout=7, - ) as response: - url = (str(response.url).split("?")[0]).strip("/") - bvid = url.split("/")[-1] - vd_info = await video.get_video_base_info(bvid) - if vd_info: - if ( - url in _tmp.keys() and time.time() - _tmp[url] > 30 - ) or url not in _tmp.keys(): - _tmp[url] = time.time() - aid = vd_info["aid"] - title = vd_info["title"] - author = vd_info["owner"]["name"] - reply = vd_info["stat"]["reply"] # 回复 - favorite = vd_info["stat"]["favorite"] # 收藏 - coin = vd_info["stat"]["coin"] # 投币 - # like = vd_info['stat']['like'] # 点赞 - # danmu = vd_info['stat']['danmaku'] # 弹幕 - date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) - try: - await parse_bilibili_json.send( - "[[_task|bilibili_parse]]" - + image(vd_info["pic"]) - + f"\nav{aid}\n标题:{title}\n" - f"UP:{author}\n" - f"上传日期:{date}\n" - f"回复:{reply},收藏:{favorite},投币:{coin}\n" - f"{url}" - ) - except ActionFailed: - logger.warning(f"{event.group_id} 发送bilibili解析失败") - else: - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" - ) - - -def resize(path: str): - A = BuildImage(0, 0, background=path, ratio=0.5) - A.save(path) diff --git a/plugins/pid_search.py b/plugins/pid_search.py deleted file mode 100755 index ff8da011..00000000 --- a/plugins/pid_search.py +++ /dev/null @@ -1,117 +0,0 @@ -from asyncio.exceptions import TimeoutError - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, CommandArg -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.manager import withdraw_message_manager -from utils.message_builder import image -from utils.utils import change_pixiv_image_links, is_number - -__zx_plugin_name__ = "pid搜索" -__plugin_usage__ = """ -usage: - 通过 pid 搜索图片 - 指令: - p搜 [pid] -""".strip() -__plugin_des__ = "通过 pid 搜索图片" -__plugin_cmd__ = ["p搜 [pid]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["p搜"], -} - -pid_search = on_command("p搜", aliases={"pixiv搜", "P搜"}, priority=5, block=True) - - -@pid_search.handle() -async def _h(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - pid = arg.extract_plain_text().strip() - if pid: - state["pid"] = pid - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -@pid_search.got("pid", prompt="需要查询的图片PID是?") -async def _g(event: MessageEvent, state: T_State, pid: str = Arg("pid")): - url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust" - if pid in ["取消", "算了"]: - await pid_search.finish("已取消操作...") - if not is_number(pid): - await pid_search.reject_arg("pid", "笨蛋,重新输入数!字!") - for _ in range(3): - try: - data = ( - await AsyncHttpx.get( - url, - params={"id": pid}, - timeout=5, - ) - ).json() - except TimeoutError: - pass - except Exception as e: - await pid_search.finish(f"发生了一些错误..{type(e)}:{e}") - else: - if data.get("error"): - await pid_search.finish(data["error"]["user_message"], at_sender=True) - data = data["illust"] - if not data["width"] and not data["height"]: - await pid_search.finish(f"没有搜索到 PID:{pid} 的图片", at_sender=True) - pid = data["id"] - title = data["title"] - author = data["user"]["name"] - author_id = data["user"]["id"] - image_list = [] - try: - image_list.append(data["meta_single_page"]["original_image_url"]) - except KeyError: - for image_url in data["meta_pages"]: - image_list.append(image_url["image_urls"]["original"]) - for i, img_url in enumerate(image_list): - img_url = change_pixiv_image_links(img_url) - if not await AsyncHttpx.download_file( - img_url, - TEMP_PATH / f"pid_search_{event.user_id}_{i}.png", - headers=headers, - ): - await pid_search.send("图片下载失败了....", at_sender=True) - tmp = "" - if isinstance(event, GroupMessageEvent): - tmp = "\n【注】将在30后撤回......" - msg_id = await pid_search.send( - Message( - f"title:{title}\n" - f"pid:{pid}\n" - f"author:{author}\n" - f"author_id:{author_id}\n" - f'{image(TEMP_PATH / f"pid_search_{event.user_id}_{i}.png")}' - f"{tmp}" - ) - ) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询图片 PID:{pid}" - ) - if isinstance(event, GroupMessageEvent): - withdraw_message_manager.append((msg_id, 30)) - break - else: - await pid_search.finish("图片下载失败了....", at_sender=True) diff --git a/plugins/pix_gallery/__init__.py b/plugins/pix_gallery/__init__.py deleted file mode 100755 index dc9c130a..00000000 --- a/plugins/pix_gallery/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -from pathlib import Path -from typing import Tuple - -import nonebot - -from configs.config import Config - -Config.add_plugin_config( - "hibiapi", - "HIBIAPI", - "https://api.obfs.dev", - help_="如果没有自建或其他hibiapi请不要修改", - default_value="https://api.obfs.dev", -) -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pximg.cf", help_="Pixiv反向代理") -Config.add_plugin_config( - "pix", - "PIX_IMAGE_SIZE", - "master", - name="PIX图库", - help_="PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度)", - default_value="master", -) -Config.add_plugin_config( - "pix", - "SEARCH_HIBIAPI_BOOKMARKS", - 5000, - help_="最低收藏,PIX使用HIBIAPI搜索图片时达到最低收藏才会添加至图库", - default_value=5000, - type=int, -) -Config.add_plugin_config( - "pix", - "WITHDRAW_PIX_MESSAGE", - (0, 1), - help_="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - default_value=(0, 1), - type=Tuple[int, int], -) -Config.add_plugin_config( - "pix", - "PIX_OMEGA_PIXIV_RATIO", - (10, 0), - help_="PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0)", - default_value=(10, 0), - type=Tuple[int, int], -) -Config.add_plugin_config( - "pix", "TIMEOUT", 10, help_="下载图片超时限制(秒)", default_value=10, type=int -) - -Config.add_plugin_config( - "pix", "SHOW_INFO", True, help_="是否显示图片的基本信息,如PID等", default_value=True, type=bool -) - -# GDict["run_sql"].append("ALTER TABLE omega_pixiv_illusts ADD classified Integer;") -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/pix_gallery/_data_source.py b/plugins/pix_gallery/_data_source.py deleted file mode 100644 index 0b9f92d8..00000000 --- a/plugins/pix_gallery/_data_source.py +++ /dev/null @@ -1,404 +0,0 @@ -import asyncio -import math -import platform -from asyncio.exceptions import TimeoutError -from asyncio.locks import Semaphore -from copy import deepcopy -from typing import List, Optional, Tuple - -import aiofiles -from asyncpg.exceptions import UniqueViolationError - -from configs.config import Config -from configs.path_config import TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage -from utils.utils import change_img_md5, change_pixiv_image_links - -from ._model.omega_pixiv_illusts import OmegaPixivIllusts -from ._model.pixiv import Pixiv - -try: - import ujson as json -except ModuleNotFoundError: - import json - -# if str(platform.system()).lower() == "windows": -# policy = asyncio.WindowsSelectorEventLoopPolicy() -# asyncio.set_event_loop_policy(policy) - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - -HIBIAPI = Config.get_config("hibiapi", "HIBIAPI") -if not HIBIAPI: - HIBIAPI = "https://api.obfs.dev" -HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI - - -async def start_update_image_url( - current_keyword: List[str], black_pid: List[str] -) -> "int, int": - """ - 开始更新图片url - :param current_keyword: 关键词 - :param black_pid: 黑名单pid - :return: pid数量和图片数量 - """ - global HIBIAPI - pid_count = 0 - pic_count = 0 - tasks = [] - semaphore = asyncio.Semaphore(10) - for keyword in current_keyword: - for page in range(1, 110): - if keyword.startswith("uid:"): - url = f"{HIBIAPI}/api/pixiv/member_illust" - params = {"id": keyword[4:], "page": page} - if page == 30: - break - elif keyword.startswith("pid:"): - url = f"{HIBIAPI}/api/pixiv/illust" - params = {"id": keyword[4:]} - else: - url = f"{HIBIAPI}/api/pixiv/search" - params = {"word": keyword, "page": page} - tasks.append( - asyncio.ensure_future( - search_image(url, keyword, params, semaphore, page, black_pid) - ) - ) - if keyword.startswith("pid:"): - break - result = await asyncio.gather(*tasks) - for x in result: - pid_count += x[0] - pic_count += x[1] - return pid_count, pic_count - - -async def search_image( - url: str, - keyword: str, - params: dict, - semaphore: Semaphore, - page: int = 1, - black: List[str] = None, -) -> "int, int": - """ - 搜索图片 - :param url: 搜索url - :param keyword: 关键词 - :param params: params参数 - :param semaphore: semaphore - :param page: 页面 - :param black: pid黑名单 - :return: pid数量和图片数量 - """ - tmp_pid = [] - pic_count = 0 - pid_count = 0 - async with semaphore: - # try: - data = (await AsyncHttpx.get(url, params=params)).json() - if ( - not data - or data.get("error") - or (not data.get("illusts") and not data.get("illust")) - ): - return 0, 0 - if url != f"{HIBIAPI}/api/pixiv/illust": - logger.info(f'{keyword}: 获取数据成功...数据总量:{len(data["illusts"])}') - data = data["illusts"] - else: - logger.info(f'获取数据成功...PID:{params.get("id")}') - data = [data["illust"]] - img_data = {} - for x in data: - pid = x["id"] - title = x["title"] - width = x["width"] - height = x["height"] - view = x["total_view"] - bookmarks = x["total_bookmarks"] - uid = x["user"]["id"] - author = x["user"]["name"] - tags = [] - for tag in x["tags"]: - for i in tag: - if tag[i]: - tags.append(tag[i]) - img_urls = [] - if x["page_count"] == 1: - img_urls.append(x["meta_single_page"]["original_image_url"]) - else: - for urls in x["meta_pages"]: - img_urls.append(urls["image_urls"]["original"]) - if ( - ( - bookmarks >= Config.get_config("pix", "SEARCH_HIBIAPI_BOOKMARKS") - or ( - url == f"{HIBIAPI}/api/pixiv/member_illust" - and bookmarks >= 1500 - ) - or (url == f"{HIBIAPI}/api/pixiv/illust") - ) - and len(img_urls) < 10 - and _check_black(img_urls, black) - ): - img_data[pid] = { - "pid": pid, - "title": title, - "width": width, - "height": height, - "view": view, - "bookmarks": bookmarks, - "img_urls": img_urls, - "uid": uid, - "author": author, - "tags": tags, - } - else: - continue - for x in img_data.keys(): - data = img_data[x] - data_copy = deepcopy(data) - del data_copy["img_urls"] - for img_url in data["img_urls"]: - img_p = img_url[img_url.rfind("_") + 1 : img_url.rfind(".")] - data_copy["img_url"] = img_url - data_copy["img_p"] = img_p - data_copy["is_r18"] = "R-18" in data["tags"] - if not await Pixiv.exists( - pid=data["pid"], img_url=img_url, img_p=img_p - ): - data_copy["img_url"] = img_url - await Pixiv.create(**data_copy) - if data["pid"] not in tmp_pid: - pid_count += 1 - tmp_pid.append(data["pid"]) - pic_count += 1 - logger.info(f'存储图片PID:{data["pid"]} IMG_P:{img_p}') - else: - logger.warning(f'{data["pid"]} | {img_url} 已存在...') - # except Exception as e: - # logger.warning(f"PIX在线搜索图片错误,已再次调用 {type(e)}:{e}") - # await search_image(url, keyword, params, semaphore, page, black) - return pid_count, pic_count - - -async def get_image(img_url: str, user_id: int) -> Optional[str]: - """ - 下载图片 - :param img_url: - :param user_id: - :return: 图片名称 - """ - if "https://www.pixiv.net/artworks" in img_url: - pid = img_url.rsplit("/", maxsplit=1)[-1] - params = {"id": pid} - for _ in range(3): - try: - response = await AsyncHttpx.get( - f"{HIBIAPI}/api/pixiv/illust", params=params - ) - if response.status_code == 200: - data = response.json() - if data.get("illust"): - if data["illust"]["page_count"] == 1: - img_url = data["illust"]["meta_single_page"][ - "original_image_url" - ] - else: - img_url = data["illust"]["meta_pages"][0]["image_urls"][ - "original" - ] - break - except TimeoutError: - pass - old_img_url = img_url - img_url = change_pixiv_image_links( - img_url, - Config.get_config("pix", "PIX_IMAGE_SIZE"), - Config.get_config("pixiv", "PIXIV_NGINX_URL"), - ) - old_img_url = change_pixiv_image_links( - old_img_url, None, Config.get_config("pixiv", "PIXIV_NGINX_URL") - ) - for _ in range(3): - try: - response = await AsyncHttpx.get( - img_url, - headers=headers, - timeout=Config.get_config("pix", "TIMEOUT"), - ) - if response.status_code == 404: - img_url = old_img_url - continue - async with aiofiles.open( - TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg", "wb" - ) as f: - await f.write(response.content) - change_img_md5( - TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg" - ) - return TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg" - except TimeoutError: - logger.warning(f"PIX:{img_url} 图片下载超时...") - pass - return None - - -async def uid_pid_exists(id_: str) -> bool: - """ - 检测 pid/uid 是否有效 - :param id_: pid/uid - """ - if id_.startswith("uid:"): - url = f"{HIBIAPI}/api/pixiv/member" - elif id_.startswith("pid:"): - url = f"{HIBIAPI}/api/pixiv/illust" - else: - return False - params = {"id": int(id_[4:])} - data = (await AsyncHttpx.get(url, params=params)).json() - if data.get("error"): - return False - return True - - -async def get_keyword_num(keyword: str) -> Tuple[int, int, int, int, int]: - """ - 查看图片相关 tag 数量 - :param keyword: 关键词tag - """ - count, r18_count = await Pixiv.get_keyword_num(keyword.split()) - count_, setu_count, r18_count_ = await OmegaPixivIllusts.get_keyword_num( - keyword.split() - ) - return count, r18_count, count_, setu_count, r18_count_ - - -async def remove_image(pid: int, img_p: Optional[str]): - """ - 删除置顶图片 - :param pid: pid - :param img_p: 图片 p 如 p0,p1 等 - """ - if img_p: - if "p" not in img_p: - img_p = f"p{img_p}" - if img_p: - await Pixiv.filter(pid=pid, img_p=img_p).delete() - else: - await Pixiv.filter(pid=pid).delete() - - -def gen_keyword_pic( - _pass_keyword: List[str], not_pass_keyword: List[str], is_superuser: bool -): - """ - 已通过或未通过的所有关键词/uid/pid - :param _pass_keyword: 通过列表 - :param not_pass_keyword: 未通过列表 - :param is_superuser: 是否超级用户 - """ - _keyword = [ - x - for x in _pass_keyword - if not x.startswith("uid:") - and not x.startswith("pid:") - and not x.startswith("black:") - ] - _uid = [x for x in _pass_keyword if x.startswith("uid:")] - _pid = [x for x in _pass_keyword if x.startswith("pid:")] - _n_keyword = [ - x - for x in not_pass_keyword - if not x.startswith("uid:") - and not x.startswith("pid:") - and not x.startswith("black:") - ] - _n_uid = [ - x - for x in not_pass_keyword - if x.startswith("uid:") and not x.startswith("black:") - ] - _n_pid = [ - x - for x in not_pass_keyword - if x.startswith("pid:") and not x.startswith("black:") - ] - img_width = 0 - img_data = { - "_keyword": {"width": 0, "data": _keyword}, - "_uid": {"width": 0, "data": _uid}, - "_pid": {"width": 0, "data": _pid}, - "_n_keyword": {"width": 0, "data": _n_keyword}, - "_n_uid": {"width": 0, "data": _n_uid}, - "_n_pid": {"width": 0, "data": _n_pid}, - } - for x in list(img_data.keys()): - img_data[x]["width"] = math.ceil(len(img_data[x]["data"]) / 40) - img_width += img_data[x]["width"] * 200 - if not is_superuser: - img_width = ( - img_width - - ( - img_data["_n_keyword"]["width"] - + img_data["_n_uid"]["width"] - + img_data["_n_pid"]["width"] - ) - * 200 - ) - del img_data["_n_keyword"] - del img_data["_n_pid"] - del img_data["_n_uid"] - current_width = 0 - A = BuildImage(img_width, 1100) - for x in list(img_data.keys()): - if img_data[x]["data"]: - img = BuildImage(img_data[x]["width"] * 200, 1100, 200, 1100, font_size=40) - start_index = 0 - end_index = 40 - total_index = img_data[x]["width"] * 40 - for _ in range(img_data[x]["width"]): - tmp = BuildImage(198, 1100, font_size=20) - text_img = BuildImage(198, 100, font_size=50) - key_str = "\n".join( - [key for key in img_data[x]["data"][start_index:end_index]] - ) - tmp.text((10, 100), key_str) - if x.find("_n") == -1: - text_img.text((24, 24), "已收录") - else: - text_img.text((24, 24), "待收录") - tmp.paste(text_img, (0, 0)) - start_index += 40 - end_index = ( - end_index + 40 if end_index + 40 <= total_index else total_index - ) - background_img = BuildImage(200, 1100, color="#FFE4C4") - background_img.paste(tmp, (1, 1)) - img.paste(background_img) - A.paste(img, (current_width, 0)) - current_width += img_data[x]["width"] * 200 - return A.pic2bs4() - - -def _check_black(img_urls: List[str], black: List[str]) -> bool: - """ - 检测pid是否在黑名单中 - :param img_urls: 图片img列表 - :param black: 黑名单 - :return: - """ - for b in black: - for img_url in img_urls: - if b in img_url: - return False - return True diff --git a/plugins/pix_gallery/_model/__init__.py b/plugins/pix_gallery/_model/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/plugins/pix_gallery/_model/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/plugins/pix_gallery/_model/omega_pixiv_illusts.py b/plugins/pix_gallery/_model/omega_pixiv_illusts.py deleted file mode 100644 index c80c5f45..00000000 --- a/plugins/pix_gallery/_model/omega_pixiv_illusts.py +++ /dev/null @@ -1,92 +0,0 @@ -from typing import List, Optional, Tuple - -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class OmegaPixivIllusts(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - pid = fields.BigIntField() - """pid""" - uid = fields.BigIntField() - """uid""" - title = fields.CharField(255) - """标题""" - uname = fields.CharField(255) - """画师名称""" - classified = fields.IntField() - """标记标签, 0=未标记, 1=已人工标记或从可信已标记来源获取""" - nsfw_tag = fields.IntField() - """nsfw标签,-1=未标记, 0=safe, 1=setu. 2=r18""" - width = fields.IntField() - """宽度""" - height = fields.IntField() - """高度""" - tags = fields.TextField() - """tags""" - url = fields.CharField(255) - """pixiv url链接""" - - class Meta: - table = "omega_pixiv_illusts" - table_description = "omega图库数据表" - unique_together = ("pid", "url") - - @classmethod - async def query_images( - cls, - keywords: Optional[List[str]] = None, - uid: Optional[int] = None, - pid: Optional[int] = None, - nsfw_tag: Optional[int] = 0, - num: int = 100, - ) -> List["OmegaPixivIllusts"]: - """ - 说明: - 查找符合条件的图片 - 参数: - :param keywords: 关键词 - :param uid: 画师uid - :param pid: 图片pid - :param nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 - :param num: 获取图片数量 - """ - if not num: - return [] - query = cls - if nsfw_tag is not None: - query = cls.filter(nsfw_tag=nsfw_tag) - if keywords: - for keyword in keywords: - query = query.filter(tags__contains=keyword) - elif uid: - query = query.filter(uid=uid) - elif pid: - query = query.filter(pid=pid) - query = query.annotate(rand=Random()).limit(num) - return await query.all() # type: ignore - - @classmethod - async def get_keyword_num( - cls, tags: Optional[List[str]] = None - ) -> Tuple[int, int, int]: - """ - 说明: - 获取相关关键词(keyword, tag)在图库中的数量 - 参数: - :param tags: 关键词/Tag - """ - query = cls - if tags: - for tag in tags: - query = query.filter(tags__contains=tag) - else: - query = query.all() - count = await query.filter(nsfw_tag=0).count() - setu_count = await query.filter(nsfw_tag=1).count() - r18_count = await query.filter(nsfw_tag=2).count() - return count, setu_count, r18_count diff --git a/plugins/pix_gallery/_model/pixiv.py b/plugins/pix_gallery/_model/pixiv.py deleted file mode 100644 index 4af995a5..00000000 --- a/plugins/pix_gallery/_model/pixiv.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import List, Optional, Tuple - -from tortoise import fields -from tortoise.contrib.postgres.functions import Random - -from services.db_context import Model - - -class Pixiv(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - pid = fields.BigIntField() - """pid""" - uid = fields.BigIntField() - """uid""" - author = fields.CharField(255) - """作者""" - title = fields.CharField(255) - """标题""" - width = fields.IntField() - """宽度""" - height = fields.IntField() - """高度""" - view = fields.IntField() - """pixiv查看数""" - bookmarks = fields.IntField() - """收藏数""" - tags = fields.TextField() - """tags""" - img_url = fields.CharField(255) - """pixiv url链接""" - img_p = fields.CharField(255) - """图片pN""" - is_r18 = fields.BooleanField() - - class Meta: - table = "pixiv" - table_description = "pix图库数据表" - unique_together = ("pid", "img_url", "img_p") - - # 0:非r18 1:r18 2:混合 - @classmethod - async def query_images( - cls, - keywords: Optional[List[str]] = None, - uid: Optional[int] = None, - pid: Optional[int] = None, - r18: Optional[int] = 0, - num: int = 100, - ) -> List[Optional["Pixiv"]]: - """ - 说明: - 查找符合条件的图片 - 参数: - :param keywords: 关键词 - :param uid: 画师uid - :param pid: 图片pid - :param r18: 是否r18,0:非r18 1:r18 2:混合 - :param num: 查找图片的数量 - """ - if not num: - return [] - query = cls - if r18 == 0: - query = query.filter(is_r18=False) - elif r18 == 1: - query = query.filter(is_r18=True) - if keywords: - for keyword in keywords: - query = query.filter(tags__contains=keyword) - elif uid: - query = query.filter(uid=uid) - elif pid: - query = query.filter(pid=pid) - query = query.annotate(rand=Random()).limit(num) - return await query.all() # type: ignore - - @classmethod - async def get_keyword_num(cls, tags: Optional[List[str]] = None) -> Tuple[int, int]: - """ - 说明: - 获取相关关键词(keyword, tag)在图库中的数量 - 参数: - :param tags: 关键词/Tag - """ - query = cls - if tags: - for tag in tags: - query = query.filter(tags__contains=tag) - else: - query = query.all() - count = await query.filter(is_r18=False).count() - r18_count = await query.filter(is_r18=True).count() - return count, r18_count diff --git a/plugins/pix_gallery/_model/pixiv_keyword_user.py b/plugins/pix_gallery/_model/pixiv_keyword_user.py deleted file mode 100644 index 829a1040..00000000 --- a/plugins/pix_gallery/_model/pixiv_keyword_user.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import List, Set, Tuple - -from tortoise import fields - -from services.db_context import Model - - -class PixivKeywordUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - keyword = fields.CharField(255, unique=True) - """关键词""" - is_pass = fields.BooleanField() - """是否通过""" - - class Meta: - table = "pixiv_keyword_users" - table_description = "pixiv关键词数据表" - - @classmethod - async def get_current_keyword(cls) -> Tuple[List[str], List[str]]: - """ - 说明: - 获取当前通过与未通过的关键词 - """ - pass_keyword = [] - not_pass_keyword = [] - for data in await cls.all().values_list("keyword", "is_pass"): - if data[1]: - pass_keyword.append(data[0]) - else: - not_pass_keyword.append(data[0]) - return pass_keyword, not_pass_keyword - - @classmethod - async def get_black_pid(cls) -> List[str]: - """ - 说明: - 获取黑名单PID - """ - black_pid = [] - keyword_list = await cls.filter(user_id="114514").values_list( - "keyword", flat=True - ) - for image in keyword_list: - black_pid.append(image[6:]) - return black_pid - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE pixiv_keyword_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE pixiv_keyword_users ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE pixiv_keyword_users ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/pix_gallery/pix.py b/plugins/pix_gallery/pix.py deleted file mode 100755 index a01fd792..00000000 --- a/plugins/pix_gallery/pix.py +++ /dev/null @@ -1,217 +0,0 @@ -import random - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from configs.config import Config -from services.log import logger -from utils.manager import withdraw_message_manager -from utils.message_builder import custom_forward_msg, image -from utils.utils import is_number - -from ._data_source import get_image -from ._model.omega_pixiv_illusts import OmegaPixivIllusts -from ._model.pixiv import Pixiv - -__zx_plugin_name__ = "PIX" -__plugin_usage__ = """ -usage: - 查看 pix 好康图库 - 指令: - pix ?*[tags]: 通过 tag 获取相似图片,不含tag时随机抽取 - pid [uid]: 通过uid获取图片 - pix pid[pid]: 查看图库中指定pid图片 - 示例:pix 萝莉 白丝 - 示例:pix 萝莉 白丝 10 (10为数量) - 示例:pix #02 (当tag只有1个tag且为数字时,使用#标记,否则将被判定为数量) - 示例:pix 34582394 (查询指定uid图片) - 示例:pix pid:12323423 (查询指定pid图片) -""".strip() -__plugin_superuser_usage__ = """ -usage: - 超级用户额外的 pix 指令 - 指令: - pix -s ?*[tags]: 通过tag获取色图,不含tag时随机 - pix -r ?*[tags]: 通过tag获取r18图,不含tag时随机 -""".strip() -__plugin_des__ = "这里是PIX图库!" -__plugin_cmd__ = [ - "pix ?*[tags]", - "pix pid [pid]", - "pix -s ?*[tags] [_superuser]", - "pix -r ?*[tags] [_superuser]", -] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["pix", "Pix", "PIX", "pIx"], -} -__plugin_block_limit__ = {"rst": "您有PIX图片正在处理,请稍等..."} -__plugin_configs__ = { - "MAX_ONCE_NUM2FORWARD": { - "value": None, - "help": "单次发送的图片数量达到指定值时转发为合并消息", - "default_value": None, - "type": int, - }, - "ALLOW_GROUP_SETU": { - "value": False, - "help": "允许非超级用户使用-s参数", - "default_value": False, - "type": bool, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "允许非超级用户使用-r参数", - "default_value": False, - "type": bool, - }, -} - - -pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True) - - -PIX_RATIO = None -OMEGA_RATIO = None - - -@pix.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - global PIX_RATIO, OMEGA_RATIO - if PIX_RATIO is None: - pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO") - PIX_RATIO = pix_omega_pixiv_ratio[0] / ( - pix_omega_pixiv_ratio[0] + pix_omega_pixiv_ratio[1] - ) - OMEGA_RATIO = 1 - PIX_RATIO - num = 1 - keyword = arg.extract_plain_text().strip() - x = keyword.split() - if "-s" in x: - x.remove("-s") - nsfw_tag = 1 - elif "-r" in x: - x.remove("-r") - nsfw_tag = 2 - else: - nsfw_tag = 0 - if str(event.user_id) not in bot.config.superusers: - if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or ( - nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18") - ): - await pix.finish("你不能看这些噢,这些都是是留给管理员看的...") - if (n := len(x)) == 1: - if is_number(x[0]) and int(x[0]) < 100: - num = int(x[0]) - keyword = "" - elif x[0].startswith("#"): - keyword = x[0][1:] - elif n > 1: - if is_number(x[-1]): - num = int(x[-1]) - if num > 10: - if str(event.user_id) not in bot.config.superusers or ( - str(event.user_id) in bot.config.superusers and num > 30 - ): - num = random.randint(1, 10) - await pix.send(f"太贪心了,就给你发 {num}张 好了") - x = x[:-1] - keyword = " ".join(x) - pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 - omega_num = num - pix_num + 15 - if is_number(keyword): - if num == 1: - pix_num = 15 - omega_num = 15 - all_image = await Pixiv.query_images( - uid=int(keyword), num=pix_num, r18=1 if nsfw_tag == 2 else 0 - ) + await OmegaPixivIllusts.query_images( - uid=int(keyword), num=omega_num, nsfw_tag=nsfw_tag - ) - elif keyword.lower().startswith("pid"): - pid = keyword.replace("pid", "").replace(":", "").replace(":", "") - if not is_number(pid): - await pix.finish("PID必须是数字...", at_sender=True) - all_image = await Pixiv.query_images( - pid=int(pid), r18=1 if nsfw_tag == 2 else 0 - ) - if not all_image: - all_image = await OmegaPixivIllusts.query_images( - pid=int(pid), nsfw_tag=nsfw_tag - ) - num = len(all_image) - else: - tmp = await Pixiv.query_images( - x, r18=1 if nsfw_tag == 2 else 0, num=pix_num - ) + await OmegaPixivIllusts.query_images(x, nsfw_tag=nsfw_tag, num=omega_num) - tmp_ = [] - all_image = [] - for x in tmp: - if x.pid not in tmp_: - all_image.append(x) - tmp_.append(x.pid) - if not all_image: - await pix.finish(f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片...", at_sender=True) - msg_list = [] - for _ in range(num): - img_url = None - author = None - if not all_image: - await pix.finish("坏了...发完了,没图了...") - img = random.choice(all_image) - all_image.remove(img) - if isinstance(img, OmegaPixivIllusts): - img_url = img.url - author = img.uname - elif isinstance(img, Pixiv): - img_url = img.img_url - author = img.author - pid = img.pid - title = img.title - uid = img.uid - _img = await get_image(img_url, event.user_id) - if _img: - if Config.get_config("pix", "SHOW_INFO"): - msg_list.append( - Message( - f"title:{title}\n" - f"author:{author}\n" - f"PID:{pid}\nUID:{uid}\n" - f"{image(_img)}" - ) - ) - else: - msg_list.append(image(_img)) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看PIX图库PID: {pid}" - ) - else: - msg_list.append("这张图似乎下载失败了") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看PIX图库PID: {pid},下载图片出错" - ) - if ( - Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") - and num >= Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") - and isinstance(event, GroupMessageEvent) - ): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg_list, bot.self_id) - ) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("pix", "WITHDRAW_PIX_MESSAGE") - ) - else: - for msg in msg_list: - msg_id = await pix.send(msg) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("pix", "WITHDRAW_PIX_MESSAGE") - ) diff --git a/plugins/pix_gallery/pix_add_keyword.py b/plugins/pix_gallery/pix_add_keyword.py deleted file mode 100755 index 0377f78d..00000000 --- a/plugins/pix_gallery/pix_add_keyword.py +++ /dev/null @@ -1,152 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.utils import is_number - -from ._data_source import uid_pid_exists -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "PIX关键词/UID/PID添加管理 [Superuser]" -__plugin_usage__ = """ -usage: - PIX关键词/UID/PID添加管理操作 - 指令: - 添加pix关键词 [Tag]: 添加一个pix搜索收录Tag - 添加pixuid [uid]: 添加一个pix搜索收录uid - 添加pixpid [pid]: 添加一个pix收录pid -""".strip() -__plugin_des__ = "PIX关键词/UID/PID添加管理" -__plugin_cmd__ = ["添加pix关键词 [Tag]", "添加pixuid [uid]", "添加pixpid [pid]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -add_keyword = on_command("添加pix关键词", aliases={"添加pix关键字"}, priority=1, block=True) - -add_black_pid = on_command("添加pix黑名单", permission=SUPERUSER, priority=1, block=True) - -# 超级用户可以通过字符 -f 来强制收录不检查是否存在 -add_uid_pid = on_command( - "添加pixuid", - aliases={ - "添加pixpid", - }, - priority=1, - block=True, -) - - -@add_keyword.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - group_id = -1 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - if msg: - # if await PixivKeywordUser.add_keyword( - # event.user_id, group_id, msg, bot.config.superusers - # ): - if not await PixivKeywordUser.exists(keyword=msg): - await PixivKeywordUser.create( - user_id=str(event.user_id), - group_id=str(group_id), - keyword=msg, - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_keyword.send( - f"已成功添加pixiv搜图关键词:{msg},请等待管理员通过该关键词!", at_sender=True - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 添加了pixiv搜图关键词:" + msg - ) - else: - await add_keyword.finish(f"该关键词 {msg} 已存在...") - else: - await add_keyword.finish(f"虚空关键词?.?.?.?") - - -@add_uid_pid.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - exists_flag = True - if msg.find("-f") != -1 and str(event.user_id) in bot.config.superusers: - exists_flag = False - msg = msg.replace("-f", "").strip() - if msg: - for msg in msg.split(): - if not is_number(msg): - await add_uid_pid.finish("UID只能是数字的说...", at_sender=True) - if cmd[0].lower().endswith("uid"): - msg = f"uid:{msg}" - else: - msg = f"pid:{msg}" - if await Pixiv.get_or_none(pid=int(msg[4:]), img_p="p0"): - await add_uid_pid.finish(f"该PID:{msg[4:]}已存在...", at_sender=True) - if not await uid_pid_exists(msg) and exists_flag: - await add_uid_pid.finish("画师或作品不存在或搜索正在CD,请稍等...", at_sender=True) - group_id = -1 - if isinstance(event, GroupMessageEvent): - group_id = event.group_id - # if await PixivKeywordUser.add_keyword( - # event.user_id, group_id, msg, bot.config.superusers - # ): - if not await PixivKeywordUser.exists(keyword=msg): - await PixivKeywordUser.create( - user_id=str(event.user_id), - group_id=str(group_id), - keyword=msg, - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_uid_pid.send( - f"已成功添加pixiv搜图UID/PID:{msg[4:]},请等待管理员通过!", at_sender=True - ) - else: - await add_uid_pid.finish(f"该UID/PID:{msg[4:]} 已存在...") - else: - await add_uid_pid.finish("湮灭吧!虚空的UID!") - - -@add_black_pid.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - pid = arg.extract_plain_text().strip() - img_p = "" - if "p" in pid: - img_p = pid.split("p")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("p")] - if not is_number(pid): - await add_black_pid.finish("PID必须全部是数字!", at_sender=True) - # if await PixivKeywordUser.add_keyword( - # 114514, - # 114514, - # f"black:{pid}{f'_p{img_p}' if img_p else ''}", - # bot.config.superusers, - # ): - if not await PixivKeywordUser.exists( - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" - ): - await PixivKeywordUser.create( - user_id=114514, - group_id=114514, - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", - is_pass=str(event.user_id) in bot.config.superusers, - ) - await add_black_pid.send(f"已添加PID:{pid} 至黑名单中...") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 添加了pixiv搜图黑名单 PID:{pid}" - ) - else: - await add_black_pid.send(f"PID:{pid} 已添加黑名单中,添加失败...") diff --git a/plugins/pix_gallery/pix_pass_del_keyword.py b/plugins/pix_gallery/pix_pass_del_keyword.py deleted file mode 100755 index 37045d80..00000000 --- a/plugins/pix_gallery/pix_pass_del_keyword.py +++ /dev/null @@ -1,205 +0,0 @@ -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Command, CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.message_builder import at -from utils.utils import is_number - -from ._data_source import remove_image -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "PIX关键词/UID/PID删除管理 [Superuser]" -__plugin_usage__ = """ -usage: - PIX关键词/UID/PID删除管理操作 - 指令: - 通过pix关键词 [关键词/pid/uid] - 取消pix关键词 [关键词/pid/uid] - 删除pix关键词 [关键词/pid/uid] - 删除pix图片 *[pid] - 示例:通过pix关键词萝莉 - 示例:通过pix关键词uid:123456 - 示例:通过pix关键词pid:123456 - 示例:删除pix图片4223442 -""".strip() -__plugin_des__ = "PIX关键词/UID/PID删除管理" -__plugin_cmd__ = [ - "通过pix关键词 [关键词/pid/uid]", - "取消pix关键词 [关键词/pid/uid]", - "删除pix关键词 [关键词/pid/uid]", - "删除pix图片 *[pid]", -] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -pass_keyword = on_command( - "通过pix关键词", - aliases={"通过pix关键字", "取消pix关键词", "取消pix关键字"}, - permission=SUPERUSER, - priority=1, - block=True, -) - -del_keyword = on_command( - "删除pix关键词", aliases={"删除pix关键字"}, permission=SUPERUSER, priority=1, block=True -) - -del_pic = on_command("删除pix图片", permission=SUPERUSER, priority=1, block=True) - - -@del_keyword.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await del_keyword.finish("好好输入要删除什么关键字啊笨蛋!") - if is_number(msg): - msg = f"uid:{msg}" - if msg.lower().startswith("pid"): - msg = "pid:" + msg.replace("pid", "").replace(":", "") - if data := await PixivKeywordUser.get_or_none(keyword=msg): - await data.delete() - await del_keyword.send(f"删除搜图关键词/UID:{msg} 成功...") - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 删除了pixiv搜图关键词:" + msg - ) - else: - await del_keyword.send(f"未查询到搜索关键词/UID/PID:{msg},删除失败!") - - -@del_pic.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - pid_arr = arg.extract_plain_text().strip() - if pid_arr: - msg = "" - black_pid = "" - flag = False - pid_arr = pid_arr.split() - if pid_arr[-1] in ["-black", "-b"]: - flag = True - pid_arr = pid_arr[:-1] - for pid in pid_arr: - img_p = None - if "p" in pid or "ugoira" in pid: - if "p" in pid: - img_p = pid.split("p")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("p")] - elif "ugoira" in pid: - img_p = pid.split("ugoira")[-1] - pid = pid.replace("_", "") - pid = pid[: pid.find("ugoira")] - if is_number(pid): - if await Pixiv.query_images(pid=int(pid), r18=2): - if await remove_image(int(pid), img_p): - msg += f'{pid}{f"_p{img_p}" if img_p else ""},' - if flag: - # if await PixivKeywordUser.add_keyword( - # 114514, - # 114514, - # f"black:{pid}{f'_p{img_p}' if img_p else ''}", - # bot.config.superusers, - # ): - if await PixivKeywordUser.exists( - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" - ): - await PixivKeywordUser.create( - user_id="114514", - group_id="114514", - keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", - is_pass=False, - ) - black_pid += f'{pid}{f"_p{img_p}" if img_p else ""},' - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 删除了PIX图片 PID:{pid}{f'_p{img_p}' if img_p else ''}" - ) - # else: - # await del_pic.send( - # f"PIX:删除pid:{pid}{f'_p{img_p}' if img_p else ''} 失败.." - # ) - else: - await del_pic.send( - f"PIX:图片pix:{pid}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.." - ) - else: - await del_pic.send(f"PID必须为数字!pid:{pid}", at_sender=True) - await del_pic.send(f"PIX:成功删除图片:{msg[:-1]}") - if flag: - await del_pic.send(f"成功图片PID加入黑名单:{black_pid[:-1]}") - else: - await del_pic.send("虚空删除?") - - -@pass_keyword.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - tmp = {"group": {}, "private": {}} - msg = arg.extract_plain_text().strip() - if not msg: - await pass_keyword.finish("通过虚空的关键词/UID?离谱...") - msg = msg.split() - flag = cmd[0][:2] == "通过" - for x in msg: - if x.lower().startswith("uid"): - x = x.replace("uid", "").replace(":", "") - x = f"uid:{x}" - elif x.lower().startswith("pid"): - x = x.replace("pid", "").replace(":", "") - x = f"pid:{x}" - if x.lower().find("pid") != -1 or x.lower().find("uid") != -1: - if not is_number(x[4:]): - await pass_keyword.send(f"UID/PID:{x} 非全数字,跳过该关键词...") - continue - data = await PixivKeywordUser.get_or_none(keyword=x) - user_id = 0 - group_id = 0 - if data: - data.is_pass = flag - await data.save(update_fields=["is_pass"]) - user_id, group_id = data.user_id, data.group_id - if not user_id: - await pass_keyword.send(f"未找到关键词/UID:{x},请检查关键词/UID是否存在...") - continue - if flag: - if group_id == -1: - if not tmp["private"].get(user_id): - tmp["private"][user_id] = {"keyword": [x]} - else: - tmp["private"][user_id]["keyword"].append(x) - else: - if not tmp["group"].get(group_id): - tmp["group"][group_id] = {} - if not tmp["group"][group_id].get(user_id): - tmp["group"][group_id][user_id] = {"keyword": [x]} - else: - tmp["group"][group_id][user_id]["keyword"].append(x) - msg = " ".join(msg) - await pass_keyword.send(f"已成功{cmd[0][:2]}搜图关键词:{msg}....") - for user in tmp["private"]: - x = ",".join(tmp["private"][user]["keyword"]) - await bot.send_private_msg( - user_id=user, message=f"你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." - ) - for group in tmp["group"]: - for user in tmp["group"][group]: - x = ",".join(tmp["group"][group][user]["keyword"]) - await bot.send_group_msg( - group_id=group, - message=Message(f"{at(user)}你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..."), - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 通过了pixiv搜图关键词/UID:" + msg - ) diff --git a/plugins/pix_gallery/pix_show_info.py b/plugins/pix_gallery/pix_show_info.py deleted file mode 100755 index 2b8aaab9..00000000 --- a/plugins/pix_gallery/pix_show_info.py +++ /dev/null @@ -1,85 +0,0 @@ -import asyncio - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent -from nonebot.params import CommandArg - -from utils.message_builder import image - -from ._data_source import gen_keyword_pic, get_keyword_num -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "查看pix图库" -__plugin_usage__ = """ -usage: - 查看pix图库 - 指令: - 查看pix图库 ?[tags]: 查看指定tag图片数量,为空时查看整个图库 -""".strip() -__plugin_des__ = "让我看看管理员私藏了多少货" -__plugin_cmd__ = ["查看pix图库 ?[tags]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查看pix图库"], -} - - -my_keyword = on_command("我的pix关键词", aliases={"我的pix关键字"}, priority=1, block=True) - -show_keyword = on_command("显示pix关键词", aliases={"显示pix关键字"}, priority=1, block=True) - -show_pix = on_command("查看pix图库", priority=1, block=True) - - -@my_keyword.handle() -async def _(event: MessageEvent): - data = await PixivKeywordUser.filter(user_id=str(event.user_id)).values_list( - "keyword", flat=True - ) - if not data: - await my_keyword.finish("您目前没有提供任何Pixiv搜图关键字...", at_sender=True) - await my_keyword.send(f"您目前提供的如下关键字:\n\t" + ",".join(data)) - - -@show_keyword.handle() -async def _(bot: Bot, event: MessageEvent): - _pass_keyword, not_pass_keyword = await PixivKeywordUser.get_current_keyword() - if _pass_keyword or not_pass_keyword: - await show_keyword.send( - image( - b64=await asyncio.get_event_loop().run_in_executor( - None, - gen_keyword_pic, - _pass_keyword, - not_pass_keyword, - str(event.user_id) in bot.config.superusers, - ) - ) - ) - else: - if str(event.user_id) in bot.config.superusers: - await show_keyword.finish(f"目前没有已收录或待收录的搜索关键词...") - else: - await show_keyword.finish(f"目前没有已收录的搜索关键词...") - - -@show_pix.handle() -async def _(arg: Message = CommandArg()): - keyword = arg.extract_plain_text().strip() - count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(keyword) - await show_pix.send( - f"PIX图库:{keyword}\n" - f"总数:{count + r18_count}\n" - f"美图:{count}\n" - f"R18:{r18_count}\n" - f"---------------\n" - f"Omega图库:{keyword}\n" - f"总数:{count_ + setu_count + r18_count_}\n" - f"美图:{count_}\n" - f"色图:{setu_count}\n" - f"R18:{r18_count_}" - ) diff --git a/plugins/pix_gallery/pix_update.py b/plugins/pix_gallery/pix_update.py deleted file mode 100755 index ce19604f..00000000 --- a/plugins/pix_gallery/pix_update.py +++ /dev/null @@ -1,207 +0,0 @@ -import asyncio -import os -import re -import time -from pathlib import Path -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Message -from nonebot.params import CommandArg -from nonebot.permission import SUPERUSER - -from services.log import logger -from utils.utils import is_number - -from ._data_source import start_update_image_url -from ._model.omega_pixiv_illusts import OmegaPixivIllusts -from ._model.pixiv import Pixiv -from ._model.pixiv_keyword_user import PixivKeywordUser - -__zx_plugin_name__ = "pix检查更新 [Superuser]" -__plugin_usage__ = """ -usage: - 更新pix收录的所有或指定数量的 关键词/uid/pid - 指令: - 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部 - pix检测更新:检测从未更新过的uid和pid - 示例:更新pix关键词keyword - 示例:更新pix关键词uid 10 -""".strip() -__plugin_des__ = "pix图库收录数据检查更新" -__plugin_cmd__ = ["更新pix关键词 *[keyword/uid/pid] [num=max]", "pix检测更新"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -start_update = on_command( - "更新pix关键词", aliases={"更新pix关键字"}, permission=SUPERUSER, priority=1, block=True -) - -check_not_update_uid_pid = on_command( - "pix检测更新", - aliases={"pix检查更新"}, - permission=SUPERUSER, - priority=1, - block=True, -) - -check_omega = on_command("检测omega图库", permission=SUPERUSER, priority=1, block=True) - - -@start_update.handle() -async def _(arg: Message = CommandArg()): - msg_sp = arg.extract_plain_text().strip().split() - _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() - _pass_keyword.reverse() - black_pid = await PixivKeywordUser.get_black_pid() - _keyword = [ - x - for x in _pass_keyword - if not x.startswith("uid:") - and not x.startswith("pid:") - and not x.startswith("black:") - ] - _uid = [x for x in _pass_keyword if x.startswith("uid:")] - _pid = [x for x in _pass_keyword if x.startswith("pid:")] - num = 9999 - msg = msg_sp[0] if len(msg_sp) else "" - if len(msg_sp) == 2: - if is_number(msg_sp[1]): - num = int(msg_sp[1]) - else: - await start_update.finish("参数错误...第二参数必须为数字") - if num < 10000: - keyword_str = ",".join( - _keyword[: num if num < len(_keyword) else len(_keyword)] - ) - uid_str = ",".join(_uid[: num if num < len(_uid) else len(_uid)]) - pid_str = ",".join(_pid[: num if num < len(_pid) else len(_pid)]) - if msg.lower() == "pid": - update_lst = _pid - info = f"开始更新Pixiv搜图PID:\n{pid_str}" - elif msg.lower() == "uid": - update_lst = _uid - info = f"开始更新Pixiv搜图UID:\n{uid_str}" - elif msg.lower() == "keyword": - update_lst = _keyword - info = f"开始更新Pixiv搜图关键词:\n{keyword_str}" - else: - update_lst = _pass_keyword - info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}" - num = num if num < len(update_lst) else len(update_lst) - else: - if msg.lower() == "pid": - update_lst = [f"pid:{num}"] - info = f"开始更新Pixiv搜图UID:\npid:{num}" - else: - update_lst = [f"uid:{num}"] - info = f"开始更新Pixiv搜图UID:\nuid:{num}" - await start_update.send(info) - start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst[:num], black_pid) - await start_update.send( - f"Pixiv搜图关键词搜图更新完成...\n" - f"累计更新PID {pid_count} 个\n" - f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time)) - ) - - -@check_not_update_uid_pid.handle() -async def _(arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - flag = False - if msg == "update": - flag = True - _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() - x_uid = [] - x_pid = [] - _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")] - _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")] - all_images = await Pixiv.query_images(r18=2) - for img in all_images: - if img.pid not in x_pid: - x_pid.append(img.pid) - if img.uid not in x_uid: - x_uid.append(img.uid) - await check_not_update_uid_pid.send( - "从未更新过的UID:" - + ",".join([f"uid:{x}" for x in _uid if x not in x_uid]) - + "\n" - + "从未更新过的PID:" - + ",".join([f"pid:{x}" for x in _pid if x not in x_pid]) - ) - if flag: - await check_not_update_uid_pid.send("开始自动自动更新PID....") - update_lst = [f"pid:{x}" for x in _uid if x not in x_uid] - black_pid = await PixivKeywordUser.get_black_pid() - start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst, black_pid) - await check_not_update_uid_pid.send( - f"Pixiv搜图关键词搜图更新完成...\n" - f"累计更新PID {pid_count} 个\n" - f"累计更新图片 {pic_count} 张" + "\n耗时:{:.2f}秒".format((time.time() - start_time)) - ) - - -@check_omega.handle() -async def _(): - async def _tasks(line: str, all_pid: List[int], length: int, index: int): - data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2] - num_list = re.findall(r"(\d+)", data) - pid = int(num_list[1]) - uid = int(num_list[2]) - id_ = 3 - while num_list[id_] not in ["0", "1"]: - id_ += 1 - classified = int(num_list[id_]) - nsfw_tag = int(num_list[id_ + 1]) - width = int(num_list[id_ + 2]) - height = int(num_list[id_ + 3]) - str_list = re.findall(r"'(.*?)',", data) - title = str_list[0] - uname = str_list[1] - tags = str_list[2] - url = str_list[3] - if pid in all_pid: - logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") - return - _, is_create = await OmegaPixivIllusts.get_or_create( - pid=pid, - title=title, - width=width, - height=height, - url=url, - uid=uid, - nsfw_tag=nsfw_tag, - tags=tags, - uname=uname, - classified=classified, - ) - if is_create: - logger.info( - f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张" - ) - else: - logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") - - omega_pixiv_illusts = None - for file in os.listdir("."): - if "omega_pixiv_artwork" in file and ".sql" in file: - omega_pixiv_illusts = Path() / file - if omega_pixiv_illusts: - with open(omega_pixiv_illusts, "r", encoding="utf8") as f: - lines = f.readlines() - tasks = [] - length = len([x for x in lines if "INSERT INTO" in x.upper()]) - all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True) - index = 0 - logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....") - for line in lines: - if "INSERT INTO" in line.upper(): - index += 1 - logger.info(f"line: {line} 加入更新计划") - tasks.append( - asyncio.ensure_future(_tasks(line, all_pid, length, index)) - ) - await asyncio.gather(*tasks) - omega_pixiv_illusts.unlink() diff --git a/plugins/pixiv_rank_search/__init__.py b/plugins/pixiv_rank_search/__init__.py deleted file mode 100755 index 351f5a49..00000000 --- a/plugins/pixiv_rank_search/__init__.py +++ /dev/null @@ -1,230 +0,0 @@ -import time -from asyncio.exceptions import TimeoutError -from typing import Type - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - NetworkError, -) -from nonebot.matcher import Matcher -from nonebot.params import CommandArg -from nonebot.rule import to_me - -from configs.config import Config -from services.log import logger -from utils.message_builder import custom_forward_msg -from utils.utils import is_number - -from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls - -__zx_plugin_name__ = "P站排行/搜图" - -__plugin_usage__ = """ -usage: - P站排行: - 可选参数: - 类型: - 1. 日排行 - 2. 周排行 - 3. 月排行 - 4. 原创排行 - 5. 新人排行 - 6. R18日排行 - 7. R18周排行 - 8. R18受男性欢迎排行 - 9. R18重口排行【慎重!】 - 【使用时选择参数序号即可,R18仅可私聊】 - p站排行 ?[参数] ?[数量] ?[日期] - 示例: - p站排行榜 [无参数默认为日榜] - p站排行榜 1 - p站排行榜 1 5 - p站排行榜 1 5 2018-4-25 - 【注意空格!!】【在线搜索会较慢】 - --------------------------------- - P站搜图: - 搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18) - 示例: - 搜图 樱岛麻衣 - 搜图 樱岛麻衣 5 - 搜图 樱岛麻衣 5 r18 - 搜图 樱岛麻衣#1000users 5 - 【多个关键词用#分割】 - 【默认为 热度排序】 - 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】 -""".strip() -__plugin_des__ = "P站排行榜直接冲,P站搜图跟着冲" -__plugin_cmd__ = ["p站排行 ?[参数] ?[数量] ?[日期]", "搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18)"] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 9, - "default_status": True, - "limit_superuser": False, - "cmd": ["p站排行", "搜图", "p站搜图", "P站搜图"], -} -__plugin_block_limit__ = {"rst": "P站排行榜或搜图正在搜索,请不要重复触发命令..."} -__plugin_configs__ = { - "TIMEOUT": {"value": 10, "help": "图片下载超时限制", "default_value": 10, "type": int}, - "MAX_PAGE_LIMIT": { - "value": 20, - "help": "作品最大页数限制,超过的作品会被略过", - "default_value": 20, - "type": int, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "允许群聊中使用 r18 参数", - "default_value": False, - "type": bool, - }, -} -Config.add_plugin_config( - "hibiapi", - "HIBIAPI", - "https://api.obfs.dev", - help_="如果没有自建或其他hibiapi请不要修改", - default_value="https://api.obfs.dev", -) -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pixiv.re", help_="Pixiv反向代理") - - -rank_dict = { - "1": "day", - "2": "week", - "3": "month", - "4": "week_original", - "5": "week_rookie", - "6": "day_r18", - "7": "week_r18", - "8": "day_male_r18", - "9": "week_r18g", -} - - -pixiv_rank = on_command( - "p站排行", - aliases={"P站排行榜", "p站排行榜", "P站排行榜", "P站排行"}, - priority=5, - block=True, - rule=to_me(), -) -pixiv_keyword = on_command("搜图", priority=5, block=True, rule=to_me()) - - -@pixiv_rank.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().strip() - msg = msg.split(" ") - msg = [m for m in msg if m] - code = 0 - info_list = [] - if not msg: - msg = ["1"] - if msg[0] in ["6", "7", "8", "9"]: - if event.message_type == "group": - await pixiv_rank.finish("羞羞脸!私聊里自己看!", at_sender=True) - if (n := len(msg)) == 0 or msg[0] == "": - info_list, code = await get_pixiv_urls(rank_dict.get("1")) - elif n == 1: - if msg[0] not in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: - await pixiv_rank.finish("要好好输入要看什么类型的排行榜呀!", at_sender=True) - info_list, code = await get_pixiv_urls(rank_dict.get(msg[0])) - elif n == 2: - info_list, code = await get_pixiv_urls(rank_dict.get(msg[0]), int(msg[1])) - elif n == 3: - if not check_date(msg[2]): - await pixiv_rank.finish("日期格式错误了", at_sender=True) - info_list, code = await get_pixiv_urls( - rank_dict.get(msg[0]), int(msg[1]), date=msg[2] - ) - else: - await pixiv_rank.finish("格式错了噢,参数不够?看看帮助?", at_sender=True) - if code != 200 and info_list: - await pixiv_rank.finish(info_list[0]) - if not info_list: - await pixiv_rank.finish("没有找到啊,等等再试试吧~V", at_sender=True) - await send_image(info_list, pixiv_rank, bot, event) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看了P站排行榜 code:{msg[0]}" - ) - - -@pixiv_keyword.handle() -async def _(bot: Bot, event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if isinstance(event, GroupMessageEvent): - if "r18" in msg.lower() and not Config.get_config( - "pixiv_rank_search", "ALLOW_GROUP_R18" - ): - await pixiv_keyword.finish("(脸红#) 你不会害羞的 八嘎!", at_sender=True) - r18 = 0 if "r18" in msg else 1 - msg = msg.replace("r18", "").strip().split() - msg = [m.strip() for m in msg if m] - keyword = None - info_list = None - num = 10 - page = 1 - if (n := len(msg)) > 0: - keyword = msg[0].replace("#", " ") - if n > 1: - if not is_number(msg[1]): - await pixiv_keyword.finish("图片数量必须是数字!", at_sender=True) - num = int(msg[1]) - if n > 2: - if not is_number(msg[2]): - await pixiv_keyword.finish("页数数量必须是数字!", at_sender=True) - page = int(msg[2]) - if keyword: - info_list, code = await search_pixiv_urls(keyword, num, page, r18) - if code != 200: - await pixiv_keyword.finish(info_list[0]) - if not info_list: - await pixiv_keyword.finish("没有找到啊,等等再试试吧~V", at_sender=True) - await send_image(info_list, pixiv_keyword, bot, event) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看了搜索 {keyword} R18:{r18}" - ) - - -def check_date(date): - try: - time.strptime(date, "%Y-%m-%d") - return True - except: - return False - - -async def send_image( - info_list: list, matcher: Type[Matcher], bot: Bot, event: MessageEvent -): - if isinstance(event, GroupMessageEvent): - await pixiv_rank.send("开始下载整理数据...") - idx = 0 - mes_list = [] - for title, author, urls in info_list: - _message = ( - f"title: {title}\nauthor: {author}\n" - + await download_pixiv_imgs(urls, event.user_id, idx) - ) - mes_list.append(_message) - idx += 1 - mes_list = custom_forward_msg(mes_list, bot.self_id) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - for title, author, urls in info_list: - try: - await matcher.send( - f"title: {title}\n" - f"author: {author}\n" - + await download_pixiv_imgs(urls, event.user_id) - ) - except (NetworkError, TimeoutError): - await matcher.send("这张图网络直接炸掉了!", at_sender=True) diff --git a/plugins/pixiv_rank_search/data_source.py b/plugins/pixiv_rank_search/data_source.py deleted file mode 100755 index d9b007a0..00000000 --- a/plugins/pixiv_rank_search/data_source.py +++ /dev/null @@ -1,162 +0,0 @@ -import platform -from asyncio.exceptions import TimeoutError -from pathlib import Path -from typing import Optional - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.message_builder import image -from utils.utils import change_img_md5 - -# if platform.system() == "Windows": -# import asyncio -# -# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def get_pixiv_urls( - mode: str, num: int = 10, page: int = 1, date: Optional[str] = None -) -> "list, int": - """ - 拿到pixiv rank图片url - :param mode: 模式 - :param num: 数量 - :param page: 页数 - :param date: 日期 - """ - params = {"mode": mode, "page": page} - if date: - params["date"] = date - hibiapi = Config.get_config("hibiapi", "HIBIAPI") - hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi - rank_url = f"{hibiapi}/api/pixiv/rank" - return await parser_data(rank_url, num, params, "rank") - - -async def search_pixiv_urls( - keyword: str, num: int, page: int, r18: int -) -> "list, list": - """ - 搜图图片的url - :param keyword: 关键词 - :param num: 数量 - :param page: 页数 - :param r18: 是否r18 - """ - params = {"word": keyword, "page": page} - hibiapi = Config.get_config("hibiapi", "HIBIAPI") - hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi - search_url = f"{hibiapi}/api/pixiv/search" - return await parser_data(search_url, num, params, "search", r18) - - -async def parser_data( - url: str, num: int, params: dict, type_: str, r18: int = 0 -) -> "list, int": - """ - 解析数据 - :param url: hibiapi搜索url - :param num: 数量 - :param params: 参数 - :param type_: 类型,rank或search - :param r18: 是否r18 - """ - info_list = [] - for _ in range(3): - try: - response = await AsyncHttpx.get( - url, - params=params, - timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), - ) - if response.status_code == 200: - data = response.json() - if data.get("illusts"): - data = data["illusts"] - break - except TimeoutError: - pass - except Exception as e: - logger.error(f"P站排行/搜图解析数据发生错误 {type(e)}:{e}") - return ["发生了一些些错误..."], 995 - else: - return ["网络不太好?没有该页数?也许过一会就好了..."], 998 - num = num if num < 30 else 30 - _data = [] - for x in data: - if x["page_count"] < Config.get_config("pixiv_rank_search", "MAX_PAGE_LIMIT"): - _data.append(x) - if len(_data) == num: - break - for x in _data: - if type_ == "search" and r18 == 1: - if "R-18" in str(x["tags"]): - continue - title = x["title"] - author = x["user"]["name"] - urls = [] - if x["page_count"] == 1: - urls.append(x["image_urls"]["large"]) - else: - for j in x["meta_pages"]: - urls.append(j["image_urls"]["large"]) - info_list.append((title, author, urls)) - return info_list, 200 - - -async def download_pixiv_imgs( - urls: list, user_id: int, forward_msg_index: int = None -) -> str: - """ - 下载图片 - :param urls: 图片链接 - :param user_id: 用户id - :param forward_msg_index: 转发消息中的图片排序 - """ - result = "" - index = 0 - for url in urls: - ws_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") - if ws_url: - url = ( - url.replace("i.pximg.net", ws_url) - .replace("i.pixiv.cat", ws_url) - .replace("_webp", "") - ) - try: - file = ( - TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" - if forward_msg_index is not None - else TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" - ) - file = Path(file) - try: - if await AsyncHttpx.download_file( - url, - file, - timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), - ): - change_img_md5(file) - if forward_msg_index is not None: - result += image( - TEMP_PATH - / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg", - ) - else: - result += image(TEMP_PATH / f"{user_id}_{index}_pixiv.jpg") - index += 1 - except OSError: - if file.exists(): - file.unlink() - except Exception as e: - logger.error(f"P站排行/搜图下载图片错误 {type(e)}:{e}") - return result diff --git a/plugins/poke/__init__.py b/plugins/poke/__init__.py deleted file mode 100755 index 180935c2..00000000 --- a/plugins/poke/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import random - -from nonebot import on_notice -from nonebot.adapters.onebot.v11 import PokeNotifyEvent - -from configs.path_config import IMAGE_PATH, RECORD_PATH -from models.ban_user import BanUser -from services.log import logger -from utils.message_builder import image, poke, record -from utils.utils import CountLimiter - -__zx_plugin_name__ = "戳一戳" - -__plugin_usage__ = """ -usage: - 戳一戳随机掉落语音或美图萝莉图 -""".strip() -__plugin_des__ = "戳一戳发送语音美图萝莉图不美哉?" -__plugin_type__ = ("其他",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["戳一戳"], -} - -poke__reply = [ - "lsp你再戳?", - "连个可爱美少女都要戳的肥宅真恶心啊。", - "你再戳!", - "?再戳试试?", - "别戳了别戳了再戳就坏了555", - "我爪巴爪巴,球球别再戳了", - "你戳你🐎呢?!", - "那...那里...那里不能戳...绝对...", - "(。´・ω・)ん?", - "有事恁叫我,别天天一个劲戳戳戳!", - "欸很烦欸!你戳🔨呢", - "?", - "再戳一下试试?", - "???", - "正在关闭对您的所有服务...关闭成功", - "啊呜,太舒服刚刚竟然睡着了。什么事?", - "正在定位您的真实地址...定位成功。轰炸机已起飞", -] - - -_clmt = CountLimiter(3) - -poke_ = on_notice(priority=5, block=False) - - -@poke_.handle() -async def _poke_event(event: PokeNotifyEvent): - if event.self_id == event.target_id: - _clmt.add(event.user_id) - if _clmt.check(event.user_id) or random.random() < 0.3: - rst = "" - if random.random() < 0.15: - await BanUser.ban(event.user_id, 1, 60) - rst = "气死我了!" - await poke_.finish(rst + random.choice(poke__reply), at_sender=True) - rand = random.random() - path = random.choice(["luoli", "meitu"]) - if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0: - index = random.randint( - 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1 - ) - result = f"id:{index}" + image( - IMAGE_PATH / "image_management" / path / f"{index}.jpg" - ) - await poke_.send(result) - logger.info(f"USER {event.user_id} 戳了戳我 回复: {result} {result}") - elif 0.3 < rand < 0.6: - voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) - result = record(RECORD_PATH / "dinggong" / voice) - await poke_.send(result) - await poke_.send(voice.split("_")[1]) - logger.info( - f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}' - ) - else: - await poke_.send(poke(event.user_id)) diff --git a/plugins/quotations.py b/plugins/quotations.py deleted file mode 100755 index 85fd3d9c..00000000 --- a/plugins/quotations.py +++ /dev/null @@ -1,40 +0,0 @@ -from nonebot import on_regex -from services.log import logger -from nonebot.adapters.onebot.v11 import Bot, MessageEvent, GroupMessageEvent -from nonebot.typing import T_State -from utils.http_utils import AsyncHttpx - - -__zx_plugin_name__ = "一言二次元语录" -__plugin_usage__ = """ -usage: - 一言二次元语录 - 指令: - 语录/二次元 -""".strip() -__plugin_des__ = "二次元语录给你力量" -__plugin_cmd__ = ["语录/二次元"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["语录", "二次元"], -} - - -quotations = on_regex("^(语录|二次元)$", priority=5, block=True) - -url = "https://international.v1.hitokoto.cn/?c=a" - - -@quotations.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - data = (await AsyncHttpx.get(url, timeout=5)).json() - result = f'{data["hitokoto"]}\t——{data["from"]}' - await quotations.send(result) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送语录:" - + result - ) diff --git a/plugins/roll.py b/plugins/roll.py deleted file mode 100755 index 6b3be616..00000000 --- a/plugins/roll.py +++ /dev/null @@ -1,65 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from services.log import logger -from configs.config import NICKNAME -import random -import asyncio - - -__zx_plugin_name__ = "roll" -__plugin_usage__ = """ -usage: - 随机数字 或 随机选择事件 - 指令: - roll: 随机 0-100 的数字 - roll *[文本]: 随机事件 - 示例:roll 吃饭 睡觉 打游戏 -""".strip() -__plugin_des__ = "犹豫不决吗?那就让我帮你决定吧" -__plugin_cmd__ = ["roll", "roll *[文本]"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["roll"], -} - - -roll = on_command("roll", priority=5, block=True) - - -@roll.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip().split() - if not msg: - await roll.finish(f"roll: {random.randint(0, 100)}", at_sender=True) - user_name = event.sender.card or event.sender.nickname - await roll.send( - random.choice( - [ - "转动命运的齿轮,拨开眼前迷雾...", - f"启动吧,命运的水晶球,为{user_name}指引方向!", - "嗯哼,在此刻转动吧!命运!", - f"在此祈愿,请为{user_name}降下指引...", - ] - ) - ) - await asyncio.sleep(1) - x = random.choice(msg) - await roll.send( - random.choice( - [ - f"让{NICKNAME}看看是什么结果!答案是:‘{x}’", - f"根据命运的指引,接下来{user_name} ‘{x}’ 会比较好", - f"祈愿被回应了!是 ‘{x}’!", - f"结束了,{user_name},命运之轮停在了 ‘{x}’!", - ] - ) - ) - logger.info( - f"(USER {event.user_id}, " - f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送roll:{msg}" - ) diff --git a/plugins/russian/__init__.py b/plugins/russian/__init__.py deleted file mode 100755 index 5d3aa444..00000000 --- a/plugins/russian/__init__.py +++ /dev/null @@ -1,536 +0,0 @@ -import asyncio -import random -import time -from typing import Tuple - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GROUP, Bot, GroupMessageEvent, Message -from nonebot.params import ArgStr, Command, CommandArg -from nonebot.typing import T_State - -from configs.config import NICKNAME, Config -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from services.log import logger -from utils.image_utils import text2image -from utils.message_builder import at, image -from utils.utils import get_message_at, is_number - -from .data_source import rank -from .model import RussianUser - -__zx_plugin_name__ = "俄罗斯轮盘" -__plugin_usage__ = """ -usage: - 又到了决斗时刻 - 指令: - 装弹 [子弹数] ?[金额=200] ?[at]: 开启游戏,装填子弹,可选自定义金额,或邀请决斗对象 - 接受对决: 接受当前存在的对决 - 拒绝对决: 拒绝邀请的对决 - 开枪: 开出未知的一枪 - 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用) - 我的战绩: 对,你的战绩 - 胜场排行/败场排行/欧洲人排行/慈善家排行/最高连胜排行/最高连败排行: 各种排行榜 - 示例:装弹 3 100 @sdd - * 注:同一时间群内只能有一场对决 * -""".strip() -__plugin_des__ = "虽然是运气游戏,但这可是战场啊少年" -__plugin_cmd__ = [ - "装弹 [子弹数] ?[金额=200] ?[at]", - "接受对决", - "拒绝对决", - "开枪", - "结算", - "我的战绩", - "胜场排行/败场排行/欧洲人排行/慈善家排行/最高连胜排行/最高连败排行", -] -__plugin_type__ = ("群内小游戏", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["俄罗斯轮盘", "装弹"], -} -__plugin_configs__ = { - "MAX_RUSSIAN_BET_GOLD": { - "value": 1000, - "help": "俄罗斯轮盘最大赌注金额", - "default_value": 1000, - "type": int, - } -} - -rs_player = {} - -russian = on_command( - "俄罗斯轮盘", aliases={"装弹", "俄罗斯转盘"}, permission=GROUP, priority=5, block=True -) - -accept = on_command( - "接受对决", aliases={"接受决斗", "接受挑战"}, permission=GROUP, priority=5, block=True -) - -refuse = on_command( - "拒绝对决", aliases={"拒绝决斗", "拒绝挑战"}, permission=GROUP, priority=5, block=True -) - -shot = on_command( - "开枪", aliases={"咔", "嘭", "嘣"}, permission=GROUP, priority=5, block=True -) - -settlement = on_command("结算", permission=GROUP, priority=5, block=True) - -record = on_command("我的战绩", permission=GROUP, priority=5, block=True) - -russian_rank = on_command( - "胜场排行", - aliases={"胜利排行", "败场排行", "失败排行", "欧洲人排行", "慈善家排行", "最高连胜排行", "最高连败排行"}, - permission=GROUP, - priority=5, - block=True, -) - - -@accept.handle() -async def _(event: GroupMessageEvent): - global rs_player - try: - if rs_player[event.group_id][1] == 0: - await accept.finish("目前没有发起对决,你接受个啥?速速装弹!", at_sender=True) - except KeyError: - await accept.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if rs_player[event.group_id][2] != 0: - if ( - rs_player[event.group_id][1] == event.user_id - or rs_player[event.group_id][2] == event.user_id - ): - await accept.finish(f"你已经身处决斗之中了啊,给我认真一点啊!", at_sender=True) - else: - await accept.finish("已经有人接受对决了,你还是乖乖等待下一场吧!", at_sender=True) - if rs_player[event.group_id][1] == event.user_id: - await accept.finish("请不要自己枪毙自己!换人来接受对决...", at_sender=True) - if ( - rs_player[event.group_id]["at"] != 0 - and rs_player[event.group_id]["at"] != event.user_id - ): - await accept.finish( - Message(f'这场对决是邀请 {at(rs_player[event.group_id]["at"])}的,不要捣乱!'), - at_sender=True, - ) - if time.time() - rs_player[event.group_id]["time"] > 30: - rs_player[event.group_id] = {} - await accept.finish("这场对决邀请已经过时了,请重新发起决斗...", at_sender=True) - - user_money = await BagUser.get_gold(event.user_id, event.group_id) - if user_money < rs_player[event.group_id]["money"]: - if ( - rs_player[event.group_id]["at"] != 0 - and rs_player[event.group_id]["at"] == event.user_id - ): - rs_player[event.group_id] = {} - await accept.finish("你的金币不足以接受这场对决!对决还未开始便结束了,请重新装弹!", at_sender=True) - else: - await accept.finish("你的金币不足以接受这场对决!", at_sender=True) - - player2_name = event.sender.card or event.sender.nickname - - rs_player[event.group_id][2] = event.user_id - rs_player[event.group_id]["player2"] = player2_name - rs_player[event.group_id]["time"] = time.time() - - await accept.send( - Message(f"{player2_name}接受了对决!\n" f"请{at(rs_player[event.group_id][1])}先开枪!") - ) - - -@refuse.handle() -async def _(event: GroupMessageEvent): - global rs_player - try: - if rs_player[event.group_id][1] == 0: - await accept.finish("你要拒绝啥?明明都没有人发起对决的说!", at_sender=True) - except KeyError: - await refuse.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if ( - rs_player[event.group_id]["at"] != 0 - and event.user_id != rs_player[event.group_id]["at"] - ): - await accept.finish("又不是找你决斗,你拒绝什么啊!气!", at_sender=True) - if rs_player[event.group_id]["at"] == event.user_id: - at_player_name = ( - await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - ).user_name - await accept.send( - Message(f"{at(rs_player[event.group_id][1])}\n" f"{at_player_name}拒绝了你的对决!") - ) - rs_player[event.group_id] = {} - - -@settlement.handle() -async def _(bot: Bot, event: GroupMessageEvent): - global rs_player - if ( - not rs_player.get(event.group_id) - or rs_player[event.group_id][1] == 0 - or rs_player[event.group_id][2] == 0 - ): - await settlement.finish("比赛并没有开始...无法结算...", at_sender=True) - if ( - event.user_id != rs_player[event.group_id][1] - and event.user_id != rs_player[event.group_id][2] - ): - await settlement.finish("吃瓜群众不要捣乱!黄牌警告!", at_sender=True) - if time.time() - rs_player[event.group_id]["time"] <= 30: - await settlement.finish( - f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]} 比赛并未超时,请继续比赛...' - ) - win_name = ( - rs_player[event.group_id]["player1"] - if rs_player[event.group_id][2] == rs_player[event.group_id]["next"] - else rs_player[event.group_id]["player2"] - ) - await settlement.send(f"这场对决是 {win_name} 胜利了") - await end_game(bot, event) - - -@russian.handle() -async def _( - bot: Bot, event: GroupMessageEvent, state: T_State, arg: Message = CommandArg() -): - global rs_player - msg = arg.extract_plain_text().strip() - try: - if ( - rs_player[event.group_id][1] - and not rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] <= 30 - ): - await russian.finish( - f'现在是 {rs_player[event.group_id]["player1"]} 发起的对决\n请等待比赛结束后再开始下一轮...' - ) - if ( - rs_player[event.group_id][1] - and rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] <= 30 - ): - await russian.finish( - f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]}的对决还未结束!' - ) - if ( - rs_player[event.group_id][1] - and rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] > 30 - ): - await russian.send("决斗已过时,强行结算...") - await end_game(bot, event) - if ( - not rs_player[event.group_id][2] - and time.time() - rs_player[event.group_id]["time"] > 30 - ): - rs_player[event.group_id][1] = 0 - rs_player[event.group_id][2] = 0 - rs_player[event.group_id]["at"] = 0 - except KeyError: - pass - if msg: - msg = msg.split() - if len(msg) == 1: - msg = msg[0] - if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state["bullet_num"] = int(msg) - else: - money = msg[1].strip() - msg = msg[0].strip() - if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state["bullet_num"] = int(msg) - if is_number(money) and 0 < int(money) <= Config.get_config( - "russian", "MAX_RUSSIAN_BET_GOLD" - ): - state["money"] = int(money) - else: - state["money"] = 200 - await russian.send( - f"赌注金额超过限制({Config.get_config('russian', 'MAX_RUSSIAN_BET_GOLD')}),已改为200(默认)" - ) - state["at"] = get_message_at(event.json()) - - -@russian.got("bullet_num", prompt="请输入装填子弹的数量!(最多6颗)") -async def _( - event: GroupMessageEvent, state: T_State, bullet_num: str = ArgStr("bullet_num") -): - global rs_player - if bullet_num in ["取消", "算了"]: - await russian.finish("已取消操作...") - try: - if rs_player[event.group_id][1] != 0: - await russian.finish("决斗已开始...", at_sender=True) - except KeyError: - pass - if not is_number(bullet_num): - await russian.reject_arg("bullet_num", "输入子弹数量必须是数字啊喂!") - bullet_num = int(bullet_num) - if bullet_num < 1 or bullet_num > 6: - await russian.reject_arg("bullet_num", "子弹数量必须大于0小于7!") - at_ = state["at"] if state.get("at") else [] - money = state["money"] if state.get("money") else 200 - user_money = await BagUser.get_gold(event.user_id, event.group_id) - if bullet_num < 0 or bullet_num > 6: - await russian.reject("子弹数量必须大于0小于7!速速重新装弹!") - if money > Config.get_config("russian", "MAX_RUSSIAN_BET_GOLD"): - await russian.finish( - f"太多了!单次金额不能超过{Config.get_config('russian', 'MAX_RUSSIAN_BET_GOLD')}!", - at_sender=True, - ) - if money > user_money: - await russian.finish("你没有足够的钱支撑起这场挑战", at_sender=True) - - player1_name = event.sender.card or event.sender.nickname - - if at_: - at_ = at_[0] - try: - at_player_name = ( - await GroupInfoUser.get_or_none(user_id=at_, group_id=event.group_id) - ).user_name - except AttributeError: - at_player_name = at(at_) - msg = f"{player1_name} 向 {at(at_)} 发起了决斗!请 {at_player_name} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!" - else: - at_ = 0 - msg = "若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】" - - rs_player[event.group_id] = { - 1: event.user_id, - "player1": player1_name, - 2: 0, - "player2": "", - "at": at_, - "next": event.user_id, - "money": money, - "bullet": random_bullet(bullet_num), - "bullet_num": bullet_num, - "null_bullet_num": 7 - bullet_num, - "index": 0, - "time": time.time(), - } - - await russian.send( - Message( - ("咔 " * bullet_num)[:-1] + f",装填完毕\n挑战金额:{money}\n" - f"第一枪的概率为:{str(float(bullet_num) / 7.0 * 100)[:5]}%\n" - f"{msg}" - ) - ) - - -@shot.handle() -async def _(bot: Bot, event: GroupMessageEvent): - global rs_player - try: - if time.time() - rs_player[event.group_id]["time"] > 30: - if rs_player[event.group_id][2] == 0: - rs_player[event.group_id][1] = 0 - await shot.finish("这场对决已经过时了,请重新装弹吧!", at_sender=True) - else: - await shot.send("决斗已过时,强行结算...") - await end_game(bot, event) - return - except KeyError: - await shot.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) - if rs_player[event.group_id][1] == 0: - await shot.finish("没有对决,也还没装弹呢,请先输入 装弹 吧!", at_sender=True) - if ( - rs_player[event.group_id][1] == event.user_id - and rs_player[event.group_id][2] == 0 - ): - await shot.finish("baka,你是要枪毙自己嘛笨蛋!", at_sender=True) - if rs_player[event.group_id][2] == 0: - await shot.finish("请这位勇士先发送 接受对决 来站上擂台...", at_sender=True) - player1_name = rs_player[event.group_id]["player1"] - player2_name = rs_player[event.group_id]["player2"] - if rs_player[event.group_id]["next"] != event.user_id: - if ( - event.user_id != rs_player[event.group_id][1] - and event.user_id != rs_player[event.group_id][2] - ): - await shot.finish( - random.choice( - [ - f"不要打扰 {player1_name} 和 {player2_name} 的决斗啊!", - f"给我好好做好一个观众!不然{NICKNAME}就要生气了", - f"不要捣乱啊baka{(await GroupInfoUser.get_or_none(user_id=event.user_id, group_id=event.group_id)).user_name}!", - ] - ), - at_sender=True, - ) - await shot.finish( - f"你的左轮不是连发的!该 " - f'{(await GroupInfoUser.get_or_none(user_id=int(rs_player[event.group_id]["next"]), group_id=event.group_id)).user_name} 开枪了' - ) - if rs_player[event.group_id]["bullet"][rs_player[event.group_id]["index"]] != 1: - await shot.send( - Message( - random.choice( - [ - "呼呼,没有爆裂的声响,你活了下来", - "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了", - '"咔",你没死,看来运气不错', - ] - ) - + f"\n下一枪中弹的概率" - f':{str(float((rs_player[event.group_id]["bullet_num"])) / float(rs_player[event.group_id]["null_bullet_num"] - 1 + rs_player[event.group_id]["bullet_num"]) * 100)[:5]}%\n' - f"轮到 {at(rs_player[event.group_id][1] if event.user_id == rs_player[event.group_id][2] else rs_player[event.group_id][2])}了" - ) - ) - rs_player[event.group_id]["null_bullet_num"] -= 1 - rs_player[event.group_id]["next"] = ( - rs_player[event.group_id][1] - if event.user_id == rs_player[event.group_id][2] - else rs_player[event.group_id][2] - ) - rs_player[event.group_id]["time"] = time.time() - rs_player[event.group_id]["index"] += 1 - else: - await shot.send( - random.choice( - [ - '"嘭!",你直接去世了', - "眼前一黑,你直接穿越到了异世界...(死亡)", - "终究还是你先走一步...", - ] - ) - + f'\n第 {rs_player[event.group_id]["index"] + 1} 发子弹送走了你...', - at_sender=True, - ) - win_name = ( - player1_name - if event.user_id == rs_player[event.group_id][2] - else player2_name - ) - await asyncio.sleep(0.5) - await shot.send(f"这场对决是 {win_name} 胜利了") - await end_game(bot, event) - - -async def end_game(bot: Bot, event: GroupMessageEvent): - global rs_player - player1_name = rs_player[event.group_id]["player1"] - player2_name = rs_player[event.group_id]["player2"] - if rs_player[event.group_id]["next"] == rs_player[event.group_id][1]: - win_user_id = rs_player[event.group_id][2] - lose_user_id = rs_player[event.group_id][1] - win_name = player2_name - lose_name = player1_name - else: - win_user_id = rs_player[event.group_id][1] - lose_user_id = rs_player[event.group_id][2] - win_name = player1_name - lose_name = player2_name - rand = random.randint(0, 5) - money = rs_player[event.group_id]["money"] - if money > 10: - fee = int(money * float(rand) / 100) - fee = 1 if fee < 1 and rand != 0 else fee - else: - fee = 0 - await RussianUser.add_count(win_user_id, event.group_id, "win") - await RussianUser.add_count(lose_user_id, event.group_id, "lose") - await RussianUser.money(win_user_id, event.group_id, "win", money - fee) - await RussianUser.money(lose_user_id, event.group_id, "lose", money) - await BagUser.add_gold(win_user_id, event.group_id, money - fee) - await BagUser.spend_gold(lose_user_id, event.group_id, money) - win_user, _ = await RussianUser.get_or_create( - user_id=str(win_user_id), group_id=str(event.group_id) - ) - lose_user, _ = await RussianUser.get_or_create( - user_id=str(lose_user_id), group_id=str(event.group_id) - ) - bullet_str = "" - for x in rs_player[event.group_id]["bullet"]: - bullet_str += "__ " if x == 0 else "| " - logger.info(f"俄罗斯轮盘:胜者:{win_name} - 败者:{lose_name} - 金币:{money}") - rs_player[event.group_id] = {} - await bot.send( - event, - message=image( - await text2image( - f"结算:\n" - f"\t胜者:{win_name}\n" - f"\t赢取金币:{money - fee}\n" - f"\t累计胜场:{win_user.win_count}\n" - f"\t累计赚取金币:{win_user.make_money}\n" - f"-------------------\n" - f"\t败者:{lose_name}\n" - f"\t输掉金币:{money}\n" - f"\t累计败场:{lose_user.fail_count}\n" - f"\t累计输掉金币:{lose_user.lose_money}\n" - f"-------------------\n" - f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n" - f"子弹排列:{bullet_str[:-1]}", - padding=10, - color="#f9f6f2", - ) - ), - ) - - -@record.handle() -async def _(event: GroupMessageEvent): - user, _ = await RussianUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - await record.send( - f"俄罗斯轮盘\n" - f"总胜利场次:{user.win_count}\n" - f"当前连胜:{user.winning_streak}\n" - f"最高连胜:{user.max_winning_streak}\n" - f"总失败场次:{user.fail_count}\n" - f"当前连败:{user.losing_streak}\n" - f"最高连败:{user.max_losing_streak}\n" - f"赚取金币:{user.make_money}\n" - f"输掉金币:{user.lose_money}", - at_sender=True, - ) - - -@russian_rank.handle() -async def _( - event: GroupMessageEvent, - cmd: Tuple[str, ...] = Command(), - arg: Message = CommandArg(), -): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - rank_image = None - if cmd[0] in ["胜场排行", "胜利排行"]: - rank_image = await rank(event.group_id, "win_rank", num) - if cmd[0] in ["败场排行", "失败排行"]: - rank_image = await rank(event.group_id, "lose_rank", num) - if cmd[0] == "欧洲人排行": - rank_image = await rank(event.group_id, "make_money", num) - if cmd[0] == "慈善家排行": - rank_image = await rank(event.group_id, "spend_money", num) - if cmd[0] == "最高连胜排行": - rank_image = await rank(event.group_id, "max_winning_streak", num) - if cmd[0] == "最高连败排行": - rank_image = await rank(event.group_id, "max_losing_streak", num) - if rank_image: - await russian_rank.send(image(b64=rank_image.pic2bs4())) - - -# 随机子弹排列 -def random_bullet(num: int) -> list: - bullet_lst = [0, 0, 0, 0, 0, 0, 0] - for i in random.sample([0, 1, 2, 3, 4, 5, 6], num): - bullet_lst[i] = 1 - return bullet_lst diff --git a/plugins/russian/data_source.py b/plugins/russian/data_source.py deleted file mode 100755 index 2f9546e0..00000000 --- a/plugins/russian/data_source.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Optional - -from utils.data_utils import init_rank -from utils.image_utils import BuildMat - -from .model import RussianUser - - -async def rank(group_id: int, itype: str, num: int) -> Optional[BuildMat]: - all_users = await RussianUser.filter(group_id=group_id).all() - all_user_id = [user.user_id for user in all_users] - if itype == "win_rank": - rank_name = "胜场排行榜" - all_user_data = [user.win_count for user in all_users] - elif itype == "lose_rank": - rank_name = "败场排行榜" - all_user_data = [user.fail_count for user in all_users] - elif itype == "make_money": - rank_name = "赢取金币排行榜" - all_user_data = [user.make_money for user in all_users] - elif itype == "spend_money": - rank_name = "输掉金币排行榜" - all_user_data = [user.lose_money for user in all_users] - elif itype == "max_winning_streak": - rank_name = "最高连胜排行榜" - all_user_data = [user.max_winning_streak for user in all_users] - else: - rank_name = "最高连败排行榜" - all_user_data = [user.max_losing_streak for user in all_users] - rst = None - if all_users: - rst = await init_rank(rank_name, all_user_id, all_user_data, group_id, num) - return rst diff --git a/plugins/russian/model.py b/plugins/russian/model.py deleted file mode 100755 index 4875f185..00000000 --- a/plugins/russian/model.py +++ /dev/null @@ -1,108 +0,0 @@ -from tortoise import fields - -from services.db_context import Model - - -class RussianUser(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255) - """群聊id""" - win_count = fields.IntField(default=0) - """胜利次数""" - fail_count = fields.IntField(default=0) - """失败次数""" - make_money = fields.IntField(default=0) - """赢得金币""" - lose_money = fields.IntField(default=0) - """输得金币""" - winning_streak = fields.IntField(default=0) - """当前连胜""" - losing_streak = fields.IntField(default=0) - """当前连败""" - max_winning_streak = fields.IntField(default=0) - """最大连胜""" - max_losing_streak = fields.IntField(default=0) - """最大连败""" - - class Meta: - table = "russian_users" - table_description = "俄罗斯轮盘数据表" - unique_together = ("user_id", "group_id") - - @classmethod - async def add_count(cls, user_id: str, group_id: str, itype: str): - """ - 说明: - 添加用户输赢次数 - 说明: - :param user_id: qq号 - :param group_id: 群号 - :param itype: 输或赢 'win' or 'lose' - """ - user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) - if itype == "win": - _max = ( - user.max_winning_streak - if user.max_winning_streak > user.winning_streak + 1 - else user.winning_streak + 1 - ) - user.win_count = user.win_count + 1 - user.winning_streak = user.winning_streak + 1 - user.losing_streak = 0 - user.max_winning_streak = _max - await user.save( - update_fields=[ - "win_count", - "winning_streak", - "losing_streak", - "max_winning_streak", - ] - ) - elif itype == "lose": - _max = ( - user.max_losing_streak - if user.max_losing_streak > user.losing_streak + 1 - else user.losing_streak + 1 - ) - user.fail_count = user.fail_count + 1 - user.losing_streak = user.losing_streak + 1 - user.winning_streak = 0 - user.max_losing_streak = _max - await user.save( - update_fields=[ - "fail_count", - "winning_streak", - "losing_streak", - "max_losing_streak", - ] - ) - - @classmethod - async def money(cls, user_id: str, group_id: str, itype: str, count: int) -> bool: - """ - 说明: - 添加用户输赢金钱 - 参数: - :param user_id: qq号 - :param group_id: 群号 - :param itype: 输或赢 'win' or 'lose' - :param count: 金钱数量 - """ - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=group_id) - if itype == "win": - user.make_money = user.make_money + count - elif itype == "lose": - user.lose_money = user.lose_money + count - await user.save(update_fields=["make_money", "lose_money"]) - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE russian_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE russian_users ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE russian_users ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/search_anime/__init__.py b/plugins/search_anime/__init__.py deleted file mode 100755 index c57cd73d..00000000 --- a/plugins/search_anime/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import ArgStr, CommandArg -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.message_builder import custom_forward_msg - -from .data_source import from_anime_get_info - -__zx_plugin_name__ = "搜番" -__plugin_usage__ = f""" -usage: - 搜索动漫资源 - 指令: - 搜番 [番剧名称或者关键词] - 示例:搜番 刀剑神域 -""".strip() -__plugin_des__ = "找不到想看的动漫吗?" -__plugin_cmd__ = ["搜番 [番剧名称或者关键词]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["搜番"], -} -__plugin_block_limit__ = {"rst": "搜索还未完成,不要重复触发!"} -__plugin_configs__ = { - "SEARCH_ANIME_MAX_INFO": { - "value": 20, - "help": "搜索动漫返回的最大数量", - "default_value": 20, - "type": int, - } -} - -search_anime = on_command("搜番", aliases={"搜动漫"}, priority=5, block=True) - - -@search_anime.handle() -async def _(state: T_State, arg: Message = CommandArg()): - if arg.extract_plain_text().strip(): - state["anime"] = arg.extract_plain_text().strip() - - -@search_anime.got("anime", prompt="是不是少了番名?") -async def _( - bot: Bot, event: MessageEvent, state: T_State, key_word: str = ArgStr("anime") -): - await search_anime.send(f"开始搜番 {key_word}", at_sender=True) - anime_report = await from_anime_get_info( - key_word, - Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"), - ) - if anime_report: - if isinstance(anime_report, str): - await search_anime.finish(anime_report) - if isinstance(event, GroupMessageEvent): - mes_list = custom_forward_msg(anime_report, bot.self_id) - await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) - else: - await search_anime.send("\n\n".join(anime_report)) - logger.info( - f"USER {event.user_id} GROUP" - f" {event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 搜索番剧 {key_word} 成功" - ) - else: - logger.warning(f"未找到番剧 {key_word}") - await search_anime.send(f"未找到番剧 {key_word}(也有可能是超时,再尝试一下?)") diff --git a/plugins/search_anime/data_source.py b/plugins/search_anime/data_source.py deleted file mode 100755 index 7adb6836..00000000 --- a/plugins/search_anime/data_source.py +++ /dev/null @@ -1,52 +0,0 @@ -from lxml import etree -import feedparser -from urllib import parse -from services.log import logger -from utils.http_utils import AsyncHttpx -from typing import List, Union -import time - - -async def from_anime_get_info(key_word: str, max_: int) -> Union[str, List[str]]: - s_time = time.time() - url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word) - try: - repass = await get_repass(url, max_) - except Exception as e: - logger.error(f"发生了一些错误 {type(e)}:{e}") - return "发生了一些错误!" - repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n") - return repass - - -async def get_repass(url: str, max_: int) -> List[str]: - put_line = [] - text = (await AsyncHttpx.get(url)).text - d = feedparser.parse(text) - max_ = max_ if max_ < len([e.link for e in d.entries]) else len([e.link for e in d.entries]) - url_list = [e.link for e in d.entries][:max_] - for u in url_list: - try: - text = (await AsyncHttpx.get(u)).text - html = etree.HTML(text) - magent = html.xpath('.//a[@id="a_magnet"]/text()')[0] - title = html.xpath(".//h3/text()")[0] - item = html.xpath( - '//div[@class="info resource-info right"]/ul/li' - ) - class_a = ( - item[0] - .xpath("string(.)")[5:] - .strip() - .replace("\xa0", "") - .replace("\t", "") - ) - size = item[3].xpath("string(.)")[5:].strip() - put_line.append( - "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent) - ) - except Exception as e: - logger.error(f"搜番发生错误 {type(e)}:{e}") - return put_line - - diff --git a/plugins/search_buff_skin_price/__init__.py b/plugins/search_buff_skin_price/__init__.py deleted file mode 100755 index ad903b08..00000000 --- a/plugins/search_buff_skin_price/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -from nonebot import on_command -from .data_source import get_price, update_buff_cookie -from services.log import logger -from nonebot.typing import T_State -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.rule import to_me -from nonebot.permission import SUPERUSER -from nonebot.params import CommandArg, ArgStr -from configs.config import NICKNAME - - -__zx_plugin_name__ = "BUFF查询皮肤" -__plugin_usage__ = """ -usage: - 在线实时获取BUFF指定皮肤所有磨损底价 - 指令: - 查询皮肤 [枪械名] [皮肤名称] - 示例:查询皮肤 ak47 二西莫夫 -""".strip() -__plugin_des__ = "BUFF皮肤底价查询" -__plugin_cmd__ = ["查询皮肤 [枪械名] [皮肤名称]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询皮肤"], -} -__plugin_block_limit__ = {"rst": "您有皮肤正在搜索,请稍等..."} -__plugin_configs__ = { - "BUFF_PROXY": {"value": None, "help": "BUFF代理,有些厂ip可能被屏蔽"}, - "COOKIE": {"value": None, "help": "BUFF的账号cookie"}, -} - - -search_skin = on_command("查询皮肤", aliases={"皮肤查询"}, priority=5, block=True) - - -@search_skin.handle() -async def _(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - raw_arg = arg.extract_plain_text().strip() - if raw_arg: - args = raw_arg.split() - if len(args) >= 2: - state["name"] = args[0] - state["skin"] = args[1] - - -@search_skin.got("name", prompt="要查询什么武器呢?") -@search_skin.got("skin", prompt="要查询该武器的什么皮肤呢?") -async def arg_handle( - event: MessageEvent, - state: T_State, - name: str = ArgStr("name"), - skin: str = ArgStr("skin"), -): - if name in ["算了", "取消"] or skin in ["算了", "取消"]: - await search_skin.finish("已取消操作...") - result = "" - if name in ["ak", "ak47"]: - name = "ak-47" - name = name + " | " + skin - try: - result, status_code = await get_price(name) - except FileNotFoundError: - await search_skin.finish(f'请先对{NICKNAME}说"设置cookie"来设置cookie!') - if status_code in [996, 997, 998]: - await search_skin.finish(result) - if result: - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询皮肤:" - + name - ) - await search_skin.finish(result) - else: - logger.info( - f"USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" 查询皮肤:{name} 没有查询到" - ) - await search_skin.finish("没有查询到哦,请检查格式吧") - - -update_buff_session = on_command( - "更新cookie", aliases={"设置cookie"}, rule=to_me(), permission=SUPERUSER, priority=1 -) - - -@update_buff_session.handle() -async def _(event: MessageEvent): - await update_buff_session.finish( - update_buff_cookie(str(event.get_message())), at_sender=True - ) diff --git a/plugins/search_buff_skin_price/data_source.py b/plugins/search_buff_skin_price/data_source.py deleted file mode 100755 index 5ab86040..00000000 --- a/plugins/search_buff_skin_price/data_source.py +++ /dev/null @@ -1,58 +0,0 @@ -from asyncio.exceptions import TimeoutError -from configs.config import Config -from utils.http_utils import AsyncHttpx -from services.log import logger - - -url = "https://buff.163.com/api/market/goods" - - -async def get_price(d_name: str) -> "str, int": - """ - 查看皮肤价格 - :param d_name: 武器皮肤,如:awp 二西莫夫 - """ - cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")} - name_list = [] - price_list = [] - parameter = {"game": "csgo", "page_num": "1", "search": d_name} - try: - response = await AsyncHttpx.get( - url, - proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"), - params=parameter, - cookies=cookie, - ) - if response.status_code == 200: - try: - if response.text.find("Login Required") != -1: - return "BUFF登录被重置,请联系管理员重新登入", 996 - data = response.json()["data"] - total_page = data["total_page"] - data = data["items"] - for _ in range(total_page): - for i in range(len(data)): - name = data[i]["name"] - price = data[i]["sell_reference_price"] - name_list.append(name) - price_list.append(price) - except Exception as e: - logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}") - return "没有查询到...", 998 - else: - return "访问失败!", response.status_code - except TimeoutError: - return "访问超时! 请重试或稍后再试!", 997 - result = f"皮肤: {d_name}({len(name_list)})\n" - for i in range(len(name_list)): - result += name_list[i] + ": " + price_list[i] + "\n" - return result[:-1], 999 - - -def update_buff_cookie(cookie: str) -> str: - Config.set_config("search_buff_skin_price", "COOKIE", cookie) - return "更新cookie成功" - - -if __name__ == "__main__": - print(get_price("awp 二西莫夫")) diff --git a/plugins/search_image/__init__.py b/plugins/search_image/__init__.py deleted file mode 100644 index d32ebf5c..00000000 --- a/plugins/search_image/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg, Depends -from nonebot.plugin import on_command -from nonebot.typing import T_State - -from services.log import logger -from utils.message_builder import custom_forward_msg -from utils.utils import get_message_img - -from .saucenao import get_saucenao_image - -__zx_plugin_name__ = "识图" -__plugin_usage__ = """ -usage: - 识别图片 [二次元图片] - 指令: - 识图 [图片] -""".strip() -__plugin_des__ = "以图搜图,看破本源" -__plugin_cmd__ = ["识图"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["识图"], -} -__plugin_configs__ = { - "MAX_FIND_IMAGE_COUNT": { - "value": 3, - "help": "识图返回的最大结果数", - "default_value": 3, - "type": int, - }, - "API_KEY": { - "value": None, - "help": "Saucenao的API_KEY,通过 https://saucenao.com/user.php?page=search-api 注册获取", - }, -} - - -search_image = on_command("识图", block=True, priority=5) - - -async def get_image_info(mod: str, url: str): - if mod == "saucenao": - return await get_saucenao_image(url) - - -def parse_image(key: str): - async def _key_parser(state: T_State, img: Message = Arg(key)): - if not get_message_img(img): - await search_image.reject_arg(key, "请发送要识别的图片!") - state[key] = img - - return _key_parser - - -@search_image.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if msg: - state["mod"] = msg - else: - state["mod"] = "saucenao" - if get_message_img(event.json()): - state["img"] = event.message - - -@search_image.got("img", prompt="图来!", parameterless=[Depends(parse_image("img"))]) -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - mod: str = ArgStr("mod"), - img: Message = Arg("img"), -): - img = get_message_img(img)[0] - await search_image.send("开始处理图片...") - msg = await get_image_info(mod, img) - if isinstance(msg, str): - await search_image.finish(msg, at_sender=True) - if isinstance(event, GroupMessageEvent): - await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg, bot.self_id) - ) - else: - for m in msg[1:]: - await search_image.send(m) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 识图:" + img - ) diff --git a/plugins/search_image/saucenao.py b/plugins/search_image/saucenao.py deleted file mode 100644 index dd0c395b..00000000 --- a/plugins/search_image/saucenao.py +++ /dev/null @@ -1,57 +0,0 @@ -from services import logger -from utils.http_utils import AsyncHttpx -from configs.config import Config -from configs.path_config import TEMP_PATH -from utils.message_builder import image -from typing import Union, List -import random - -API_URL_SAUCENAO = "https://saucenao.com/search.php" -API_URL_ASCII2D = "https://ascii2d.net/search/url/" -API_URL_IQDB = "https://iqdb.org/" - - -async def get_saucenao_image(url: str) -> Union[str, List[str]]: - api_key = Config.get_config("search_image", "API_KEY") - if not api_key: - return "Saucenao 缺失API_KEY!" - - params = { - "output_type": 2, - "api_key": api_key, - "testmode": 1, - "numres": 6, - "db": 999, - "url": url, - } - data = (await AsyncHttpx.post(API_URL_SAUCENAO, params=params)).json() - if data["header"]["status"] != 0: - return f"Saucenao识图失败..status:{data['header']['status']}" - data = data["results"] - data = ( - data - if len(data) < Config.get_config("search_image", "MAX_FIND_IMAGE_COUNT") - else data[: Config.get_config("search_image", "MAX_FIND_IMAGE_COUNT")] - ) - msg_list = [] - index = random.randint(0, 10000) - if await AsyncHttpx.download_file( - url, TEMP_PATH / f"saucenao_search_{index}.jpg" - ): - msg_list.append(image(TEMP_PATH / f"saucenao_search_{index}.jpg")) - for info in data: - try: - similarity = info["header"]["similarity"] - tmp = f"相似度:{similarity}%\n" - for x in info["data"].keys(): - if x != "ext_urls": - tmp += f"{x}:{info['data'][x]}\n" - try: - if "source" not in info["data"].keys(): - tmp += f'source:{info["data"]["ext_urls"][0]}\n' - except KeyError: - tmp += f'source:{info["header"]["thumbnail"]}\n' - msg_list.append(tmp[:-1]) - except Exception as e: - logger.warning(f"识图获取图片信息发生错误 {type(e)}:{e}") - return msg_list diff --git a/plugins/self_message/__init__.py b/plugins/self_message/__init__.py deleted file mode 100644 index 6d87f811..00000000 --- a/plugins/self_message/__init__.py +++ /dev/null @@ -1,90 +0,0 @@ -from datetime import datetime - -from nonebot import on -from nonebot.adapters.onebot.v11 import ( - Bot, - Event, - GroupMessageEvent, - PrivateMessageEvent, -) -from nonebot.message import handle_event - -from configs.config import Config - -from ._rule import rule - -__zx_plugin_name__ = "自身消息触发 [Hidden]" -__plugin_version__ = 0.1 - -Config.add_plugin_config( - "self_message", - "STATUS", - False, - help_="允许真寻自身触发命令,需要在go-cqhttp配置文件中report-self-message修改为true,触发命令时需前缀cmd且受权限影响,例如:cmd签到", - default_value=False, - type=bool, -) - -message_sent = on( - type="message_sent", - priority=999, - block=False, - rule=rule(), -) - - -@message_sent.handle() -async def handle_message_sent(bot: Bot, event: Event): - msg = str(getattr(event, "message", "")) - self_id = event.self_id - user_id = getattr(event, "user_id", -1) - msg_id = getattr(event, "message_id", -1) - msg_type = getattr(event, "message_type") - if ( - str(user_id) not in bot.config.superusers and self_id != user_id - ) or not msg.lower().startswith("cmd"): - return - msg = msg[3:] - if msg_type == "group": - new_event = GroupMessageEvent.parse_obj( - { - "time": getattr(event, "time", int(datetime.now().timestamp())), - "self_id": self_id, - "user_id": user_id, - "message": msg, - "raw_message": getattr(event, "raw_message", ""), - "post_type": "message", - "sub_type": getattr(event, "sub_type", "normal"), - "group_id": getattr(event, "group_id", -1), - "message_type": getattr(event, "message_type", "group"), - "message_id": msg_id, - "font": getattr(event, "font", 0), - "sender": getattr(event, "sender", {"user_id": user_id}), - "to_me": True, - } - ) - # await _check_reply(bot, new_event) - # _check_at_me(bot, new_event) - # _check_nickname(bot, new_event) - await handle_event(bot=bot, event=new_event) - elif msg_type == "private": - target_id = getattr(event, "target_id") - if target_id == user_id: - new_event = PrivateMessageEvent.parse_obj( - { - "time": getattr(event, "time", int(datetime.now().timestamp())), - "self_id": self_id, - "user_id": user_id, - "message": getattr(event, "message", ""), - "raw_message": getattr(event, "raw_message", ""), - "post_type": "message", - "sub_type": getattr(event, "sub_type"), - "message_type": msg_type, - "message_id": msg_id, - "font": getattr(event, "font", 0), - "sender": getattr(event, "sender"), - "to_me": True, - "target_id": target_id, - } - ) - await handle_event(bot=bot, event=new_event) diff --git a/plugins/self_message/_rule.py b/plugins/self_message/_rule.py deleted file mode 100644 index eb1f1cca..00000000 --- a/plugins/self_message/_rule.py +++ /dev/null @@ -1,9 +0,0 @@ -from configs.config import Config -from nonebot.internal.rule import Rule - - -def rule() -> Rule: - async def _rule() -> bool: - return Config.get_config("self_message", "STATUS") - - return Rule(_rule) diff --git a/plugins/send_dinggong_voice/__init__.py b/plugins/send_dinggong_voice/__init__.py deleted file mode 100755 index 5fe37c44..00000000 --- a/plugins/send_dinggong_voice/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import random - -from nonebot import on_keyword -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.rule import to_me -from nonebot.typing import T_State - -from configs.path_config import RECORD_PATH -from services.log import logger -from utils.message_builder import record - -__zx_plugin_name__ = "骂我" -__plugin_usage__ = """ -usage: - 多骂我一点,球球了 - 指令: - 骂老子 -""".strip() -__plugin_des__ = "请狠狠的骂我一次!" -__plugin_cmd__ = ["骂老子/骂我"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["骂老子", "骂我"], -} -__plugin_cd_limit__ = {"cd": 3, "rst": "就...就算求我骂你也得慢慢来..."} - - -dg_voice = on_keyword({"骂"}, rule=to_me(), priority=5, block=True) - - -@dg_voice.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - if len(str((event.get_message()))) > 1: - voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) - result = record( - RECORD_PATH / "dinggong" / voice, - ) - await dg_voice.send(result) - await dg_voice.send(voice.split("_")[1]) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送钉宫骂人:" - + result - ) diff --git a/plugins/send_setu_/__init__.py b/plugins/send_setu_/__init__.py deleted file mode 100755 index 5f878a25..00000000 --- a/plugins/send_setu_/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import nonebot - - -nonebot.load_plugins("plugins/send_setu_") diff --git a/plugins/send_setu_/_model.py b/plugins/send_setu_/_model.py deleted file mode 100644 index ba2920ec..00000000 --- a/plugins/send_setu_/_model.py +++ /dev/null @@ -1,84 +0,0 @@ -from typing import List, Optional - -from tortoise import fields -from tortoise.contrib.postgres.functions import Random -from tortoise.expressions import Q - -from services.db_context import Model - - -class Setu(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - local_id = fields.IntField() - """本地存储下标""" - title = fields.CharField(255) - """标题""" - author = fields.CharField(255) - """作者""" - pid = fields.BigIntField() - """pid""" - img_hash: str = fields.TextField() - """图片hash""" - img_url = fields.CharField(255) - """pixiv url链接""" - is_r18 = fields.BooleanField() - """是否r18""" - tags = fields.TextField() - """tags""" - - class Meta: - table = "setu" - table_description = "色图数据表" - unique_together = ("pid", "img_url") - - @classmethod - async def query_image( - cls, - local_id: Optional[int] = None, - tags: Optional[List[str]] = None, - r18: bool = False, - limit: int = 50, - ): - """ - 说明: - 通过tag查找色图 - 参数: - :param local_id: 本地色图 id - :param tags: tags - :param r18: 是否 r18,0:非r18 1:r18 2:混合 - :param limit: 获取数量 - """ - if local_id: - return await cls.filter(is_r18=r18, local_id=local_id).first() - query = cls.filter(is_r18=r18) - if tags: - for tag in tags: - query = query.filter( - Q(tags__contains=tag) - | Q(title__contains=tag) - | Q(author__contains=tag) - ) - query = query.annotate(rand=Random()).limit(limit) - return await query.all() - - @classmethod - async def delete_image(cls, pid: int, img_url: str) -> int: - """ - 说明: - 删除图片并替换 - 参数: - :param pid: 图片pid - """ - print(pid) - return_id = -1 - if query := await cls.get_or_none(pid=pid, img_url=img_url): - num = await cls.filter(is_r18=query.is_r18).count() - last_image = await cls.get_or_none(is_r18=query.is_r18, local_id=num - 1) - if last_image: - return_id = last_image.local_id - last_image.local_id = query.local_id - await last_image.save(update_fields=["local_id"]) - await query.delete() - return return_id diff --git a/plugins/send_setu_/send_setu/__init__.py b/plugins/send_setu_/send_setu/__init__.py deleted file mode 100755 index b08cf58c..00000000 --- a/plugins/send_setu_/send_setu/__init__.py +++ /dev/null @@ -1,426 +0,0 @@ -import random -from typing import Any, Optional, Tuple, Type - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - Event, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, -) -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.params import CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import NICKNAME, Config -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.depends import OneCommand -from utils.manager import withdraw_message_manager -from utils.message_builder import custom_forward_msg -from utils.utils import is_number - -from .._model import Setu -from .data_source import ( - add_data_to_database, - check_local_exists_or_download, - gen_message, - get_luoxiang, - get_setu_list, - get_setu_urls, - search_online_setu, -) - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "色图" -__plugin_usage__ = f""" -usage: - 搜索 lolicon 图库,每日色图time... - 指令: - 色图: 随机本地色图 - 色图r: 随机在线十张r18涩图 - 色图 [id]: 本地指定id色图 - 色图 *[tags]: 在线搜索指定tag色图 - 色图r *[tags]: 同上 - [1-9]张涩图: 本地随机色图连发 - [1-9]张[tags]的涩图: 在线搜索指定tag色图连发 - [1-9]张涩图r[tags]: 同上 - 示例:色图 萝莉|少女 白丝|黑丝 - 示例:色图 萝莉 猫娘 - 注: - tag至多取前20项,| 为或,萝莉|少女=萝莉或者少女 -""".strip() -__plugin_des__ = "不要小看涩图啊混蛋!" -__plugin_cmd__ = [ - "色图 ?[id]", - "色图 ?[tags]", - "色图r ?[tags]", - "[1-9]张?[tags]色图", - "[1-9]张色图?[tags]", -] -__plugin_type__ = ("来点好康的",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 9, - "default_status": True, - "limit_superuser": False, - "cmd": ["色图", "涩图", "瑟图"], -} -__plugin_block_limit__ = {} -__plugin_cd_limit__ = { - "rst": "您冲的太快了,请稍后再冲.", -} -__plugin_configs__ = { - "WITHDRAW_SETU_MESSAGE": { - "value": (0, 1), - "help": "自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", - "default_value": (0, 1), - "type": Tuple[int, int], - }, - "ONLY_USE_LOCAL_SETU": { - "value": False, - "help": "仅仅使用本地色图,不在线搜索", - "default_value": False, - "type": bool, - }, - "INITIAL_SETU_PROBABILITY": { - "value": 0.7, - "help": "初始色图概率,总概率 = 初始色图概率 + 好感度", - "default_value": 0.7, - "type": float, - }, - "DOWNLOAD_SETU": { - "value": True, - "help": "是否存储下载的色图,使用本地色图可以加快图片发送速度", - "default_value": True, - "type": bool, - }, - "TIMEOUT": {"value": 10, "help": "色图下载超时限制(秒)", "default_value": 10, "type": int}, - "SHOW_INFO": { - "value": True, - "help": "是否显示色图的基本信息,如PID等", - "default_value": True, - "type": bool, - }, - "ALLOW_GROUP_R18": { - "value": False, - "help": "在群聊中启用R18权限", - "default_value": False, - "type": bool, - }, - "MAX_ONCE_NUM2FORWARD": { - "value": None, - "help": "单次发送的图片数量达到指定值时转发为合并消息", - "default_value": None, - "type": int, - }, - "MAX_ONCE_NUM": { - "value": 10, - "help": "单次发送图片数量限制", - "default_value": 10, - "type": int, - }, -} -Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pixiv.re", help_="Pixiv反向代理") - -setu_data_list = [] - - -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - global setu_data_list - if isinstance(event, MessageEvent): - if matcher.plugin_name == "send_setu": - # 添加数据至数据库 - try: - await add_data_to_database(setu_data_list) - logger.info("色图数据自动存储数据库成功...") - setu_data_list = [] - except Exception: - pass - - -setu = on_command( - "色图", aliases={"涩图", "不够色", "来一发", "再来点", "色图r"}, priority=5, block=True -) - -setu_reg = on_regex("(.*)[份|发|张|个|次|点](.*)[瑟|色|涩]图(r?)(.*)$", priority=5, block=True) - - -@setu.handle() -async def _( - bot: Bot, - event: MessageEvent, - cmd: str = OneCommand(), - arg: Message = CommandArg(), -): - msg = arg.extract_plain_text().strip() - if isinstance(event, GroupMessageEvent): - user, _ = await SignGroupUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - impression = user.impression - if luox := get_luoxiang(impression): - await setu.finish(luox) - r18 = False - num = 1 - # 是否看r18 - if cmd == "色图r" and isinstance(event, PrivateMessageEvent): - r18 = True - num = 10 - elif cmd == "色图r" and isinstance(event, GroupMessageEvent): - if not Config.get_config("send_setu", "ALLOW_GROUP_R18"): - await setu.finish( - random.choice(["这种不好意思的东西怎么可能给这么多人看啦", "羞羞脸!给我滚出克私聊!", "变态变态变态变态大变态!"]) - ) - else: - r18 = False - # 有 数字 的话先尝试本地色图id - if msg and is_number(msg): - setu_list, code = await get_setu_list(int(msg), r18=r18) - if code != 200: - await setu.finish(setu_list[0], at_sender=True) - setu_img, code = await check_local_exists_or_download(setu_list[0]) - msg_id = await setu.send(gen_message(setu_list[0]) + setu_img, at_sender=True) - logger.info( - f"发送色图 {setu_list[0].local_id}.jpg", - cmd, - event.user_id, - getattr(event, "group_id", None), - ) - if msg_id: - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - return - await send_setu_handle(bot, setu, event, cmd, msg, num, r18) - - -num_key = { - "一": 1, - "二": 2, - "两": 2, - "双": 2, - "三": 3, - "四": 4, - "五": 5, - "六": 6, - "七": 7, - "八": 8, - "九": 9, -} - - -@setu_reg.handle() -async def _(bot: Bot, event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - if isinstance(event, GroupMessageEvent): - user, _ = await SignGroupUser.get_or_create( - user_id=str(event.user_id), group_id=str(event.group_id) - ) - impression = user.impression - if luox := get_luoxiang(impression): - await setu.finish(luox, at_sender=True) - num, tags, r18, tags2 = reg_group - num = num or "一" - tags = tags[:-1] if tags and tags[-1] == "的" else tags - if num_key.get(num): - num = num_key[num] - try: - num = int(num) - except ValueError: - num = 1 - if ( - r18 - and not Config.get_config("send_setu", "ALLOW_GROUP_R18") - and isinstance(event, GroupMessageEvent) - ): - await setu.finish( - random.choice(["这种不好意思的东西怎么可能给这么多人看啦", "羞羞脸!给我滚出克私聊!", "变态变态变态变态大变态!"]) - ) - else: - limit = Config.get_config("send_setu", "MAX_ONCE_NUM") - if limit and num > limit: - num = limit - await setu.send(f"一次只能给你看 {num} 张哦") - await send_setu_handle( - bot, setu_reg, event, "色图r" if r18 else "色图", tags + " " + tags2, num, r18 - ) - - -async def send_setu_handle( - bot: Bot, - matcher: Type[Matcher], - event: MessageEvent, - command: str, - msg: str, - num: int, - r18: bool, -): - global setu_data_list - # 非 id,在线搜索 - tags = msg.split() - # 真寻的色图?怎么可能 - if f"{NICKNAME}" in tags: - await matcher.finish("咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀") - # 本地先拿图,下载失败补上去 - setu_list, code = None, 200 - setu_count = await Setu.filter(is_r18=r18).count() - max_once_num2forward = Config.get_config("send_setu", "MAX_ONCE_NUM2FORWARD") - if ( - not Config.get_config("send_setu", "ONLY_USE_LOCAL_SETU") and tags - ) or setu_count <= 0: - # 先尝试获取在线图片 - urls, text_list, add_databases_list, code = await get_setu_urls( - tags, num, r18, command - ) - for x in add_databases_list: - setu_data_list.append(x) - # 未找到符合的色图,想来本地应该也没有 - if code == 401: - await setu.finish(urls[0], at_sender=True) - if code == 200: - forward_list = [] - for i in range(len(urls)): - try: - msg_id = None - setu_img, index = await search_online_setu(urls[i]) - # 下载成功的话 - if index != -1: - logger.info( - f"发送色图 {index}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append(Message(f"{text_list[i]}\n{setu_img}")) - else: - msg_id = await matcher.send( - Message(f"{text_list[i]}\n{setu_img}") - ) - else: - if setu_list is None: - setu_list, code = await get_setu_list(tags=tags, r18=r18) - if code != 200: - await setu.finish(setu_list[0], at_sender=True) - if setu_list: - setu_image = random.choice(setu_list) - setu_list.remove(setu_image) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append( - gen_message(setu_image) - + ( - await check_local_exists_or_download(setu_image) - )[0] - ) - else: - msg_id = await matcher.send( - gen_message(setu_image) - + ( - await check_local_exists_or_download(setu_image) - )[0] - ) - logger.info( - f"发送本地色图 {setu_image.local_id}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - else: - msg_id = await matcher.send(text_list[i] + "\n" + setu_img) - if msg_id: - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - except ActionFailed: - await matcher.finish("坏了,这张图色过头了,我自己看看就行了!", at_sender=True) - if forward_list and isinstance(event, GroupMessageEvent): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, - messages=custom_forward_msg(forward_list, bot.self_id), - ) - withdraw_message_manager.withdraw_message( - event, - msg_id, - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - return - if code != 200: - await matcher.finish("网络连接失败...", at_sender=True) - # 本地无图 - if setu_list is None: - setu_list, code = await get_setu_list(tags=tags, r18=r18) - if code != 200: - await matcher.finish(setu_list[0], at_sender=True) - # 开始发图 - forward_list = [] - for _ in range(num): - if not setu_list: - await setu.finish("坏了,已经没图了,被榨干了!") - setu_image = random.choice(setu_list) - setu_list.remove(setu_image) - if ( - max_once_num2forward - and num >= max_once_num2forward - and isinstance(event, GroupMessageEvent) - ): - forward_list.append( - Message( - gen_message(setu_image) - + (await check_local_exists_or_download(setu_image))[0] - ) - ) - else: - try: - msg_id = await matcher.send( - gen_message(setu_image) - + (await check_local_exists_or_download(setu_image))[0] - ) - withdraw_message_manager.withdraw_message( - event, - msg_id["message_id"], - Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE"), - ) - logger.info( - f"发送本地色图 {setu_image.local_id}.png", - "command", - event.user_id, - getattr(event, "group_id", None), - ) - except ActionFailed: - await matcher.finish("坏了,这张图色过头了,我自己看看就行了!", at_sender=True) - if forward_list and isinstance(event, GroupMessageEvent): - msg_id = await bot.send_group_forward_msg( - group_id=event.group_id, - messages=custom_forward_msg(forward_list, bot.self_id), - ) - withdraw_message_manager.withdraw_message( - event, msg_id, Config.get_config("send_setu", "WITHDRAW_SETU_MESSAGE") - ) diff --git a/plugins/send_setu_/send_setu/data_source.py b/plugins/send_setu_/send_setu/data_source.py deleted file mode 100755 index 70cea1cd..00000000 --- a/plugins/send_setu_/send_setu/data_source.py +++ /dev/null @@ -1,267 +0,0 @@ -import asyncio -import os -import random -import re -from asyncio.exceptions import TimeoutError -from typing import Any, List, Optional, Tuple, Union - -from asyncpg.exceptions import UniqueViolationError -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import compressed_image, get_img_hash -from utils.message_builder import image -from utils.utils import change_img_md5, change_pixiv_image_links - -from .._model import Setu - -url = "https://api.lolicon.app/setu/v2" -path = "_setu" -r18_path = "_r18" -host_pattern = re.compile(r"https?://([^/]+)") - - -# 获取url -async def get_setu_urls( - tags: List[str], num: int = 1, r18: bool = False, command: str = "" -) -> Tuple[List[str], List[str], List[tuple], int]: - tags = tags[:3] if len(tags) > 3 else tags - params = { - "r18": 1 if r18 else 0, # 添加r18参数 0为否,1为是,2为混合 - "tag": tags, # 若指定tag - "num": 20, # 一次返回的结果数量 - "size": ["original"], - } - for count in range(3): - logger.debug(f"尝试获取图片URL第 {count+1} 次", "色图") - try: - response = await AsyncHttpx.get( - url, timeout=Config.get_config("send_setu", "TIMEOUT"), params=params - ) - if response.status_code == 200: - data = response.json() - if not data["error"]: - data = data["data"] - ( - urls, - text_list, - add_databases_list, - ) = await asyncio.get_event_loop().run_in_executor( - None, _setu_data_process, data, command - ) - num = num if num < len(data) else len(data) - random_idx = random.sample(range(len(data)), num) - x_urls = [] - x_text_lst = [] - for x in random_idx: - x_urls.append(urls[x]) - x_text_lst.append(text_list[x]) - if not x_urls: - return ["没找到符合条件的色图..."], [], [], 401 - return x_urls, x_text_lst, add_databases_list, 200 - else: - return ["没找到符合条件的色图..."], [], [], 401 - except TimeoutError as e: - logger.error(f"获取图片URL超时", "色图", e=e) - except Exception as e: - logger.error(f"访问页面错误", "色图", e=e) - return ["我网线被人拔了..QAQ"], [], [], 999 - - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def search_online_setu( - url_: str, id_: Optional[int] = None, path_: Optional[str] = None -) -> Tuple[Union[MessageSegment, str], int]: - """ - 下载色图 - :param url_: 色图url - :param id_: 本地id - :param path_: 存储路径 - """ - url_ = change_pixiv_image_links(url_) - index = random.randint(1, 100000) if id_ is None else id_ - base_path = IMAGE_PATH / path_ if path_ else TEMP_PATH - file_name = f"{index}_temp_setu.jpg" if path_ == TEMP_PATH else f"{index}.jpg" - file = base_path / file_name - base_path.mkdir(parents=True, exist_ok=True) - for i in range(3): - logger.debug(f"尝试在线搜索第 {i+1} 次", "色图") - try: - if not await AsyncHttpx.download_file( - url_, - file, - timeout=Config.get_config("send_setu", "TIMEOUT"), - ): - continue - if id_ is not None: - if os.path.getsize(base_path / f"{index}.jpg") > 1024 * 1024 * 1.5: - compressed_image( - base_path / f"{index}.jpg", - ) - logger.info(f"下载 lolicon 图片 {url_} 成功, id:{index}") - change_img_md5(file) - return image(file), index - except TimeoutError as e: - logger.error(f"下载图片超时", "色图", e=e) - except Exception as e: - logger.error(f"下载图片错误", "色图", e=e) - return "图片被小怪兽恰掉啦..!QAQ", -1 - - -# 检测本地是否有id涩图,无的话则下载 -async def check_local_exists_or_download( - setu_image: Setu, -) -> Tuple[Union[MessageSegment, str], int]: - path_ = None - id_ = None - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - id_ = setu_image.local_id - path_ = r18_path if setu_image.is_r18 else path - file = IMAGE_PATH / path_ / f"{setu_image.local_id}.jpg" - if file.exists(): - change_img_md5(file) - return image(file), 200 - return await search_online_setu(setu_image.img_url, id_, path_) - - -# 添加涩图数据到数据库 -async def add_data_to_database(lst: List[tuple]): - tmp = [] - for x in lst: - if x not in tmp: - tmp.append(x) - if tmp: - for x in tmp: - try: - idx = await Setu.filter(is_r18="R-18" in x[5]).count() - if not await Setu.exists(pid=x[2], img_url=x[4]): - await Setu.create( - local_id=idx, - title=x[0], - author=x[1], - pid=x[2], - img_hash=x[3], - img_url=x[4], - tags=x[5], - is_r18="R-18" in x[5], - ) - except UniqueViolationError: - pass - - -# 拿到本地色图列表 -async def get_setu_list( - index: Optional[int] = None, tags: Optional[List[str]] = None, r18: bool = False -) -> Tuple[list, int]: - if index: - image_count = await Setu.filter(is_r18=r18).count() - 1 - if index < 0 or index > image_count: - return [f"超过当前上下限!({image_count})"], 999 - image_list = [await Setu.query_image(index, r18=r18)] - elif tags: - image_list = await Setu.query_image(tags=tags, r18=r18) - else: - image_list = await Setu.query_image(r18=r18) - if not image_list: - return ["没找到符合条件的色图..."], 998 - return image_list, 200 # type: ignore - - -# 初始化消息 -def gen_message(setu_image: Setu) -> str: - """判断是否获取图片信息 - - Args: - setu_image (Setu): Setu - - Returns: - str: 图片信息 - """ - local_id = setu_image.local_id - title = setu_image.title - author = setu_image.author - pid = setu_image.pid - if Config.get_config("send_setu", "SHOW_INFO"): - return f"id:{local_id}\n" f"title:{title}\n" f"author:{author}\n" f"PID:{pid}\n" - return "" - - -# 罗翔老师! -def get_luoxiang(impression): - initial_setu_probability = Config.get_config( - "send_setu", "INITIAL_SETU_PROBABILITY" - ) - if initial_setu_probability: - probability = float(impression) + initial_setu_probability * 100 - if probability < random.randint(1, 101): - return ( - "我为什么要给你发这个?" - + image( - IMAGE_PATH - / "luoxiang" - / random.choice(os.listdir(IMAGE_PATH / "luoxiang")) - ) - + f"\n(快向{NICKNAME}签到提升好感度吧!)" - ) - return None - - -async def find_img_index(img_url, user_id): - if not await AsyncHttpx.download_file( - img_url, - TEMP_PATH / f"{user_id}_find_setu_index.jpg", - timeout=Config.get_config("send_setu", "TIMEOUT"), - ): - return "检索图片下载上失败..." - img_hash = str(get_img_hash(TEMP_PATH / f"{user_id}_find_setu_index.jpg")) - if setu_img := await Setu.get_or_none(img_hash=img_hash): - return ( - f"id:{setu_img.local_id}\n" - f"title:{setu_img.title}\n" - f"author:{setu_img.author}\n" - f"PID:{setu_img.pid}" - ) - return "该图不在色图库中或色图库未更新!" - - -# 处理色图数据 -def _setu_data_process( - data: dict, command: str -) -> Tuple[List[str], List[str], List[Tuple[Any, ...]]]: - urls = [] - text_list = [] - add_databases_list = [] - for i in range(len(data)): - img_url = data[i]["urls"]["original"] - img_url = change_pixiv_image_links(img_url) - title = data[i]["title"] - author = data[i]["author"] - pid = data[i]["pid"] - urls.append(img_url) - text_list.append(f"title:{title}\nauthor:{author}\nPID:{pid}") - tags = [] - for j in range(len(data[i]["tags"])): - tags.append(data[i]["tags"][j]) - if command != "色图r": - if "R-18" in tags: - tags.remove("R-18") - add_databases_list.append( - ( - title, - author, - pid, - "", - img_url, - ",".join(tags), - ) - ) - return urls, text_list, add_databases_list diff --git a/plugins/send_setu_/update_setu/__init__.py b/plugins/send_setu_/update_setu/__init__.py deleted file mode 100755 index 5c955a27..00000000 --- a/plugins/send_setu_/update_setu/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -from utils.utils import scheduler -from nonebot import on_command -from nonebot.permission import SUPERUSER -from nonebot.rule import to_me -from .data_source import update_setu_img -from configs.config import Config - - -__zx_plugin_name__ = "更新色图 [Superuser]" -__plugin_usage__ = """ -usage: - 更新数据库内存在的色图 - 指令: - 更新色图 -""".strip() -__plugin_cmd__ = ["更新色图"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_block_limit__ = { - "rst": "色图正在更新..." -} - - -update_setu = on_command( - "更新色图", rule=to_me(), permission=SUPERUSER, priority=1, block=True -) - - -@update_setu.handle() -async def _(): - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - await update_setu.send("开始更新色图...", at_sender=True) - await update_setu_img(True) - else: - await update_setu.finish("更新色图配置未开启") - - -# 更新色图 -@scheduler.scheduled_job( - "cron", - hour=4, - minute=30, -) -async def _(): - if Config.get_config("send_setu", "DOWNLOAD_SETU"): - await update_setu_img() diff --git a/plugins/send_setu_/update_setu/data_source.py b/plugins/send_setu_/update_setu/data_source.py deleted file mode 100755 index 52d548b3..00000000 --- a/plugins/send_setu_/update_setu/data_source.py +++ /dev/null @@ -1,178 +0,0 @@ -import os -import shutil -from datetime import datetime - -import nonebot -import ujson as json -from asyncpg.exceptions import UniqueViolationError -from nonebot import Driver -from PIL import UnidentifiedImageError - -from configs.config import Config -from configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import compressed_image, get_img_hash -from utils.utils import change_pixiv_image_links, get_bot - -from .._model import Setu - -driver: Driver = nonebot.get_driver() - -_path = IMAGE_PATH - - -# 替换旧色图数据,修复local_id一直是50的问题 -@driver.on_startup -async def update_old_setu_data(): - path = TEXT_PATH - setu_data_file = path / "setu_data.json" - r18_data_file = path / "r18_setu_data.json" - if setu_data_file.exists() or r18_data_file.exists(): - index = 0 - r18_index = 0 - count = 0 - fail_count = 0 - for file in [setu_data_file, r18_data_file]: - if file.exists(): - data = json.load(open(file, "r", encoding="utf8")) - for x in data: - if file == setu_data_file: - idx = index - if "R-18" in data[x]["tags"]: - data[x]["tags"].remove("R-18") - else: - idx = r18_index - img_url = ( - data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net") - if "i.pixiv.cat" in data[x]["img_url"] - else data[x]["img_url"] - ) - # idx = r18_index if 'R-18' in data[x]["tags"] else index - try: - if not await Setu.exists(pid=data[x]["pid"], url=img_url): - await Setu.create( - local_id=idx, - title=data[x]["title"], - author=data[x]["author"], - pid=data[x]["pid"], - img_hash=data[x]["img_hash"], - img_url=img_url, - is_r18="R-18" in data[x]["tags"], - tags=",".join(data[x]["tags"]), - ) - count += 1 - if "R-18" in data[x]["tags"]: - r18_index += 1 - else: - index += 1 - logger.info(f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....') - except UniqueViolationError: - fail_count += 1 - logger.info( - f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....' - ) - file.unlink() - setu_url_path = path / "setu_url.json" - setu_r18_url_path = path / "setu_r18_url.json" - if setu_url_path.exists(): - setu_url_path.unlink() - if setu_r18_url_path.exists(): - setu_r18_url_path.unlink() - logger.info(f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条") - - -# 删除色图rar文件夹 -shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True) -shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True) -shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True) - -headers = { - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" - " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Referer": "https://www.pixiv.net", -} - - -async def update_setu_img(flag: bool = False): - """ - 更新色图 - :param flag: 是否手动更新 - """ - image_list = await Setu.all().order_by("local_id") - image_list.reverse() - _success = 0 - error_info = [] - error_type = [] - count = 0 - for image in image_list: - count += 1 - path = _path / "_r18" if image.is_r18 else _path / "_setu" - local_image = path / f"{image.local_id}.jpg" - path.mkdir(exist_ok=True, parents=True) - TEMP_PATH.mkdir(exist_ok=True, parents=True) - if not local_image.exists() or not image.img_hash: - temp_file = TEMP_PATH / f"{image.local_id}.jpg" - if temp_file.exists(): - temp_file.unlink() - url_ = change_pixiv_image_links(image.img_url) - try: - if not await AsyncHttpx.download_file( - url_, TEMP_PATH / f"{image.local_id}.jpg" - ): - continue - _success += 1 - try: - if ( - os.path.getsize( - TEMP_PATH / f"{image.local_id}.jpg", - ) - > 1024 * 1024 * 1.5 - ): - compressed_image( - TEMP_PATH / f"{image.local_id}.jpg", - path / f"{image.local_id}.jpg", - ) - else: - logger.info( - f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg " - f"--> /{path}/{image.local_id}.jpg" - ) - os.rename( - TEMP_PATH / f"{image.local_id}.jpg", - path / f"{image.local_id}.jpg", - ) - except FileNotFoundError: - logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...") - continue - img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg")) - image.img_hash = img_hash - await image.save(update_fields=["img_hash"]) - # await Setu.update_setu_data(image.pid, img_hash=img_hash) - except UnidentifiedImageError: - # 图片已删除 - unlink = False - with open(local_image, "r") as f: - if "404 Not Found" in f.read(): - unlink = True - if unlink: - local_image.unlink() - max_num = await Setu.delete_image(image.pid, image.img_url) - if (path / f"{max_num}.jpg").exists(): - os.rename(path / f"{max_num}.jpg", local_image) - logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换") - except Exception as e: - _success -= 1 - logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") - if type(e) not in error_type: - error_type.append(type(e)) - error_info.append(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") - else: - logger.info(f"更新色图 {image.local_id}.jpg 已存在") - if _success or error_info or flag: - if bot := get_bot(): - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,' - f"以下为更新时未知错误:\n" + "\n".join(error_info), - ) diff --git a/plugins/sign_in/__init__.py b/plugins/sign_in/__init__.py deleted file mode 100755 index 12299138..00000000 --- a/plugins/sign_in/__init__.py +++ /dev/null @@ -1,174 +0,0 @@ -from pathlib import Path -from typing import Any, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message -from nonebot.adapters.onebot.v11.permission import GROUP -from nonebot.params import CommandArg, RegexGroup - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.message_builder import image -from utils.utils import is_number, scheduler - -from .goods_register import driver -from .group_user_checkin import ( - check_in_all, - group_impression_rank, - group_user_check, - group_user_check_in, - impression_rank, -) -from .utils import clear_sign_data_pic - -try: - import ujson as json -except ModuleNotFoundError: - import json - -__zx_plugin_name__ = "签到" -__plugin_usage__ = """ -usage: - 每日签到 - 会影响色图概率和开箱次数,以及签到的随机道具获取 - 指令: - 签到 ?[all]: all代表签到所有群 - 我的签到 - 好感度排行 - 好感度总排行 - * 签到时有 3% 概率 * 2 * -""".strip() -__plugin_des__ = "每日签到,证明你在这里" -__plugin_cmd__ = ["签到 ?[all]", "我的签到", "好感度排行", "好感度总排行"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["签到"], -} -__plugin_cd_limit__ = {} -__plugin_configs__ = { - "MAX_SIGN_GOLD": { - "value": 200, - "help": "签到好感度加成额外获得的最大金币数", - "default_value": 200, - "type": int, - }, - "SIGN_CARD1_PROB": { - "value": 0.2, - "help": "签到好感度双倍加持卡Ⅰ掉落概率", - "default_value": 0.2, - "type": float, - }, - "SIGN_CARD2_PROB": { - "value": 0.09, - "help": "签到好感度双倍加持卡Ⅱ掉落概率", - "default_value": 0.09, - "type": float, - }, - "SIGN_CARD3_PROB": { - "value": 0.05, - "help": "签到好感度双倍加持卡Ⅲ掉落概率", - "default_value": 0.05, - "type": float, - }, -} - -Config.add_plugin_config( - "send_setu", - "INITIAL_SETU_PROBABILITY", - 0.7, - help_="初始色图概率,总概率 = 初始色图概率 + 好感度", - default_value=0.7, - type=float, -) - - -_file = DATA_PATH / "not_show_sign_rank_user.json" -try: - data = json.load(open(_file, "r", encoding="utf8")) -except (FileNotFoundError, ValueError, TypeError): - data = {"0": []} - - -sign = on_regex("^签到(all)?$", priority=5, permission=GROUP, block=True) -my_sign = on_command( - cmd="我的签到", aliases={"好感度"}, priority=5, permission=GROUP, block=True -) -sign_rank = on_command( - cmd="积分排行", - aliases={"好感度排行", "签到排行", "积分排行", "好感排行", "好感度排名,签到排名,积分排名"}, - priority=5, - permission=GROUP, - block=True, -) -total_sign_rank = on_command( - "签到总排行", aliases={"好感度总排行", "好感度总榜", "签到总榜"}, priority=5, block=True -) - - -@sign.handle() -async def _(event: GroupMessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - nickname = event.sender.card or event.sender.nickname - await sign.send( - await group_user_check_in(nickname, event.user_id, event.group_id), - at_sender=True, - ) - if reg_group[0]: - await check_in_all(nickname, event.user_id) - - -@my_sign.handle() -async def _(event: GroupMessageEvent): - nickname = event.sender.card or event.sender.nickname - await my_sign.send( - await group_user_check(nickname, event.user_id, event.group_id), - at_sender=True, - ) - - -@sign_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - num = arg.extract_plain_text().strip() - if is_number(num) and 51 > int(num) > 10: - num = int(num) - else: - num = 10 - _image = await group_impression_rank(event.group_id, num) - if _image: - await sign_rank.send(image(b64=_image.pic2bs4())) - - -@total_sign_rank.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if not msg: - await total_sign_rank.send("请稍等..正在整理数据...") - await total_sign_rank.send(image(b64=await impression_rank(0, data))) - elif msg in ["屏蔽我"]: - if event.user_id in data["0"]: - await total_sign_rank.finish("您已经在屏蔽名单中了,请勿重复添加!", at_sender=True) - data["0"].append(event.user_id) - await total_sign_rank.send("设置成功,您不会出现在签到总榜中!", at_sender=True) - elif msg in ["显示我"]: - if event.user_id not in data["0"]: - await total_sign_rank.finish("您不在屏蔽名单中!", at_sender=True) - data["0"].remove(event.user_id) - await total_sign_rank.send("设置成功,签到总榜将会显示您的头像名称以及好感度!", at_sender=True) - with open(_file, "w", encoding="utf8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - -@scheduler.scheduled_job( - "interval", - hours=1, -) -async def _(): - try: - clear_sign_data_pic() - logger.info("清理日常签到图片数据数据完成....") - except Exception as e: - logger.error(f"清理日常签到图片数据数据失败..{type(e)}: {e}") diff --git a/plugins/sign_in/config.py b/plugins/sign_in/config.py deleted file mode 100755 index 025de8dd..00000000 --- a/plugins/sign_in/config.py +++ /dev/null @@ -1,64 +0,0 @@ -from configs.path_config import IMAGE_PATH - - -SIGN_RESOURCE_PATH = IMAGE_PATH / 'sign' / 'sign_res' -SIGN_TODAY_CARD_PATH = IMAGE_PATH / 'sign' / 'today_card' -SIGN_BORDER_PATH = SIGN_RESOURCE_PATH / 'border' -SIGN_BACKGROUND_PATH = SIGN_RESOURCE_PATH / 'background' - -SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True) -SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True) - - -lik2relation = { - '0': '路人', - '1': '陌生', - '2': '初识', - '3': '普通', - '4': '熟悉', - '5': '信赖', - '6': '相知', - '7': '厚谊', - '8': '亲密' -} - -level2attitude = { - '0': '排斥', - '1': '警惕', - '2': '可以交流', - '3': '一般', - '4': '是个好人', - '5': '好朋友', - '6': '可以分享小秘密', - '7': '喜欢', - '8': '恋人' -} - -weekdays = { - 1: 'Mon', - 2: 'Tue', - 3: 'Wed', - 4: 'Thu', - 5: 'Fri', - 6: 'Sat', - 7: 'Sun' -} - -lik2level = { - 9999: '9', - 400: '8', - 270: '7', - 200: '6', - 140: '5', - 90: '4', - 50: '3', - 25: '2', - 10: '1', - 0: '0' -} - - - - - - diff --git a/plugins/sign_in/goods_register.py b/plugins/sign_in/goods_register.py deleted file mode 100644 index e8cf9e5a..00000000 --- a/plugins/sign_in/goods_register.py +++ /dev/null @@ -1,59 +0,0 @@ -import nonebot -from nonebot import Driver - -from configs.config import Config -from models.sign_group_user import SignGroupUser -from utils.decorator.shop import NotMeetUseConditionsException, shop_register - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def _(): - """ - 导入内置的三个商品 - """ - - @shop_register( - name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), - price=(30, 150, 250), - des=( - "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", - ), - load_status=bool(Config.get_config("shop", "IMPORT_DEFAULT_SHOP_GOODS")), - icon=( - "favorability_card_1.png", - "favorability_card_2.png", - "favorability_card_3.png", - ), - **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, - ) - async def sign_card(user_id: int, group_id: int, prob: float): - user, _ = await SignGroupUser.get_or_create(user_id=str(user_id), group_id=str(group_id)) - user.add_probability = prob - await user.save(update_fields=["add_probability"]) - - @shop_register( - name="测试道具A", - price=99, - des="随便侧而出", - load_status=False, - icon="sword.png", - ) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "使用测试道具") - - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用前函数(before handle)") - - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第二个使用前函数(before handle)222") - raise NotMeetUseConditionsException("太笨了!") # 抛出异常,阻断使用,并返回信息 - - @shop_register.after_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用后函数(after handle)") diff --git a/plugins/sign_in/group_user_checkin.py b/plugins/sign_in/group_user_checkin.py deleted file mode 100755 index ce5f9721..00000000 --- a/plugins/sign_in/group_user_checkin.py +++ /dev/null @@ -1,205 +0,0 @@ -import asyncio -import math -import os -import random -import secrets -from datetime import datetime, timedelta -from io import BytesIO -from typing import Optional - -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME -from models.bag_user import BagUser -from models.group_member_info import GroupInfoUser -from models.sign_group_user import SignGroupUser -from services.log import logger -from utils.data_utils import init_rank -from utils.image_utils import BuildImage, BuildMat -from utils.utils import get_user_avatar - -from .random_event import random_event -from .utils import SIGN_TODAY_CARD_PATH, get_card - - -async def group_user_check_in( - nickname: str, user_id: int, group: int -) -> MessageSegment: - "Returns string describing the result of checking in" - present = datetime.now() - # 取得相应用户 - user, is_create = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group) - ) - # 如果同一天签到过,特殊处理 - if not is_create and ( - user.checkin_time_last.date() >= present.date() - or f"{user}_{group}_sign_{datetime.now().date()}" - in os.listdir(SIGN_TODAY_CARD_PATH) - ): - gold = await BagUser.get_gold(user_id, group) - return await get_card(user, nickname, -1, gold, "") - return await _handle_check_in(nickname, user_id, group, present) # ok - - -async def check_in_all(nickname: str, user_id: str): - """ - 说明: - 签到所有群 - 参数: - :param nickname: 昵称 - :param user_id: 用户id - """ - present = datetime.now() - for u in await SignGroupUser.filter(user_id=user_id).all(): - group = u.group_id - if not ( - u.checkin_time_last.date() >= present.date() - or f"{u}_{group}_sign_{datetime.now().date()}" - in os.listdir(SIGN_TODAY_CARD_PATH) - ): - await _handle_check_in(nickname, user_id, group, present) - - -async def _handle_check_in( - nickname: str, user_id: str, group: str, present: datetime -) -> MessageSegment: - user, _ = await SignGroupUser.get_or_create(user_id=user_id, group_id=group) - impression_added = (secrets.randbelow(99) + 1) / 100 - critx2 = random.random() - add_probability = float(user.add_probability) - specify_probability = user.specify_probability - if critx2 + add_probability > 0.97: - impression_added *= 2 - elif critx2 < specify_probability: - impression_added *= 2 - await SignGroupUser.sign(user, impression_added) - gold = random.randint(1, 100) - gift, gift_type = random_event(float(user.impression)) - if gift_type == "gold": - await BagUser.add_gold(user_id, group, gold + gift) - gift = f"额外金币 + {gift}" - else: - await BagUser.add_gold(user_id, group, gold) - await BagUser.add_property(user_id, group, gift) - gift += " + 1" - - logger.info( - f"(USER {user.user_id}, GROUP {user.group_id})" - f" CHECKED IN successfully. score: {user.impression:.2f} " - f"(+{impression_added:.2f}).获取金币:{gold + gift if gift == 'gold' else gold}" - ) - if critx2 + add_probability > 0.97 or critx2 < specify_probability: - return await get_card(user, nickname, impression_added, gold, gift, True) - else: - return await get_card(user, nickname, impression_added, gold, gift) - - -async def group_user_check(nickname: str, user_id: str, group: str) -> MessageSegment: - # heuristic: if users find they have never checked in they are probable to check in - user, _ = await SignGroupUser.get_or_create( - user_id=str(user_id), group_id=str(group) - ) - gold = await BagUser.get_gold(user_id, group) - return await get_card(user, nickname, None, gold, "", is_card_view=True) - - -async def group_impression_rank(group: int, num: int) -> Optional[BuildMat]: - user_qq_list, impression_list, _ = await SignGroupUser.get_all_impression(group) - return await init_rank("好感度排行榜", user_qq_list, impression_list, group, num) - - -async def random_gold(user_id, group_id, impression): - if impression < 1: - impression = 1 - gold = random.randint(1, 100) + random.randint(1, int(impression)) - if await BagUser.add_gold(user_id, group_id, gold): - return gold - else: - return 0 - - -# 签到总榜 -async def impression_rank(group_id: int, data: dict): - user_qq_list, impression_list, group_list = await SignGroupUser.get_all_impression( - group_id - ) - users, impressions, groups = [], [], [] - num = 0 - for i in range(105 if len(user_qq_list) > 105 else len(user_qq_list)): - impression = max(impression_list) - index = impression_list.index(impression) - user = user_qq_list[index] - group = group_list[index] - user_qq_list.pop(index) - impression_list.pop(index) - group_list.pop(index) - if user not in users and impression < 100000: - if user not in data["0"]: - users.append(user) - impressions.append(impression) - groups.append(group) - else: - num += 1 - for i in range(num): - impression = max(impression_list) - index = impression_list.index(impression) - user = user_qq_list[index] - group = group_list[index] - user_qq_list.pop(index) - impression_list.pop(index) - group_list.pop(index) - if user not in users and impression < 100000: - users.append(user) - impressions.append(impression) - groups.append(group) - return (await asyncio.gather(*[_pst(users, impressions, groups)]))[0] - - -async def _pst(users: list, impressions: list, groups: list): - lens = len(users) - count = math.ceil(lens / 33) - width = 10 - idx = 0 - A = BuildImage(1740, 3300, color="#FFE4C4") - for _ in range(count): - col_img = BuildImage(550, 3300, 550, 100, color="#FFE4C4") - for _ in range(33 if int(lens / 33) >= 1 else lens % 33 - 1): - idx += 1 - if idx > 100: - break - impression = max(impressions) - index = impressions.index(impression) - user = users[index] - group = groups[index] - impressions.pop(index) - users.pop(index) - groups.pop(index) - if user_ := await GroupInfoUser.get_or_none( - user_id=str(user), group_id=str(group) - ): - user_name = user_.user_name - else: - user_name = f"我名字呢?" - user_name = user_name if len(user_name) < 11 else user_name[:10] + "..." - ava = await get_user_avatar(user) - if ava: - ava = BuildImage(50, 50, background=BytesIO(ava)) - else: - ava = BuildImage(50, 50, color="white") - ava.circle() - bk = BuildImage(550, 100, color="#FFE4C4", font_size=30) - font_w, font_h = bk.getsize(f"{idx}") - bk.text((5, int((100 - font_h) / 2)), f"{idx}.") - bk.paste(ava, (55, int((100 - 50) / 2)), True) - bk.text((120, int((100 - font_h) / 2)), f"{user_name}") - bk.text((460, int((100 - font_h) / 2)), f"[{impression:.2f}]") - col_img.paste(bk) - A.paste(col_img, (width, 0)) - lens -= 33 - width += 580 - W = BuildImage(1740, 3700, color="#FFE4C4", font_size=130) - W.paste(A, (0, 260)) - font_w, font_h = W.getsize(f"{NICKNAME}的好感度总榜") - W.text((int((1740 - font_w) / 2), int((260 - font_h) / 2)), f"{NICKNAME}的好感度总榜") - return W.pic2bs4() diff --git a/plugins/sign_in/random_event.py b/plugins/sign_in/random_event.py deleted file mode 100755 index b451d848..00000000 --- a/plugins/sign_in/random_event.py +++ /dev/null @@ -1,31 +0,0 @@ -import random -from typing import Tuple, Union - -from configs.config import Config - -PROB_DATA = None - - -def random_event(impression: float) -> Tuple[Union[str, int], str]: - """ - 签到随机事件 - :param impression: 好感度 - :return: 额外奖励 和 类型 - """ - global PROB_DATA - if not PROB_DATA: - PROB_DATA = { - Config.get_config("sign_in", "SIGN_CARD3_PROB"): "好感度双倍加持卡Ⅲ", - Config.get_config("sign_in", "SIGN_CARD2_PROB"): "好感度双倍加持卡Ⅱ", - Config.get_config("sign_in", "SIGN_CARD1_PROB"): "好感度双倍加持卡Ⅰ", - } - rand = random.random() - impression / 1000 - for prob in PROB_DATA.keys(): - if rand <= prob: - return PROB_DATA[prob], "props" - gold = random.randint( - 1, random.randint(1, int(1 if impression < 1 else impression)) - ) - max_sign_gold = Config.get_config("sign_in", "MAX_SIGN_GOLD") - gold = max_sign_gold if gold > max_sign_gold else gold - return gold, "gold" diff --git a/plugins/sign_in/utils.py b/plugins/sign_in/utils.py deleted file mode 100755 index d8a86b0f..00000000 --- a/plugins/sign_in/utils.py +++ /dev/null @@ -1,361 +0,0 @@ -import asyncio -import os -import random -from datetime import datetime -from io import BytesIO -from pathlib import Path -from typing import List, Optional - -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import MessageSegment - -from configs.config import NICKNAME, Config -from configs.path_config import IMAGE_PATH -from models.group_member_info import GroupInfoUser -from models.sign_group_user import SignGroupUser -from utils.image_utils import BuildImage -from utils.message_builder import image -from utils.utils import get_user_avatar - -from .config import ( - SIGN_BACKGROUND_PATH, - SIGN_BORDER_PATH, - SIGN_RESOURCE_PATH, - SIGN_TODAY_CARD_PATH, - level2attitude, - lik2level, - lik2relation, -) - -driver: Driver = nonebot.get_driver() - - -@driver.on_startup -async def init_image(): - SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) - SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) - if not await GroupInfoUser.get_or_none(user_id="114514"): - await GroupInfoUser.create( - user_id="114514", - group_id="114514", - user_name="", - uid=0, - ) - generate_progress_bar_pic() - clear_sign_data_pic() - - -async def get_card( - user: SignGroupUser, - nickname: str, - add_impression: Optional[float], - gold: Optional[int], - gift: str, - is_double: bool = False, - is_card_view: bool = False, -) -> MessageSegment: - user_id = user.user_id - date = datetime.now().date() - _type = "view" if is_card_view else "sign" - card_file = ( - Path(SIGN_TODAY_CARD_PATH) / f"{user_id}_{user.group_id}_{_type}_{date}.png" - ) - if card_file.exists(): - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_{_type}_{date}.png" - ) - else: - if add_impression == -1: - card_file = ( - Path(SIGN_TODAY_CARD_PATH) - / f"{user_id}_{user.group_id}_view_{date}.png" - ) - if card_file.exists(): - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_view_{date}.png" - ) - is_card_view = True - ava = BytesIO(await get_user_avatar(user_id)) - uid = await GroupInfoUser.get_group_member_uid(user.user_id, user.group_id) - impression_list = None - if is_card_view: - _, impression_list, _ = await SignGroupUser.get_all_impression( - user.group_id - ) - return await asyncio.get_event_loop().run_in_executor( - None, - _generate_card, - user, - nickname, - user_id, - add_impression, - gold, - gift, - uid, - ava, - impression_list, - is_double, - is_card_view, - ) - - -def _generate_card( - user: "SignGroupUser", - nickname: str, - user_id: str, - impression: Optional[float], - gold: Optional[int], - gift: str, - uid: str, - ava_bytes: BytesIO, - impression_list: List[float], - is_double: bool = False, - is_card_view: bool = False, -) -> MessageSegment: - ava_bk = BuildImage(140, 140, is_alpha=True) - ava_border = BuildImage( - 140, - 140, - background=SIGN_BORDER_PATH / "ava_border_01.png", - ) - ava = BuildImage(102, 102, background=ava_bytes) - ava.circle() - ava_bk.paste(ava, center_type="center") - ava_bk.paste(ava_border, alpha=True, center_type="center") - add_impression = impression - impression = float(user.impression) - info_img = BuildImage(250, 150, color=(255, 255, 255, 0), font_size=15) - level, next_impression, previous_impression = get_level_and_next_impression( - impression - ) - interpolation = next_impression - impression - if level == "9": - level = "8" - interpolation = 0 - info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]") - info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}") - info_img.text((0, 40), f"· 距离升级还差 {interpolation:.2f} 好感度") - - bar_bk = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png") - bar = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar.png") - ratio = 1 - (next_impression - user.impression) / ( - next_impression - previous_impression - ) - if next_impression == 0: - ratio = 0 - bar.resize(w=int(bar.w * ratio) or bar.w, h=bar.h) - bar_bk.paste( - bar, - alpha=True, - ) - font_size = 30 - if "好感度双倍加持卡" in gift: - font_size = 20 - gift_border = BuildImage( - 270, - 100, - background=SIGN_BORDER_PATH / "gift_border_02.png", - font_size=font_size, - ) - gift_border.text((0, 0), gift, center_type="center") - - bk = BuildImage( - 876, - 424, - background=SIGN_BACKGROUND_PATH - / random.choice(os.listdir(SIGN_BACKGROUND_PATH)), - font_size=25, - ) - A = BuildImage(876, 274, background=SIGN_RESOURCE_PATH / "white.png") - line = BuildImage(2, 180, color="black") - A.transparent(2) - A.paste(ava_bk, (25, 80), True) - A.paste(line, (200, 70)) - - nickname_img = BuildImage( - 0, - 0, - plain_text=nickname, - color=(255, 255, 255, 0), - font_size=50, - font_color=(255, 255, 255), - ) - if uid: - uid = f"{uid}".rjust(12, "0") - uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] - else: - uid = "XXXX XXXX XXXX" - uid_img = BuildImage( - 0, - 0, - plain_text=f"UID: {uid}", - color=(255, 255, 255, 0), - font_size=30, - font_color=(255, 255, 255), - ) - sign_day_img = BuildImage( - 0, - 0, - plain_text=f"{user.checkin_count}", - color=(255, 255, 255, 0), - font_size=40, - font_color=(211, 64, 33), - ) - lik_text1_img = BuildImage( - 0, 0, plain_text="当前", color=(255, 255, 255, 0), font_size=20 - ) - lik_text2_img = BuildImage( - 0, - 0, - plain_text=f"好感度:{user.impression:.2f}", - color=(255, 255, 255, 0), - font_size=30, - ) - watermark = BuildImage( - 0, - 0, - plain_text=f"{NICKNAME}@{datetime.now().year}", - color=(255, 255, 255, 0), - font_size=15, - font_color=(155, 155, 155), - ) - today_data = BuildImage(300, 300, color=(255, 255, 255, 0), font_size=20) - if is_card_view: - today_sign_text_img = BuildImage( - 0, 0, plain_text="", color=(255, 255, 255, 0), font_size=30 - ) - if impression_list: - impression_list.sort(reverse=True) - index = impression_list.index(impression) - rank_img = BuildImage( - 0, - 0, - plain_text=f"* 此群好感排名第 {index + 1} 位", - color=(255, 255, 255, 0), - font_size=30, - ) - A.paste(rank_img, ((A.w - rank_img.w - 10), 20), True) - today_data.text( - (0, 0), - f"上次签到日期:{'从未' if user.checkin_time_last == datetime.min else user.checkin_time_last.date()}", - ) - today_data.text((0, 25), f"总金币:{gold}") - default_setu_prob = ( - Config.get_config("send_setu", "INITIAL_SETU_PROBABILITY") * 100 # type: ignore - ) - today_data.text( - (0, 50), - f"色图概率:{(default_setu_prob + float(user.impression) if user.impression < 100 else 100):.2f}%", - ) - today_data.text((0, 75), f"开箱次数:{(20 + int(user.impression / 3))}") - _type = "view" - else: - A.paste(gift_border, (570, 140), True) - today_sign_text_img = BuildImage( - 0, 0, plain_text="今日签到", color=(255, 255, 255, 0), font_size=30 - ) - if is_double: - today_data.text((0, 0), f"好感度 + {add_impression / 2:.2f} × 2") - else: - today_data.text((0, 0), f"好感度 + {add_impression:.2f}") - today_data.text((0, 25), f"金币 + {gold}") - _type = "sign" - current_date = datetime.now() - current_datetime_str = current_date.strftime("%Y-%m-%d %a %H:%M:%S") - data = current_date.date() - data_img = BuildImage( - 0, - 0, - plain_text=f"时间:{current_datetime_str}", - color=(255, 255, 255, 0), - font_size=20, - ) - bk.paste(nickname_img, (30, 15), True) - bk.paste(uid_img, (30, 85), True) - bk.paste(A, (0, 150), alpha=True) - bk.text((30, 167), "Accumulative check-in for") - _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.w + 45 - bk.paste(sign_day_img, (346, 158), True) - bk.text((_x, 167), "days") - bk.paste(data_img, (220, 370), True) - bk.paste(lik_text1_img, (220, 240), True) - bk.paste(lik_text2_img, (262, 234), True) - bk.paste(bar_bk, (225, 275), True) - bk.paste(info_img, (220, 305), True) - bk.paste(today_sign_text_img, (550, 180), True) - bk.paste(today_data, (580, 220), True) - bk.paste(watermark, (15, 400), True) - bk.save(SIGN_TODAY_CARD_PATH / f"{user_id}_{user.group_id}_{_type}_{data}.png") - return image( - IMAGE_PATH - / "sign" - / "today_card" - / f"{user_id}_{user.group_id}_{_type}_{data}.png" - ) - - -def generate_progress_bar_pic(): - bg_2 = (254, 1, 254) - bg_1 = (0, 245, 246) - - bk = BuildImage(1000, 50, is_alpha=True) - img_x = BuildImage(50, 50, color=bg_2) - img_x.circle() - img_x.crop((25, 0, 50, 50)) - img_y = BuildImage(50, 50, color=bg_1) - img_y.circle() - img_y.crop((0, 0, 25, 50)) - A = BuildImage(950, 50) - width, height = A.size - - step_r = (bg_2[0] - bg_1[0]) / width - step_g = (bg_2[1] - bg_1[1]) / width - step_b = (bg_2[2] - bg_1[2]) / width - - for y in range(0, width): - bg_r = round(bg_1[0] + step_r * y) - bg_g = round(bg_1[1] + step_g * y) - bg_b = round(bg_1[2] + step_b * y) - for x in range(0, height): - A.point((y, x), fill=(bg_r, bg_g, bg_b)) - bk.paste(img_y, (0, 0), True) - bk.paste(A, (25, 0)) - bk.paste(img_x, (975, 0), True) - bk.save(SIGN_RESOURCE_PATH / "bar.png") - - A = BuildImage(950, 50) - bk = BuildImage(1000, 50, is_alpha=True) - img_x = BuildImage(50, 50) - img_x.circle() - img_x.crop((25, 0, 50, 50)) - img_y = BuildImage(50, 50) - img_y.circle() - img_y.crop((0, 0, 25, 50)) - bk.paste(img_y, (0, 0), True) - bk.paste(A, (25, 0)) - bk.paste(img_x, (975, 0), True) - bk.save(SIGN_RESOURCE_PATH / "bar_white.png") - - -def get_level_and_next_impression(impression: float): - if impression == 0: - return lik2level[10], 10, 0 - keys = list(lik2level.keys()) - for i in range(len(keys)): - if impression > keys[i]: - return lik2level[keys[i]], keys[i - 1], keys[i] - return lik2level[10], 10, 0 - - -def clear_sign_data_pic(): - date = datetime.now().date() - for file in os.listdir(SIGN_TODAY_CARD_PATH): - if str(date) not in file: - os.remove(SIGN_TODAY_CARD_PATH / file) diff --git a/plugins/statistics/__init__.py b/plugins/statistics/__init__.py deleted file mode 100755 index 4ed43619..00000000 --- a/plugins/statistics/__init__.py +++ /dev/null @@ -1,127 +0,0 @@ -from configs.path_config import DATA_PATH -import nonebot -import os -try: - import ujson as json -except ModuleNotFoundError: - import json - -nonebot.load_plugins("plugins/statistics") - -old_file1 = DATA_PATH / "_prefix_count.json" -old_file2 = DATA_PATH / "_prefix_user_count.json" -new_path = DATA_PATH / "statistics" -new_path.mkdir(parents=True, exist_ok=True) -if old_file1.exists(): - os.rename(old_file1, new_path / "_prefix_count.json") -if old_file2.exists(): - os.rename(old_file2, new_path / "_prefix_user_count.json") - - -# 修改旧数据 - -statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - -for file in [statistics_group_file, statistics_user_file]: - if file.exists(): - with open(file, "r", encoding="utf8") as f: - data = json.load(f) - if not (statistics_group_file.parent / f"{file}.bak").exists(): - with open(f"{file}.bak", "w", encoding="utf8") as wf: - json.dump(data, wf, ensure_ascii=False, indent=4) - for x in ["total_statistics", "day_statistics"]: - for key in data[x].keys(): - num = 0 - if data[x][key].get("ai") is not None: - if data[x][key].get("Ai") is not None: - data[x][key]["Ai"] += data[x][key]["ai"] - else: - data[x][key]["Ai"] = data[x][key]["ai"] - del data[x][key]["ai"] - if data[x][key].get("抽卡") is not None: - if data[x][key].get("游戏抽卡") is not None: - data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] - else: - data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] - del data[x][key]["抽卡"] - if data[x][key].get("我的道具") is not None: - num += data[x][key]["我的道具"] - del data[x][key]["我的道具"] - if data[x][key].get("使用道具") is not None: - num += data[x][key]["使用道具"] - del data[x][key]["使用道具"] - if data[x][key].get("我的金币") is not None: - num += data[x][key]["我的金币"] - del data[x][key]["我的金币"] - if data[x][key].get("购买") is not None: - num += data[x][key]["购买"] - del data[x][key]["购买"] - if data[x][key].get("商店") is not None: - data[x][key]["商店"] += num - else: - data[x][key]["商店"] = num - for x in ["week_statistics", "month_statistics"]: - for key in data[x].keys(): - if key == "total": - if data[x][key].get("ai") is not None: - if data[x][key].get("Ai") is not None: - data[x][key]["Ai"] += data[x][key]["ai"] - else: - data[x][key]["Ai"] = data[x][key]["ai"] - del data[x][key]["ai"] - if data[x][key].get("抽卡") is not None: - if data[x][key].get("游戏抽卡") is not None: - data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] - else: - data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] - del data[x][key]["抽卡"] - if data[x][key].get("我的道具") is not None: - num += data[x][key]["我的道具"] - del data[x][key]["我的道具"] - if data[x][key].get("使用道具") is not None: - num += data[x][key]["使用道具"] - del data[x][key]["使用道具"] - if data[x][key].get("我的金币") is not None: - num += data[x][key]["我的金币"] - del data[x][key]["我的金币"] - if data[x][key].get("购买") is not None: - num += data[x][key]["购买"] - del data[x][key]["购买"] - if data[x][key].get("商店") is not None: - data[x][key]["商店"] += num - else: - data[x][key]["商店"] = num - else: - for day in data[x][key].keys(): - num = 0 - if data[x][key][day].get("ai") is not None: - if data[x][key][day].get("Ai") is not None: - data[x][key][day]["Ai"] += data[x][key][day]["ai"] - else: - data[x][key][day]["Ai"] = data[x][key][day]["ai"] - del data[x][key][day]["ai"] - if data[x][key][day].get("抽卡") is not None: - if data[x][key][day].get("游戏抽卡") is not None: - data[x][key][day]["游戏抽卡"] += data[x][key][day]["抽卡"] - else: - data[x][key][day]["游戏抽卡"] = data[x][key][day]["抽卡"] - del data[x][key][day]["抽卡"] - if data[x][key][day].get("我的道具") is not None: - num += data[x][key][day]["我的道具"] - del data[x][key][day]["我的道具"] - if data[x][key][day].get("使用道具") is not None: - num += data[x][key][day]["使用道具"] - del data[x][key][day]["使用道具"] - if data[x][key][day].get("我的金币") is not None: - num += data[x][key][day]["我的金币"] - del data[x][key][day]["我的金币"] - if data[x][key][day].get("购买") is not None: - num += data[x][key][day]["购买"] - del data[x][key][day]["购买"] - if data[x][key][day].get("商店") is not None: - data[x][key][day]["商店"] += num - else: - data[x][key][day]["商店"] = num - with open(file, "w", encoding="utf8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) diff --git a/plugins/statistics/_config.py b/plugins/statistics/_config.py deleted file mode 100644 index edee1069..00000000 --- a/plugins/statistics/_config.py +++ /dev/null @@ -1,26 +0,0 @@ -from enum import Enum -from typing import NamedTuple - - -class SearchType(Enum): - - """ - 查询类型 - """ - - DAY = "day_statistics" - """天""" - WEEK = "week_statistics" - """周""" - MONTH = "month_statistics" - """月""" - TOTAL = "total_statistics" - """总数""" - - -class ParseData(NamedTuple): - - global_search: bool - """是否全局搜索""" - search_type: SearchType - """搜索类型""" diff --git a/plugins/statistics/statistics_handle.py b/plugins/statistics/statistics_handle.py deleted file mode 100755 index f0515a4b..00000000 --- a/plugins/statistics/statistics_handle.py +++ /dev/null @@ -1,281 +0,0 @@ -import asyncio -import os - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, Message, MessageEvent -from nonebot.params import CommandArg - -from configs.path_config import DATA_PATH, IMAGE_PATH -from models.group_info import GroupInfo -from utils.depends import OneCommand -from utils.image_utils import BuildMat -from utils.manager import plugins2settings_manager -from utils.message_builder import image - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "功能调用统计可视化" -__plugin_usage__ = """ -usage: - 功能调用统计可视化 - 指令: - 功能调用统计 - 日功能调用统计 - 周功能调用统计 ?[功能] - 月功能调用统计 ?[功能] - 我的功能调用统计 - 我的日功能调用统计 ?[功能] - 我的周功能调用统计 ?[功能] - 我的月功能调用统计 ?[功能] -""".strip() -__plugin_superuser_usage__ = """ -usage: - 功能调用统计可视化 - 指令: - 全局功能调用统计 - 全局日功能调用统计 - 全局周功能调用统计 ?[功能] - 全局月功能调用统计 ?[功能] -""".strip() -__plugin_des__ = "功能调用统计可视化" -__plugin_cmd__ = [ - "功能调用统计", - "全局功能调用统计 [_superuser]", - "全局日功能调用统计 [_superuser]", - "全局周功能调用统计 ?[功能] [_superuser]", - "全局月功能调用统计 ?[功能] [_superuser]", - "周功能调用统计 ?[功能]", - "月功能调用统计 ?[功能]", - "我的功能调用统计", - "我的日功能调用统计 ?[功能]", - "我的周功能调用统计 ?[功能]", - "我的月功能调用统计 ?[功能]", -] -__plugin_type__ = ("数据统计", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["功能调用统计"], -} - - -statistics = on_command( - "功能调用统计", - aliases={ - "全局功能调用统计", - "全局日功能调用统计", - "全局周功能调用统计", - "全局月功能调用统计", - "日功能调用统计", - "周功能调用统计", - "月功能调用统计", - "我的功能调用统计", - "我的日功能调用统计", - "我的周功能调用统计", - "我的月功能调用统计", - }, - priority=5, - block=True, -) - - -statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - - -@statistics.handle() -async def _(bot: Bot, event: MessageEvent, cmd: str = OneCommand(), arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - if cmd[0][:2] == "全局": - if str(event.user_id) in bot.config.superusers: - data: dict = json.load(open(statistics_group_file, "r", encoding="utf8")) - if cmd[0][2] == '日': - _type = 'day_statistics' - elif cmd[0][2] == '周': - _type = 'week_statistics' - elif cmd[0][2] == '月': - _type = 'month_statistics' - else: - _type = 'total_statistics' - tmp_dict = {} - data = data[_type] - if _type in ["day_statistics", "total_statistics"]: - for key in data['total']: - tmp_dict[key] = data['total'][key] - else: - for group in data.keys(): - if group != 'total': - for day in data[group].keys(): - for plugin_name in data[group][day].keys(): - if data[group][day][plugin_name] is not None: - if tmp_dict.get(plugin_name) is None: - tmp_dict[plugin_name] = 1 - else: - tmp_dict[plugin_name] += data[group][day][plugin_name] - bar_graph = await init_bar_graph(tmp_dict, cmd[0]) - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - await statistics.finish(image(b64=bar_graph.pic2bs4())) - return - if cmd[0][:2] == "我的": - _type = "user" - key = str(event.user_id) - cmd = list(cmd) - cmd[0] = cmd[0][2:] - if not statistics_user_file.exists(): - await statistics.finish("统计文件不存在...", at_sender=True) - else: - if not isinstance(event, GroupMessageEvent): - await statistics.finish("请在群内调用此功能...") - _type = "group" - key = str(event.group_id) - if not statistics_group_file.exists(): - await statistics.finish("统计文件不存在...", at_sender=True) - plugin = "" - if cmd[0][0] == "日": - arg = "day_statistics" - elif cmd[0][0] == "周": - arg = "week_statistics" - elif cmd[0][0] == "月": - arg = "month_statistics" - else: - arg = "total_statistics" - if msg: - plugin = plugins2settings_manager.get_plugin_module(msg) - if not plugin: - if arg not in ["day_statistics", "total_statistics"]: - await statistics.finish("未找到此功能的调用...", at_sender=True) - if _type == "group": - data: dict = json.load(open(statistics_group_file, "r", encoding="utf8")) - if not data[arg].get(str(event.group_id)): - await statistics.finish("该群统计数据不存在...", at_sender=True) - else: - data: dict = json.load(open(statistics_user_file, "r", encoding="utf8")) - if not data[arg].get(str(event.user_id)): - await statistics.finish("该用户统计数据不存在...", at_sender=True) - day_index = data["day_index"] - data = data[arg][key] - if _type == "group": - group = await GroupInfo.filter(group_id=str(event.group_id)).first() - name = group if group else str(event.group_id) - else: - name = event.sender.card or event.sender.nickname - img = await generate_statistics_img(data, arg, name, plugin, day_index) - await statistics.send(image(b64=img)) - - -async def generate_statistics_img( - data: dict, arg: str, name: str, plugin: str, day_index: int -): - try: - plugin = plugins2settings_manager.get_plugin_data(plugin).cmd[0] - except (KeyError, IndexError, AttributeError): - pass - bar_graph = None - if arg == "day_statistics": - bar_graph = await init_bar_graph(data, f"{name} 日功能调用统计") - elif arg == "week_statistics": - if plugin: - current_week = day_index % 7 - week_lst = [] - if current_week == 0: - week_lst = [1, 2, 3, 4, 5, 6, 7] - else: - for i in range(current_week + 1, 7): - week_lst.append(str(i)) - for i in range(current_week + 1): - week_lst.append(str(i)) - count = [] - for i in range(7): - if int(week_lst[i]) == 7: - try: - count.append(data[str(0)][plugin]) - except KeyError: - count.append(0) - else: - try: - count.append(data[str(week_lst[i])][plugin]) - except KeyError: - count.append(0) - week_lst = ["7" if i == "0" else i for i in week_lst] - bar_graph = BuildMat( - y=count, - mat_type="line", - title=f"{name} 周 {plugin} 功能调用统计【为7天统计】", - x_index=week_lst, - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - else: - bar_graph = await init_bar_graph(update_data(data), f"{name} 周功能调用统计【为7天统计】") - elif arg == "month_statistics": - if plugin: - day_index = day_index % 30 - day_lst = [] - for i in range(day_index + 1, 30): - day_lst.append(i) - for i in range(day_index + 1): - day_lst.append(i) - count = [data[str(day_lst[i])][plugin] for i in range(30)] - day_lst = [str(x + 1) for x in day_lst] - bar_graph = BuildMat( - y=count, - mat_type="line", - title=f"{name} 月 {plugin} 功能调用统计【为30天统计】", - x_index=day_lst, - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - else: - bar_graph = await init_bar_graph(update_data(data), f"{name} 月功能调用统计【为30天统计】") - elif arg == "total_statistics": - bar_graph = await init_bar_graph(data, f"{name} 功能调用统计") - await asyncio.get_event_loop().run_in_executor(None, bar_graph.gen_graph) - return bar_graph.pic2bs4() - - -async def init_bar_graph(data: dict, title: str) -> BuildMat: - return await asyncio.get_event_loop().run_in_executor(None, _init_bar_graph, data, title) - - -def _init_bar_graph(data: dict, title: str) -> BuildMat: - bar_graph = BuildMat( - y=[data[x] for x in data.keys() if data[x] != 0], - mat_type="barh", - title=title, - x_index=[x for x in data.keys() if data[x] != 0], - display_num=True, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - return bar_graph - - -def update_data(data: dict): - tmp_dict = {} - for day in data.keys(): - for plugin_name in data[day].keys(): - # print(f'{day}:{plugin_name} = {data[day][plugin_name]}') - if data[day][plugin_name] is not None: - if tmp_dict.get(plugin_name) is None: - tmp_dict[plugin_name] = 1 - else: - tmp_dict[plugin_name] += data[day][plugin_name] - return tmp_dict \ No newline at end of file diff --git a/plugins/statistics/statistics_hook.py b/plugins/statistics/statistics_hook.py deleted file mode 100755 index 9fea8b2c..00000000 --- a/plugins/statistics/statistics_hook.py +++ /dev/null @@ -1,217 +0,0 @@ -from datetime import datetime - -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent, MessageEvent -from nonebot.matcher import Matcher -from nonebot.message import run_postprocessor -from nonebot.typing import Optional, T_State - -from configs.path_config import DATA_PATH -from models.statistics import Statistics -from utils.manager import plugins2settings_manager -from utils.utils import scheduler - -try: - import ujson as json -except ModuleNotFoundError: - import json - - -__zx_plugin_name__ = "功能调用统计 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - -# statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" -# statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" - -# try: -# with open(statistics_group_file, "r", encoding="utf8") as f: -# _prefix_count_dict = json.load(f) -# except (FileNotFoundError, ValueError): -# _prefix_count_dict = { -# "total_statistics": { -# "total": {}, -# }, -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": str(datetime.now().date()), -# "day_index": 0, -# } - -# try: -# with open(statistics_user_file, "r", encoding="utf8") as f: -# _prefix_user_count_dict = json.load(f) -# except (FileNotFoundError, ValueError): -# _prefix_user_count_dict = { -# "total_statistics": { -# "total": {}, -# }, -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": str(datetime.now().date()), -# "day_index": 0, -# } - - -# # 以前版本转换 -# if _prefix_count_dict.get("day_index") is None: -# tmp = _prefix_count_dict.copy() -# _prefix_count_dict = { -# "total_statistics": tmp["total_statistics"], -# "day_statistics": { -# "total": {}, -# }, -# "week_statistics": { -# "total": {}, -# }, -# "month_statistics": { -# "total": {}, -# }, -# "start_time": tmp["start_time"], -# "day_index": 0, -# } - - -# 添加命令次数 -@run_postprocessor -async def _( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: MessageEvent, - state: T_State, -): - # global _prefix_count_dict - if ( - matcher.type == "message" - and matcher.priority not in [1, 999] - and matcher.plugin_name not in ["update_info", "statistics_handle"] - ): - await Statistics.create( - user_id=str(event.user_id), - group_id=getattr(event, "group_id", None), - plugin_name=matcher.plugin_name, - create_time=datetime.now(), - ) - # module = matcher.plugin_name - # day_index = _prefix_count_dict["day_index"] - # try: - # group_id = str(event.group_id) - # except AttributeError: - # group_id = "total" - # user_id = str(event.user_id) - # plugin_name = plugins2settings_manager.get_plugin_data(module) - # if plugin_name and plugin_name.cmd: - # plugin_name = plugin_name.cmd[0] - # check_exists_key(group_id, user_id, plugin_name) - # for data in [_prefix_count_dict, _prefix_user_count_dict]: - # data["total_statistics"]["total"][plugin_name] += 1 - # data["day_statistics"]["total"][plugin_name] += 1 - # data["week_statistics"]["total"][plugin_name] += 1 - # data["month_statistics"]["total"][plugin_name] += 1 - # # print(_prefix_count_dict) - # if group_id != "total": - # for data in [_prefix_count_dict, _prefix_user_count_dict]: - # if data == _prefix_count_dict: - # key = group_id - # else: - # key = user_id - # data["total_statistics"][key][plugin_name] += 1 - # data["day_statistics"][key][plugin_name] += 1 - # data["week_statistics"][key][str(day_index % 7)][plugin_name] += 1 - # data["month_statistics"][key][str(day_index % 30)][plugin_name] += 1 - # with open(statistics_group_file, "w", encoding="utf8") as f: - # json.dump(_prefix_count_dict, f, indent=4, ensure_ascii=False) - # with open(statistics_user_file, "w", encoding="utf8") as f: - # json.dump(_prefix_user_count_dict, f, ensure_ascii=False, indent=4) - - -# def check_exists_key(group_id: str, user_id: str, plugin_name: str): -# global _prefix_count_dict, _prefix_user_count_dict -# for data in [_prefix_count_dict, _prefix_user_count_dict]: -# if data == _prefix_count_dict: -# key = group_id -# else: -# key = user_id -# if not data["total_statistics"]["total"].get(plugin_name): -# data["total_statistics"]["total"][plugin_name] = 0 -# if not data["day_statistics"]["total"].get(plugin_name): -# data["day_statistics"]["total"][plugin_name] = 0 -# if not data["week_statistics"]["total"].get(plugin_name): -# data["week_statistics"]["total"][plugin_name] = 0 -# if not data["month_statistics"]["total"].get(plugin_name): -# data["month_statistics"]["total"][plugin_name] = 0 - -# if not data["total_statistics"].get(key): -# data["total_statistics"][key] = {} -# if not data["total_statistics"][key].get(plugin_name): -# data["total_statistics"][key][plugin_name] = 0 -# if not data["day_statistics"].get(key): -# data["day_statistics"][key] = {} -# if not data["day_statistics"][key].get(plugin_name): -# data["day_statistics"][key][plugin_name] = 0 - -# if key != "total": -# if not data["week_statistics"].get(key): -# data["week_statistics"][key] = {} -# if data["week_statistics"][key].get("0") is None: -# for i in range(7): -# data["week_statistics"][key][str(i)] = {} -# if data["week_statistics"][key]["0"].get(plugin_name) is None: -# for i in range(7): -# data["week_statistics"][key][str(i)][plugin_name] = 0 - -# if not data["month_statistics"].get(key): -# data["month_statistics"][key] = {} -# if data["month_statistics"][key].get("0") is None: -# for i in range(30): -# data["month_statistics"][key][str(i)] = {} -# if data["month_statistics"][key]["0"].get(plugin_name) is None: -# for i in range(30): -# data["month_statistics"][key][str(i)][plugin_name] = 0 - - -# 天 -# @scheduler.scheduled_job( -# "cron", -# hour=0, -# minute=1, -# ) -# async def _(): -# for data in [_prefix_count_dict, _prefix_user_count_dict]: -# data["day_index"] += 1 -# for x in data["day_statistics"].keys(): -# for key in data["day_statistics"][x].keys(): -# try: -# data["day_statistics"][x][key] = 0 -# except KeyError: -# pass -# for type_ in ["week_statistics", "month_statistics"]: -# index = str( -# data["day_index"] % 7 -# if type_ == "week_statistics" -# else data["day_index"] % 30 -# ) -# for x in data[type_].keys(): -# try: -# for key in data[type_][x][index].keys(): -# data[type_][x][index][key] = 0 -# except KeyError: -# pass -# with open(statistics_group_file, "w", encoding="utf8") as f: -# json.dump(_prefix_count_dict, f, indent=4, ensure_ascii=False) -# with open(statistics_user_file, "w", encoding="utf8") as f: -# json.dump(_prefix_user_count_dict, f, indent=4, ensure_ascii=False) diff --git a/plugins/statistics/utils.py b/plugins/statistics/utils.py deleted file mode 100644 index c35b5672..00000000 --- a/plugins/statistics/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import List -from nonebot.adapters.onebot.v11 import MessageEvent -from ._config import SearchType - - -def parse_data(cmd: str, event: MessageEvent, superusers: List[str]): - search_type = SearchType.TOTAL - if cmd[:2] == "全局": - if str(event.user_id) in superusers: - if cmd[2] == '日': - search_type = SearchType.DAY - elif cmd[2] == '周': - _type = SearchType.WEEK - elif cmd[2] == '月': - _type = SearchType.MONTH - diff --git a/plugins/translate/__init__.py b/plugins/translate/__init__.py deleted file mode 100755 index e6ac9464..00000000 --- a/plugins/translate/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, Tuple - -from nonebot import on_regex -from nonebot.adapters.onebot.v11 import MessageEvent -from nonebot.params import RegexGroup -from nonebot.typing import T_State - -from services.log import logger -from utils.depends import CheckConfig - -from .data_source import CheckParam, language, translate_msg - -__zx_plugin_name__ = "翻译" -__plugin_usage__ = """ -usage: - 出国旅游小助手 - Regex: 翻译(form:.*?)?(to:.*?)? (.+) - 一般只需要设置to:,form:按照百度自动检测 - 指令: - 翻译语种: (查看form与to可用值) - 示例: - 翻译 你好: 将中文翻译为英文 - 翻译 Hello: 将英文翻译为中文 - 翻译to:el 你好: 将"你好"翻译为希腊语 - 翻译to:希腊语 你好: 允许form和to使用中文 - 翻译form:zhto:jp 你好: 指定原语种并将"你好"翻译为日文 -""".strip() -__plugin_des__ = "出国旅游好助手" -__plugin_cmd__ = ["翻译"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["翻译"], -} -__plugin_configs__ = { - "APPID": { - "value": None, - "help": "百度翻译APPID", - "type": str, - }, - "SECRET_KEY": { - "value": None, - "help": "百度翻译秘钥", - "type": str, - }, -} - -translate = on_regex("^翻译(form:.*?)?(to:.*?)? (.+)", priority=5, block=True) - -translate_language = on_regex("^翻译语种$", priority=5, block=True) - - -@translate_language.handle() -async def _(event: MessageEvent): - s = "" - for key, value in language.items(): - s += f"{key}: {value}," - await translate_language.send(s[:-1]) - logger.info(f"查看翻译语种", "翻译语种", event.user_id, getattr(event, "group_id", None)) - - -@translate.handle( - parameterless=[ - CheckConfig(config="APPID"), - CheckConfig(config="SECRET_KEY"), - CheckParam(), - ] -) -async def _( - event: MessageEvent, state: T_State, reg_group: Tuple[Any, ...] = RegexGroup() -): - _, _, msg = reg_group - await translate.send(await translate_msg(msg, state["form"], state["to"])) - logger.info(f"翻译: {msg}", "翻译", event.user_id, getattr(event, "group_id", None)) diff --git a/plugins/translate/data_source.py b/plugins/translate/data_source.py deleted file mode 100755 index 517938e2..00000000 --- a/plugins/translate/data_source.py +++ /dev/null @@ -1,119 +0,0 @@ -import time -from hashlib import md5 -from typing import Any, Tuple - -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from utils.http_utils import AsyncHttpx - -URL = "http://api.fanyi.baidu.com/api/trans/vip/translate" - - -language = { - "自动": "auto", - "粤语": "yue", - "韩语": "kor", - "泰语": "th", - "葡萄牙语": "pt", - "希腊语": "el", - "保加利亚语": "bul", - "芬兰语": "fin", - "斯洛文尼亚语": "slo", - "繁体中文": "cht", - "中文": "zh", - "文言文": "wyw", - "法语": "fra", - "阿拉伯语": "ara", - "德语": "de", - "荷兰语": "nl", - "爱沙尼亚语": "est", - "捷克语": "cs", - "瑞典语": "swe", - "越南语": "vie", - "英语": "en", - "日语": "jp", - "西班牙语": "spa", - "俄语": "ru", - "意大利语": "it", - "波兰语": "pl", - "丹麦语": "dan", - "罗马尼亚语": "rom", - "匈牙利语": "hu", -} - - -def CheckParam(): - """ - 检查翻译内容是否在language中 - """ - - async def dependency( - matcher: Matcher, - state: T_State, - reg_group: Tuple[Any, ...] = RegexGroup(), - ): - form, to, _ = reg_group - values = language.values() - if form: - form = form.split(":")[-1] - if form not in language and form not in values: - await matcher.finish("FORM选择的语种不存在") - state["form"] = form - else: - state["form"] = "auto" - if to: - to = to.split(":")[-1] - if to not in language and to not in values: - await matcher.finish("TO选择的语种不存在") - state["to"] = to - else: - state["to"] = "auto" - - return Depends(dependency) - - -async def translate_msg(word: str, form: str, to: str) -> str: - """翻译 - - Args: - word (str): 翻译文字 - form (str): 源语言 - to (str): 目标语言 - - Returns: - str: 翻译后的文字 - """ - if form in language: - form = language[form] - if to in language: - to = language[to] - salt = str(time.time()) - app_id = Config.get_config("translate", "APPID") - secret_key = Config.get_config("translate", "SECRET_KEY") - sign = app_id + word + salt + secret_key # type: ignore - md5_ = md5() - md5_.update(sign.encode("utf-8")) - sign = md5_.hexdigest() - params = { - "q": word, - "from": form, - "to": to, - "appid": app_id, - "salt": salt, - "sign": sign, - } - url = URL + "?" - for key, value in params.items(): - url += f"{key}={value}&" - url = url[:-1] - resp = await AsyncHttpx.get(url) - data = resp.json() - if data.get("error_code"): - return data.get("error_msg") - if trans_result := data.get("trans_result"): - return trans_result[0]["dst"] - return "没有找到翻译捏" diff --git a/plugins/update_gocqhttp/__init__.py b/plugins/update_gocqhttp/__init__.py deleted file mode 100755 index a5edde60..00000000 --- a/plugins/update_gocqhttp/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from pathlib import Path -from typing import List - -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent -from nonebot.permission import SUPERUSER -from nonebot.typing import T_State - -from configs.config import Config -from services.log import logger -from utils.utils import get_bot, scheduler - -from .data_source import download_gocq_lasted, upload_gocq_lasted - -__zx_plugin_name__ = "更新gocq [Superuser]" -__plugin_usage__ = """ -usage: - 下载最新版gocq并上传至群文件 - 指令: - 更新gocq -""".strip() -__plugin_cmd__ = ["更新gocq"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "UPDATE_GOCQ_GROUP": { - "value": [], - "help": "需要为哪些群更新最新版gocq吗?(上传最新版gocq)示例:[434995955, 239483248]", - "default_value": [], - "type": List[int], - } -} - -path = str((Path() / "resources" / "gocqhttp_file").absolute()) + "/" - -lasted_gocqhttp = on_command("更新gocq", permission=SUPERUSER, priority=5, block=True) - - -@lasted_gocqhttp.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - # try: - if event.group_id in Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - await lasted_gocqhttp.send("检测中...") - info = await download_gocq_lasted(path) - if info == "gocqhttp没有更新!": - await lasted_gocqhttp.finish("gocqhttp没有更新!") - try: - for file in os.listdir(path): - await upload_gocq_lasted(path, file, event.group_id) - logger.info(f"更新了cqhttp...{file}") - await lasted_gocqhttp.send(f"gocqhttp更新了,已上传成功!\n更新内容:\n{info}") - except Exception as e: - logger.error(f"更新gocq错误 e:{e}") - - -# 更新gocq -@scheduler.scheduled_job( - "cron", - hour=3, - minute=1, -) -async def _(): - if Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - bot = get_bot() - try: - info = await download_gocq_lasted(path) - if info == "gocqhttp没有更新!": - logger.info("gocqhttp没有更新!") - return - for group in Config.get_config("update_gocqhttp", "UPDATE_GOCQ_GROUP"): - for file in os.listdir(path): - await upload_gocq_lasted(path, file, group) - await bot.send_group_msg( - group_id=group, message=f"gocqhttp更新了,已上传成功!\n更新内容:\n{info}" - ) - except Exception as e: - logger.error(f"自动更新gocq出错 e:{e}") diff --git a/plugins/update_gocqhttp/data_source.py b/plugins/update_gocqhttp/data_source.py deleted file mode 100755 index 1572e90e..00000000 --- a/plugins/update_gocqhttp/data_source.py +++ /dev/null @@ -1,73 +0,0 @@ -from utils.utils import get_bot -from bs4 import BeautifulSoup -from utils.http_utils import AsyncHttpx -import asyncio -import platform -import os - -# if platform.system() == "Windows": -# asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - -url = "https://github.com/Mrs4s/go-cqhttp/releases" - - -async def download_gocq_lasted(path: str): - text = (await AsyncHttpx.get(url)).text - soup = BeautifulSoup(text, "lxml") - a = soup.find("div", {"class": "release-header"}).find("a") - title = a.text - _url = a.get("href") - for file in os.listdir(path): - if file.endswith(".zip"): - if ( - file == title + "-windows-amd64.zip" - or file == title + "_windows_amd64.zip" - ): - return "gocqhttp没有更新!" - for file in os.listdir(path): - os.remove(path + file) - text = (await AsyncHttpx.get("https://github.com" + _url)).text - update_info = "" - soup = BeautifulSoup(text, "lxml") - info_div = soup.find("div", {"class": "markdown-body"}) - for p in info_div.find_all("p"): - update_info += p.text.replace("
", "\n") + "\n" - div_all = soup.select( - "div.d-flex.flex-justify-between.flex-items-center.py-1.py-md-2.Box-body.px-2" - ) - for div in div_all: - if ( - div.find("a").find("span").text == title + "-windows-amd64.zip" - or div.find("a").find("span").text == title + "-linux-arm64.tar.gz" - or div.find("a").find("span").text == "go-cqhttp_windows_amd64.zip" - or div.find("a").find("span").text == "go-cqhttp_linux_arm64.tar.gz" - ): - file_url = div.find("a").get("href") - if div.find("a").find("span").text.find("windows") == -1: - tag = "-linux-arm64.tar.gz" - else: - tag = "-windows-amd64.zip" - await AsyncHttpx.download_file( - "https://github.com" + file_url, path + title + tag - ) - return update_info - - -async def upload_gocq_lasted(path, name, group_id): - bot = get_bot() - folder_id = 0 - for folder in (await bot.get_group_root_files(group_id=group_id))["folders"]: - if folder["folder_name"] == "gocq": - folder_id = folder["folder_id"] - if not folder_id: - await bot.send_group_msg(group_id=group_id, message=f"请创建gocq文件夹后重试!") - for file in os.listdir(path): - os.remove(path + file) - else: - await bot.upload_group_file( - group_id=group_id, folder=folder_id, file=path + name, name=name - ) - - -# asyncio.get_event_loop().run_until_complete(download_gocq_lasted()) diff --git a/plugins/update_picture.py b/plugins/update_picture.py deleted file mode 100755 index bdf7f552..00000000 --- a/plugins/update_picture.py +++ /dev/null @@ -1,303 +0,0 @@ -from typing import Union - -import cv2 -import numpy as np -from nonebot import on_command -from nonebot.adapters.onebot.v11 import GroupMessageEvent, Message, MessageEvent -from nonebot.params import Arg, ArgStr, CommandArg, Depends -from nonebot.rule import to_me -from nonebot.typing import T_State -from PIL import Image, ImageFilter - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH, TEMP_PATH -from services.log import logger -from utils.http_utils import AsyncHttpx -from utils.image_utils import BuildImage, pic2b64 -from utils.message_builder import image -from utils.utils import get_message_img, is_number - -__zx_plugin_name__ = "各种图片简易操作" -__plugin_usage__ = """ -usage: - 简易的基础图片操作,输入 指定操作 或 序号 来进行选择 - 指令: - 1.修改尺寸 [宽] [高] [图片] - 2.等比压缩 [比例] [图片] - 3.旋转图片 [角度] [图片] - 4.水平翻转 [图片] - 5.铅笔滤镜 [图片] - 6.模糊效果 [图片] - 7.锐化效果 [图片] - 8.高斯模糊 [图片] - 9.边缘检测 [图片] - 10.底色替换 [红/蓝] [红/蓝/白/绿/黄] [图片] - 示例:图片修改尺寸 100 200 [图片] - 示例:图片 2 0.3 [图片] -""".strip() -__plugin_des__ = "10种快捷的图片简易操作" -__plugin_cmd__ = [ - "改图 修改尺寸 [宽] [高] [图片]", - "改图 等比压缩 [比例] [图片]", - "改图 旋转图片 [角度] [图片]", - "改图 水平翻转 [图片]", - "改图 铅笔滤镜 [图片]", - "改图 模糊效果 [图片]", - "改图 锐化效果 [图片]", - "改图 高斯模糊 [图片]", - "改图 边缘检测 [图片]", - "改图 底色替换 [红/蓝] [红/蓝/白/绿/黄] [图片]", -] -__plugin_type__ = ("一些工具", 1) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["修改图片", "改图", "操作图片"], -} - -method_flag = "" - -update_img = on_command( - "修改图片", aliases={"操作图片", "改图"}, priority=5, rule=to_me(), block=True -) - -method_list = [ - "修改尺寸", - "等比压缩", - "旋转图片", - "水平翻转", - "铅笔滤镜", - "模糊效果", - "锐化效果", - "高斯模糊", - "边缘检测", - "底色替换", -] -method_str = "" -method_oper = [] -for i in range(len(method_list)): - method_str += f"\n{i + 1}.{method_list[i]}" - method_oper.append(method_list[i]) - method_oper.append(str(i + 1)) - -update_img_help = BuildImage(960, 700, font_size=24) -update_img_help.text((10, 10), __plugin_usage__) -update_img_help.save(IMAGE_PATH / "update_img_help.png") - - -def parse_key(key: str): - async def _key_parser(state: T_State, inp: Union[Message, str] = Arg(key)): - if key != "img_list" and isinstance(inp, Message): - inp = inp.extract_plain_text().strip() - if inp in ["取消", "算了"]: - await update_img.finish("已取消操作..") - if key == "method": - if inp not in method_oper: - await update_img.reject_arg("method", f"操作不正确,请重新输入!{method_str}") - elif key == "x": - method = state["method"] - if method in ["1", "修改尺寸"]: - if not is_number(inp) or int(inp) < 1: - await update_img.reject_arg("x", "宽度不正确!请重新输入数字...") - elif method in ["2", "等比压缩", "3", "旋转图片"]: - if not is_number(inp): - await update_img.reject_arg("x", "比率不正确!请重新输入数字...") - elif method in ["10", "底色替换"]: - if inp not in ["红色", "蓝色", "红", "蓝"]: - await update_img.reject_arg("x", "请输入支持的被替换的底色:\n红色 蓝色") - elif key == "y": - method = state["method"] - if method in ["1", "修改尺寸"]: - if not is_number(inp) or int(inp) < 1: - await update_img.reject_arg("y", "长度不正确!请重新输入数字...") - elif method in ["10", "底色替换"]: - if inp not in [ - "红色", - "白色", - "蓝色", - "绿色", - "黄色", - "红", - "白", - "蓝", - "绿", - "黄", - ]: - await update_img.reject_arg("y", "请输入支持的替换的底色:\n红色 蓝色 白色 绿色") - elif key == "img_list": - if not get_message_img(inp): - await update_img.reject_arg("img_list", "没图?没图?没图?来图速来!") - state[key] = inp - - return _key_parser - - -@update_img.handle() -async def _(event: MessageEvent, state: T_State, arg: Message = CommandArg()): - if str(event.get_message()) in ["帮助"]: - await update_img.finish(image("update_img_help.png")) - raw_arg = arg.extract_plain_text().strip() - img_list = get_message_img(event.json()) - if raw_arg: - args = raw_arg.split("[")[0].split() - state["method"] = args[0] - if len(args) == 2: - if args[0] in ["等比压缩", "旋转图片"]: - if is_number(args[1]): - state["x"] = args[1] - state["y"] = "" - elif len(args) > 2: - if args[0] in ["修改尺寸"]: - if is_number(args[1]): - state["x"] = args[1] - if is_number(args[2]): - state["y"] = args[2] - if args[0] in ["底色替换"]: - if args[1] in ["红色", "蓝色", "蓝", "红"]: - state["x"] = args[1] - if args[2] in ["红色", "白色", "蓝色", "绿色", "黄色", "红", "白", "蓝", "绿", "黄"]: - state["y"] = args[2] - if args[0] in ["水平翻转", "铅笔滤镜", "模糊效果", "锐化效果", "高斯模糊", "边缘检测"]: - state["x"] = "" - state["y"] = "" - if img_list: - state["img_list"] = event.message - - -@update_img.got( - "method", - prompt=f"要使用图片的什么操作呢?{method_str}", - parameterless=[Depends(parse_key("method"))], -) -@update_img.got( - "x", prompt="[宽度? 比率? 旋转角度? 底色?]", parameterless=[Depends(parse_key("x"))] -) -@update_img.got("y", prompt="[长度? 0 0 底色?]", parameterless=[Depends(parse_key("y"))]) -@update_img.got( - "img_list", prompt="图呢图呢图呢图呢?GKD!", parameterless=[Depends(parse_key("img_list"))] -) -async def _( - event: MessageEvent, - state: T_State, - method: str = ArgStr("method"), - x: str = ArgStr("x"), - y: str = ArgStr("y"), - img_list: Message = Arg("img_list"), -): - x = x or "" - y = y or "" - img_list = get_message_img(img_list) - if is_number(x): - x = float(x) - if is_number(y): - y = int(y) - index = 0 - result = "" - for img_url in img_list: - if await AsyncHttpx.download_file( - img_url, TEMP_PATH / f"{event.user_id}_{index}_update.png" - ): - index += 1 - else: - await update_img.finish("获取图片超时了...", at_sender=True) - if index == 0: - return - if method in ["修改尺寸", "1"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.convert("RGB") - img = img.resize((int(x), int(y)), Image.ANTIALIAS) - result += image(b64=pic2b64(img)) - await update_img.finish(result, at_sender=True) - if method in ["等比压缩", "2"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - width, height = img.size - img = img.convert("RGB") - if width * x < 8000 and height * x < 8000: - img = img.resize((int(x * width), int(x * height))) - result += image(b64=pic2b64(img)) - else: - await update_img.finish(f"{NICKNAME}不支持图片压缩后宽或高大于8000的存在!!") - if method in ["旋转图片", "3"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.rotate(x) - result += image(b64=pic2b64(img)) - if method in ["水平翻转", "4"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png") - img = img.transpose(Image.FLIP_LEFT_RIGHT) - result += image(b64=pic2b64(img)) - if method in ["铅笔滤镜", "5"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.CONTOUR - ) - result += image(b64=pic2b64(img)) - if method in ["模糊效果", "6"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.BLUR - ) - result += image(b64=pic2b64(img)) - if method in ["锐化效果", "7"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.EDGE_ENHANCE - ) - result += image(b64=pic2b64(img)) - if method in ["高斯模糊", "8"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.GaussianBlur - ) - result += image(b64=pic2b64(img)) - if method in ["边缘检测", "9"]: - for i in range(index): - img = Image.open(TEMP_PATH / f"{event.user_id}_{i}_update.png").filter( - ImageFilter.FIND_EDGES - ) - result += image(b64=pic2b64(img)) - if method in ["底色替换", "10"]: - if x in ["蓝色", "蓝"]: - lower = np.array([90, 70, 70]) - upper = np.array([110, 255, 255]) - if x in ["红色", "红"]: - lower = np.array([0, 135, 135]) - upper = np.array([180, 245, 230]) - if y in ["蓝色", "蓝"]: - color = (255, 0, 0) - if y in ["红色", "红"]: - color = (0, 0, 255) - if y in ["白色", "白"]: - color = (255, 255, 255) - if y in ["绿色", "绿"]: - color = (0, 255, 0) - if y in ["黄色", "黄"]: - color = (0, 255, 255) - for k in range(index): - img = cv2.imread(TEMP_PATH / f"{event.user_id}_{k}_update.png") - img = cv2.resize(img, None, fx=0.3, fy=0.3) - rows, cols, channels = img.shape - hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) - mask = cv2.inRange(hsv, lower, upper) - # erode = cv2.erode(mask, None, iterations=1) - dilate = cv2.dilate(mask, None, iterations=1) - for i in range(rows): - for j in range(cols): - if dilate[i, j] == 255: - img[i, j] = color - cv2.imwrite(TEMP_PATH / f"{event.user_id}_{k}_ok_update.png", img) - for i in range(index): - result += image(TEMP_PATH / f"{event.user_id}_{i}_ok_update.png") - if is_number(method): - method = method_list[int(method) - 1] - logger.info( - f"(USER {event.user_id}, GROUP" - f" {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 使用{method}" - ) - await update_img.finish(result, at_sender=True) diff --git a/plugins/wbtop/__init__.py b/plugins/wbtop/__init__.py deleted file mode 100644 index f6a13cf6..00000000 --- a/plugins/wbtop/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from services.log import logger -from .data_source import gen_wbtop_pic,get_wbtop -from utils.utils import is_number -from configs.path_config import IMAGE_PATH -from utils.http_utils import AsyncPlaywright -import asyncio -import datetime -__zx_plugin_name__ = "微博热搜" -__plugin_usage__ = """ -usage: - 在QQ上吃个瓜 - 指令: - 微博热搜:发送实时热搜 - 微博热搜 [id]:截图该热搜页面 - 示例:微博热搜 5 -""".strip() -__plugin_des__ = "刚买完瓜,在吃瓜现场" -__plugin_cmd__ = ["微博热搜", "微博热搜 [id]"] -__plugin_version__ = 0.2 -__plugin_author__ = "HibiKier & yajiwa" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["微博热搜"], -} - -wbtop = on_command("wbtop", aliases={"微博热搜"}, priority=5, block=True) - - -wbtop_url = "https://weibo.com/ajax/side/hotSearch" - -wbtop_data = [] - - -@wbtop.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - global wbtop_data - msg = arg.extract_plain_text().strip() - if wbtop_data: - now_time = datetime.datetime.now() - if now_time > wbtop_data["time"] + datetime.timedelta(minutes=5): - data, code = await get_wbtop(wbtop_url) - if code != 200: - await wbtop.finish(data, at_sender=True) - else: - wbtop_data = data - else: - data, code = await get_wbtop(wbtop_url) - if code != 200: - await wbtop.finish(data, at_sender=True) - else: - wbtop_data = data - - if not msg: - img = await asyncio.get_event_loop().run_in_executor( - None, gen_wbtop_pic, wbtop_data["data"] - ) - await wbtop.send(img) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询微博热搜" - ) - if is_number(msg) and 0 < int(msg) <= 50: - url = wbtop_data["data"][int(msg) - 1]["url"] - await wbtop.send("开始截取数据...") - img = await AsyncPlaywright.screenshot( - url, - f"{IMAGE_PATH}/temp/wbtop_{event.user_id}.png", - "#pl_feed_main", - wait_time=12 - ) - if img: - await wbtop.finish(img) - else: - await wbtop.finish("发生了一些错误.....") - diff --git a/plugins/wbtop/data_source.py b/plugins/wbtop/data_source.py deleted file mode 100644 index c3bbc915..00000000 --- a/plugins/wbtop/data_source.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageSegment -from utils.image_utils import BuildImage -from utils.message_builder import image -from configs.path_config import IMAGE_PATH -from typing import Tuple, Union -from utils.http_utils import AsyncHttpx -import datetime - - -async def get_wbtop(url: str) -> Tuple[Union[dict, str], int]: - """ - :param url: 请求链接 - """ - n = 0 - while True: - try: - data = [] - get_response = (await AsyncHttpx.get(url, timeout=20)) - if get_response.status_code == 200: - data_json = get_response.json()['data']['realtime'] - for data_item in data_json: - # 如果是广告,则不添加 - if 'is_ad' in data_item: - continue - dic = { - 'hot_word': data_item['note'], - 'hot_word_num': str(data_item['num']), - 'url': 'https://s.weibo.com/weibo?q=%23' + data_item['word'] + '%23', - } - data.append(dic) - if not data: - return "没有搜索到...", 997 - return {'data': data, 'time': datetime.datetime.now()}, 200 - else: - if n > 2: - return f'获取失败,请十分钟后再试', 999 - else: - n += 1 - continue - except TimeoutError: - return "超时了....", 998 - - -def gen_wbtop_pic(data: dict) -> MessageSegment: - """ - 生成微博热搜图片 - :param data: 微博热搜数据 - """ - bk = BuildImage(700, 32 * 50 + 280, 700, 32, color="#797979") - wbtop_bk = BuildImage(700, 280, background=f"{IMAGE_PATH}/other/webtop.png") - bk.paste(wbtop_bk) - text_bk = BuildImage(700, 32 * 50, 700, 32, color="#797979") - for i, data in enumerate(data): - title = f"{i + 1}. {data['hot_word']}" - hot = str(data["hot_word_num"]) - img = BuildImage(700, 30, font_size=20) - w, h = img.getsize(title) - img.text((10, int((30 - h) / 2)), title) - img.text((580, int((30 - h) / 2)), hot) - text_bk.paste(img) - bk.paste(text_bk, (0, 280)) - return image(b64=bk.pic2bs4()) diff --git a/plugins/weather/__init__.py b/plugins/weather/__init__.py deleted file mode 100755 index 64eb2de0..00000000 --- a/plugins/weather/__init__.py +++ /dev/null @@ -1,58 +0,0 @@ -from nonebot import on_regex -from .data_source import get_weather_of_city, get_city_list -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent -from jieba import posseg -from services.log import logger -from nonebot.params import RegexGroup -from typing import Tuple, Any - - -__zx_plugin_name__ = "天气查询" -__plugin_usage__ = """ -usage: - 普普通通的查天气吧 - 指令: - [城市]天气 -""".strip() -__plugin_des__ = "出门要看看天气,不要忘了带伞" -__plugin_cmd__ = ["[城市]天气/天气[城市]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询天气", "天气", "天气查询", "查天气"], -} - - -weather = on_regex(r".{0,10}?(.*)的?天气.{0,10}", priority=5, block=True) - - -@weather.handle() -async def _(event: MessageEvent, reg_group: Tuple[Any, ...] = RegexGroup()): - msg = reg_group[0] - if msg and msg[-1] != "市": - msg += "市" - city = "" - if msg: - city_list = get_city_list() - for word in posseg.lcut(msg): - if word.flag == "ns" or word.word[:-1] in city_list: - city = str(word.word).strip() - break - if word.word == "火星": - await weather.finish( - "没想到你个小呆子还真的想看火星天气!\n火星大气中含有95%的二氧化碳,气压低,加之极度的干燥," - "就阻止了水的形成积聚。这意味着火星几乎没有云,冰层覆盖了火星的两极,它们的融化和冻结受到火星与太" - "阳远近距离的影响,它产生了强大的尘埃云,阻挡了太阳光,使冰层的融化慢下来。\n所以说火星天气太恶劣了," - "去过一次就不想再去第二次了" - ) - if city: - city_weather = await get_weather_of_city(city) - logger.info( - f'(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else "private"} ) ' - f"查询天气:" + city - ) - await weather.finish(city_weather) diff --git a/plugins/weather/data_source.py b/plugins/weather/data_source.py deleted file mode 100755 index 8fd54f5c..00000000 --- a/plugins/weather/data_source.py +++ /dev/null @@ -1,79 +0,0 @@ -from utils.message_builder import image -from configs.path_config import TEXT_PATH -from configs.config import NICKNAME -from typing import List -from nonebot import Driver -from utils.http_utils import AsyncHttpx -import ujson as json -import nonebot - -driver: Driver = nonebot.get_driver() - -china_city = TEXT_PATH / "china_city.json" - -data = {} - - -async def get_weather_of_city(city: str) -> str: - """ - 获取城市天气数据 - :param city: 城市 - """ - code = _check_exists_city(city) - if code == 999: - return "不要查一个省份的天气啊,很累人的!" - elif code == 998: - return f"{NICKNAME}没查到!!试试查火星的天气?" - else: - data_json = ( - await AsyncHttpx.get( - f"https://v0.yiketianqi.com/api?unescape=1&version=v91&appid=43656176&appsecret=I42og6Lm&ext=&cityid=&city={city[:-1]}" - ) - ).json() - if wh := data_json.get('data'): - w_type = wh[0]["wea_day"] - w_max = wh[0]["tem1"] - w_min = wh[0]["tem2"] - fengli = wh[0]["win_speed"] - ganmao = wh[0]["narrative"] - fengxiang = ','.join(wh[0].get('win', [])) - repass = f"{city}的天气是 {w_type} 天\n最高温度: {w_max}\n最低温度: {w_min}\n风力: {fengli} {fengxiang}\n{ganmao}" - return repass - else: - return data_json.get("errmsg") or "好像出错了?再试试?" - - -def _check_exists_city(city: str) -> int: - """ - 检测城市是否存在合法 - :param city: 城市名称 - """ - global data - city = city if city[-1] != "市" else city[:-1] - for province in data.keys(): - for city_ in data[province]: - if city_ == city: - return 200 - for province in data.keys(): - if city == province: - return 999 - return 998 - - -def get_city_list() -> List[str]: - """ - 获取城市列表 - """ - global data - if not data: - try: - with open(china_city, "r", encoding="utf8") as f: - data = json.load(f) - except FileNotFoundError: - data = {} - city_list = [] - for p in data.keys(): - for c in data[p]: - city_list.append(c) - city_list.append(p) - return city_list diff --git a/plugins/web_ui/__init__.py b/plugins/web_ui/__init__.py deleted file mode 100644 index 00bcd2b5..00000000 --- a/plugins/web_ui/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -import asyncio - -import nonebot -from fastapi import APIRouter, FastAPI -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.log import default_filter, default_format -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.typing import T_State - -from configs.config import Config as gConfig -from services.log import logger, logger_ -from utils.manager import plugins2settings_manager - -from .api.logs import router as ws_log_routes -from .api.logs.log_manager import LOG_STORAGE -from .api.tabs.database import router as database_router -from .api.tabs.main import router as main_router -from .api.tabs.main import ws_router as status_routes -from .api.tabs.manage import router as manage_router -from .api.tabs.manage import ws_router as chat_routes -from .api.tabs.plugin_manage import router as plugin_router -from .api.tabs.system import router as system_router -from .auth import router as auth_router - -driver = nonebot.get_driver() - -gConfig.add_plugin_config("web-ui", "username", "admin", name="web-ui", help_="前端管理用户名") - -gConfig.add_plugin_config("web-ui", "password", None, name="web-ui", help_="前端管理密码") - - -BaseApiRouter = APIRouter(prefix="/zhenxun/api") - - -BaseApiRouter.include_router(auth_router) -BaseApiRouter.include_router(main_router) -BaseApiRouter.include_router(manage_router) -BaseApiRouter.include_router(database_router) -BaseApiRouter.include_router(plugin_router) -BaseApiRouter.include_router(system_router) - - -WsApiRouter = APIRouter(prefix="/zhenxun/socket") - -WsApiRouter.include_router(ws_log_routes) -WsApiRouter.include_router(status_routes) -WsApiRouter.include_router(chat_routes) - - -@driver.on_startup -def _(): - try: - async def log_sink(message: str): - loop = None - if not loop: - try: - loop = asyncio.get_running_loop() - except Exception as e: - logger.warning('Web Ui log_sink', e=e) - if not loop: - loop = asyncio.new_event_loop() - loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) - - logger_.add( - log_sink, colorize=True, filter=default_filter, format=default_format - ) - - app: FastAPI = nonebot.get_app() - app.include_router(BaseApiRouter) - app.include_router(WsApiRouter) - logger.info("API启动成功", "Web UI") - except Exception as e: - logger.error("API启动失败", "Web UI", e=e) diff --git a/plugins/web_ui/api/__init__.py b/plugins/web_ui/api/__init__.py deleted file mode 100644 index 32d31b27..00000000 --- a/plugins/web_ui/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .tabs import * diff --git a/plugins/web_ui/api/logs/__init__.py b/plugins/web_ui/api/logs/__init__.py deleted file mode 100644 index d6684888..00000000 --- a/plugins/web_ui/api/logs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .logs import * diff --git a/plugins/web_ui/api/logs/log_manager.py b/plugins/web_ui/api/logs/log_manager.py deleted file mode 100644 index f375313d..00000000 --- a/plugins/web_ui/api/logs/log_manager.py +++ /dev/null @@ -1,39 +0,0 @@ -import asyncio -import re -from typing import Awaitable, Callable, Dict, Generic, List, Set, TypeVar -from urllib.parse import urlparse - -PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" - -_T = TypeVar("_T") -LogListener = Callable[[_T], Awaitable[None]] - - -class LogStorage(Generic[_T]): - - """ - 日志存储 - """ - - def __init__(self, rotation: float = 5 * 60): - self.count, self.rotation = 0, rotation - self.logs: Dict[int, str] = {} - self.listeners: Set[LogListener[str]] = set() - - async def add(self, log: str): - seq = self.count = self.count + 1 - self.logs[seq] = log - asyncio.get_running_loop().call_later(self.rotation, self.remove, seq) - await asyncio.gather( - *map(lambda listener: listener(log), self.listeners), - return_exceptions=True, - ) - return seq - - def remove(self, seq: int): - del self.logs[seq] - return - - -LOG_STORAGE: LogStorage[str] = LogStorage[str]() - diff --git a/plugins/web_ui/api/logs/logs.py b/plugins/web_ui/api/logs/logs.py deleted file mode 100644 index e6abebf3..00000000 --- a/plugins/web_ui/api/logs/logs.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List - -from fastapi import APIRouter, WebSocket -from loguru import logger -from nonebot.utils import escape_tag -from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState - -from .log_manager import LOG_STORAGE - -router = APIRouter() - - -@router.get("/logs", response_model=List[str]) -async def system_logs_history(reverse: bool = False): - """历史日志 - - 参数: - reverse: 反转顺序. - """ - return LOG_STORAGE.list(reverse=reverse) # type: ignore - - -@router.websocket("/logs") -async def system_logs_realtime(websocket: WebSocket): - await websocket.accept() - - async def log_listener(log: str): - await websocket.send_text(log) - - LOG_STORAGE.listeners.add(log_listener) - try: - while websocket.client_state == WebSocketState.CONNECTED: - recv = await websocket.receive() - logger.trace( - f"{system_logs_realtime.__name__!r} received " - f"{escape_tag(repr(recv))}" - ) - except WebSocketDisconnect: - pass - finally: - LOG_STORAGE.listeners.remove(log_listener) - return diff --git a/plugins/web_ui/api/tabs/__init__.py b/plugins/web_ui/api/tabs/__init__.py deleted file mode 100644 index 99ed6ea1..00000000 --- a/plugins/web_ui/api/tabs/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .database import * -from .main import * -from .manage import * -from .plugin_manage import * -from .system import * diff --git a/plugins/web_ui/api/tabs/database/__init__.py b/plugins/web_ui/api/tabs/database/__init__.py deleted file mode 100644 index f047d711..00000000 --- a/plugins/web_ui/api/tabs/database/__init__.py +++ /dev/null @@ -1,104 +0,0 @@ -from os import name -from typing import Optional - -import nonebot -from fastapi import APIRouter, Request -from nonebot.drivers import Driver -from tortoise import Tortoise -from tortoise.exceptions import OperationalError - -from configs.config import NICKNAME -from services.db_context import TestSQL -from utils.utils import get_matchers - -from ....base_model import BaseResultModel, QueryModel, Result -from ....config import QueryDateType -from ....utils import authentication -from .models.model import SqlModel, SqlText -from .models.sql_log import SqlLog - -router = APIRouter(prefix="/database") - - -driver: Driver = nonebot.get_driver() - - -SQL_DICT = {} - - -SELECT_TABLE_SQL = """ -select a.tablename as name,d.description as desc from pg_tables a - left join pg_class c on relname=tablename - left join pg_description d on oid=objoid and objsubid=0 where a.schemaname = 'public' -""" - -SELECT_TABLE_COLUMN_SQL = """ -SELECT column_name, data_type, character_maximum_length as max_length, is_nullable -FROM information_schema.columns -WHERE table_name = '{}'; -""" - -@driver.on_startup -async def _(): - for matcher in get_matchers(True): - if _plugin := matcher.plugin: - try: - _module = _plugin.module - except AttributeError: - pass - else: - plugin_name = matcher.plugin_name - if plugin_name in SQL_DICT: - raise ValueError(f"{plugin_name} 常用SQL plugin_name 重复") - SqlModel( - name=getattr(_module, "__plugin_name__", None) or plugin_name or "", - plugin_name=plugin_name or "", - sql_list=getattr(_module, "sql_list", []), - ) - SQL_DICT[plugin_name] = SqlModel - -@router.get("/get_table_list", dependencies=[authentication()], description="获取数据库表") -async def _() -> Result: - db = Tortoise.get_connection("default") - query = await db.execute_query_dict(SELECT_TABLE_SQL) - return Result.ok(query) - -@router.get("/get_table_column", dependencies=[authentication()], description="获取表字段") -async def _(table_name: str) -> Result: - db = Tortoise.get_connection("default") - print(SELECT_TABLE_COLUMN_SQL.format(table_name)) - query = await db.execute_query_dict(SELECT_TABLE_COLUMN_SQL.format(table_name)) - return Result.ok(query) - -@router.post("/exec_sql", dependencies=[authentication()], description="执行sql") -async def _(sql: SqlText, request: Request) -> Result: - ip = request.client.host if request.client else "unknown" - try: - if sql.sql.lower().startswith("select"): - db = Tortoise.get_connection("default") - res = await db.execute_query_dict(sql.sql) - await SqlLog.add(ip or "0.0.0.0", sql.sql, "") - return Result.ok(res, "执行成功啦!") - else: - result = await TestSQL.raw(sql.sql) - await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) - return Result.ok(info="执行成功啦!") - except OperationalError as e: - await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) - return Result.warning_(f"sql执行错误: {e}") - - -@router.post("/get_sql_log", dependencies=[authentication()], description="sql日志列表") -async def _(query: QueryModel) -> Result: - total = await SqlLog.all().count() - if (total % query.size): - total += 1 - data = await SqlLog.all().order_by("-id").offset((query.index - 1) * query.size).limit(query.size) - return Result.ok(BaseResultModel(total=total, data=data)) - - -@router.get("/get_common_sql", dependencies=[authentication()], description="常用sql") -async def _(plugin_name: Optional[str] = None) -> Result: - if plugin_name: - return Result.ok(SQL_DICT.get(plugin_name)) - return Result.ok(str(SQL_DICT)) diff --git a/plugins/web_ui/api/tabs/database/models/model.py b/plugins/web_ui/api/tabs/database/models/model.py deleted file mode 100644 index 37c682a6..00000000 --- a/plugins/web_ui/api/tabs/database/models/model.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List - -from pydantic import BaseModel - -from utils.models import CommonSql - - -class SqlText(BaseModel): - """ - sql语句 - """ - - sql: str - - -class SqlModel(BaseModel): - """ - 常用sql - """ - - name: str - """插件中文名称""" - plugin_name: str - """插件名称""" - sql_list: List[CommonSql] - """插件列表""" diff --git a/plugins/web_ui/api/tabs/database/models/sql_log.py b/plugins/web_ui/api/tabs/database/models/sql_log.py deleted file mode 100644 index d58ddd34..00000000 --- a/plugins/web_ui/api/tabs/database/models/sql_log.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Optional, Union - -from tortoise import fields - -from services.db_context import Model - - -class SqlLog(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - ip = fields.CharField(255) - """ip""" - sql = fields.CharField(255) - """sql""" - result = fields.CharField(255, null=True) - """结果""" - is_suc = fields.BooleanField(default=True) - """是否成功""" - create_time = fields.DatetimeField(auto_now_add=True) - """创建时间""" - - class Meta: - table = "sql_log" - table_description = "sql执行日志" - - @classmethod - async def add( - cls, ip: str, sql: str, result: Optional[str] = None, is_suc: bool = True - ): - """ - 说明: - 获取用户在群内的等级 - 参数: - :param ip: ip - :param sql: sql - :param result: 返回结果 - :param is_suc: 是否成功 - """ - await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc) diff --git a/plugins/web_ui/api/tabs/main/__init__.py b/plugins/web_ui/api/tabs/main/__init__.py deleted file mode 100644 index ac892e65..00000000 --- a/plugins/web_ui/api/tabs/main/__init__.py +++ /dev/null @@ -1,266 +0,0 @@ -import asyncio -import time -from datetime import datetime, timedelta -from pathlib import Path -from typing import List, Optional - -import nonebot -from fastapi import APIRouter, WebSocket -from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState -from tortoise.functions import Count -from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK - -from configs.config import NICKNAME -from models.chat_history import ChatHistory -from models.group_info import GroupInfo -from models.statistics import Statistics -from services.log import logger -from utils.manager import plugin_data_manager, plugins2settings_manager, plugins_manager - -from ....base_model import Result -from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType -from ....utils import authentication, get_system_status -from .data_source import bot_live -from .model import ActiveGroup, BaseInfo, ChatHistoryCount, HotPlugin - -run_time = time.time() - -ws_router = APIRouter() -router = APIRouter(prefix="/main") - - - -@router.get("/get_base_info", dependencies=[authentication()], description="基础信息") -async def _(bot_id: Optional[str] = None) -> Result: - """ - 获取Bot基础信息 - - Args: - bot_id (Optional[str], optional): bot_id. Defaults to None. - - Returns: - Result: 获取指定bot信息与bot列表 - """ - bot_list: List[BaseInfo] = [] - if bots := nonebot.get_bots(): - select_bot: BaseInfo - for key, bot in bots.items(): - login_info = await bot.get_login_info() - bot_list.append( - BaseInfo( - bot=bot, # type: ignore - self_id=bot.self_id, - nickname=login_info["nickname"], - ava_url=AVA_URL.format(bot.self_id), - ) - ) - # 获取指定qq号的bot信息,若无指定 则获取第一个 - if _bl := [b for b in bot_list if b.self_id == bot_id]: - select_bot = _bl[0] - else: - select_bot = bot_list[0] - select_bot.is_select = True - select_bot.config = select_bot.bot.config - now = datetime.now() - # 今日累计接收消息 - select_bot.received_messages = await ChatHistory.filter( - bot_id=select_bot.self_id, - create_time__gte=now - timedelta(hours=now.hour), - ).count() - # 群聊数量 - select_bot.group_count = len(await select_bot.bot.get_group_list()) - # 好友数量 - select_bot.friend_count = len(await select_bot.bot.get_friend_list()) - for bot in bot_list: - bot.bot = None # type: ignore - # 插件加载数量 - select_bot.plugin_count = len(plugins2settings_manager) - pm_data = plugins_manager.get_data() - select_bot.fail_plugin_count = len([pd for pd in pm_data if pm_data[pd].error]) - select_bot.success_plugin_count = ( - select_bot.plugin_count - select_bot.fail_plugin_count - ) - # 连接时间 - select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 - if select_bot.connect_time: - connect_date = datetime.fromtimestamp(select_bot.connect_time) - select_bot.connect_date = connect_date.strftime("%Y-%m-%d %H:%M:%S") - version_file = Path() / "__version__" - if version_file.exists(): - if text := version_file.open().read(): - if ver := text.replace("__version__: ", "").strip(): - select_bot.version = ver - day_call = await Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)).count() - select_bot.day_call = day_call - return Result.ok(bot_list, "拿到信息啦!") - return Result.warning_("无Bot连接...") - - -@router.get( - "/get_all_ch_count", dependencies=[authentication()], description="获取接收消息数量" -) -async def _(bot_id: str) -> Result: - now = datetime.now() - all_count = await ChatHistory.filter(bot_id=bot_id).count() - day_count = await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) - ).count() - week_count = await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=7) - ).count() - month_count = await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=30) - ).count() - year_count = await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=365) - ).count() - return Result.ok( - ChatHistoryCount( - num=all_count, - day=day_count, - week=week_count, - month=month_count, - year=year_count, - ) - ) - - -@router.get("/get_ch_count", dependencies=[authentication()], description="获取接收消息数量") -async def _(bot_id: str, query_type: Optional[QueryDateType] = None) -> Result: - if bots := nonebot.get_bots(): - if not query_type: - return Result.ok(await ChatHistory.filter(bot_id=bot_id).count()) - now = datetime.now() - if query_type == QueryDateType.DAY: - return Result.ok( - await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) - ).count() - ) - if query_type == QueryDateType.WEEK: - return Result.ok( - await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=7) - ).count() - ) - if query_type == QueryDateType.MONTH: - return Result.ok( - await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=30) - ).count() - ) - if query_type == QueryDateType.YEAR: - return Result.ok( - await ChatHistory.filter( - bot_id=bot_id, create_time__gte=now - timedelta(days=365) - ).count() - ) - return Result.warning_("无Bot连接...") - - -@router.get("get_fg_count", dependencies=[authentication()], description="好友/群组数量") -async def _(bot_id: str) -> Result: - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - bot = bots[bot_id] - data = { - "friend_count": len(await bot.get_friend_list()), - "group_count": len(await bot.get_group_list()), - } - return Result.ok(data) - return Result.warning_("无Bot连接...") - - -@router.get("/get_run_time", dependencies=[authentication()], description="获取nb运行时间") -async def _() -> Result: - return Result.ok(int(time.time() - run_time)) - - -@router.get("/get_active_group", dependencies=[authentication()], description="获取活跃群聊") -async def _(date_type: Optional[QueryDateType] = None) -> Result: - query = ChatHistory - now = datetime.now() - if date_type == QueryDateType.DAY: - query = ChatHistory.filter(create_time__gte=now - timedelta(hours=now.hour)) - if date_type == QueryDateType.WEEK: - query = ChatHistory.filter(create_time__gte=now - timedelta(days=7)) - if date_type == QueryDateType.MONTH: - query = ChatHistory.filter(create_time__gte=now - timedelta(days=30)) - if date_type == QueryDateType.YEAR: - query = ChatHistory.filter(create_time__gte=now - timedelta(days=365)) - data_list = ( - await query.annotate(count=Count("id")).filter(group_id__not_isnull=True) - .group_by("group_id").order_by("-count").limit(5) - .values_list("group_id", "count") - ) - active_group_list = [] - id2name = {} - if data_list: - if info_list := await GroupInfo.filter(group_id__in=[x[0] for x in data_list]).all(): - for group_info in info_list: - id2name[group_info.group_id] = group_info.group_name - for data in data_list: - active_group_list.append( - ActiveGroup( - group_id=data[0], - name=id2name.get(data[0]) or data[0], - chat_num=data[1], - ava_img=GROUP_AVA_URL.format(data[0], data[0]), - ) - ) - active_group_list = sorted( - active_group_list, key=lambda x: x.chat_num, reverse=True - ) - if len(active_group_list) > 5: - active_group_list = active_group_list[:5] - return Result.ok(active_group_list) - - -@router.get("/get_hot_plugin", dependencies=[authentication()], description="获取热门插件") -async def _(date_type: Optional[QueryDateType] = None) -> Result: - query = Statistics - now = datetime.now() - if date_type == QueryDateType.DAY: - query = Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)) - if date_type == QueryDateType.WEEK: - query = Statistics.filter(create_time__gte=now - timedelta(days=7)) - if date_type == QueryDateType.MONTH: - query = Statistics.filter(create_time__gte=now - timedelta(days=30)) - if date_type == QueryDateType.YEAR: - query = Statistics.filter(create_time__gte=now - timedelta(days=365)) - data_list = ( - await query.annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) - .values_list("plugin_name", "count") - ) - hot_plugin_list = [] - for data in data_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name - hot_plugin_list.append( - HotPlugin( - module=data[0], - name=name, - count=data[1], - ) - ) - hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) - if len(hot_plugin_list) > 5: - hot_plugin_list = hot_plugin_list[:5] - return Result.ok(hot_plugin_list) - - -@ws_router.websocket("/system_status") -async def system_logs_realtime(websocket: WebSocket, sleep: Optional[int] = 5): - await websocket.accept() - logger.debug("ws system_status is connect") - try: - while websocket.client_state == WebSocketState.CONNECTED: - system_status = await get_system_status() - await websocket.send_text(system_status.json()) - await asyncio.sleep(sleep) - except (WebSocketDisconnect, ConnectionClosedError, ConnectionClosedOK): - pass - return diff --git a/plugins/web_ui/api/tabs/main/data_source.py b/plugins/web_ui/api/tabs/main/data_source.py deleted file mode 100644 index 42a3df42..00000000 --- a/plugins/web_ui/api/tabs/main/data_source.py +++ /dev/null @@ -1,36 +0,0 @@ -import time -from typing import Optional - -import nonebot -from nonebot import Driver -from nonebot.adapters.onebot.v11 import Bot - -driver: Driver = nonebot.get_driver() - - -class BotLive: - def __init__(self): - self._data = {} - - def add(self, bot_id: str): - self._data[bot_id] = time.time() - - def get(self, bot_id: str) -> Optional[int]: - return self._data.get(bot_id) - - def remove(self, bot_id: str): - if bot_id in self._data: - del self._data[bot_id] - - -bot_live = BotLive() - - -@driver.on_bot_connect -async def _(bot: Bot): - bot_live.add(bot.self_id) - - -@driver.on_bot_disconnect -async def _(bot: Bot): - bot_live.remove(bot.self_id) diff --git a/plugins/web_ui/api/tabs/main/model.py b/plugins/web_ui/api/tabs/main/model.py deleted file mode 100644 index 7dd770c6..00000000 --- a/plugins/web_ui/api/tabs/main/model.py +++ /dev/null @@ -1,107 +0,0 @@ -from datetime import datetime -from typing import Optional, Union - -from nonebot.adapters.onebot.v11 import Bot -from nonebot.config import Config -from pydantic import BaseModel - - -class SystemStatus(BaseModel): - """ - 系统状态 - """ - - cpu: float - memory: float - disk: float - - -class BaseInfo(BaseModel): - """ - 基础信息 - """ - - bot: Bot - """Bot""" - self_id: str - """SELF ID""" - nickname: str - """昵称""" - ava_url: str - """头像url""" - friend_count: int = 0 - """好友数量""" - group_count: int = 0 - """群聊数量""" - received_messages: int = 0 - """今日 累计接收消息""" - connect_time: int = 0 - """连接时间""" - connect_date: Optional[datetime] = None - """连接日期""" - - plugin_count: int = 0 - """加载插件数量""" - success_plugin_count: int = 0 - """加载成功插件数量""" - fail_plugin_count: int = 0 - """加载失败插件数量""" - - is_select: bool = False - """当前选择""" - - config: Optional[Config] = None - """nb配置""" - day_call: int = 0 - """今日调用插件次数""" - version: str = "unknown" - """真寻版本""" - - - class Config: - arbitrary_types_allowed = True - - -class ChatHistoryCount(BaseModel): - """ - 聊天记录数量 - """ - - num: int - """总数""" - day: int - """一天内""" - week: int - """一周内""" - month: int - """一月内""" - year: int - """一年内""" - - -class ActiveGroup(BaseModel): - """ - 活跃群聊数据 - """ - - group_id: Union[str, int] - """群组id""" - name: str - """群组名称""" - chat_num: int - """发言数量""" - ava_img: str - """群组头像""" - - -class HotPlugin(BaseModel): - """ - 热门插件 - """ - - module: str - """模块名""" - name: str - """插件名称""" - count: int - """调用次数""" diff --git a/plugins/web_ui/api/tabs/manage/__init__.py b/plugins/web_ui/api/tabs/manage/__init__.py deleted file mode 100644 index 3bfdf9d5..00000000 --- a/plugins/web_ui/api/tabs/manage/__init__.py +++ /dev/null @@ -1,462 +0,0 @@ -import re -from typing import Literal, Optional - -import nonebot -from fastapi import APIRouter -from nonebot.adapters.onebot.v11.exception import ActionFailed -from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState -from tortoise.functions import Count -from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK - -from configs.config import NICKNAME -from models.ban_user import BanUser -from models.chat_history import ChatHistory -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from models.group_member_info import GroupInfoUser -from models.statistics import Statistics -from services.log import logger -from utils.manager import group_manager, plugin_data_manager, requests_manager -from utils.utils import get_bot - -from ....base_model import Result -from ....config import AVA_URL, GROUP_AVA_URL -from ....utils import authentication -from ...logs.log_manager import LOG_STORAGE -from .model import ( - DeleteFriend, - Friend, - FriendRequestResult, - GroupDetail, - GroupRequestResult, - GroupResult, - HandleRequest, - LeaveGroup, - Message, - MessageItem, - Plugin, - ReqResult, - SendMessage, - Task, - UpdateGroup, - UserDetail, -) - -ws_router = APIRouter() -router = APIRouter(prefix="/manage") - -SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" - -GROUP_PATTERN = r'.*?Message (-?\d*) from (\d*)@\[群:(\d*)] "(.*)"' - -PRIVATE_PATTERN = r'.*?Message (-?\d*) from (\d*) "(.*)"' - -AT_PATTERN = r'\[CQ:at,qq=(.*)\]' - -IMAGE_PATTERN = r'\[CQ:image,.*,url=(.*);.*?\]' - -@router.get("/get_group_list", dependencies=[authentication()], description="获取群组列表") -async def _(bot_id: str) -> Result: - """ - 获取群信息 - """ - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - group_list_result = [] - try: - group_info = {} - group_list = await bots[bot_id].get_group_list() - for g in group_list: - gid = g['group_id'] - g['ava_url'] = GROUP_AVA_URL.format(gid, gid) - group_list_result.append(GroupResult(**g)) - except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") - return Result.warning_("无Bot连接...") - - -@router.post("/update_group", dependencies=[authentication()], description="修改群组信息") -async def _(group: UpdateGroup) -> Result: - try: - group_id = group.group_id - group_manager.set_group_level(group_id, group.level) - if group.status: - group_manager.turn_on_group_bot_status(group_id) - else: - group_manager.shutdown_group_bot_status(group_id) - all_task = group_manager.get_task_data().keys() - if group.task: - for task in all_task: - if task in group.task: - group_manager.open_group_task(group_id, task) - else: - group_manager.close_group_task(group_id, task) - group_manager[group_id].close_plugins = group.close_plugins - group_manager.save() - except Exception as e: - logger.error("调用API错误", "/get_group", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已完成记录!") - - -@router.get("/get_friend_list", dependencies=[authentication()], description="获取好友列表") -async def _(bot_id: str) -> Result: - """ - 获取群信息 - """ - if bots := nonebot.get_bots(): - if bot_id not in bots: - return Result.warning_("指定Bot未连接...") - try: - friend_list = await bots[bot_id].get_friend_list() - for f in friend_list: - f['ava_url'] = AVA_URL.format(f['user_id']) - return Result.ok([Friend(**f) for f in friend_list if str(f['user_id']) != bot_id], "拿到了新鲜出炉的数据!") - except Exception as e: - logger.error("调用API错误", "/get_group_list", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.warning_("无Bot连接...") - - -@router.get("/get_request_count", dependencies=[authentication()], description="获取请求数量") -def _() -> Result: - data = { - "friend_count": len(requests_manager.get_data().get("private") or []), - "group_count": len(requests_manager.get_data().get("group") or []), - } - return Result.ok(data, f"{NICKNAME}带来了最新的数据!") - - -@router.get("/get_request_list", dependencies=[authentication()], description="获取请求列表") -def _() -> Result: - try: - req_result = ReqResult() - data = requests_manager.get_data() - for type_ in requests_manager.get_data(): - for x in data[type_]: - data[type_][x]["oid"] = x - data[type_][x]['type'] = type_ - if type_ == "private": - data[type_][x]['ava_url'] = AVA_URL.format(data[type_][x]['id']) - req_result.friend.append(FriendRequestResult(**data[type_][x])) - else: - gid = data[type_][x]['id'] - data[type_][x]['ava_url'] = GROUP_AVA_URL.format(gid, gid) - req_result.group.append(GroupRequestResult(**data[type_][x])) - req_result.friend.reverse() - req_result.group.reverse() - except Exception as e: - logger.error("调用API错误", "/get_request", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!") - - -@router.delete("/clear_request", dependencies=[authentication()], description="清空请求列表") -def _(request_type: Literal["private", "group"]) -> Result: - """ - 清空请求 - :param type_: 类型 - """ - requests_manager.clear(request_type) - return Result.ok(info="成功清除了数据!") - - -@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - try: - flag = await requests_manager.refused(bots[bot_id], parma.flag, parma.request_type) # type: ignore - except ActionFailed as e: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - if flag == 1: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("该请求已失效...") - elif flag == 2: - return Result.warning_("未找到此Id请求...") - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/refuse_request", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/delete_request", dependencies=[authentication()], description="忽略请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.ok(info="成功处理了请求!") - - -@router.post("/approve_request", dependencies=[authentication()], description="同意请求") -async def _(parma: HandleRequest) -> Result: - """ - 操作请求 - :param parma: 参数 - """ - try: - if bots := nonebot.get_bots(): - bot_id = parma.bot_id - if bot_id not in nonebot.get_bots(): - return Result.warning_("指定Bot未连接...") - if parma.request_type == "group": - if rid := requests_manager.get_group_id(parma.flag): - if group := await GroupInfo.get_or_none(group_id=str(rid)): - await group.update_or_create(group_flag=1) - else: - group_info = await bots[bot_id].get_group_info(group_id=rid) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - try: - await requests_manager.approve(bots[bot_id], parma.flag, parma.request_type) # type: ignore - return Result.ok(info="成功处理了请求!") - except ActionFailed as e: - requests_manager.delete_request(parma.flag, parma.request_type) - return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/approve_request", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/leave_group", dependencies=[authentication()], description="退群") -async def _(param: LeaveGroup) -> Result: - try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - group_list = await bots[bot_id].get_group_list() - if param.group_id not in [str(g["group_id"]) for g in group_list]: - return Result.warning_("Bot未在该群聊中...") - await bots[bot_id].set_group_leave(group_id=param.group_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("无Bot连接...") - except Exception as e: - logger.error("调用API错误", "/leave_group", e=e) - return Result.fail(f"{type(e)}: {e}") - - -@router.post("/delete_friend", dependencies=[authentication()], description="删除好友") -async def _(param: DeleteFriend) -> Result: - try: - if bots := nonebot.get_bots(): - bot_id = param.bot_id - friend_list = await bots[bot_id].get_friend_list() - if param.user_id not in [str(g["user_id"]) for g in friend_list]: - return Result.warning_("Bot未有其好友...") - await bots[bot_id].delete_friend(user_id=param.user_id) - return Result.ok(info="成功处理了请求!") - return Result.warning_("Bot未连接...") - except Exception as e: - logger.error("调用API错误", "/delete_friend", e=e) - return Result.fail(f"{type(e)}: {e}") - - - -@router.get("/get_friend_detail", dependencies=[authentication()], description="获取好友详情") -async def _(bot_id: str, user_id: str) -> Result: - if bots := nonebot.get_bots(): - if bot_id in bots: - if fd := [x for x in await bots[bot_id].get_friend_list() if str(x['user_id']) == user_id]: - like_plugin_list = ( - await Statistics.filter(user_id=user_id).annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - for data in like_plugin_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name - like_plugin[name] = data[1] - user = fd[0] - user_detail = UserDetail( - user_id=user_id, - ava_url=AVA_URL.format(user_id), - nickname=user['nickname'], - remark=user['remark'], - is_ban=await BanUser.is_ban(user_id), - chat_count=await ChatHistory.filter(user_id=user_id).count(), - call_count=await Statistics.filter(user_id=user_id).count(), - like_plugin=like_plugin, - ) - return Result.ok(user_detail) - else: - return Result.warning_("未添加指定好友...") - return Result.warning_("无Bot连接...") - - -@router.get("/get_group_detail", dependencies=[authentication()], description="获取群组详情") -async def _(bot_id: str, group_id: str) -> Result: - if bots := nonebot.get_bots(): - if bot_id in bots: - group_info = await bots[bot_id].get_group_info(group_id=int(group_id)) - g = group_manager[group_id] - if not g: - return Result.warning_("指定群组未被收录...") - if group_info: - like_plugin_list = ( - await Statistics.filter(group_id=group_id).annotate(count=Count("id")) - .group_by("plugin_name").order_by("-count").limit(5) - .values_list("plugin_name", "count") - ) - like_plugin = {} - for data in like_plugin_list: - name = data[0] - if plugin_data := plugin_data_manager.get(data[0]): - name = plugin_data.name - like_plugin[name] = data[1] - close_plugins = [] - for module in g.close_plugins: - module_ = module.replace(":super", "") - is_super_block = module.endswith(":super") - plugin = Plugin(module=module_, plugin_name=module, is_super_block=is_super_block) - if plugin_data := plugin_data_manager.get(module_): - plugin.plugin_name = plugin_data.name - close_plugins.append(plugin) - task_list = [] - task_data = group_manager.get_task_data() - for tn, status in g.group_task_status.items(): - task_list.append( - Task( - name=tn, - zh_name=task_data.get(tn) or tn, - status=status - ) - ) - group_detail = GroupDetail( - group_id=group_id, - ava_url=GROUP_AVA_URL.format(group_id, group_id), - name=group_info['group_name'], - member_count=group_info['member_count'], - max_member_count=group_info['max_member_count'], - chat_count=await ChatHistory.filter(group_id=group_id).count(), - call_count=await Statistics.filter(group_id=group_id).count(), - like_plugin=like_plugin, - level=g.level, - status=g.status, - close_plugins=close_plugins, - task=task_list - ) - return Result.ok(group_detail) - else: - return Result.warning_("未添加指定群组...") - return Result.warning_("无Bot连接...") - - -@router.post("/send_message", dependencies=[authentication()], description="获取群组详情") -async def _(param: SendMessage) -> Result: - if bots := nonebot.get_bots(): - if param.bot_id in bots: - try: - if param.user_id: - await bots[param.bot_id].send_private_msg(user_id=str(param.user_id), message=param.message) - else: - await bots[param.bot_id].send_group_msg(group_id=str(param.group_id), message=param.message) - except Exception as e: - return Result.fail(str(e)) - return Result.ok("发送成功!") - return Result.warning_("指定Bot未连接...") - return Result.warning_("无Bot连接...") - -MSG_LIST = [] - -ID2NAME = {} - - -async def message_handle(sub_log: str, type: Literal["private", "group"]): - global MSG_LIST, ID2NAME - pattern = PRIVATE_PATTERN if type == 'private' else GROUP_PATTERN - msg_id = None - uid = None - gid = None - msg = None - img_list = re.findall(IMAGE_PATTERN, sub_log) - if r := re.search(pattern, sub_log): - if type == 'private': - msg_id = r.group(1) - uid = r.group(2) - msg = r.group(3) - if uid not in ID2NAME: - user = await FriendUser.filter(user_id=uid).first() - ID2NAME[uid] = user.user_name or user.nickname - else: - msg_id = r.group(1) - uid = r.group(2) - gid = r.group(3) - msg = r.group(4) - if gid not in ID2NAME: - user = await GroupInfoUser.filter(user_id=uid, group_id=gid).first() - ID2NAME[uid] = user.user_name or user.nickname - if at_list := re.findall(AT_PATTERN, msg): - user_list = await GroupInfoUser.filter(user_id__in=at_list, group_id=gid).all() - id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list} - for qq in at_list: - msg = re.sub(rf'\[CQ:at,qq={qq}\]', f"@{id2name[qq] or ''}", msg) - if msg_id in MSG_LIST: - return - MSG_LIST.append(msg_id) - messages = [] - rep = re.split(r'\[CQ:image.*\]', msg) - if img_list: - for i in range(len(rep)): - messages.append(MessageItem(type="text", msg=rep[i])) - if i < len(img_list): - messages.append(MessageItem(type="img", msg=img_list[i])) - else: - messages = [MessageItem(type="text", msg=x) for x in rep] - return Message( - object_id=uid if type == 'private' else gid, - user_id=uid, - group_id=gid, - message=messages, - name=ID2NAME.get(uid) or "", - ava_url=AVA_URL.format(uid), - ) - -@ws_router.websocket("/chat") -async def _(websocket: WebSocket): - await websocket.accept() - - async def log_listener(log: str): - global MSG_LIST, ID2NAME - sub_log = re.sub(SUB_PATTERN, "", log) - img_list = re.findall(IMAGE_PATTERN, sub_log) - if "message.private.friend" in log: - if message := await message_handle(sub_log, 'private'): - await websocket.send_json(message.dict()) - else: - if r := re.search(GROUP_PATTERN, sub_log): - if message := await message_handle(sub_log, 'group'): - await websocket.send_json(message.dict()) - if len(MSG_LIST) > 30: - MSG_LIST = MSG_LIST[-1:] - LOG_STORAGE.listeners.add(log_listener) - try: - while websocket.client_state == WebSocketState.CONNECTED: - recv = await websocket.receive() - except WebSocketDisconnect: - pass - finally: - LOG_STORAGE.listeners.remove(log_listener) - return \ No newline at end of file diff --git a/plugins/web_ui/api/tabs/manage/model.py b/plugins/web_ui/api/tabs/manage/model.py deleted file mode 100644 index ab8b83f1..00000000 --- a/plugins/web_ui/api/tabs/manage/model.py +++ /dev/null @@ -1,270 +0,0 @@ -from typing import Dict, List, Literal, Optional, Union - -from matplotlib.dates import FR -from nonebot.adapters.onebot.v11 import Bot -from pydantic import BaseModel - - -class Group(BaseModel): - """ - 群组信息 - """ - - group_id: Union[str, int] - """群组id""" - group_name: str - """群组名称""" - member_count: int - """成员人数""" - max_member_count: int - """群组最大人数""" - - -class Task(BaseModel): - """ - 被动技能 - """ - - name: str - """被动名称""" - zh_name: str - """被动中文名称""" - status: bool - """状态""" - -class Plugin(BaseModel): - """ - 插件 - """ - - module: str - """模块名""" - plugin_name: str - """中文名""" - is_super_block: bool - """是否超级用户禁用""" - - -class GroupResult(BaseModel): - """ - 群组返回数据 - """ - - group_id: Union[str, int] - """群组id""" - group_name: str - """群组名称""" - ava_url: str - """群组头像""" - - -class Friend(BaseModel): - """ - 好友数据 - """ - - user_id: Union[str, int] - """用户id""" - nickname: str = "" - """昵称""" - remark: str = "" - """备注""" - ava_url: str = "" - """头像url""" - -class UpdateGroup(BaseModel): - """ - 更新群组信息 - """ - - group_id: str - """群号""" - status: bool - """状态""" - level: int - """群权限""" - task: List[str] - """被动状态""" - close_plugins: List[str] - """关闭插件""" - - -class FriendRequestResult(BaseModel): - """ - 好友/群组请求管理 - """ - - bot_id: Union[str, int] - """bot_id""" - oid: str - """排序""" - id: int - """id""" - flag: str - """flag""" - nickname: Optional[str] - """昵称""" - level: Optional[int] - """等级""" - sex: Optional[str] - """性别""" - age: Optional[int] - """年龄""" - from_: Optional[str] - """来自""" - comment: Optional[str] - """备注信息""" - ava_url: str - """头像""" - type: str - """类型 private group""" - - -class GroupRequestResult(FriendRequestResult): - """ - 群聊邀请请求 - """ - - invite_group: Union[int, str] - """邀请群聊""" - group_name: Optional[str] - """群聊名称""" - - -class HandleRequest(BaseModel): - """ - 操作请求接收数据 - """ - - bot_id: Optional[str] = None - """bot_id""" - flag: str - """flag""" - request_type: Literal["private", "group"] - """类型""" - - -class LeaveGroup(BaseModel): - """ - 退出群聊 - """ - - bot_id: str - """bot_id""" - group_id: str - """群聊id""" - - -class DeleteFriend(BaseModel): - """ - 删除好友 - """ - - bot_id: str - """bot_id""" - user_id: str - """用户id""" - -class ReqResult(BaseModel): - """ - 好友/群组请求列表 - """ - - friend: List[FriendRequestResult] = [] - """好友请求列表""" - group: List[GroupRequestResult] = [] - """群组请求列表""" - - -class UserDetail(BaseModel): - """ - 用户详情 - """ - - user_id: str - """用户id""" - ava_url: str - """头像url""" - nickname: str - """昵称""" - remark: str - """备注""" - is_ban: bool - """是否被ban""" - chat_count: int - """发言次数""" - call_count: int - """功能调用次数""" - like_plugin: Dict[str, int] - """最喜爱的功能""" - - -class GroupDetail(BaseModel): - """ - 用户详情 - """ - - group_id: str - """群组id""" - ava_url: str - """头像url""" - name: str - """名称""" - member_count: int - """成员数""" - max_member_count: int - """最大成员数""" - chat_count: int - """发言次数""" - call_count: int - """功能调用次数""" - like_plugin: Dict[str, int] - """最喜爱的功能""" - level: int - """群权限""" - status: bool - """状态(睡眠)""" - close_plugins: List[Plugin] - """关闭的插件""" - task: List[Task] - """被动列表""" - -class MessageItem(BaseModel): - - type: str - """消息类型""" - msg: str - """内容""" - -class Message(BaseModel): - """ - 消息 - """ - - object_id: str - """主体id user_id 或 group_id""" - user_id: str - """用户id""" - group_id: Optional[str] = None - """群组id""" - message: List[MessageItem] - """消息""" - name: str - """用户名称""" - ava_url: str - """用户头像""" - - - -class SendMessage(BaseModel): - """ - 发送消息 - """ - bot_id: str - """bot id""" - user_id: Optional[str] = None - """用户id""" - group_id: Optional[str] = None - """群组id""" - message: str - """消息""" diff --git a/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/plugins/web_ui/api/tabs/plugin_manage/__init__.py deleted file mode 100644 index 20e5fd97..00000000 --- a/plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ /dev/null @@ -1,198 +0,0 @@ -import re -from typing import List, Optional - -import cattrs -from fastapi import APIRouter, Query - -from configs.config import Config -from services.log import logger -from utils.manager import plugin_data_manager, plugins2settings_manager, plugins_manager -from utils.manager.models import PluginData, PluginSetting, PluginType - -from ....base_model import Result -from ....utils import authentication -from .model import ( - PluginConfig, - PluginCount, - PluginDetail, - PluginInfo, - PluginSwitch, - UpdatePlugin, -) - -router = APIRouter(prefix="/plugin") - - -@router.get("/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表") -def _( - plugin_type: List[PluginType] = Query(None), menu_type: Optional[str] = None -) -> Result: - """ - 获取插件列表 - :param plugin_type: 类型 normal, superuser, hidden, admin - :param menu_type: 菜单类型 - """ - try: - plugin_list: List[PluginInfo] = [] - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data and plugin_data.plugin_type in plugin_type: - setting = plugin_data.plugin_setting or PluginSetting() - plugin = plugin_data.plugin_status - menu_type_ = getattr(setting, "plugin_type", ["无"])[0] - if menu_type and menu_type != menu_type_: - continue - plugin_info = PluginInfo( - module=module, - plugin_name=plugin_data.name, - default_status=getattr(setting, "default_status", False), - limit_superuser=getattr(setting, "limit_superuser", False), - cost_gold=getattr(setting, "cost_gold", 0), - menu_type=menu_type_, - version=(plugin.version or 0) if plugin else 0, - level=getattr(setting, "level", 5), - status=plugin.status if plugin else False, - author=plugin.author if plugin else None - ) - plugin_info.version = (plugin.version or 0) if plugin else 0 - plugin_list.append(plugin_info) - except Exception as e: - logger.error("调用API错误", "/get_plugins", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") - -@router.get("/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量") -def _() -> Result: - plugin_count = PluginCount() - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data and plugin_data.plugin_type == PluginType.NORMAL: - plugin_count.normal += 1 - elif plugin_data and plugin_data.plugin_type == PluginType.ADMIN: - plugin_count.admin += 1 - elif plugin_data and plugin_data.plugin_type == PluginType.SUPERUSER: - plugin_count.superuser += 1 - else: - plugin_count.other += 1 - return Result.ok(plugin_count) - -@router.post("/update_plugin", dependencies=[authentication()], description="更新插件参数") -def _(plugin: UpdatePlugin) -> Result: - """ - 修改插件信息 - :param plugin: 插件内容 - """ - try: - module = plugin.module - if p2s := plugins2settings_manager.get(module): - p2s.default_status = plugin.default_status - p2s.limit_superuser = plugin.limit_superuser - p2s.cost_gold = plugin.cost_gold - # p2s.cmd = plugin.cmd.split(",") if plugin.cmd else [] - p2s.level = plugin.level - menu_lin = None - if len(p2s.plugin_type) > 1: - menu_lin = p2s.menu_type[1] - if menu_lin is not None: - p2s.plugin_type = (plugin.menu_type, menu_lin) - else: - p2s.plugin_type = (plugin.menu_type,) - if pm := plugins_manager.get(module): - if plugin.block_type: - pm.block_type = plugin.block_type - pm.status = False - else: - pm.block_type = None - pm.status = True - plugins2settings_manager.save() - plugins_manager.save() - # 配置项 - if plugin.configs and (configs := Config.get(module)): - for key in plugin.configs: - if c := configs.configs.get(key): - value = plugin.configs[key] - # if isinstance(c.value, (list, tuple)) or isinstance( - # c.default_value, (list, tuple) - # ): - # value = value.split(",") - if c.type and value is not None: - value = cattrs.structure(value, c.type) - Config.set_config(module, key, value) - plugin_data_manager.reload() - except Exception as e: - logger.error("调用API错误", "/update_plugins", e=e) - return Result.fail(f"{type(e)}: {e}") - return Result.ok(info="已经帮你写好啦!") - - -@router.post("/change_switch", dependencies=[authentication()], description="开关插件") -def _(param: PluginSwitch) -> Result: - if pm := plugins_manager.get(param.module): - pm.block_type = None if param.status else 'all' - pm.status = param.status - plugins_manager.save() - return Result.ok(info="成功改变了开关状态!") - return Result.warning_("未获取该插件的配置!") - - -@router.get("/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型") -def _() -> Result: - menu_type_list = [] - for module in plugin_data_manager.keys(): - plugin_data: Optional[PluginData] = plugin_data_manager[module] - if plugin_data: - setting = plugin_data.plugin_setting or PluginSetting() - menu_type = getattr(setting, "plugin_type", ["无"])[0] - if menu_type not in menu_type_list: - menu_type_list.append(menu_type) - return Result.ok(menu_type_list) - - -@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") -def _(module: str) -> Result: - if plugin_data := plugin_data_manager.get(module): - setting = plugin_data.plugin_setting or PluginSetting() - plugin = plugin_data.plugin_status - config_list = [] - if config := Config.get(module): - for cfg in config.configs: - type_str = "" - type_inner = None - x = str(config.configs[cfg].type) - r = re.search(r"",str(config.configs[cfg].type)) - if r: - type_str = r.group(1) - else: - r = re.search(r"typing\.(.*)\[(.*)\]",str(config.configs[cfg].type)) - if r: - type_str = r.group(1) - if type_str: - type_str = type_str.lower() - type_inner = r.group(2) - if type_inner: - type_inner = [x.strip() for x in type_inner.split(",")] - config_list.append(PluginConfig( - module=module, - key=cfg, - value=config.configs[cfg].value, - help=config.configs[cfg].help, - default_value=config.configs[cfg].default_value, - type=type_str, - type_inner=type_inner - )) - plugin_info = PluginDetail( - module=module, - plugin_name=plugin_data.name, - default_status=getattr(setting, "default_status", False), - limit_superuser=getattr(setting, "limit_superuser", False), - cost_gold=getattr(setting, "cost_gold", 0), - menu_type=getattr(setting, "plugin_type", ["无"])[0], - version=(plugin.version or 0) if plugin else 0, - level=getattr(setting, "level", 5), - status=plugin.status if plugin else False, - author=plugin.author if plugin else None, - config_list=config_list, - block_type=getattr(plugin, "block_type", None) - ) - return Result.ok(plugin_info) - return Result.warning_("未获取到插件详情...") \ No newline at end of file diff --git a/plugins/web_ui/api/tabs/plugin_manage/model.py b/plugins/web_ui/api/tabs/plugin_manage/model.py deleted file mode 100644 index 31c1ae9a..00000000 --- a/plugins/web_ui/api/tabs/plugin_manage/model.py +++ /dev/null @@ -1,148 +0,0 @@ -from typing import Any, Dict, List, Optional, Union - -from pydantic import BaseModel - -from utils.manager.models import Plugin as PluginManager -from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting -from utils.typing import BLOCK_TYPE - - -class PluginSwitch(BaseModel): - """ - 插件开关 - """ - - module: str - """模块""" - status: bool - """开关状态""" - - -class UpdateConfig(BaseModel): - """ - 配置项修改参数 - """ - - module: str - """模块""" - key: str - """配置项key""" - value: Any - """配置项值""" - - -class UpdatePlugin(BaseModel): - """ - 插件修改参数 - """ - - module: str - """模块""" - default_status: bool - """默认开关""" - limit_superuser: bool - """限制超级用户""" - cost_gold: int - """金币花费""" - menu_type: str - """插件菜单类型""" - level: int - """插件所需群权限""" - block_type: Optional[BLOCK_TYPE] = None - """禁用类型""" - configs: Optional[Dict[str, Any]] = None - """配置项""" - - -class PluginInfo(BaseModel): - """ - 基本插件信息 - """ - - module: str - """插件名称""" - plugin_name: str - """插件中文名称""" - default_status: bool - """默认开关""" - limit_superuser: bool - """限制超级用户""" - cost_gold: int - """花费金币""" - menu_type: str - """插件菜单类型""" - version: Union[int, str, float] - """插件版本""" - level: int - """群权限""" - status: bool - """当前状态""" - author: Optional[str] = None - """作者""" - block_type: BLOCK_TYPE = None - """禁用类型""" - - -class PluginConfig(BaseModel): - """ - 插件配置项 - """ - - module: str - """模块""" - key: str - """键""" - value: Any - """值""" - help: Optional[str] = None - """帮助""" - default_value: Any - """默认值""" - type: Optional[Any] = None - """值类型""" - type_inner: Optional[List[str]] = None - """List Tuple等内部类型检验""" - - - -class Plugin(BaseModel): - """ - 插件 - """ - - module: str - """模块名称""" - plugin_settings: Optional[PluginSetting] - """settings""" - plugin_manager: Optional[PluginManager] - """manager""" - plugin_config: Optional[Dict[str, PluginConfig]] - """配置项""" - cd_limit: Optional[PluginCd] - """cd限制""" - block_limit: Optional[PluginBlock] - """阻断限制""" - count_limit: Optional[PluginCount] - """次数限制""" - -class PluginCount(BaseModel): - """ - 插件数量 - """ - - normal: int = 0 - """普通插件""" - admin: int = 0 - """管理员插件""" - superuser: int = 0 - """超级用户插件""" - other: int = 0 - """其他插件""" - - -class PluginDetail(PluginInfo): - """ - 插件详情 - """ - - config_list: List[PluginConfig] \ No newline at end of file diff --git a/plugins/web_ui/api/tabs/system/__init__.py b/plugins/web_ui/api/tabs/system/__init__.py deleted file mode 100644 index 61430144..00000000 --- a/plugins/web_ui/api/tabs/system/__init__.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -import shutil -from pathlib import Path -from typing import List, Optional - -from fastapi import APIRouter - -from ....base_model import Result -from ....utils import authentication, get_system_disk -from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile - -router = APIRouter(prefix="/system") - - - -@router.get("/get_dir_list", dependencies=[authentication()], description="获取文件列表") -async def _(path: Optional[str] = None) -> Result: - base_path = Path(path) if path else Path() - data_list = [] - for file in os.listdir(base_path): - data_list.append(DirFile(is_file=not (base_path / file).is_dir(), name=file, parent=path)) - return Result.ok(data_list) - - -@router.get("/get_resources_size", dependencies=[authentication()], description="获取文件列表") -async def _(full_path: Optional[str] = None) -> Result: - return Result.ok(await get_system_disk(full_path)) - - -@router.post("/delete_file", dependencies=[authentication()], description="删除文件") -async def _(param: DeleteFile) -> Result: - path = Path(param.full_path) - if not path or not path.exists(): - return Result.warning_("文件不存在...") - try: - path.unlink() - return Result.ok('删除成功!') - except Exception as e: - return Result.warning_('删除失败: ' + str(e)) - -@router.post("/delete_folder", dependencies=[authentication()], description="删除文件夹") -async def _(param: DeleteFile) -> Result: - path = Path(param.full_path) - if not path or not path.exists() or path.is_file(): - return Result.warning_("文件夹不存在...") - try: - shutil.rmtree(path.absolute()) - return Result.ok('删除成功!') - except Exception as e: - return Result.warning_('删除失败: ' + str(e)) - - -@router.post("/rename_file", dependencies=[authentication()], description="重命名文件") -async def _(param: RenameFile) -> Result: - path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) - if not path or not path.exists(): - return Result.warning_("文件不存在...") - try: - path.rename(path.parent / param.name) - return Result.ok('重命名成功!') - except Exception as e: - return Result.warning_('重命名失败: ' + str(e)) - - -@router.post("/rename_folder", dependencies=[authentication()], description="重命名文件夹") -async def _(param: RenameFile) -> Result: - path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) - if not path or not path.exists() or path.is_file(): - return Result.warning_("文件夹不存在...") - try: - new_path = path.parent / param.name - shutil.move(path.absolute(), new_path.absolute()) - return Result.ok('重命名成功!') - except Exception as e: - return Result.warning_('重命名失败: ' + str(e)) - - -@router.post("/add_file", dependencies=[authentication()], description="新建文件") -async def _(param: AddFile) -> Result: - path = (Path(param.parent) / param.name) if param.parent else Path(param.name) - if path.exists(): - return Result.warning_("文件已存在...") - try: - path.open('w') - return Result.ok('新建文件成功!') - except Exception as e: - return Result.warning_('新建文件失败: ' + str(e)) - - -@router.post("/add_folder", dependencies=[authentication()], description="新建文件夹") -async def _(param: AddFile) -> Result: - path = (Path(param.parent) / param.name) if param.parent else Path(param.name) - if path.exists(): - return Result.warning_("文件夹已存在...") - try: - path.mkdir() - return Result.ok('新建文件夹成功!') - except Exception as e: - return Result.warning_('新建文件夹失败: ' + str(e)) - - -@router.get("/read_file", dependencies=[authentication()], description="读取文件") -async def _(full_path: str) -> Result: - path = Path(full_path) - if not path.exists(): - return Result.warning_("文件不存在...") - try: - text = path.read_text(encoding='utf-8') - return Result.ok(text) - except Exception as e: - return Result.warning_('新建文件夹失败: ' + str(e)) - -@router.post("/save_file", dependencies=[authentication()], description="读取文件") -async def _(param: SaveFile) -> Result: - path = Path(param.full_path) - try: - with path.open('w') as f: - f.write(param.content) - return Result.ok("更新成功!") - except Exception as e: - return Result.warning_('新建文件夹失败: ' + str(e)) \ No newline at end of file diff --git a/plugins/web_ui/api/tabs/system/model.py b/plugins/web_ui/api/tabs/system/model.py deleted file mode 100644 index b3b5a45f..00000000 --- a/plugins/web_ui/api/tabs/system/model.py +++ /dev/null @@ -1,64 +0,0 @@ - - - -from datetime import datetime -from typing import Literal, Optional - -from pydantic import BaseModel - - -class DirFile(BaseModel): - - """ - 文件或文件夹 - """ - - is_file: bool - """是否为文件""" - name: str - """文件夹或文件名称""" - parent: Optional[str] = None - """父级""" - -class DeleteFile(BaseModel): - - """ - 删除文件 - """ - - full_path: str - """文件全路径""" - -class RenameFile(BaseModel): - - """ - 删除文件 - """ - parent: Optional[str] - """父路径""" - old_name: str - """旧名称""" - name: str - """新名称""" - - -class AddFile(BaseModel): - - """ - 新建文件 - """ - parent: Optional[str] - """父路径""" - name: str - """新名称""" - - -class SaveFile(BaseModel): - - """ - 保存文件 - """ - full_path: str - """全路径""" - content: str - """内容""" diff --git a/plugins/web_ui/auth/__init__.py b/plugins/web_ui/auth/__init__.py deleted file mode 100644 index d927977f..00000000 --- a/plugins/web_ui/auth/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -import json -from datetime import timedelta - -import nonebot -from fastapi import APIRouter, Depends -from fastapi.security import OAuth2PasswordRequestForm - -from configs.config import Config - -from ..base_model import Result -from ..utils import ( - ACCESS_TOKEN_EXPIRE_MINUTES, - create_token, - get_user, - token_data, - token_file, -) - -app = nonebot.get_app() - - -router = APIRouter() - - -@router.post("/login") -async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): - username = Config.get_config("web-ui", "username") - password = Config.get_config("web-ui", "password") - if not username or not password: - return Result.fail("你滴配置文件里用户名密码配置项为空", 998) - if username != form_data.username or password != form_data.password: - return Result.fail("真笨, 账号密码都能记错!", 999) - access_token = create_token( - user=get_user(form_data.username), expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) - ) - token_data["token"].append(access_token) - if len(token_data["token"]) > 3: - token_data["token"] = token_data["token"][1:] - with open(token_file, "w", encoding="utf8") as f: - json.dump(token_data, f, ensure_ascii=False, indent=4) - return Result.ok( - {"access_token": access_token, "token_type": "bearer"}, "欢迎回家, 欧尼酱!" - ) diff --git a/plugins/web_ui/base_model.py b/plugins/web_ui/base_model.py deleted file mode 100644 index ee7a93e4..00000000 --- a/plugins/web_ui/base_model.py +++ /dev/null @@ -1,117 +0,0 @@ -from datetime import datetime -from logging import warning -from typing import Any, Dict, Generic, List, Optional, TypeVar, Union - -from nonebot.adapters.onebot.v11 import Bot -from pydantic import BaseModel, validator - -from configs.utils import Config -from utils.manager.models import Plugin as PluginManager -from utils.manager.models import PluginBlock, PluginCd, PluginCount, PluginSetting - -T = TypeVar("T") - - -class User(BaseModel): - username: str - password: str - - -class Token(BaseModel): - access_token: str - token_type: str - - -class Result(BaseModel): - """ - 总体返回 - """ - - suc: bool - """调用状态""" - code: int = 200 - """code""" - info: str = "操作成功" - """info""" - warning: Optional[str] = None - """警告信息""" - data: Any = None - """返回数据""" - - @classmethod - def warning_(cls, info: str, code: int = 200) -> "Result": - return cls(suc=True, warning=info, code=code) - - @classmethod - def fail(cls, info: str = "异常错误", code: int = 500) -> "Result": - return cls(suc=False, info=info, code=code) - - @classmethod - def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> "Result": - return cls(suc=True, info=info, code=code, data=data) - - -class QueryModel(BaseModel, Generic[T]): - """ - 基本查询条件 - """ - - index: int - """页数""" - size: int - """每页数量""" - data: T - """携带数据""" - - @validator("index") - def index_validator(cls, index): - if index < 1: - raise ValueError("查询下标小于1...") - return index - - @validator("size") - def size_validator(cls, size): - if size < 1: - raise ValueError("每页数量小于1...") - return size - - -class BaseResultModel(BaseModel): - """ - 基础返回 - """ - - total: int - """总页数""" - data: Any - """数据""" - - -class SystemStatus(BaseModel): - """ - 系统状态 - """ - - cpu: float - memory: float - disk: float - check_time: datetime - - - - -class SystemFolderSize(BaseModel): - """ - 资源文件占比 - """ - - name: str - """名称""" - size: float - """大小""" - full_path: Optional[str] - """完整路径""" - is_dir: bool - """是否为文件夹""" - - diff --git a/plugins/web_ui/config.py b/plugins/web_ui/config.py deleted file mode 100644 index b6e6fde0..00000000 --- a/plugins/web_ui/config.py +++ /dev/null @@ -1,86 +0,0 @@ -from datetime import datetime -from typing import Any, Dict, List, Optional, Union - -import nonebot -from fastapi import APIRouter -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel -from strenum import StrEnum - -app = nonebot.get_app() - -origins = ["*"] - -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" - -GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" - - -class QueryDateType(StrEnum): - - """ - 查询日期类型 - """ - - DAY = "day" - """日""" - WEEK = "week" - """周""" - MONTH = "month" - """月""" - YEAR = "year" - """年""" - - -# class SystemNetwork(BaseModel): -# """ -# 系统网络状态 -# """ - -# baidu: int -# google: int - - -# class SystemFolderSize(BaseModel): -# """ -# 资源文件占比 -# """ - -# font_dir_size: float -# image_dir_size: float -# text_dir_size: float -# record_dir_size: float -# temp_dir_size: float -# data_dir_size: float -# log_dir_size: float -# check_time: datetime - - -# class SystemStatusList(BaseModel): -# """ -# 状态记录 -# """ - -# cpu_data: List[Dict[str, Union[float, str]]] -# memory_data: List[Dict[str, Union[float, str]]] -# disk_data: List[Dict[str, Union[float, str]]] - - -# class SystemResult(BaseModel): -# """ -# 系统api返回 -# """ - -# status: SystemStatus -# network: SystemNetwork -# disk: SystemFolderSize -# check_time: datetime diff --git a/plugins/web_ui/utils.py b/plugins/web_ui/utils.py deleted file mode 100644 index eba64b63..00000000 --- a/plugins/web_ui/utils.py +++ /dev/null @@ -1,170 +0,0 @@ -import os -from datetime import datetime, timedelta -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -import psutil -import ujson as json -from fastapi import Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer -from jose import JWTError, jwt -from nonebot.utils import run_sync - -from configs.config import Config -from configs.path_config import ( - DATA_PATH, - FONT_PATH, - IMAGE_PATH, - LOG_PATH, - RECORD_PATH, - TEMP_PATH, - TEXT_PATH, -) - -from .base_model import SystemFolderSize, SystemStatus, User - -SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login") - -token_file = DATA_PATH / "web_ui" / "token.json" -token_file.parent.mkdir(parents=True, exist_ok=True) -token_data = {"token": []} -if token_file.exists(): - try: - token_data = json.load(open(token_file, "r", encoding="utf8")) - except json.JSONDecodeError: - pass - - -def get_user(uname: str) -> Optional[User]: - """获取账号密码 - - 参数: - uname: uname - - 返回: - Optional[User]: 用户信息 - """ - username = Config.get_config("web-ui", "username") - password = Config.get_config("web-ui", "password") - if username and password and uname == username: - return User(username=username, password=password) - - -def create_token(user: User, expires_delta: Optional[timedelta] = None): - """创建token - - 参数: - user: 用户信息 - expires_delta: 过期时间. - """ - expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) - return jwt.encode( - claims={"sub": user.username, "exp": expire}, - key=SECRET_KEY, - algorithm=ALGORITHM, - ) - - -def authentication(): - """权限验证 - - - 异常: - JWTError: JWTError - HTTPException: HTTPException - """ - # if token not in token_data["token"]: - def inner(token: str = Depends(oauth2_scheme)): - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username, expire = payload.get("sub"), payload.get("exp") - user = get_user(username) # type: ignore - if user is None: - raise JWTError - except JWTError: - raise HTTPException(status_code=400, detail="登录验证失败或已失效, 踢出房间!") - - return Depends(inner) - - -def _get_dir_size(dir_path: Path) -> float: - """ - 说明: - 获取文件夹大小 - 参数: - :param dir_path: 文件夹路径 - """ - size = 0 - for root, dirs, files in os.walk(dir_path): - size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) - return size - - -@run_sync -def get_system_status() -> SystemStatus: - """ - 说明: - 获取系统信息等 - """ - cpu = psutil.cpu_percent() - memory = psutil.virtual_memory().percent - disk = psutil.disk_usage("/").percent - return SystemStatus( - cpu=cpu, - memory=memory, - disk=disk, - check_time=datetime.now().replace(microsecond=0), - ) - - -@run_sync -def get_system_disk( - full_path: Optional[str], -) -> List[SystemFolderSize]: - """ - 说明: - 获取资源文件大小等 - """ - base_path = Path(full_path) if full_path else Path() - other_size = 0 - data_list = [] - for file in os.listdir(base_path): - f = base_path / file - if f.is_dir(): - size = _get_dir_size(f) / 1024 / 1024 - data_list.append(SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True)) - else: - other_size += f.stat().st_size / 1024 / 1024 - if other_size: - data_list.append(SystemFolderSize(name='other_file', size=other_size, full_path=full_path, is_dir=False)) - return data_list - # else: - # if type_ == "image": - # dir_path = IMAGE_PATH - # elif type_ == "font": - # dir_path = FONT_PATH - # elif type_ == "text": - # dir_path = TEXT_PATH - # elif type_ == "record": - # dir_path = RECORD_PATH - # elif type_ == "data": - # dir_path = DATA_PATH - # elif type_ == "temp": - # dir_path = TEMP_PATH - # else: - # dir_path = LOG_PATH - # dir_map = {} - # other_file_size = 0 - # for file in os.listdir(dir_path): - # file = Path(dir_path / file) - # if file.is_dir(): - # dir_map[file.name] = _get_dir_size(file) / 1024 / 1024 - # else: - # other_file_size += os.path.getsize(file) / 1024 / 1024 - # dir_map["其他文件"] = other_file_size - # dir_map["check_time"] = datetime.now().replace(microsecond=0) - # return dir_map diff --git a/plugins/what_anime/__init__.py b/plugins/what_anime/__init__.py deleted file mode 100755 index bae70bd2..00000000 --- a/plugins/what_anime/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message, Bot -from nonebot.internal.params import ArgStr, Arg -from nonebot.params import CommandArg - -from .data_source import get_anime -from nonebot import on_command -from nonebot.typing import T_State -from utils.utils import get_message_img -from services.log import logger - - -__zx_plugin_name__ = "识番" -__plugin_usage__ = """ -usage: - api.trace.moe 以图识番 - 指令: - 识番 [图片] -""".strip() -__plugin_des__ = "以图识番" -__plugin_cmd__ = ["识番 [图片]"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["识番"], -} - - -what_anime = on_command("识番", priority=5, block=True) - - -@what_anime.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State, args: Message = CommandArg()): - img_url = get_message_img(event.json()) - if img_url: - state["img_url"] = args - - -@what_anime.got("img_url", prompt="虚空识番?来图来图GKD") -async def _(bot: Bot, event: MessageEvent, state: T_State, img_url: Message = Arg("img_url")): - img_url = get_message_img(img_url) - if not img_url: - await what_anime.reject_arg("img_url", "发送的必须是图片!") - img_url = img_url[0] - await what_anime.send("开始识别.....") - anime_data_report = await get_anime(img_url) - if anime_data_report: - await what_anime.send(anime_data_report, at_sender=True) - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" - f" 识番 {img_url} --> {anime_data_report}" - ) - else: - logger.info( - f"USER {event.user_id} GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 识番 {img_url} 未找到" - ) - await what_anime.send(f"没有寻找到该番剧,果咩..", at_sender=True) diff --git a/plugins/what_anime/data_source.py b/plugins/what_anime/data_source.py deleted file mode 100755 index 87fc071a..00000000 --- a/plugins/what_anime/data_source.py +++ /dev/null @@ -1,47 +0,0 @@ -import time -from services.log import logger -from utils.langconv import * -from utils.http_utils import AsyncHttpx - - -async def get_anime(anime: str) -> str: - s_time = time.time() - url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime) - logger.debug("[info]Now starting get the {}".format(url)) - try: - anime_json = (await AsyncHttpx.get(url)).json() - if not anime_json["error"]: - if anime_json == "Error reading imagenull": - return "图像源错误,注意必须是静态图片哦" - repass = "" - # 拿到动漫 中文名 - for anime in anime_json["result"][:5]: - synonyms = anime["anilist"]["synonyms"] - for x in synonyms: - _count_ch = 0 - for word in x: - if "\u4e00" <= word <= "\u9fff": - _count_ch += 1 - if _count_ch > 3: - anime_name = x - break - else: - anime_name = anime["anilist"]["title"]["native"] - episode = anime["episode"] - from_ = int(anime["from"]) - m, s = divmod(from_, 60) - similarity = anime["similarity"] - putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format( - Converter("zh-hans").convert(anime_name), - episode if episode else "?", - m, - s, - similarity, - ) - repass += putline + "\n" - return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1] - else: - return f'访问错误 error:{anime_json["error"]}' - except Exception as e: - logger.error(f"识番发生错误 {type(e)}:{e}") - return "发生了奇怪的错误,那就没办法了,再试一次?" diff --git a/plugins/white2black_image.py b/plugins/white2black_image.py deleted file mode 100755 index 730740c7..00000000 --- a/plugins/white2black_image.py +++ /dev/null @@ -1,143 +0,0 @@ -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot import on_command -from nonebot.params import CommandArg -from utils.utils import get_message_img, is_chinese -from utils.message_builder import image -from configs.path_config import TEMP_PATH -from utils.image_utils import BuildImage -from services.log import logger -from utils.http_utils import AsyncHttpx - -# ZH_CN2EN 中文 » 英语 -# ZH_CN2JA 中文 » 日语 -# ZH_CN2KR 中文 » 韩语 -# ZH_CN2FR 中文 » 法语 -# ZH_CN2RU 中文 » 俄语 -# ZH_CN2SP 中文 » 西语 -# EN2ZH_CN 英语 » 中文 -# JA2ZH_CN 日语 » 中文 -# KR2ZH_CN 韩语 » 中文 -# FR2ZH_CN 法语 » 中文 -# RU2ZH_CN 俄语 » 中文 -# SP2ZH_CN 西语 » 中文 - - -__zx_plugin_name__ = "黑白草图" -__plugin_usage__ = """ -usage: - 将图片黑白化并配上中文与日语 - 指令: - 黑白图 [文本] [图片] -""".strip() -__plugin_des__ = "为设想过得黑白草图" -__plugin_cmd__ = ["黑白图"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["黑白图", "黑白草图"], -} - -w2b_img = on_command("黑白草图", aliases={"黑白图"}, priority=5, block=True) - - -@w2b_img.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - # try: - img = get_message_img(event.json()) - msg = arg.extract_plain_text().strip() - if not img or not msg: - await w2b_img.finish(f"格式错误:\n" + __plugin_usage__) - img = img[0] - if not await AsyncHttpx.download_file( - img, TEMP_PATH / f"{event.user_id}_w2b.png" - ): - await w2b_img.finish("下载图片失败...请稍后再试...") - msg = await get_translate(msg) - w2b = BuildImage(0, 0, background=TEMP_PATH / f"{event.user_id}_w2b.png") - w2b.convert("L") - msg_sp = msg.split("<|>") - w, h = w2b.size - add_h, font_size = init_h_font_size(h) - bg = BuildImage(w, h + add_h, color="black", font_size=int(font_size)) - bg.paste(w2b) - chinese_msg = formalization_msg(msg) - if not bg.check_font_size(chinese_msg): - if len(msg_sp) == 1: - centered_text(bg, chinese_msg, add_h) - else: - centered_text(bg, chinese_msg + "<|>" + msg_sp[1], add_h) - elif not bg.check_font_size(msg_sp[0]): - centered_text(bg, msg, add_h) - else: - ratio = (bg.getsize(msg_sp[0])[0] + 20) / bg.w - add_h = add_h * ratio - bg.resize(ratio) - centered_text(bg, msg, add_h) - await w2b_img.send(image(b64=bg.pic2bs4())) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 制作黑白草图 {msg}" - ) - - -def centered_text(img: BuildImage, text: str, add_h: int): - top_h = img.h - add_h + (img.h / 100) - bottom_h = img.h - (img.h / 100) - text_sp = text.split("<|>") - w, h = img.getsize(text_sp[0]) - if len(text_sp) == 1: - w = int((img.w - w) / 2) - h = int(top_h + (bottom_h - top_h - h) / 2) - img.text((w, h), text_sp[0], (255, 255, 255)) - else: - br_h = int(top_h + (bottom_h - top_h) / 2) - w = int((img.w - w) / 2) - h = int(top_h + (br_h - top_h - h) / 2) - img.text((w, h), text_sp[0], (255, 255, 255)) - w, h = img.getsize(text_sp[1]) - w = int((img.w - w) / 2) - h = int(br_h + (bottom_h - br_h - h) / 2) - img.text((w, h), text_sp[1], (255, 255, 255)) - - -async def get_translate(msg: str) -> str: - url = f"http://fanyi.youdao.com/translate?smartresult=dict&smartresult=rule&smartresult=ugc&sessionFrom=null" - data = { - "type": "ZH_CN2JA", - "i": msg, - "doctype": "json", - "version": "2.1", - "keyfrom": "fanyi.web", - "ue": "UTF-8", - "action": "FY_BY_CLICKBUTTON", - "typoResult": "true", - } - data = (await AsyncHttpx.post(url, data=data)).json() - if data["errorCode"] == 0: - translate = data["translateResult"][0][0]["tgt"] - msg += "<|>" + translate - return msg - - -def formalization_msg(msg: str) -> str: - rst = "" - for i in range(len(msg)): - if is_chinese(msg[i]): - rst += msg[i] + " " - else: - rst += msg[i] - if i + 1 < len(msg) and is_chinese(msg[i + 1]) and msg[i].isalpha(): - rst += " " - return rst - - -def init_h_font_size(h): - # 高度 字体 - if h < 400: - return init_h_font_size(400) - elif 400 < h < 800: - return init_h_font_size(800) - return h * 0.2, h * 0.05 diff --git a/plugins/withdraw.py b/plugins/withdraw.py deleted file mode 100755 index 795c8cc7..00000000 --- a/plugins/withdraw.py +++ /dev/null @@ -1,29 +0,0 @@ -from nonebot import on_command -from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent -from nonebot.typing import T_State -import re - - -__zx_plugin_name__ = "消息撤回 [Admin]" -__plugin_usage__ = """ -usage: - 简易的消息撤回机制 - 指令: - [回复]撤回 -""".strip() -__plugin_des__ = "消息撤回机制" -__plugin_cmd__ = ["[回复]撤回"] -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "admin_level": 0, -} - - -withdraw_msg = on_command("撤回", priority=5, block=True) - - -@withdraw_msg.handle() -async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - if event.reply: - await bot.delete_msg(message_id=event.reply.message_id) diff --git a/plugins/word_bank/__init__.py b/plugins/word_bank/__init__.py deleted file mode 100644 index 53560769..00000000 --- a/plugins/word_bank/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from pathlib import Path - -import nonebot - -from configs.config import Config -from utils.utils import GDict - -Config.add_plugin_config( - "word_bank", - "WORD_BANK_LEVEL [LEVEL]", - 5, - name="词库问答", - help_="设置增删词库的权限等级", - default_value=5, - type=int, -) - -GDict["run_sql"].append("ALTER TABLE word_bank2 ADD to_me VARCHAR(255);") - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/plugins/word_bank/_config.py b/plugins/word_bank/_config.py deleted file mode 100644 index d1f6ab67..00000000 --- a/plugins/word_bank/_config.py +++ /dev/null @@ -1,23 +0,0 @@ - - -scope2int = { - "全局": 0, - "群聊": 1, - "私聊": 2, -} - -type2int = { - "精准": 0, - "模糊": 1, - "正则": 2, - "图片": 3, -} - -int2type = { - 0: "精准", - 1: "模糊", - 2: "正则", - 3: "图片", -} - - diff --git a/plugins/word_bank/_data_source.py b/plugins/word_bank/_data_source.py deleted file mode 100644 index ea383222..00000000 --- a/plugins/word_bank/_data_source.py +++ /dev/null @@ -1,270 +0,0 @@ -import random -import time -from pathlib import Path -from typing import Any, List, Optional, Tuple, Union - -import nonebot -from nonebot.adapters.onebot.v11 import Message, MessageSegment - -from services import logger -from utils.image_utils import text2image -from utils.message_builder import image -from utils.utils import is_number - -from ._model import WordBank - -driver = nonebot.get_driver() - - -async def get_problem_str( - id_: Union[str, int], group_id: Optional[str] = None, word_scope: int = 1 -) -> Tuple[str, int]: - """ - 说明: - 通过id获取问题字符串 - 参数: - :param id_: 下标 - :param group_id: 群号 - :param word_scope: 获取类型 - """ - if word_scope in [0, 2]: - all_problem = await WordBank.get_problem_by_scope(word_scope) - elif group_id: - all_problem = await WordBank.get_group_all_problem(group_id) - else: - raise Exception("词条类型与群组id不能为空") - if isinstance(id_, str) and id_.startswith("id:"): - id_ = id_.split(":")[-1] - if not is_number(id_) or int(id_) < 0 or int(id_) > len(all_problem): - return "id必须为数字且在范围内", 999 - return all_problem[int(id_)][0], 200 - - -async def update_word( - params: str, group_id: Optional[str] = None, word_scope: int = 1 -) -> str: - """ - 说明: - 修改群词条 - 参数: - :param params: 参数 - :param group_id: 群号 - :param word_scope: 词条范围 - """ - return await word_handle(params, group_id, "update", word_scope) - - -async def delete_word( - params: str, group_id: Optional[str] = None, word_scope: int = 1 -) -> str: - """ - 说明: - 删除群词条 - 参数: - :param params: 参数 - :param group_id: 群号 - :param word_scope: 词条范围 - """ - return await word_handle(params, group_id, "delete", word_scope) - - -async def word_handle( - params_: str, group_id: Optional[str], type_: str, word_scope: int = 0 -) -> str: - """ - 说明: - 词条操作 - 参数: - :param params: 参数 - :param group_id: 群号 - :param type_: 类型 - :param word_scope: 词条范围 - """ - params = params_.split() - problem = params[0] - if problem.startswith("id:"): - problem, code = await get_problem_str(problem, group_id, word_scope) - if code != 200: - return problem - if type_ == "delete": - index = params[1] if len(params) > 1 else None - if index: - answer_num = len( - await WordBank.get_problem_all_answer(problem, group_id=group_id) - ) - if not is_number(index) or int(index) < 0 or int(index) > answer_num: - return "指定回答下标id必须为数字且在范围内" - index = int(index) - if await WordBank.delete_group_problem(problem, group_id, index, word_scope): # type: ignore - return "删除词条成功" - return "词条不存在" - if type_ == "update": - replace_str = params[1] - await WordBank.update_group_problem( - problem, replace_str, group_id, word_scope=word_scope - ) - return "修改词条成功" - return "类型错误" - - -async def show_word( - problem: str, - id_: Optional[int], - gid: Optional[int], - group_id: Optional[str] = None, - word_scope: Optional[int] = None, -) -> Union[str, List[Union[str, Message]]]: - if problem: - msg_list = [] - if word_scope is not None: - problem = (await WordBank.get_problem_by_scope(word_scope))[id_][0] # type: ignore - id_ = None - _problem_list = await WordBank.get_problem_all_answer( - problem, - id_ if id_ is not None else gid, - group_id if gid is None else None, - word_scope, - ) - for index, msg in enumerate(_problem_list): - if isinstance(msg, Message): - tmp = "" - for seg in msg: - tmp += seg - msg = tmp - msg_list.append(f"{index}." + msg) - msg_list = [ - f'词条:{problem or (f"id: {id_}" if id_ is not None else f"gid: {gid}")} 的回答' - ] + msg_list - return msg_list # type: ignore - else: - if group_id: - _problem_list = await WordBank.get_group_all_problem(group_id) - elif word_scope is not None: - _problem_list = await WordBank.get_problem_by_scope(word_scope) - else: - raise Exception("群组id和词条范围不能都为空") - global_problem_list = await WordBank.get_problem_by_scope(0) - if not _problem_list and not global_problem_list: - return "未收录任何词条.." - msg_list = await build_message(_problem_list) - global_msg_list = await build_message(global_problem_list) - if global_msg_list: - msg_list.append("###以下为全局词条###") - msg_list = msg_list + global_msg_list - return msg_list - - -async def build_message(_problem_list: List[Tuple[Any, Union[MessageSegment, str]]]): - index = 0 - str_temp_list = [] - msg_list = [] - temp_str = "" - for _, problem in _problem_list: - if len(temp_str.split("\n")) > 50: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - temp_str = "" - if isinstance(problem, str): - if problem not in str_temp_list: - str_temp_list.append(problem) - temp_str += f"{index}. {problem}\n" - else: - if temp_str: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - temp_str = "" - msg_list.append(f"{index}." + problem) - index += 1 - if temp_str: - img = await text2image( - temp_str, - padding=10, - color="#f9f6f2", - ) - msg_list.append(image(b64=img.pic2bs4())) - return msg_list - - -@driver.on_startup -async def _(): - try: - from ._old_model import WordBank as OldWordBank - except ModuleNotFoundError: - return - if await WordBank.get_group_all_problem(0): - return - logger.info("开始迁移词条 纯文本 数据") - try: - word_list = await OldWordBank.get_all() - new_answer_path = Path() / "data" / "word_bank" / "answer" - new_problem_path = Path() / "data" / "word_bank" / "problem" - new_answer_path.mkdir(exist_ok=True, parents=True) - for word in word_list: - problem: str = word.problem - user_id = word.user_id - group_id = word.group_id - format_ = word.format - answer = word.answer - # 仅对纯文本做处理 - if ( - "[CQ" not in problem - and "[CQ" not in answer - and "[_to_me" not in problem - ): - if not format_: - await WordBank.add_problem_answer( - user_id, group_id, 1, 0, problem, answer - ) - else: - placeholder = [] - for m in format_.split(""): - x = m.split("<_s>") - if x[0]: - idx, file_name = x[0], x[1] - if "jpg" in file_name: - answer = answer.replace( - f"[__placeholder_{idx}]", - f"[image:placeholder_{idx}]", - ) - file = ( - Path() - / "data" - / "word_bank" - / f"{group_id}" - / file_name - ) - rand = int(time.time()) + random.randint(1, 100000) - if file.exists(): - new_file = ( - new_answer_path - / f"{group_id}" - / f"{user_id}_{rand}.jpg" - ) - new_file.parent.mkdir(exist_ok=True, parents=True) - with open(file, "rb") as rb: - with open(new_file, "wb") as wb: - wb.write(rb.read()) - # file.rename(new_file) - placeholder.append( - f"answer/{group_id}/{user_id}_{rand}.jpg" - ) - await WordBank._move( - user_id, - group_id, - problem, - answer, - ",".join(placeholder), - ) - await WordBank.add_problem_answer(0, 0, 999, 0, "_[OK", "_[OK") - logger.info("词条 纯文本 数据迁移完成") - (Path() / "plugins" / "word_bank" / "_old_model.py").unlink() - except Exception as e: - logger.warning(f"迁移词条发生错误,如果为首次安装请无视 {type(e)}:{e}") diff --git a/plugins/word_bank/_model.py b/plugins/word_bank/_model.py deleted file mode 100644 index cfd1c647..00000000 --- a/plugins/word_bank/_model.py +++ /dev/null @@ -1,528 +0,0 @@ -import random -import re -import time -import uuid -from datetime import datetime -from typing import Any, List, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import ( - GroupMessageEvent, - Message, - MessageEvent, - MessageSegment, -) -from nonebot.internal.adapter.template import MessageTemplate -from tortoise import Tortoise, fields -from tortoise.expressions import Q - -from configs.path_config import DATA_PATH -from services.db_context import Model -from utils.http_utils import AsyncHttpx -from utils.image_utils import get_img_hash -from utils.message_builder import at, face, image -from utils.utils import get_message_img - -from ._config import int2type - -path = DATA_PATH / "word_bank" - - -class WordBank(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - user_id = fields.CharField(255) - """用户id""" - group_id = fields.CharField(255, null=True) - """群聊id""" - word_scope = fields.IntField(default=0) - """生效范围 0: 全局 1: 群聊 2: 私聊""" - word_type = fields.IntField(default=0) - """词条类型 0: 完全匹配 1: 模糊 2: 正则 3: 图片""" - status = fields.BooleanField() - """词条状态""" - problem = fields.TextField() - """问题,为图片时使用图片hash""" - answer = fields.TextField() - """回答""" - placeholder = fields.TextField(null=True) - """占位符""" - image_path = fields.TextField(null=True) - """使用图片作为问题时图片存储的路径""" - to_me = fields.CharField(255, null=True) - """昵称开头时存储的昵称""" - create_time = fields.DatetimeField(auto_now=True) - """创建时间""" - update_time = fields.DatetimeField(auto_now_add=True) - """更新时间""" - - class Meta: - table = "word_bank2" - table_description = "词条数据库" - - @classmethod - async def exists( - cls, - user_id: Optional[str], - group_id: Optional[str], - problem: str, - answer: Optional[str], - word_scope: Optional[int] = None, - word_type: Optional[int] = None, - ) -> bool: - """ - 说明: - 检测问题是否存在 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param problem: 问题 - :param answer: 回答 - :param word_scope: 词条范围 - :param word_type: 词条类型 - """ - query = cls.filter(problem=problem) - if user_id: - query = query.filter(user_id=user_id) - if group_id: - query = query.filter(group_id=group_id) - if answer: - query = query.filter(answer=answer) - if word_type is not None: - query = query.filter(word_type=word_type) - if word_scope is not None: - query = query.filter(word_scope=word_scope) - return bool(await query.first()) - - @classmethod - async def add_problem_answer( - cls, - user_id: str, - group_id: Optional[str], - word_scope: int, - word_type: int, - problem: Union[str, Message], - answer: Union[str, Message], - to_me_nickname: Optional[str] = None, - ): - """ - 说明: - 添加或新增一个问答 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param word_scope: 词条范围, - :param word_type: 词条类型, - :param problem: 问题 - :param answer: 回答 - :param to_me_nickname: at真寻名称 - """ - # 对图片做额外处理 - image_path = None - if word_type == 3: - url = get_message_img(problem)[0] - _file = ( - path / "problem" / f"{group_id}" / f"{user_id}_{int(time.time())}.jpg" - ) - _file.parent.mkdir(exist_ok=True, parents=True) - await AsyncHttpx.download_file(url, _file) - problem = str(get_img_hash(_file)) - image_path = f"problem/{group_id}/{user_id}_{int(time.time())}.jpg" - answer, _list = await cls._answer2format(answer, user_id, group_id) - if not await cls.exists( - user_id, group_id, str(problem), answer, word_scope, word_type - ): - await cls.create( - user_id=user_id, - group_id=group_id, - word_scope=word_scope, - word_type=word_type, - status=True, - problem=str(problem).strip(), - answer=answer, - image_path=image_path, - placeholder=",".join(_list), - create_time=datetime.now().replace(microsecond=0), - update_time=datetime.now().replace(microsecond=0), - to_me=to_me_nickname, - ) - - @classmethod - async def _answer2format( - cls, answer: Union[str, Message], user_id: str, group_id: Optional[str] - ) -> Tuple[str, List[Any]]: - """ - 说明: - 将CQ码转化为占位符 - 参数: - :param answer: 回答内容 - :param user_id: 用户id - :param group_id: 群号 - """ - if isinstance(answer, str): - return answer, [] - _list = [] - text = "" - index = 0 - for seg in answer: - placeholder = uuid.uuid1() - if isinstance(seg, str): - text += seg - elif seg.type == "text": - text += seg.data["text"] - elif seg.type == "face": - text += f"[face:placeholder_{placeholder}]" - _list.append(seg.data["id"]) - elif seg.type == "at": - text += f"[at:placeholder_{placeholder}]" - _list.append(seg.data["qq"]) - else: - text += f"[image:placeholder_{placeholder}]" - index += 1 - _file = ( - path - / "answer" - / f"{group_id or user_id}" - / f"{user_id}_{placeholder}.jpg" - ) - _file.parent.mkdir(exist_ok=True, parents=True) - await AsyncHttpx.download_file(seg.data["url"], _file) - _list.append( - f"answer/{group_id or user_id}/{user_id}_{placeholder}.jpg" - ) - return text, _list - - @classmethod - async def _format2answer( - cls, - problem: str, - answer: Union[str, Message], - user_id: int, - group_id: int, - query: Optional["WordBank"] = None, - ) -> Union[str, Message]: - """ - 说明: - 将占位符转换为CQ码 - 参数: - :param problem: 问题内容 - :param answer: 回答内容 - :param user_id: 用户id - :param group_id: 群号 - """ - if not query: - query = await cls.get_or_none( - problem=problem, - user_id=user_id, - group_id=group_id, - answer=answer, - ) - if not answer: - answer = query.answer # type: ignore - if query and query.placeholder: - type_list = re.findall(rf"\[(.*?):placeholder_.*?]", str(answer)) - temp_answer = re.sub(rf"\[(.*?):placeholder_.*?]", "{}", str(answer)) - seg_list = [] - for t, p in zip(type_list, query.placeholder.split(",")): - if t == "image": - seg_list.append(image(path / p)) - elif t == "face": - seg_list.append(face(int(p))) - elif t == "at": - seg_list.append(at(p)) - return MessageTemplate(temp_answer, Message).format(*seg_list) # type: ignore - return answer - - @classmethod - async def check_problem( - cls, - event: MessageEvent, - problem: str, - word_scope: Optional[int] = None, - word_type: Optional[int] = None, - ) -> Optional[Any]: - """ - 说明: - 检测是否包含该问题并获取所有回答 - 参数: - :param event: event - :param problem: 问题内容 - :param word_scope: 词条范围 - :param word_type: 词条类型 - """ - query = cls - if isinstance(event, GroupMessageEvent): - if word_scope: - query = query.filter(word_scope=word_scope) - else: - query = query.filter(Q(group_id=event.group_id) | Q(word_scope=0)) - else: - query = query.filter(Q(word_scope=2) | Q(word_scope=0)) - if word_type: - query = query.filter(word_scope=word_type) - # 完全匹配 - if data_list := await query.filter( - Q(Q(word_type=0) | Q(word_type=3)), Q(problem=problem) - ).all(): - return data_list - db = Tortoise.get_connection("default") - # 模糊匹配 - sql = query.filter(word_type=1).sql() + " and POSITION(problem in $1) > 0" - data_list = await db.execute_query_dict(sql, [problem]) - if data_list: - return [cls(**data) for data in data_list] - # 正则 - sql = ( - query.filter(word_type=2, word_scope__not=999).sql() + " and $1 ~ problem;" - ) - data_list = await db.execute_query_dict(sql, [problem]) - if data_list: - return [cls(**data) for data in data_list] - return None - - @classmethod - async def get_answer( - cls, - event: MessageEvent, - problem: str, - word_scope: Optional[int] = None, - word_type: Optional[int] = None, - ) -> Optional[Union[str, Message]]: - """ - 说明: - 根据问题内容获取随机回答 - 参数: - :param event: event - :param problem: 问题内容 - :param word_scope: 词条范围 - :param word_type: 词条类型 - """ - data_list = await cls.check_problem(event, problem, word_scope, word_type) - if data_list: - random_answer = random.choice(data_list) - temp_answer = random_answer.answer - if random_answer.word_type == 2: - r = re.search(random_answer.problem, problem) - has_placeholder = re.search(rf"\$(\d)", random_answer.answer) - if r and r.groups() and has_placeholder: - pats = re.sub(r"\$(\d)", r"\\\1", random_answer.answer) - random_answer.answer = re.sub(random_answer.problem, pats, problem) - return ( - await cls._format2answer( - random_answer.problem, - random_answer.answer, - random_answer.user_id, - random_answer.group_id, - random_answer, - ) - if random_answer.placeholder - else random_answer.answer - ) - - @classmethod - async def get_problem_all_answer( - cls, - problem: str, - index: Optional[int] = None, - group_id: Optional[str] = None, - word_scope: Optional[int] = 0, - ) -> List[Union[str, Message]]: - """ - 说明: - 获取指定问题所有回答 - 参数: - :param problem: 问题 - :param index: 下标 - :param group_id: 群号 - :param word_scope: 词条范围 - """ - if index is not None: - if group_id: - problem_ = (await cls.filter(group_id=group_id).all())[index] - else: - problem_ = (await cls.filter(word_scope=(word_scope or 0)).all())[index] - problem = problem_.problem - answer = cls.filter(problem=problem) - if group_id: - answer = answer.filter(group_id=group_id) - return [await cls._format2answer("", "", 0, 0, x) for x in (await answer.all())] - - @classmethod - async def delete_group_problem( - cls, - problem: str, - group_id: Optional[str], - index: Optional[int] = None, - word_scope: int = 1, - ): - """ - 说明: - 删除指定问题全部或指定回答 - 参数: - :param problem: 问题文本 - :param group_id: 群号 - :param index: 回答下标 - :param word_scope: 词条范围 - """ - if await cls.exists(None, group_id, problem, None, word_scope): - if index is not None: - if group_id: - query = await cls.filter(group_id=group_id, problem=problem).all() - else: - query = await cls.filter(word_scope=0, problem=problem).all() - await query[index].delete() - else: - if group_id: - await WordBank.filter(group_id=group_id, problem=problem).delete() - else: - await WordBank.filter( - word_scope=word_scope, problem=problem - ).delete() - return True - return False - - @classmethod - async def update_group_problem( - cls, - problem: str, - replace_str: str, - group_id: Optional[str], - index: Optional[int] = None, - word_scope: int = 1, - ): - """ - 说明: - 修改词条问题 - 参数: - :param problem: 问题 - :param replace_str: 替换问题 - :param group_id: 群号 - :param index: 下标 - :param word_scope: 词条范围 - """ - if index is not None: - if group_id: - query = await cls.filter(group_id=group_id, problem=problem).all() - else: - query = await cls.filter(word_scope=word_scope, problem=problem).all() - query[index].problem = replace_str - await query[index].save(update_fields=["problem"]) - else: - if group_id: - await cls.filter(group_id=group_id, problem=problem).update( - problem=replace_str - ) - else: - await cls.filter(word_scope=word_scope, problem=problem).update( - problem=replace_str - ) - - @classmethod - async def get_group_all_problem( - cls, group_id: str - ) -> List[Tuple[Any, Union[MessageSegment, str]]]: - """ - 说明: - 获取群聊所有词条 - 参数: - :param group_id: 群号 - """ - return cls._handle_problem( - await cls.filter(group_id=group_id).all() # type: ignore - ) - - @classmethod - async def get_problem_by_scope(cls, word_scope: int): - """ - 说明: - 通过词条范围获取词条 - 参数: - :param word_scope: 词条范围 - """ - return cls._handle_problem( - await cls.filter(word_scope=word_scope).all() # type: ignore - ) - - @classmethod - async def get_problem_by_type(cls, word_type: int): - """ - 说明: - 通过词条类型获取词条 - 参数: - :param word_type: 词条类型 - """ - return cls._handle_problem( - await cls.filter(word_type=word_type).all() # type: ignore - ) - - @classmethod - def _handle_problem(cls, msg_list: List["WordBank"]): - """ - 说明: - 格式化处理问题 - 参数: - :param msg_list: 消息列表 - """ - _tmp = [] - problem_list = [] - for q in msg_list: - if q.problem not in _tmp: - problem = ( - q.problem, - image(path / q.image_path) - if q.image_path - else f"[{int2type[q.word_type]}] " + q.problem, - ) - problem_list.append(problem) - _tmp.append(q.problem) - return problem_list - - @classmethod - async def _move( - cls, - user_id: str, - group_id: Optional[str], - problem: Union[str, Message], - answer: Union[str, Message], - placeholder: str, - ): - """ - 说明: - 旧词条图片移动方法 - 参数: - :param user_id: 用户id - :param group_id: 群号 - :param problem: 问题 - :param answer: 回答 - :param placeholder: 占位符 - """ - word_scope = 0 - word_type = 0 - # 对图片做额外处理 - if not await cls.exists( - user_id, group_id, problem, answer, word_scope, word_type - ): - await cls.create( - user_id=user_id, - group_id=group_id, - word_scope=word_scope, - word_type=word_type, - status=True, - problem=problem, - answer=answer, - image_path=None, - placeholder=placeholder, - create_time=datetime.now().replace(microsecond=0), - update_time=datetime.now().replace(microsecond=0), - ) - - @classmethod - async def _run_script(cls): - return [ - "ALTER TABLE word_bank2 ADD to_me varchar(255);", # 添加 to_me 字段 - "ALTER TABLE word_bank2 ALTER COLUMN create_time TYPE timestamp with time zone USING create_time::timestamp with time zone;", - "ALTER TABLE word_bank2 ALTER COLUMN update_time TYPE timestamp with time zone USING update_time::timestamp with time zone;", - "ALTER TABLE word_bank2 RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id - "ALTER TABLE word_bank2 ALTER COLUMN user_id TYPE character varying(255);", - "ALTER TABLE word_bank2 ALTER COLUMN group_id TYPE character varying(255);", - ] diff --git a/plugins/word_bank/_rule.py b/plugins/word_bank/_rule.py deleted file mode 100644 index 6b787d5c..00000000 --- a/plugins/word_bank/_rule.py +++ /dev/null @@ -1,53 +0,0 @@ -from io import BytesIO -from typing import List - -import imagehash -from nonebot.adapters.onebot.v11 import Bot, MessageEvent -from nonebot.typing import T_State -from PIL import Image - -from services.log import logger -from utils.depends import ImageList -from utils.http_utils import AsyncHttpx -from utils.utils import get_message_at, get_message_img, get_message_text - -from ._model import WordBank - - -async def check( - bot: Bot, - event: MessageEvent, - state: T_State, -) -> bool: - text = get_message_text(event.message) - img_list = get_message_img(event.message) - problem = text - if not text and len(img_list) == 1: - try: - r = await AsyncHttpx.get(img_list[0]) - problem = str(imagehash.average_hash(Image.open(BytesIO(r.content)))) - except Exception as e: - logger.warning(f"获取图片失败", "词条检测", e=e) - if get_message_at(event.message): - temp = "" - for seg in event.message: - if seg.type == "at": - temp += f"[at:{seg.data['qq']}]" - elif seg.type == "text": - temp += seg.data["text"] - problem = temp - if event.to_me and bot.config.nickname: - if str(event.original_message).startswith("[CQ:at"): - problem = f"[at:{bot.self_id}]" + problem - else: - if problem and bot.config.nickname: - nickname = [ - nk - for nk in bot.config.nickname - if str(event.original_message).startswith(nk) - ] - problem = nickname[0] + problem if nickname else problem - if problem and (await WordBank.check_problem(event, problem) is not None): - state["problem"] = problem - return True - return False diff --git a/plugins/word_bank/message_handle.py b/plugins/word_bank/message_handle.py deleted file mode 100644 index 9f9fe4d7..00000000 --- a/plugins/word_bank/message_handle.py +++ /dev/null @@ -1,29 +0,0 @@ -from nonebot import on_message -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.typing import T_State - -from configs.path_config import DATA_PATH -from services import logger - -from ._model import WordBank -from ._rule import check - -__zx_plugin_name__ = "词库问答回复操作 [Hidden]" - -data_dir = DATA_PATH / "word_bank" -data_dir.mkdir(parents=True, exist_ok=True) - -message_handle = on_message(priority=6, block=True, rule=check) - - -@message_handle.handle() -async def _(event: MessageEvent, state: T_State): - if problem := state.get("problem"): - if msg := await WordBank.get_answer(event, problem): - await message_handle.send(msg) - logger.info( - f" 触发词条 {problem}", - "词条检测", - event.user_id, - getattr(event, "group_id", None), - ) diff --git a/plugins/word_bank/word_handle.py b/plugins/word_bank/word_handle.py deleted file mode 100644 index 92677dcf..00000000 --- a/plugins/word_bank/word_handle.py +++ /dev/null @@ -1,359 +0,0 @@ -import re -from typing import Any, List, Optional, Tuple - -from nonebot import on_command, on_regex -from nonebot.adapters.onebot.v11 import ( - Bot, - GroupMessageEvent, - Message, - MessageEvent, - PrivateMessageEvent, - unescape, -) -from nonebot.exception import FinishedException -from nonebot.internal.params import Arg, ArgStr -from nonebot.params import Command, CommandArg, RegexGroup -from nonebot.typing import T_State - -from configs.config import Config -from configs.path_config import DATA_PATH -from services.log import logger -from utils.depends import AtList, ImageList -from utils.message_builder import custom_forward_msg -from utils.utils import get_message_at, get_message_img, is_number - -from ._config import scope2int, type2int -from ._data_source import delete_word, show_word, update_word -from ._model import WordBank - -__zx_plugin_name__ = "词库问答 [Admin]" -__plugin_usage__ = r""" -usage: - 对指定问题的随机回答,对相同问题可以设置多个不同回答 - 删除词条后每个词条的id可能会变化,请查看后再删除 - 更推荐使用id方式删除 - 问题回答支持的CQ:at, face, image - 查看词条命令:群聊时为 群词条+全局词条,私聊时为 私聊词条+全局词条 - 添加词条正则:添加词条(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*) - 正则问可以通过$1类推()捕获的组 - 指令: - 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答 - 删除词条 [问题/下标] ?[下标]:删除指定词条指定或全部回答 - 修改词条 [问题/下标] [新问题]:修改词条问题 - 查看词条 ?[问题/下标]:查看全部词条或对应词条回答 - 示例:添加词条问图片答嗨嗨嗨 - [图片]... - 示例:添加词条@萝莉 我来啦 - 示例:添加词条问谁是萝莉答是我 - 示例:添加词条正则问那个(.+)是萝莉答没错$1是萝莉 - 示例:删除词条 谁是萝莉 - 示例:删除词条 谁是萝莉 0 - 示例:删除词条 id:0 1 - 示例:修改词条 谁是萝莉 是你 - 示例:修改词条 id:0 是你 - 示例:查看词条 - 示例:查看词条 谁是萝莉 - 示例:查看词条 id:0 (群/私聊词条) - 示例:查看词条 gid:0 (全局词条) -""".strip() -__plugin_superuser_usage__ = r""" -usage: - 在私聊中超级用户额外设置 - 指令: - (全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*):添加问答词条,可重复添加相同问题的不同回答 - 全局添加词条 - 私聊添加词条 - (私聊情况下)删除词条: 删除私聊词条 - (私聊情况下)删除全局词条 - (私聊情况下)修改词条: 修改私聊词条 - (私聊情况下)修改全局词条 - 用法与普通用法相同 -""".strip() -__plugin_des__ = "自定义词条内容随机回复" -__plugin_cmd__ = [ - "添加词条 ?[模糊/关键字]问...答..", - "删除词条 [问题/下标] ?[下标]", - "修改词条 [问题/下标] ?[下标/新回答] [新回答]", - "查看词条 ?[问题/下标]", -] -__plugin_version__ = 0.3 -__plugin_author__ = "HibiKier & yajiwa" -__plugin_settings__ = { - "admin_level": Config.get_config("word_bank", "WORD_BANK_LEVEL [LEVEL]"), - "cmd": ["词库问答", "添加词条", "删除词条", "修改词条", "查看词条"], -} - -data_dir = DATA_PATH / "word_bank" -data_dir.mkdir(parents=True, exist_ok=True) - -add_word = on_regex( - r"^(全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)", priority=5, block=True -) - -delete_word_matcher = on_command("删除词条", aliases={"删除全局词条"}, priority=5, block=True) - -update_word_matcher = on_command("修改词条", aliases={"修改全局词条"}, priority=5, block=True) - -show_word_matcher = on_command("显示词条", aliases={"查看词条"}, priority=5, block=True) - - -@add_word.handle() -async def _( - bot: Bot, - event: MessageEvent, - state: T_State, - reg_group: Tuple[Any, ...] = RegexGroup(), - img_list: List[str] = ImageList(), - at_list: List[int] = AtList(), -): - if ( - isinstance(event, PrivateMessageEvent) - and str(event.user_id) not in bot.config.superusers - ): - await add_word.finish("权限不足捏") - word_scope, word_type, problem, answer = reg_group - if not word_scope and isinstance(event, PrivateMessageEvent): - word_scope = "私聊" - if ( - word_scope - and word_scope in ["全局", "私聊"] - and str(event.user_id) not in bot.config.superusers - ): - await add_word.finish("权限不足,无法添加该范围词条") - if (not problem or not problem.strip()) and word_type != "图片": - await add_word.finish("词条问题不能为空!") - if (not answer or not answer.strip()) and not len(img_list) and not len(at_list): - await add_word.finish("词条回答不能为空!") - if word_type != "图片": - state["problem_image"] = "YES" - answer = event.message - # 对at问题对额外处理 - if at_list: - is_first = True - cur_p = None - answer = "" - problem = "" - for index, seg in enumerate(event.message): - r = re.search("添加词条(模糊|正则|图片)?问", str(seg)) - if seg.type == "text" and r and is_first: - is_first = False - seg_ = str(seg).split(f"添加词条{r.group(1) or ''}问")[-1] - cur_p = "problem" - # 纯文本 - if "答" in seg_: - answer_index = seg_.index("答") - problem = unescape(seg_[:answer_index]).strip() - answer = unescape(seg_[answer_index + 1 :]).strip() - cur_p = "answer" - else: - problem = unescape(seg_) - continue - if cur_p == "problem": - if seg.type == "text" and "答" in str(seg): - seg_ = str(seg) - answer_index = seg_.index("答") - problem += seg_[:answer_index] - answer += seg_[answer_index + 1 :] - cur_p = "answer" - else: - if seg.type == "at": - problem += f"[at:{seg.data['qq']}]" - else: - problem += ( - unescape(str(seg)).strip() if seg.type == "text" else seg - ) - else: - if seg.type == "text": - answer += unescape(str(seg)) - else: - answer += seg - event.message[0] = event.message[0].data["text"].split("答", maxsplit=1)[-1].strip() - state["word_scope"] = word_scope - state["word_type"] = word_type - state["problem"] = problem - state["answer"] = answer - - -@add_word.got("problem_image", prompt="请发送该回答设置的问题图片") -async def _( - bot: Bot, - event: MessageEvent, - word_scope: Optional[str] = ArgStr("word_scope"), - word_type: Optional[str] = ArgStr("word_type"), - problem: Optional[str] = ArgStr("problem"), - answer: Message = Arg("answer"), - problem_image: Message = Arg("problem_image"), -): - try: - if word_type == "正则" and problem: - problem = unescape(problem) - try: - re.compile(problem) - except re.error: - await add_word.finish(f"添加词条失败,正则表达式 {problem} 非法!") - # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent): - # word_scope = "私聊" - nickname = None - if problem and bot.config.nickname: - nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)] - await WordBank.add_problem_answer( - str(event.user_id), - str(event.group_id) - if isinstance(event, GroupMessageEvent) - and (not word_scope or word_scope == "私聊") - else "0", - scope2int[word_scope] if word_scope else 1, - type2int[word_type] if word_type else 0, - problem or problem_image, - answer, - nickname[0] if nickname else None, - ) - except Exception as e: - if isinstance(e, FinishedException): - await add_word.finish() - logger.error( - f"添加词条 {problem} 错误...", - "添加词条", - event.user_id, - getattr(event, "group_id", None), - e=e, - ) - await add_word.finish(f"添加词条 {problem} 发生错误!") - await add_word.send("添加词条 " + (problem or problem_image) + " 成功!") - logger.info( - f"添加词条 {problem} 成功!", "添加词条", event.user_id, getattr(event, "group_id", None) - ) - - -@delete_word_matcher.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - if not (msg := arg.extract_plain_text().strip()): - await delete_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - result = await delete_word(msg, str(event.group_id)) - await delete_word_matcher.send(result) - logger.info(f"删除词条:" + msg, "删除词条", event.user_id, event.group_id) - - -@delete_word_matcher.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - arg: Message = CommandArg(), - cmd: Tuple[str, ...] = Command(), -): - if str(event.user_id) not in bot.config.superusers: - await delete_word_matcher.finish("权限不足捏!") - if not (msg := arg.extract_plain_text().strip()): - await delete_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - result = await delete_word(msg, word_scope=2 if cmd[0] == "删除词条" else 0) - await delete_word_matcher.send(result) - logger.info(f"删除词条:" + msg, "删除词条", event.user_id) - - -@update_word_matcher.handle() -async def _(event: GroupMessageEvent, arg: Message = CommandArg()): - if not (msg := arg.extract_plain_text().strip()): - await update_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - if len(msg.split()) < 2: - await update_word_matcher.finish("此命令需要两个参数,请查看帮助") - result = await update_word(msg, str(event.group_id)) - await update_word_matcher.send(result) - logger.info(f"更新词条词条:" + msg, "更新词条", event.user_id, event.group_id) - - -@update_word_matcher.handle() -async def _( - bot: Bot, - event: PrivateMessageEvent, - arg: Message = CommandArg(), - cmd: Tuple[str, ...] = Command(), -): - if str(event.user_id) not in bot.config.superusers: - await delete_word_matcher.finish("权限不足捏!") - if not (msg := arg.extract_plain_text().strip()): - await update_word_matcher.finish("此命令之后需要跟随指定词条,通过“显示词条“查看") - if len(msg.split()) < 2: - await update_word_matcher.finish("此命令需要两个参数,请查看帮助") - result = await update_word(msg, word_scope=2 if cmd[0] == "修改词条" else 0) - await update_word_matcher.send(result) - logger.info(f"更新词条词条:" + msg, "更新词条", event.user_id) - - -@show_word_matcher.handle() -async def _(bot: Bot, event: GroupMessageEvent, arg: Message = CommandArg()): - if problem := arg.extract_plain_text().strip(): - id_ = None - gid = None - if problem.startswith("id:"): - id_ = problem.split(":")[-1] - if ( - not is_number(id_) - or int(id_) < 0 - or int(id_) - >= len(await WordBank.get_group_all_problem(str(event.group_id))) - ): - await show_word_matcher.finish("id必须为数字且在范围内") - id_ = int(id_) - if problem.startswith("gid:"): - gid = problem.split(":")[-1] - if ( - not is_number(gid) - or int(gid) < 0 - or int(gid) >= len(await WordBank.get_problem_by_scope(0)) - ): - await show_word_matcher.finish("gid必须为数字且在范围内") - gid = int(gid) - msg_list = await show_word( - problem, id_, gid, None if gid else str(event.group_id) - ) - else: - msg_list = await show_word(problem, None, None, str(event.group_id)) - if isinstance(msg_list, str): - await show_word_matcher.send(msg_list) - else: - await bot.send_group_forward_msg( - group_id=event.group_id, messages=custom_forward_msg(msg_list, bot.self_id) - ) - logger.info( - f"查看词条回答:" + problem, "显示词条", event.user_id, getattr(event, "group_id", None) - ) - - -@show_word_matcher.handle() -async def _(event: PrivateMessageEvent, arg: Message = CommandArg()): - if problem := arg.extract_plain_text().strip(): - id_ = None - gid = None - if problem.startswith("id:"): - id_ = problem.split(":")[-1] - if ( - not is_number(id_) - or int(id_) < 0 - or int(id_) > len(await WordBank.get_problem_by_scope(2)) - ): - await show_word_matcher.finish("id必须为数字且在范围内") - id_ = int(id_) - if problem.startswith("gid:"): - gid = problem.split(":")[-1] - if ( - not is_number(gid) - or int(gid) < 0 - or int(gid) > len(await WordBank.get_problem_by_scope(0)) - ): - await show_word_matcher.finish("gid必须为数字且在范围内") - gid = int(gid) - msg_list = await show_word( - problem, id_, gid, word_scope=2 if id_ is not None else None - ) - else: - msg_list = await show_word(problem, None, None, word_scope=2) - if isinstance(msg_list, str): - await show_word_matcher.send(msg_list) - else: - t = "" - for msg in msg_list: - t += msg + "\n" - await show_word_matcher.send(t[:-1]) - logger.info( - f"查看词条回答:" + problem, "显示词条", event.user_id, getattr(event, "group_id", None) - ) diff --git a/plugins/word_clouds/__init__.py b/plugins/word_clouds/__init__.py deleted file mode 100644 index ab703941..00000000 --- a/plugins/word_clouds/__init__.py +++ /dev/null @@ -1,207 +0,0 @@ -import re -from datetime import datetime, timedelta -from typing import Tuple, Union - -import pytz -from nonebot import get_driver, on_command -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.adapters.onebot.v11.event import GroupMessageEvent -from nonebot.matcher import Matcher -from nonebot.params import Arg, Command, CommandArg, Depends -from nonebot.typing import T_State - -from configs.config import Config - -from .data_source import draw_word_cloud, get_list_msg - -__zx_plugin_name__ = "词云" - -__plugin_usage__ = """ -usage: - 词云 - 指令: - 今日词云:获取今天的词云 - 昨日词云:获取昨天的词云 - 本周词云:获取本周词云 - 本月词云:获取本月词云 - 年度词云:获取年度词云 - - 历史词云(支持 ISO8601 格式的日期与时间,如 2022-02-22T22:22:22) - 获取某日的词云 - 历史词云 2022-01-01 - 获取指定时间段的词云 - 历史词云 - 示例:历史词云 2022-01-01~2022-02-22 - 示例:历史词云 2022-02-22T11:11:11~2022-02-22T22:22:22 - - 如果想要获取自己的发言,可在命令前添加 我的 - 示例:我的今日词云 -""".strip() -__plugin_des__ = "词云" -__plugin_cmd__ = ["今日词云", "昨日词云", "本周词云"] -__plugin_version__ = 0.1 -__plugin_author__ = "yajiwa" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": __plugin_cmd__, -} -wordcloud_cmd = on_command( - "wordcloud", - aliases={ - "词云", - "今日词云", - "昨日词云", - "本周词云", - "本月词云", - "年度词云", - "历史词云", - "我的今日词云", - "我的昨日词云", - "我的本周词云", - "我的本月词云", - "我的年度词云", - "我的历史词云", - }, - block=True, - priority=5, -) -Config.add_plugin_config( - "word_clouds", - "WORD_CLOUDS_TEMPLATE", - 1, - help_="词云模板 参1:图片生成,默认使用真寻图片,可在项目路径resources/image/wordcloud下配置图片,多张则随机 | 参2/其他:黑底图片", - type=int, -) - - -def parse_datetime(key: str): - """解析数字,并将结果存入 state 中""" - - async def _key_parser( - matcher: Matcher, - state: T_State, - input_: Union[datetime, Message] = Arg(key), - ): - if isinstance(input_, datetime): - return - - plaintext = input_.extract_plain_text() - try: - state[key] = get_datetime_fromisoformat_with_timezone(plaintext) - except ValueError: - await matcher.reject_arg(key, "请输入正确的日期,不然我没法理解呢!") - - return _key_parser - - -def get_datetime_now_with_timezone() -> datetime: - """获取当前时间,并包含时区信息""" - return datetime.now().astimezone() - - -def get_datetime_fromisoformat_with_timezone(date_string: str) -> datetime: - """从 iso8601 格式字符串中获取时间,并包含时区信息""" - return datetime.fromisoformat(date_string).astimezone() - - -@wordcloud_cmd.handle() -async def handle_first_receive( - event: GroupMessageEvent, - state: T_State, - commands: Tuple[str, ...] = Command(), - args: Message = CommandArg(), -): - command = commands[0] - - if command.startswith("我的"): - state["my"] = True - command = command[2:] - else: - state["my"] = False - - if command == "今日词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) - state["stop"] = dt - elif command == "昨日词云": - dt = get_datetime_now_with_timezone() - state["stop"] = dt.replace(hour=0, minute=0, second=0, microsecond=0) - state["start"] = state["stop"] - timedelta(days=1) - elif command == "本周词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace( - hour=0, minute=0, second=0, microsecond=0 - ) - timedelta(days=dt.weekday()) - state["stop"] = dt - elif command == "本月词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0) - state["stop"] = dt - elif command == "年度词云": - dt = get_datetime_now_with_timezone() - state["start"] = dt.replace( - month=1, day=1, hour=0, minute=0, second=0, microsecond=0 - ) - state["stop"] = dt - elif command == "历史词云": - plaintext = args.extract_plain_text().strip() - match = re.match(r"^(.+?)(?:~(.+))?$", plaintext) - if match: - start = match.group(1) - stop = match.group(2) - try: - state["start"] = get_datetime_fromisoformat_with_timezone(start) - if stop: - state["stop"] = get_datetime_fromisoformat_with_timezone(stop) - else: - # 如果没有指定结束日期,则认为是指查询这一天的词云 - state["start"] = state["start"].replace( - hour=0, minute=0, second=0, microsecond=0 - ) - state["stop"] = state["start"] + timedelta(days=1) - except ValueError: - await wordcloud_cmd.finish("请输入正确的日期,不然我没法理解呢!") - else: - await wordcloud_cmd.finish() - - -@wordcloud_cmd.got( - "start", - prompt="请输入你要查询的起始日期(如 2022-01-01)", - parameterless=[Depends(parse_datetime("start"))], -) -@wordcloud_cmd.got( - "stop", - prompt="请输入你要查询的结束日期(如 2022-02-22)", - parameterless=[Depends(parse_datetime("stop"))], -) -async def handle_message( - event: GroupMessageEvent, - start: datetime = Arg(), - stop: datetime = Arg(), - my: bool = Arg(), -): - # 是否只查询自己的记录 - if my: - user_id = int(event.user_id) - else: - user_id = None - # 将时间转换到 东八 时区 - messages = await get_list_msg( - user_id, - int(event.group_id), - days=( - start.astimezone(pytz.timezone("Asia/Shanghai")), - stop.astimezone(pytz.timezone("Asia/Shanghai")), - ), - ) - if messages: - image_bytes = await draw_word_cloud(messages, get_driver().config) - if image_bytes: - await wordcloud_cmd.finish(MessageSegment.image(image_bytes), at_sender=my) - else: - await wordcloud_cmd.finish("生成词云失败", at_sender=my) - else: - await wordcloud_cmd.finish("没有获取到词云数据", at_sender=my) diff --git a/plugins/word_clouds/data_source.py b/plugins/word_clouds/data_source.py deleted file mode 100644 index 4f941fbf..00000000 --- a/plugins/word_clouds/data_source.py +++ /dev/null @@ -1,129 +0,0 @@ -import asyncio -import os -import random -import re -from io import BytesIO -from typing import List - -import jieba -import jieba.analyse -import matplotlib.pyplot as plt -import numpy as np -from emoji import replace_emoji # type: ignore -from PIL import Image as IMG -from wordcloud import ImageColorGenerator, WordCloud - -from configs.config import Config -from configs.path_config import FONT_PATH, IMAGE_PATH -from models.chat_history import ChatHistory -from services import logger -from utils.http_utils import AsyncHttpx - - -async def pre_precess(msg: List[str], config) -> str: - return await asyncio.get_event_loop().run_in_executor( - None, _pre_precess, msg, config - ) - - -def _pre_precess(msg: List[str], config) -> str: - """对消息进行预处理""" - # 过滤掉命令 - command_start = tuple([i for i in config.command_start if i]) - msg = " ".join([m for m in msg if not m.startswith(command_start)]) - - # 去除网址 - msg = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", msg) - - # 去除 \u200b - msg = re.sub(r"[\u200b]", "", msg) - - # 去除cq码 - msg = re.sub(r"\[CQ:.*?]", "", msg) - - # 去除[] - msg = re.sub("[ (1|3);]", "", msg) - - # 去除 emoji - # https://github.com/carpedm20/emoji - msg = replace_emoji(msg) - return msg - - -async def draw_word_cloud(messages, config): - wordcloud_dir = IMAGE_PATH / "wordcloud" - wordcloud_dir.mkdir(exist_ok=True, parents=True) - # 默认用真寻图片 - zx_logo_path = wordcloud_dir / "default.png" - wordcloud_ttf = FONT_PATH / "STKAITI.TTF" - if not os.listdir(wordcloud_dir): - url = "https://ghproxy.com/https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/resources/image/wordcloud/default.png" - try: - await AsyncHttpx.download_file(url, zx_logo_path) - except Exception as e: - logger.error(f"词云图片资源下载发生错误 {type(e)}:{e}") - return False - if not wordcloud_ttf.exists(): - ttf_url = "https://ghproxy.com/https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/resources/font/STKAITI.TTF" - try: - await AsyncHttpx.download_file(ttf_url, wordcloud_ttf) - except Exception as e: - logger.error(f"词云字体资源下载发生错误 {type(e)}:{e}") - return False - - topK = min(int(len(messages)), 100000) - read_name = jieba.analyse.extract_tags( - await pre_precess(messages, config), topK=topK, withWeight=True, allowPOS=() - ) - name = [] - value = [] - for t in read_name: - name.append(t[0]) - value.append(t[1]) - for i in range(len(name)): - name[i] = str(name[i]) - dic = dict(zip(name, value)) - if Config.get_config("word_clouds", "WORD_CLOUDS_TEMPLATE") == 1: - - def random_pic(base_path: str) -> str: - path_dir = os.listdir(base_path) - path = random.sample(path_dir, 1)[0] - return str(base_path) + "/" + str(path) - - mask = np.array(IMG.open(random_pic(wordcloud_dir))) - wc = WordCloud( - font_path=f"{wordcloud_ttf}", - background_color="white", - max_font_size=100, - width=1920, - height=1080, - mask=mask, - ) - wc.generate_from_frequencies(dic) - image_colors = ImageColorGenerator(mask, default_color=(255, 255, 255)) - wc.recolor(color_func=image_colors) - plt.imshow(wc.recolor(color_func=image_colors), interpolation="bilinear") - plt.axis("off") - else: - wc = WordCloud( - font_path=str(wordcloud_ttf), - width=1920, - height=1200, - background_color="black", - ) - wc.generate_from_frequencies(dic) - bytes_io = BytesIO() - img = wc.to_image() - img.save(bytes_io, format="PNG") - return bytes_io.getvalue() - - -async def get_list_msg(user_id, group_id, days): - messages_list = await ChatHistory().get_message( - uid=user_id, gid=group_id, type_="group", days=days - ) - if messages_list: - messages = [i.text for i in messages_list] - return messages - else: - return False diff --git a/plugins/yiqing/__init__.py b/plugins/yiqing/__init__.py deleted file mode 100755 index 0b8ffc5e..00000000 --- a/plugins/yiqing/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -from nonebot import on_command -from .data_source import get_yiqing_data, get_city_and_province_list -from services.log import logger -from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message -from nonebot.params import CommandArg -from configs.config import NICKNAME -from .other_than import get_other_data - -__zx_plugin_name__ = "疫情查询" -__plugin_usage__ = """ -usage: - 全国疫情查询 - 指令: - 疫情 中国/美国/英国... - 疫情 [省份/城市] - * 当省份与城市重名时,可在后添加 "市" 或 "省" * - 示例:疫情 吉林 <- [省] - 示例:疫情 吉林市 <- [市] -""".strip() -__plugin_des__ = "实时疫情数据查询" -__plugin_cmd__ = ["疫情 [省份/城市]", "疫情 中国"] -__plugin_type__ = ("一些工具",) -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier & yzyyz1387" -__plugin_settings__ = { - "level": 5, - "default_status": True, - "limit_superuser": False, - "cmd": ["查询疫情", "疫情", "疫情查询"], -} - - -yiqing = on_command("疫情", aliases={"查询疫情", "疫情查询"}, priority=5, block=True) - - -@yiqing.handle() -async def _(event: MessageEvent, arg: Message = CommandArg()): - msg = arg.extract_plain_text().strip() - city_and_province_list = get_city_and_province_list() - if msg: - if msg in city_and_province_list or msg[:-1] in city_and_province_list: - result = await get_yiqing_data(msg) - if result: - await yiqing.send(result) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情: {msg}" - ) - else: - await yiqing.send("查询失败!!!!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情失败" - ) - else: - rely = await get_other_data(msg) - if rely: - await yiqing.send(rely) - logger.info( - f"(USER {event.user_id}, GROUP " - f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询疫情成功" - ) - else: - await yiqing.send(f"{NICKNAME}没有查到{msg}的疫情查询...") diff --git a/plugins/yiqing/data_source.py b/plugins/yiqing/data_source.py deleted file mode 100755 index 9755ae53..00000000 --- a/plugins/yiqing/data_source.py +++ /dev/null @@ -1,110 +0,0 @@ -from configs.path_config import TEXT_PATH -from typing import List, Union -from utils.http_utils import AsyncHttpx -from utils.image_utils import text2image -from utils.message_builder import image -from nonebot.adapters.onebot.v11 import MessageSegment -import ujson as json - -china_city = TEXT_PATH / "china_city.json" - -data = {} - - -url = "https://api.inews.qq.com/newsqa/v1/query/inner/publish/modules/list?modules=diseaseh5Shelf" - - -async def get_yiqing_data(area: str) -> Union[str, MessageSegment]: - """ - 查看疫情数据 - :param area: 省份/城市 - """ - global data - province = None - city = None - province_type = "省" - if area == "中国": - province = area - province_type = "" - elif area[-1] == '省' or (area in data.keys() and area[-1] != "市"): - province = area if area[-1] != "省" else area[:-1] - if len(data[province]) == 1: - province_type = "市" - city = "" - else: - area = area[:-1] if area[-1] == "市" else area - for p in data.keys(): - if area in data[p]: - province = p - city = area - epidemic_data = (await AsyncHttpx.get(url)).json()["data"]["diseaseh5Shelf"] - last_update_time = epidemic_data["lastUpdateTime"] - if area == "中国": - data_ = epidemic_data["areaTree"][0] - else: - try: - data_ = [ - x - for x in epidemic_data["areaTree"][0]["children"] - if x["name"] == province - ][0] - if city: - data_ = [x for x in data_["children"] if x["name"] == city][0] - except IndexError: - return "未查询到..." - confirm = data_["total"]["confirm"] # 累计确诊 - heal = data_["total"]["heal"] # 累计治愈 - dead = data_["total"]["dead"] # 累计死亡 - now_confirm = data_["total"]["nowConfirm"] # 目前确诊 - add_confirm = data_["today"]["confirm"] # 新增确诊 - add_wzz = data_["today"]["wzz_add"] #新增无症状 - wzz=data_["total"]["wzz"] #目前无症状 - grade = "" - _grade_color = "" - # if data_["total"].get("grade"): - # grade = data_["total"]["grade"] - # if "中风险" in grade: - # _grade_color = "#fa9424" - # else: - # _grade_color = "red" - - dead_rate = f"{dead / confirm * 100:.2f}" # 死亡率 - heal_rate = f"{heal / confirm * 100:.2f}" # 治愈率 - - x = f"{city}市" if city else f"{province}{province_type}" - return image(b64=(await text2image( - f""" - {x} 疫情数据 {f"({grade})" if grade else ""}: - 目前确诊: - 确诊人数:{now_confirm}(+{add_confirm}) - 新增无症状:{add_wzz} - ----------------- - 累计数据: - 无症状人数:{wzz} - 确诊人数:{confirm} - 治愈人数:{heal} - 死亡人数:{dead} - 治愈率:{heal_rate}% - 死亡率:{dead_rate}% - 更新日期:{last_update_time} - """, font_size=30, color="#f9f6f2" - )).pic2bs4()) - - -def get_city_and_province_list() -> List[str]: - """ - 获取城市省份列表 - """ - global data - if not data: - try: - with open(china_city, "r", encoding="utf8") as f: - data = json.load(f) - except FileNotFoundError: - data = {} - city_list = ["中国"] - for p in data.keys(): - for c in data[p]: - city_list.append(c) - city_list.append(p) - return city_list diff --git a/plugins/yiqing/other_than.py b/plugins/yiqing/other_than.py deleted file mode 100644 index 28b5fca1..00000000 --- a/plugins/yiqing/other_than.py +++ /dev/null @@ -1,93 +0,0 @@ -# python3 -# -*- coding: utf-8 -*- -# @Time : 2021/12/23 23:04 -# @Author : yzyyz -# @Email : youzyyz1384@qq.com -# @File : other_than.py -# @Software: PyCharm -from utils.http_utils import AsyncHttpx -from nonebot.adapters.onebot.v11 import MessageSegment -from typing import Optional -from services.log import logger -from utils.image_utils import text2image -from utils.message_builder import image -from json.decoder import JSONDecodeError -import re -import json - -__doc__ = """爬虫实现国外疫情数据(找不到好接口)""" - - -def intcomma(value) -> str: - """ - 数字格式化 - """ - orig = str(value) - new = re.sub(r"^(-?\d+)(\d{3})", r"\g<1>,\g<2>", orig) - return new if orig == new else intcomma(new) - - -async def get_other_data(place: str, count: int = 0) -> Optional[MessageSegment]: - """ - :param place: 地名 - :param count: 递归次数 - :return: 格式化字符串 - """ - if count == 5: - return None - try: - html = ( - (await AsyncHttpx.get("https://news.ifeng.com/c/special/7uLj4F83Cqm")) - .text.replace("\n", "") - .replace(" ", "") - ) - find_data = re.compile(r"varallData=(.*?);") - sum_ = re.findall(find_data, html)[0] - sum_ = json.loads(sum_) - except JSONDecodeError: - return await get_other_data(place, count + 1) - except Exception as e: - logger.error(f"疫情查询发生错误 {type(e)}:{e}") - return None - try: - other_country = sum_["yiqing_v2"]["dataList"][29]["child"] - for country in other_country: - if place == country["name2"]: - return image( - b64=(await text2image( - f" {place} 疫情数据:\n" - "——————————————\n" - f" 新增病例:{intcomma(country['quezhen_add'])}\n" - f" 现有确诊:{intcomma(country['quezhen_xianyou'])}\n" - f" 累计确诊:{intcomma(country['quezhen'])}\n" - f" 累计治愈:{intcomma(country['zhiyu'])}\n" - f" 死亡:{intcomma(country['siwang'])}\n" - "——————————————" - # f"更新时间:{country['sys_publishDateTime']}" - # 时间无法精确到分钟,网页用了js我暂时找不到 - , - font_size=30, - color="#f9f6f2", - padding=15 - )).pic2bs4() - ) - else: - for city in country["child"]: - if place == city["name3"]: - return image( - b64=(await text2image( - f"\n{place} 疫情数据:\n" - "——————————————\n" - f"\t新增病例:{intcomma(city['quezhen_add'])}\n" - f"\t累计确诊:{intcomma(city['quezhen'])}\n" - f"\t累计治愈:{intcomma(city['zhiyu'])}\n" - f"\t死亡:{intcomma(city['siwang'])}\n" - "——————————————\n", - font_size=30, - color="#f9f6f2", - padding=15 - )).pic2bs4() - ) - except Exception as e: - logger.error(f"疫情查询发生错误 {type(e)}:{e}") - return None diff --git a/poetry.lock b/poetry.lock index 7d4904eb..f30884aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,90 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. - -[[package]] -name = "aiofiles" -version = "0.8.0" -description = "File support for asyncio." -category = "main" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "aiofiles-0.8.0-py3-none-any.whl", hash = "sha256:7a973fc22b29e9962d0897805ace5856e6a566ab1f0c8e5c91ff6c866519c937"}, - {file = "aiofiles-0.8.0.tar.gz", hash = "sha256:8334f23235248a3b2e83b2c3a78a22674f39969b96397126cc93664d9a901e59"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "aiohttp" -version = "3.7.4.post0" -description = "Async http client/server framework (asyncio)" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, - {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, -] - -[package.dependencies] -async-timeout = ">=3.0,<4.0" -attrs = ">=17.3.0" -chardet = ">=2.0,<5.0" -multidict = ">=4.5,<7.0" -typing-extensions = ">=3.6.5" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiosqlite" version = "0.17.0" description = "asyncio bridge to the standard sqlite3 module" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -102,25 +21,24 @@ reference = "ali" [[package]] name = "anyio" -version = "4.0.0" +version = "3.7.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"}, - {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"}, + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.22)"] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] [package.source] type = "legacy" @@ -131,7 +49,6 @@ reference = "ali" name = "apscheduler" version = "3.10.4" description = "In-process task scheduler with Cron-like capabilities" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -142,7 +59,7 @@ files = [ [package.dependencies] pytz = "*" six = ">=1.4.0" -tzlocal = ">=2.0,<3.0.0 || >=4.0.0" +tzlocal = ">=2.0,<3.dev0 || >=4.dev0" [package.extras] doc = ["sphinx", "sphinx-rtd-theme"] @@ -162,15 +79,82 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "async-timeout" -version = "3.0.1" -description = "Timeout context manager for asyncio programs" -category = "main" +name = "arclet-alconna" +version = "1.7.42" +description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.8" files = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, + {file = "arclet_alconna-1.7.42-py3-none-any.whl", hash = "sha256:fa78944121d4afa4e2c0247a98967ddb1e76cf63b94c8c3f4f393c52f6d23e75"}, + {file = "arclet_alconna-1.7.42.tar.gz", hash = "sha256:a5a1cca37d0c3d58607ee22485e636fa0b01d40eb43194e542b2c3d6a5d2e70b"}, +] + +[package.dependencies] +nepattern = ">=0.5.14,<0.6.0" +tarina = ">=0.4.1" +typing-extensions = ">=4.5.0" + +[package.extras] +full = ["arclet-alconna-tools (>=0.2.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "arclet-alconna-tools" +version = "0.6.11" +description = "Builtin Tools for Alconna" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arclet-alconna-tools-0.6.11.tar.gz", hash = "sha256:079f1ccd84120c65288e50014de2117a0dc7c52e5c2d2d718ad9fd95afb40232"}, + {file = "arclet_alconna_tools-0.6.11-py3-none-any.whl", hash = "sha256:d3bc7d70040fbc1c0b44a9b751089f87c6c487161d7c930dd4018d7ef468d91f"}, +] + +[package.dependencies] +arclet-alconna = ">=1.7.39" +nepattern = ">=0.5.15" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "arrow" +version = "1.3.0" +description = "Better dates & times for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, + {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, +] + +[package.dependencies] +python-dateutil = ">=2.7.0" +types-python-dateutil = ">=2.8.10" + +[package.extras] +doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, ] [package.source] @@ -180,57 +164,60 @@ reference = "ali" [[package]] name = "asyncpg" -version = "0.28.0" +version = "0.29.0" description = "An asyncio PostgreSQL driver" -category = "main" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "asyncpg-0.28.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a6d1b954d2b296292ddff4e0060f494bb4270d87fb3655dd23c5c6096d16d83"}, - {file = "asyncpg-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0740f836985fd2bd73dca42c50c6074d1d61376e134d7ad3ad7566c4f79f8184"}, - {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e907cf620a819fab1737f2dd90c0f185e2a796f139ac7de6aa3212a8af96c050"}, - {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b339984d55e8202e0c4b252e9573e26e5afa05617ed02252544f7b3e6de3e9"}, - {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c402745185414e4c204a02daca3d22d732b37359db4d2e705172324e2d94e85"}, - {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c88eef5e096296626e9688f00ab627231f709d0e7e3fb84bb4413dff81d996d7"}, - {file = "asyncpg-0.28.0-cp310-cp310-win32.whl", hash = "sha256:90a7bae882a9e65a9e448fdad3e090c2609bb4637d2a9c90bfdcebbfc334bf89"}, - {file = "asyncpg-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:76aacdcd5e2e9999e83c8fbcb748208b60925cc714a578925adcb446d709016c"}, - {file = "asyncpg-0.28.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0e08fe2c9b3618459caaef35979d45f4e4f8d4f79490c9fa3367251366af207"}, - {file = "asyncpg-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b24e521f6060ff5d35f761a623b0042c84b9c9b9fb82786aadca95a9cb4a893b"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99417210461a41891c4ff301490a8713d1ca99b694fef05dabd7139f9d64bd6c"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f029c5adf08c47b10bcdc857001bbef551ae51c57b3110964844a9d79ca0f267"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d6abf6c2f5152f46fff06b0e74f25800ce8ec6c80967f0bc789974de3c652"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d7fa81ada2807bc50fea1dc741b26a4e99258825ba55913b0ddbf199a10d69d8"}, - {file = "asyncpg-0.28.0-cp311-cp311-win32.whl", hash = "sha256:f33c5685e97821533df3ada9384e7784bd1e7865d2b22f153f2e4bd4a083e102"}, - {file = "asyncpg-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e7337c98fb493079d686a4a6965e8bcb059b8e1b8ec42106322fc6c1c889bb0"}, - {file = "asyncpg-0.28.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1c56092465e718a9fdcc726cc3d9dcf3a692e4834031c9a9f871d92a75d20d48"}, - {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4acd6830a7da0eb4426249d71353e8895b350daae2380cb26d11e0d4a01c5472"}, - {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63861bb4a540fa033a56db3bb58b0c128c56fad5d24e6d0a8c37cb29b17c1c7d"}, - {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a93a94ae777c70772073d0512f21c74ac82a8a49be3a1d982e3f259ab5f27307"}, - {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d14681110e51a9bc9c065c4e7944e8139076a778e56d6f6a306a26e740ed86d2"}, - {file = "asyncpg-0.28.0-cp37-cp37m-win32.whl", hash = "sha256:8aec08e7310f9ab322925ae5c768532e1d78cfb6440f63c078b8392a38aa636a"}, - {file = "asyncpg-0.28.0-cp37-cp37m-win_amd64.whl", hash = "sha256:319f5fa1ab0432bc91fb39b3960b0d591e6b5c7844dafc92c79e3f1bff96abef"}, - {file = "asyncpg-0.28.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b337ededaabc91c26bf577bfcd19b5508d879c0ad009722be5bb0a9dd30b85a0"}, - {file = "asyncpg-0.28.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d32b680a9b16d2957a0a3cc6b7fa39068baba8e6b728f2e0a148a67644578f4"}, - {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f62f04cdf38441a70f279505ef3b4eadf64479b17e707c950515846a2df197"}, - {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f20cac332c2576c79c2e8e6464791c1f1628416d1115935a34ddd7121bfc6a4"}, - {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:59f9712ce01e146ff71d95d561fb68bd2d588a35a187116ef05028675462d5ed"}, - {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc9e9f9ff1aa0eddcc3247a180ac9e9b51a62311e988809ac6152e8fb8097756"}, - {file = "asyncpg-0.28.0-cp38-cp38-win32.whl", hash = "sha256:9e721dccd3838fcff66da98709ed884df1e30a95f6ba19f595a3706b4bc757e3"}, - {file = "asyncpg-0.28.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ba7d06a0bea539e0487234511d4adf81dc8762249858ed2a580534e1720db00"}, - {file = "asyncpg-0.28.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d009b08602b8b18edef3a731f2ce6d3f57d8dac2a0a4140367e194eabd3de457"}, - {file = "asyncpg-0.28.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec46a58d81446d580fb21b376ec6baecab7288ce5a578943e2fc7ab73bf7eb39"}, - {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b48ceed606cce9e64fd5480a9b0b9a95cea2b798bb95129687abd8599c8b019"}, - {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8858f713810f4fe67876728680f42e93b7e7d5c7b61cf2118ef9153ec16b9423"}, - {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5e18438a0730d1c0c1715016eacda6e9a505fc5aa931b37c97d928d44941b4bf"}, - {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e9c433f6fcdd61c21a715ee9128a3ca48be8ac16fa07be69262f016bb0f4dbd2"}, - {file = "asyncpg-0.28.0-cp39-cp39-win32.whl", hash = "sha256:41e97248d9076bc8e4849da9e33e051be7ba37cd507cbd51dfe4b2d99c70e3dc"}, - {file = "asyncpg-0.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ed77f00c6aacfe9d79e9eff9e21729ce92a4b38e80ea99a58ed382f42ebd55b"}, - {file = "asyncpg-0.28.0.tar.gz", hash = "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, ] +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + [package.extras] docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] [package.source] type = "legacy" @@ -239,22 +226,22 @@ reference = "ali" [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [package.source] type = "legacy" @@ -262,58 +249,18 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "backports-zoneinfo" -version = "0.2.1" -description = "Backport of the standard library zoneinfo module" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"}, - {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"}, - {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"}, - {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"}, - {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"}, -] - -[package.extras] -tzdata = ["tzdata"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "beautifulsoup4" -version = "4.9.3" -description = "Screen-scraping library" -category = "main" +name = "binaryornot" +version = "0.4.4" +description = "Ultra-lightweight pure Python package to check if a file is binary or text." optional = false python-versions = "*" files = [ - {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, - {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, - {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, + {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, + {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] [package.dependencies] -soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""} - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] +chardet = ">=3.0.2" [package.source] type = "legacy" @@ -321,83 +268,23 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "bilireq" -version = "0.2.10" -description = "又一个哔哩哔哩请求库" -category = "main" +name = "cashews" +version = "6.4.0" +description = "cache tools with async power" optional = false -python-versions = "<4.0,>=3.8" +python-versions = ">=3.8" files = [ - {file = "bilireq-0.2.10-py3-none-any.whl", hash = "sha256:685da236c2c3b0416a9172f81193180b43a4bcc259e88b27a68c0cde9ccee2c0"}, - {file = "bilireq-0.2.10.tar.gz", hash = "sha256:b5a4e825bb9b415792453d29861f0f0908b583f55a7c31ddca3c39e792ccc154"}, + {file = "cashews-6.4.0-py3-none-any.whl", hash = "sha256:6b7121a0629a17aa72d22bf4007462a9fbcdcd418b8ec1083f2806950c265e58"}, + {file = "cashews-6.4.0.tar.gz", hash = "sha256:0f5ec89b4e8d2944e9403c5fc24fb2947003d279e338de40f2fd3ebc9145c4e3"}, ] -[package.dependencies] -grpcio = ">=1.56.2" -httpx = "*" -protobuf = ">=4.23.4" -rsa = ">=4.9" - [package.extras] -qrcode = ["qrcode[pil]"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "black" -version = "22.12.0" -description = "The uncompromising code formatter." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "cachetools" -version = "5.3.1" -description = "Extensible memoizing collections and decorators" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, -] +dill = ["dill"] +diskcache = ["diskcache (>=5.0.0)"] +lint = ["mypy (>=1.5.0)", "types-redis"] +redis = ["redis (>=4.3.1,!=5.0.1)"] +speedup = ["bitarray (<3.0.0)", "hiredis", "xxhash (<4.0.0)"] +tests = ["hypothesis", "pytest", "pytest-asyncio (==0.23.3)"] [package.source] type = "legacy" @@ -406,19 +293,28 @@ reference = "ali" [[package]] name = "cattrs" -version = "22.2.0" +version = "23.2.3" description = "Composable complex class support for attrs and dataclasses." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "cattrs-22.2.0-py3-none-any.whl", hash = "sha256:bc12b1f0d000b9f9bee83335887d532a1d3e99a833d1bf0882151c97d3e68c21"}, - {file = "cattrs-22.2.0.tar.gz", hash = "sha256:f0eed5642399423cf656e7b66ce92cdc5b963ecafd041d1b24d136fdde7acf6d"}, + {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"}, + {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"}, ] [package.dependencies] -attrs = ">=20" -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +attrs = ">=23.1.0" +exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} + +[package.extras] +bson = ["pymongo (>=4.4.0)"] +cbor2 = ["cbor2 (>=5.4.6)"] +msgpack = ["msgpack (>=1.0.5)"] +orjson = ["orjson (>=3.9.2)"] +pyyaml = ["pyyaml (>=6.0)"] +tomlkit = ["tomlkit (>=0.11.8)"] +ujson = ["ujson (>=5.7.0)"] [package.source] type = "legacy" @@ -427,14 +323,13 @@ reference = "ali" [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [package.source] @@ -444,14 +339,117 @@ reference = "ali" [[package]] name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" +version = "5.2.0" +description = "Universal encoding detector for Python 3" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [package.source] @@ -463,7 +461,6 @@ reference = "ali" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -479,32 +476,10 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "cn2an" -version = "0.5.22" -description = "Convert Chinese numerals and Arabic numerals." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cn2an-0.5.22-py3-none-any.whl", hash = "sha256:cba4c8f305b43da01f50696047cca3116c727424ac62338da6a3426e01454f3e"}, - {file = "cn2an-0.5.22.tar.gz", hash = "sha256:27ae5b56441d7329ed2ececffa026bfa8fc353dcf1fb0d9146b303b9cce3ac37"}, -] - -[package.dependencies] -proces = ">=0.1.3" -setuptools = ">=47.3.1" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -518,128 +493,25 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "commonmark" -version = "0.9.1" -description = "Python parser for the CommonMark Markdown spec" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] - -[package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "cycler" -version = "0.11.0" -description = "Composable style cycles" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "dateparser" -version = "1.1.8" -description = "Date parsing library designed to parse dates from HTML pages" -category = "main" +name = "cookiecutter" +version = "2.5.0" +description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." optional = false python-versions = ">=3.7" files = [ - {file = "dateparser-1.1.8-py2.py3-none-any.whl", hash = "sha256:070b29b5bbf4b1ec2cd51c96ea040dc68a614de703910a91ad1abba18f9f379f"}, - {file = "dateparser-1.1.8.tar.gz", hash = "sha256:86b8b7517efcc558f085a142cdb7620f0921543fcabdb538c8a4c4001d8178e3"}, + {file = "cookiecutter-2.5.0-py3-none-any.whl", hash = "sha256:8aa2f12ed11bc05628651e9dc4353a10571dd9908aaaaeec959a2b9ea465a5d2"}, + {file = "cookiecutter-2.5.0.tar.gz", hash = "sha256:e61e9034748e3f41b8bd2c11f00d030784b48711c4d5c42363c50989a65331ec"}, ] [package.dependencies] -python-dateutil = "*" -pytz = "*" -regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" -tzlocal = "*" - -[package.extras] -calendars = ["convertdate", "hijri-converter"] -fasttext = ["fasttext"] -langdetect = ["langdetect"] +arrow = "*" +binaryornot = ">=0.4.4" +click = ">=7.0,<9.0.0" +Jinja2 = ">=2.7,<4.0.0" +python-slugify = ">=4.0.0" +pyyaml = ">=5.3.1" +requests = ">=2.23.0" +rich = "*" [package.source] type = "legacy" @@ -647,43 +519,16 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "ecdsa" -version = "0.18.0" -description = "ECDSA cryptographic signature library (pure python)" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, -] - -[package.dependencies] -six = ">=1.9.0" - -[package.extras] -gmpy = ["gmpy"] -gmpy2 = ["gmpy2"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "emoji" -version = "1.7.0" -description = "Emoji for Python" -category = "main" +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "emoji-1.7.0.tar.gz", hash = "sha256:65c54533ea3c78f30d0729288998715f418d7467de89ec258a31c0ce8660a1d1"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] -[package.extras] -dev = ["coverage", "coveralls", "pytest"] - [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -691,14 +536,13 @@ reference = "ali" [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -710,109 +554,35 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "fastapi" -version = "0.95.1" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"}, - {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"}, -] - -[package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = ">=0.26.1,<0.27.0" - -[package.extras] -all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "feedparser" -version = "6.0.10" -description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "feedparser-6.0.10-py3-none-any.whl", hash = "sha256:79c257d526d13b944e965f6095700587f27388e50ea16fd245babe4dfae7024f"}, - {file = "feedparser-6.0.10.tar.gz", hash = "sha256:27da485f4637ce7163cdeab13a80312b93b7d0c1b775bef4a47629a3110bca51"}, -] - -[package.dependencies] -sgmllib3k = "*" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "fonttools" -version = "4.42.1" -description = "Tools to manipulate font files" -category = "main" +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ed1a13a27f59d1fc1920394a7f596792e9d546c9ca5a044419dca70c37815d7c"}, - {file = "fonttools-4.42.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b1ce7a45978b821a06d375b83763b27a3a5e8a2e4570b3065abad240a18760"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f720fa82a11c0f9042376fd509b5ed88dab7e3cd602eee63a1af08883b37342b"}, - {file = "fonttools-4.42.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db55cbaea02a20b49fefbd8e9d62bd481aaabe1f2301dabc575acc6b358874fa"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a35981d90feebeaef05e46e33e6b9e5b5e618504672ca9cd0ff96b171e4bfff"}, - {file = "fonttools-4.42.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:68a02bbe020dc22ee0540e040117535f06df9358106d3775e8817d826047f3fd"}, - {file = "fonttools-4.42.1-cp310-cp310-win32.whl", hash = "sha256:12a7c247d1b946829bfa2f331107a629ea77dc5391dfd34fdcd78efa61f354ca"}, - {file = "fonttools-4.42.1-cp310-cp310-win_amd64.whl", hash = "sha256:a398bdadb055f8de69f62b0fc70625f7cbdab436bbb31eef5816e28cab083ee8"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:689508b918332fb40ce117131633647731d098b1b10d092234aa959b4251add5"}, - {file = "fonttools-4.42.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e36344e48af3e3bde867a1ca54f97c308735dd8697005c2d24a86054a114a71"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19b7db825c8adee96fac0692e6e1ecd858cae9affb3b4812cdb9d934a898b29e"}, - {file = "fonttools-4.42.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:113337c2d29665839b7d90b39f99b3cac731f72a0eda9306165a305c7c31d341"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:37983b6bdab42c501202500a2be3a572f50d4efe3237e0686ee9d5f794d76b35"}, - {file = "fonttools-4.42.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6ed2662a3d9c832afa36405f8748c250be94ae5dfc5283d668308391f2102861"}, - {file = "fonttools-4.42.1-cp311-cp311-win32.whl", hash = "sha256:179737095eb98332a2744e8f12037b2977f22948cf23ff96656928923ddf560a"}, - {file = "fonttools-4.42.1-cp311-cp311-win_amd64.whl", hash = "sha256:f2b82f46917d8722e6b5eafeefb4fb585d23babd15d8246c664cd88a5bddd19c"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:62f481ac772fd68901573956231aea3e4b1ad87b9b1089a61613a91e2b50bb9b"}, - {file = "fonttools-4.42.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2f806990160d1ce42d287aa419df3ffc42dfefe60d473695fb048355fe0c6a0"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db372213d39fa33af667c2aa586a0c1235e88e9c850f5dd5c8e1f17515861868"}, - {file = "fonttools-4.42.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d18fc642fd0ac29236ff88ecfccff229ec0386090a839dd3f1162e9a7944a40"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8708b98c278012ad267ee8a7433baeb809948855e81922878118464b274c909d"}, - {file = "fonttools-4.42.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c95b0724a6deea2c8c5d3222191783ced0a2f09bd6d33f93e563f6f1a4b3b3a4"}, - {file = "fonttools-4.42.1-cp38-cp38-win32.whl", hash = "sha256:4aa79366e442dbca6e2c8595645a3a605d9eeabdb7a094d745ed6106816bef5d"}, - {file = "fonttools-4.42.1-cp38-cp38-win_amd64.whl", hash = "sha256:acb47f6f8680de24c1ab65ebde39dd035768e2a9b571a07c7b8da95f6c8815fd"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb289b7a815638a7613d46bcf324c9106804725b2bb8ad913c12b6958ffc4ec"}, - {file = "fonttools-4.42.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:53eb5091ddc8b1199330bb7b4a8a2e7995ad5d43376cadce84523d8223ef3136"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46a0ec8adbc6ff13494eb0c9c2e643b6f009ce7320cf640de106fb614e4d4360"}, - {file = "fonttools-4.42.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cc7d685b8eeca7ae69dc6416833fbfea61660684b7089bca666067cb2937dcf"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:be24fcb80493b2c94eae21df70017351851652a37de514de553435b256b2f249"}, - {file = "fonttools-4.42.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:515607ec756d7865f23070682622c49d922901943697871fc292277cf1e71967"}, - {file = "fonttools-4.42.1-cp39-cp39-win32.whl", hash = "sha256:0eb79a2da5eb6457a6f8ab904838454accc7d4cccdaff1fd2bd3a0679ea33d64"}, - {file = "fonttools-4.42.1-cp39-cp39-win_amd64.whl", hash = "sha256:7286aed4ea271df9eab8d7a9b29e507094b51397812f7ce051ecd77915a6e26b"}, - {file = "fonttools-4.42.1-py3-none-any.whl", hash = "sha256:9398f244e28e0596e2ee6024f808b06060109e33ed38dcc9bded452fd9bbb853"}, - {file = "fonttools-4.42.1.tar.gz", hash = "sha256:c391cd5af88aacaf41dd7cfb96eeedfad297b5899a39e12f4c2c3706d0a3329d"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "fleep" +version = "1.0.1" +description = "File format determination library" +optional = false +python-versions = ">=3.1" +files = [ + {file = "fleep-1.0.1.tar.gz", hash = "sha256:c8f62b258ee5364d7f6c1ed1f3f278e99020fc3f0a60a24ad1e10846e31d104c"}, +] [package.source] type = "legacy" @@ -821,144 +591,74 @@ reference = "ali" [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.3" description = "Lightweight in-process concurrent programming" -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" -files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, -] - -[package.extras] -docs = ["Sphinx", "docutils (<0.18)"] -test = ["objgraph", "psutil"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "grpcio" -version = "1.57.0" -description = "HTTP/2-based RPC framework" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "grpcio-1.57.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:092fa155b945015754bdf988be47793c377b52b88d546e45c6a9f9579ac7f7b6"}, - {file = "grpcio-1.57.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2f7349786da979a94690cc5c2b804cab4e8774a3cf59be40d037c4342c906649"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:82640e57fb86ea1d71ea9ab54f7e942502cf98a429a200b2e743d8672171734f"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40b72effd4c789de94ce1be2b5f88d7b9b5f7379fe9645f198854112a6567d9a"}, - {file = "grpcio-1.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f708a6a17868ad8bf586598bee69abded4996b18adf26fd2d91191383b79019"}, - {file = "grpcio-1.57.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:60fe15288a0a65d5c1cb5b4a62b1850d07336e3ba728257a810317be14f0c527"}, - {file = "grpcio-1.57.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6907b1cf8bb29b058081d2aad677b15757a44ef2d4d8d9130271d2ad5e33efca"}, - {file = "grpcio-1.57.0-cp310-cp310-win32.whl", hash = "sha256:57b183e8b252825c4dd29114d6c13559be95387aafc10a7be645462a0fc98bbb"}, - {file = "grpcio-1.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7b400807fa749a9eb286e2cd893e501b110b4d356a218426cb9c825a0474ca56"}, - {file = "grpcio-1.57.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:c6ebecfb7a31385393203eb04ed8b6a08f5002f53df3d59e5e795edb80999652"}, - {file = "grpcio-1.57.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:00258cbe3f5188629828363ae8ff78477ce976a6f63fb2bb5e90088396faa82e"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:23e7d8849a0e58b806253fd206ac105b328171e01b8f18c7d5922274958cc87e"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5371bcd861e679d63b8274f73ac281751d34bd54eccdbfcd6aa00e692a82cd7b"}, - {file = "grpcio-1.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aed90d93b731929e742967e236f842a4a2174dc5db077c8f9ad2c5996f89f63e"}, - {file = "grpcio-1.57.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe752639919aad9ffb0dee0d87f29a6467d1ef764f13c4644d212a9a853a078d"}, - {file = "grpcio-1.57.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fada6b07ec4f0befe05218181f4b85176f11d531911b64c715d1875c4736d73a"}, - {file = "grpcio-1.57.0-cp311-cp311-win32.whl", hash = "sha256:bb396952cfa7ad2f01061fbc7dc1ad91dd9d69243bcb8110cf4e36924785a0fe"}, - {file = "grpcio-1.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:e503cb45ed12b924b5b988ba9576dc9949b2f5283b8e33b21dcb6be74a7c58d0"}, - {file = "grpcio-1.57.0-cp37-cp37m-linux_armv7l.whl", hash = "sha256:fd173b4cf02b20f60860dc2ffe30115c18972d7d6d2d69df97ac38dee03be5bf"}, - {file = "grpcio-1.57.0-cp37-cp37m-macosx_10_10_universal2.whl", hash = "sha256:d7f8df114d6b4cf5a916b98389aeaf1e3132035420a88beea4e3d977e5f267a5"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_aarch64.whl", hash = "sha256:76c44efa4ede1f42a9d5b2fed1fe9377e73a109bef8675fb0728eb80b0b8e8f2"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4faea2cfdf762a664ab90589b66f416274887641ae17817de510b8178356bf73"}, - {file = "grpcio-1.57.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60b83c43faeb6d0a9831f0351d7787a0753f5087cc6fa218d78fdf38e5acef0"}, - {file = "grpcio-1.57.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b363bbb5253e5f9c23d8a0a034dfdf1b7c9e7f12e602fc788c435171e96daccc"}, - {file = "grpcio-1.57.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f1fb0fd4a1e9b11ac21c30c169d169ef434c6e9344ee0ab27cfa6f605f6387b2"}, - {file = "grpcio-1.57.0-cp37-cp37m-win_amd64.whl", hash = "sha256:34950353539e7d93f61c6796a007c705d663f3be41166358e3d88c45760c7d98"}, - {file = "grpcio-1.57.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:871f9999e0211f9551f368612460442a5436d9444606184652117d6a688c9f51"}, - {file = "grpcio-1.57.0-cp38-cp38-macosx_10_10_universal2.whl", hash = "sha256:a8a8e560e8dbbdf29288872e91efd22af71e88b0e5736b0daf7773c1fecd99f0"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:2313b124e475aa9017a9844bdc5eafb2d5abdda9d456af16fc4535408c7d6da6"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4098b6b638d9e0ca839a81656a2fd4bc26c9486ea707e8b1437d6f9d61c3941"}, - {file = "grpcio-1.57.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e5b58e32ae14658085c16986d11e99abd002ddbf51c8daae8a0671fffb3467f"}, - {file = "grpcio-1.57.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0f80bf37f09e1caba6a8063e56e2b87fa335add314cf2b78ebf7cb45aa7e3d06"}, - {file = "grpcio-1.57.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5b7a4ce8f862fe32b2a10b57752cf3169f5fe2915acfe7e6a1e155db3da99e79"}, - {file = "grpcio-1.57.0-cp38-cp38-win32.whl", hash = "sha256:9338bacf172e942e62e5889b6364e56657fbf8ac68062e8b25c48843e7b202bb"}, - {file = "grpcio-1.57.0-cp38-cp38-win_amd64.whl", hash = "sha256:e1cb52fa2d67d7f7fab310b600f22ce1ff04d562d46e9e0ac3e3403c2bb4cc16"}, - {file = "grpcio-1.57.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:fee387d2fab144e8a34e0e9c5ca0f45c9376b99de45628265cfa9886b1dbe62b"}, - {file = "grpcio-1.57.0-cp39-cp39-macosx_10_10_universal2.whl", hash = "sha256:b53333627283e7241fcc217323f225c37783b5f0472316edcaa4479a213abfa6"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:f19ac6ac0a256cf77d3cc926ef0b4e64a9725cc612f97228cd5dc4bd9dbab03b"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fdf04e402f12e1de8074458549337febb3b45f21076cc02ef4ff786aff687e"}, - {file = "grpcio-1.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5613a2fecc82f95d6c51d15b9a72705553aa0d7c932fad7aed7afb51dc982ee5"}, - {file = "grpcio-1.57.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b670c2faa92124b7397b42303e4d8eb64a4cd0b7a77e35a9e865a55d61c57ef9"}, - {file = "grpcio-1.57.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a635589201b18510ff988161b7b573f50c6a48fae9cb567657920ca82022b37"}, - {file = "grpcio-1.57.0-cp39-cp39-win32.whl", hash = "sha256:d78d8b86fcdfa1e4c21f8896614b6cc7ee01a2a758ec0c4382d662f2a62cf766"}, - {file = "grpcio-1.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:20ec6fc4ad47d1b6e12deec5045ec3cd5402d9a1597f738263e98f490fe07056"}, - {file = "grpcio-1.57.0.tar.gz", hash = "sha256:4b089f7ad1eb00a104078bab8015b0ed0ebcb3b589e527ab009c53893fd4e613"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.57.0)"] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] [package.source] type = "legacy" @@ -969,7 +669,6 @@ reference = "ali" name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -984,78 +683,24 @@ reference = "ali" [[package]] name = "httpcore" -version = "0.16.3" +version = "1.0.2" description = "A minimal low-level HTTP client." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] -anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" [package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "httptools" -version = "0.6.0" -description = "A collection of framework independent HTTP protocol utils." -category = "main" -optional = false -python-versions = ">=3.5.0" -files = [ - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:818325afee467d483bfab1647a72054246d29f9053fd17cc4b86cda09cc60339"}, - {file = "httptools-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72205730bf1be875003692ca54a4a7c35fac77b4746008966061d9d41a61b0f5"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33eb1d4e609c835966e969a31b1dedf5ba16b38cab356c2ce4f3e33ffa94cad3"}, - {file = "httptools-0.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdc6675ec6cb79d27e0575750ac6e2b47032742e24eed011b8db73f2da9ed40"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:463c3bc5ef64b9cf091be9ac0e0556199503f6e80456b790a917774a616aff6e"}, - {file = "httptools-0.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82f228b88b0e8c6099a9c4757ce9fdbb8b45548074f8d0b1f0fc071e35655d1c"}, - {file = "httptools-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:0781fedc610293a2716bc7fa142d4c85e6776bc59d617a807ff91246a95dea35"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:721e503245d591527cddd0f6fd771d156c509e831caa7a57929b55ac91ee2b51"}, - {file = "httptools-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:274bf20eeb41b0956e34f6a81f84d26ed57c84dd9253f13dcb7174b27ccd8aaf"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:259920bbae18740a40236807915def554132ad70af5067e562f4660b62c59b90"}, - {file = "httptools-0.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f959e4770b3fc8ee4dbc3578fd910fab9003e093f20ac8c621452c4d62e517cb"}, - {file = "httptools-0.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e22896b42b95b3237eccc42278cd72c0df6f23247d886b7ded3163452481e38"}, - {file = "httptools-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:38f3cafedd6aa20ae05f81f2e616ea6f92116c8a0f8dcb79dc798df3356836e2"}, - {file = "httptools-0.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:47043a6e0ea753f006a9d0dd076a8f8c99bc0ecae86a0888448eb3076c43d717"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a541579bed0270d1ac10245a3e71e5beeb1903b5fbbc8d8b4d4e728d48ff1d"}, - {file = "httptools-0.6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65d802e7b2538a9756df5acc062300c160907b02e15ed15ba035b02bce43e89c"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:26326e0a8fe56829f3af483200d914a7cd16d8d398d14e36888b56de30bec81a"}, - {file = "httptools-0.6.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e41ccac9e77cd045f3e4ee0fc62cbf3d54d7d4b375431eb855561f26ee7a9ec4"}, - {file = "httptools-0.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4e748fc0d5c4a629988ef50ac1aef99dfb5e8996583a73a717fc2cac4ab89932"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cf8169e839a0d740f3d3c9c4fa630ac1a5aaf81641a34575ca6773ed7ce041a1"}, - {file = "httptools-0.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dcc14c090ab57b35908d4a4585ec5c0715439df07be2913405991dbb37e049d"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0b0571806a5168013b8c3d180d9f9d6997365a4212cb18ea20df18b938aa0b"}, - {file = "httptools-0.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb4a608c631f7dcbdf986f40af7a030521a10ba6bc3d36b28c1dc9e9035a3c0"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93f89975465133619aea8b1952bc6fa0e6bad22a447c6d982fc338fbb4c89649"}, - {file = "httptools-0.6.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:73e9d66a5a28b2d5d9fbd9e197a31edd02be310186db423b28e6052472dc8201"}, - {file = "httptools-0.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:22c01fcd53648162730a71c42842f73b50f989daae36534c818b3f5050b54589"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f96d2a351b5625a9fd9133c95744e8ca06f7a4f8f0b8231e4bbaae2c485046a"}, - {file = "httptools-0.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72ec7c70bd9f95ef1083d14a755f321d181f046ca685b6358676737a5fecd26a"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b703d15dbe082cc23266bf5d9448e764c7cb3fcfe7cb358d79d3fd8248673ef9"}, - {file = "httptools-0.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c723ed5982f8ead00f8e7605c53e55ffe47c47465d878305ebe0082b6a1755"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0a816bb425c116a160fbc6f34cece097fd22ece15059d68932af686520966bd"}, - {file = "httptools-0.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:dea66d94e5a3f68c5e9d86e0894653b87d952e624845e0b0e3ad1c733c6cc75d"}, - {file = "httptools-0.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:23b09537086a5a611fad5696fc8963d67c7e7f98cb329d38ee114d588b0b74cd"}, - {file = "httptools-0.6.0.tar.gz", hash = "sha256:9fc6e409ad38cbd68b177cd5158fc4042c796b82ca88d99ec78f07bed6c6b796"}, -] - -[package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] [package.source] type = "legacy" @@ -1064,27 +709,27 @@ reference = "ali" [[package]] name = "httpx" -version = "0.23.3" +version = "0.26.0" description = "The next generation HTTP client." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, + {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, ] [package.dependencies] +anyio = "*" certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = "==1.*" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [package.source] type = "legacy" @@ -1093,14 +738,13 @@ reference = "ali" [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [package.source] @@ -1108,83 +752,10 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "imagehash" -version = "4.3.1" -description = "Image Hashing library" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "ImageHash-4.3.1-py2.py3-none-any.whl", hash = "sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5"}, - {file = "ImageHash-4.3.1.tar.gz", hash = "sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70"}, -] - -[package.dependencies] -numpy = "*" -pillow = "*" -PyWavelets = "*" -scipy = "*" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "importlib-metadata" -version = "6.8.0" -description = "Read metadata from Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"}, - {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "importlib-resources" -version = "6.0.1" -description = "Read resources from Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"}, - {file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "iso8601" version = "1.1.0" description = "Simple module to parse ISO 8601 dates" -category = "main" optional = false python-versions = ">=3.6.2,<4.0" files = [ @@ -1197,32 +768,15 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "jieba" -version = "0.42.1" -description = "Chinese Words Segmentation Utilities" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] @@ -1236,135 +790,15 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "kiwisolver" -version = "1.4.5" -description = "A fast implementation of the Cassowary constraint solver" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, - {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "loguru" -version = "0.7.1" +version = "0.7.2" description = "Python logging made (stupidly) simple" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "loguru-0.7.1-py3-none-any.whl", hash = "sha256:046bf970cb3cad77a28d607cbf042ac25a407db987a1e801c7f7e692469982f9"}, - {file = "loguru-0.7.1.tar.gz", hash = "sha256:7ba2a7d81b79a412b0ded69bd921e012335e80fd39937a633570f273a343579e"}, + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, ] [package.dependencies] @@ -1372,7 +806,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "pre-commit (==3.3.1)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] [package.source] type = "legacy" @@ -1380,104 +814,28 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "lxml" -version = "4.6.5" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +python-versions = ">=3.8" files = [ - {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, - {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, - {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, - {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, - {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, - {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, - {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, - {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, - {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, - {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, - {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, - {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, - {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, - {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, - {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, - {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, - {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, - {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, - {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, - {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, - {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, - {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, - {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, - {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, - {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, - {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.7)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "markdown" -version = "3.4.4" -description = "Python implementation of John Gruber's Markdown." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "Markdown-3.4.4-py3-none-any.whl", hash = "sha256:a4c1b65c0957b4bd9e7d86ddc7b3c9868fb9670660f6f99f6d1bca8954d5a941"}, - {file = "Markdown-3.4.4.tar.gz", hash = "sha256:225c6123522495d4119a90b3a3ba31a1e87a70369e03f14799ea9c0d7183a3d6"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +mdurl = ">=0.1,<1.0" [package.extras] -docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.0)", "mkdocs-nature (>=0.4)"] -testing = ["coverage", "pyyaml"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [package.source] type = "legacy" @@ -1486,62 +844,71 @@ reference = "ali" [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.4" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, + {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, + {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, + {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, + {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, + {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, + {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, + {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, ] [package.source] @@ -1550,68 +917,16 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "matplotlib" -version = "3.7.2" -description = "Python plotting package" -category = "main" +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:2699f7e73a76d4c110f4f25be9d2496d6ab4f17345307738557d345f099e07de"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a8035ba590658bae7562786c9cc6ea1a84aa49d3afab157e414c9e2ea74f496d"}, - {file = "matplotlib-3.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8e4a49493add46ad4a8c92f63e19d548b2b6ebbed75c6b4c7f46f57d36cdd1"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71667eb2ccca4c3537d9414b1bc00554cb7f91527c17ee4ec38027201f8f1603"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:152ee0b569a37630d8628534c628456b28686e085d51394da6b71ef84c4da201"}, - {file = "matplotlib-3.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f8dddd1f5939e60aacb8fa08f19551f4b0140fab16a3669d5cd6e9cb28fc8"}, - {file = "matplotlib-3.7.2-cp310-cp310-win32.whl", hash = "sha256:fdbb46fad4fb47443b5b8ac76904b2e7a66556844f33370861b4788db0f8816a"}, - {file = "matplotlib-3.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:23fb1750934e5f0128f9423db27c474aa32534cec21f7b2153262b066a581fd1"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:30e1409b857aa8a747c5d4f85f63a79e479835f8dffc52992ac1f3f25837b544"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:50e0a55ec74bf2d7a0ebf50ac580a209582c2dd0f7ab51bc270f1b4a0027454e"}, - {file = "matplotlib-3.7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ac60daa1dc83e8821eed155796b0f7888b6b916cf61d620a4ddd8200ac70cd64"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305e3da477dc8607336ba10bac96986d6308d614706cae2efe7d3ffa60465b24"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c308b255efb9b06b23874236ec0f10f026673ad6515f602027cc8ac7805352d"}, - {file = "matplotlib-3.7.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60c521e21031632aa0d87ca5ba0c1c05f3daacadb34c093585a0be6780f698e4"}, - {file = "matplotlib-3.7.2-cp311-cp311-win32.whl", hash = "sha256:26bede320d77e469fdf1bde212de0ec889169b04f7f1179b8930d66f82b30cbc"}, - {file = "matplotlib-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:af4860132c8c05261a5f5f8467f1b269bf1c7c23902d75f2be57c4a7f2394b3e"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:a1733b8e84e7e40a9853e505fe68cc54339f97273bdfe6f3ed980095f769ddc7"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d9881356dc48e58910c53af82b57183879129fa30492be69058c5b0d9fddf391"}, - {file = "matplotlib-3.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f081c03f413f59390a80b3e351cc2b2ea0205839714dbc364519bcf51f4b56ca"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cd120fca3407a225168238b790bd5c528f0fafde6172b140a2f3ab7a4ea63e9"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a2c1590b90aa7bd741b54c62b78de05d4186271e34e2377e0289d943b3522273"}, - {file = "matplotlib-3.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d2ff3c984b8a569bc1383cd468fc06b70d7b59d5c2854ca39f1436ae8394117"}, - {file = "matplotlib-3.7.2-cp38-cp38-win32.whl", hash = "sha256:5dea00b62d28654b71ca92463656d80646675628d0828e08a5f3b57e12869e13"}, - {file = "matplotlib-3.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f506a1776ee94f9e131af1ac6efa6e5bc7cb606a3e389b0ccb6e657f60bb676"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:6515e878f91894c2e4340d81f0911857998ccaf04dbc1bba781e3d89cbf70608"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:71f7a8c6b124e904db550f5b9fe483d28b896d4135e45c4ea381ad3b8a0e3256"}, - {file = "matplotlib-3.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12f01b92ecd518e0697da4d97d163b2b3aa55eb3eb4e2c98235b3396d7dad55f"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7e28d6396563955f7af437894a36bf2b279462239a41028323e04b85179058b"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbcf59334ff645e6a67cd5f78b4b2cdb76384cdf587fa0d2dc85f634a72e1a3e"}, - {file = "matplotlib-3.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:318c89edde72ff95d8df67d82aca03861240512994a597a435a1011ba18dbc7f"}, - {file = "matplotlib-3.7.2-cp39-cp39-win32.whl", hash = "sha256:ce55289d5659b5b12b3db4dc9b7075b70cef5631e56530f14b2945e8836f2d20"}, - {file = "matplotlib-3.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:2ecb5be2b2815431c81dc115667e33da0f5a1bcf6143980d180d09a717c4a12e"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fdcd28360dbb6203fb5219b1a5658df226ac9bebc2542a9e8f457de959d713d0"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3cca3e842b11b55b52c6fb8bd6a4088693829acbfcdb3e815fa9b7d5c92c1b"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebf577c7a6744e9e1bd3fee45fc74a02710b214f94e2bde344912d85e0c9af7c"}, - {file = "matplotlib-3.7.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:936bba394682049919dda062d33435b3be211dc3dcaa011e09634f060ec878b2"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bc221ffbc2150458b1cd71cdd9ddd5bb37962b036e41b8be258280b5b01da1dd"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35d74ebdb3f71f112b36c2629cf32323adfbf42679e2751252acd468f5001c07"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717157e61b3a71d3d26ad4e1770dc85156c9af435659a25ee6407dc866cb258d"}, - {file = "matplotlib-3.7.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:20f844d6be031948148ba49605c8b96dfe7d3711d1b63592830d650622458c11"}, - {file = "matplotlib-3.7.2.tar.gz", hash = "sha256:a8cdb91dddb04436bd2f098b8fdf4b81352e68cf4d2c6756fcc414791076569b"}, + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} -kiwisolver = ">=1.0.1" -numpy = ">=1.20" -packaging = ">=20.0" -pillow = ">=6.2.0" -pyparsing = ">=2.3.1,<3.1" -python-dateutil = ">=2.7" - [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -1619,75 +934,67 @@ reference = "ali" [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.7" description = "MessagePack serializer" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, + {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, + {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, + {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, + {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, + {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, + {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, + {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, + {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, + {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, + {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, + {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, ] [package.source] @@ -1699,7 +1006,6 @@ reference = "ali" name = "multidict" version = "6.0.4" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1785,17 +1091,110 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "main" +name = "nb-cli" +version = "1.3.0" +description = "CLI for nonebot2" optional = false -python-versions = ">=3.5" +python-versions = "<4.0,>=3.9" files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, + {file = "nb_cli-1.3.0-py3-none-any.whl", hash = "sha256:e8346f2bc2cca873037c47738fc3203689a328d583595f8caefadebeecd5fbe4"}, + {file = "nb_cli-1.3.0.tar.gz", hash = "sha256:cc890de5ccb35a498e413ff9deb2049c4ab9c5c73a78340a85372e2f71d6cd87"}, ] +[package.dependencies] +anyio = ">=3.6,<4.0" +cashews = ">=6.0,<7.0" +click = ">=8.1,<9.0" +cookiecutter = ">=2.2,<3.0" +httpx = ">=0.18,<1.0" +jinja2 = ">=3.0,<4.0" +noneprompt = ">=0.1.9,<1.0.0" +pydantic = ">=1.9,<2.0" +pyfiglet = ">=1.0.1,<2.0.0" +tomlkit = ">=0.10,<1.0" +typing-extensions = ">=4.4,<5.0" +virtualenv = ">=20.21,<21.0" +watchfiles = ">=0.16,<1.0" +wcwidth = ">=0.2,<1.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nepattern" +version = "0.5.15" +description = "a complex pattern, support typing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nepattern-0.5.15-py3-none-any.whl", hash = "sha256:c68fc7c0c9b7835c956a89e0f91fd380b8e07880e183414871e83ef4a9fa0dbd"}, + {file = "nepattern-0.5.15.tar.gz", hash = "sha256:3b04b91b5856b9826b61737933911f570a75ba8116b9e2ff8fa83b4aa0211203"}, +] + +[package.dependencies] +tarina = ">=0.3.3" +typing-extensions = ">=4.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-discord" +version = "0.1.3" +description = "Discord adapter for nonebot2" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_adapter_discord-0.1.3-py3-none-any.whl", hash = "sha256:4af3ef98e9d70d68880b43cb8a4421be8ca4961832eaf95377e2a339c73f0644"}, + {file = "nonebot_adapter_discord-0.1.3.tar.gz", hash = "sha256:73b492c63747ff2d5c8c1e49236f5bf2ff2ca01adacf273f64899193f9210ffe"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-dodo" +version = "0.1.4" +description = "Dodo adapter for nonebot2" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_adapter_dodo-0.1.4-py3-none-any.whl", hash = "sha256:3bbe8ce1d686923dc7347d49e9e7164a93bc87e79626d6067e77b7c3d41d6861"}, + {file = "nonebot_adapter_dodo-0.1.4.tar.gz", hash = "sha256:21375ee712e97fe546ef24654dcb479f51e972335f13b4208af9ef53cc5fca29"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-adapter-kaiheila" +version = "0.3.0" +description = "kaiheila adapter for nonebot2" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_adapter_kaiheila-0.3.0-py3-none-any.whl", hash = "sha256:8868f57f9ac591dff46d5cf711c419aa76ece22a5dd2380abc33d337132b5157"}, + {file = "nonebot_adapter_kaiheila-0.3.0.tar.gz", hash = "sha256:b32b4e9d911b98ae0270540ed7fa414e94f74b228363b6b012ae2f2d54ed4e21"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" +typing-extensions = ">=4.8.0,<5.0.0" + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -1803,19 +1202,18 @@ reference = "ali" [[package]] name = "nonebot-adapter-onebot" -version = "2.2.4" +version = "2.3.1" description = "OneBot(CQHTTP) adapter for nonebot2" -category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot_adapter_onebot-2.2.4-py3-none-any.whl", hash = "sha256:ae9971bb77a2984d6ca097d5565132723b051dafdfd1cef954a62f45684ae62c"}, - {file = "nonebot_adapter_onebot-2.2.4.tar.gz", hash = "sha256:1024b503514f87d6262adf1bde6f160b3a159afc4f6c21987eece3dacb4762dd"}, + {file = "nonebot_adapter_onebot-2.3.1-py3-none-any.whl", hash = "sha256:c4085f1fc1a62e46c737452b9ce3d6eb374812c78a419bb4fa378f48bd8e4088"}, + {file = "nonebot_adapter_onebot-2.3.1.tar.gz", hash = "sha256:10cec3aee454700e6d2144748bd898772db7bd95247d51d3ccd3b31919e24689"}, ] [package.dependencies] msgpack = ">=1.0.3,<2.0.0" -nonebot2 = ">=2.0.0-beta.3,<3.0.0" +nonebot2 = ">=2.1.0,<3.0.0" typing-extensions = ">=4.0.0,<5.0.0" [package.source] @@ -1823,21 +1221,43 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "nonebot-plugin-alconna" +version = "0.36.0" +description = "Alconna Adapter for Nonebot" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nonebot_plugin_alconna-0.36.0-py3-none-any.whl", hash = "sha256:37f8afc272924802fe75146df5f68b44e8e5537420cbb983d2d9d65195e625e7"}, + {file = "nonebot_plugin_alconna-0.36.0.tar.gz", hash = "sha256:e524fac76ee0f1a08817007e649c2b491b44094e0262a3d36fcef3e1259edfa2"}, +] + +[package.dependencies] +arclet-alconna = ">=1.7.38,<2.0.0" +arclet-alconna-tools = ">=0.6.7,<0.7.0" +fleep = ">=1.0.1" +nepattern = ">=0.5.14,<0.6.0" +nonebot2 = ">=2.1.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nonebot-plugin-apscheduler" -version = "0.2.0" +version = "0.3.0" description = "APScheduler Support for NoneBot2" -category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot-plugin-apscheduler-0.2.0.tar.gz", hash = "sha256:7b63e99a611b657533b48fcf1f8c6627c18c2eb3fa820a906cd4ec4666c0ceb0"}, - {file = "nonebot_plugin_apscheduler-0.2.0-py3-none-any.whl", hash = "sha256:9285ee84ca1cfa4db73c86cedb5911bbbd25a21ec0cd5f22447cd12f89e48fb4"}, + {file = "nonebot_plugin_apscheduler-0.3.0-py3-none-any.whl", hash = "sha256:ec5e0267293fc9803e543c6086d3e109ac87bf6dccea5473d219cad826238aae"}, + {file = "nonebot_plugin_apscheduler-0.3.0.tar.gz", hash = "sha256:7c41cc1d49ea6af7c4518c72cd15f8c2f549071b8bc8bfc4b21fbdd0a4875cfd"}, ] [package.dependencies] apscheduler = ">=3.7.0,<4.0.0" -nonebot2 = ">=2.0.0-rc.1,<3.0.0" +nonebot2 = ">=2.0.0,<3.0.0" [package.source] type = "legacy" @@ -1845,26 +1265,41 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "nonebot-plugin-htmlrender" -version = "0.2.2" -description = "通过浏览器渲染图片" -category = "main" +name = "nonebot-plugin-send-anything-anywhere" +version = "0.5.0" +description = "An adaptor for nonebot2 adaptors" optional = false -python-versions = "<4.0,>=3.8" +python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot_plugin_htmlrender-0.2.2-py3-none-any.whl", hash = "sha256:0415d8123a3c1a8bc321762631e1bfa33611530122ad662b95a7a772c24a0725"}, - {file = "nonebot_plugin_htmlrender-0.2.2.tar.gz", hash = "sha256:d79c4fa1c9bd655ac12caa4289da58d249f67ed45d29754565e05e4efbfa1e85"}, + {file = "nonebot_plugin_send_anything_anywhere-0.5.0-py3-none-any.whl", hash = "sha256:bff64f5f337643ba34b9ea0bdd8d86d3ee6285a29b9083d416a67d4815e83ddf"}, + {file = "nonebot_plugin_send_anything_anywhere-0.5.0.tar.gz", hash = "sha256:0230db94ca5654e2b0462b144db7ea74b763ee04fa7bc53deecacf32362e5268"}, ] [package.dependencies] -aiofiles = ">=0.8.0" -jinja2 = ">=3.0.3" -markdown = ">=3.3.6" -nonebot2 = {version = ">=2.0.0", extras = ["fastapi"]} -playwright = ">=1.17.2" -Pygments = ">=2.10.0" -pymdown-extensions = ">=9.1" -python-markdown-math = ">=0.8" +anyio = ">=3.6.2,<4.0.0" +nonebot2 = ">=2.0.0,<3.0.0" +pydantic = ">=1.10.5,<2.0.0" +strenum = ">=0.4.8,<0.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "nonebot-plugin-session" +version = "0.2.3" +description = "Nonebot2 会话信息提取与会话id定义" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_plugin_session-0.2.3-py3-none-any.whl", hash = "sha256:5f652a0c082231c1cea72deb994a81e50f77ba532e14d30fdec09772f69079fd"}, + {file = "nonebot_plugin_session-0.2.3.tar.gz", hash = "sha256:33af37400f5005927c4ff861e593774bedc314fba00cfe06f482e582d9f447b7"}, +] + +[package.dependencies] +nonebot2 = ">=2.0.0,<3.0.0" +strenum = ">=0.4.8,<0.5.0" [package.source] type = "legacy" @@ -1873,29 +1308,26 @@ reference = "ali" [[package]] name = "nonebot2" -version = "2.0.1" +version = "2.1.3" description = "An asynchronous python bot framework." -category = "main" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot2-2.0.1-py3-none-any.whl", hash = "sha256:58111068df7a6c13cca2a412dd0f6f88d7bf2a2af3e92ae770fd913a9421743e"}, - {file = "nonebot2-2.0.1.tar.gz", hash = "sha256:c61294644aef08f2b427301ca1c358d34e6cfaa7025d694a502ad66e9508e7c2"}, + {file = "nonebot2-2.1.3-py3-none-any.whl", hash = "sha256:c36c1a60ce4355d9777fee431c08619f22ffd60f7060993fbbbd1fe67b6368f7"}, + {file = "nonebot2-2.1.3.tar.gz", hash = "sha256:e750e615f1ad2503721ce055fbe55ec3b061277135d995be112fecd27f7232e5"}, ] [package.dependencies] -fastapi = {version = ">=0.93.0,<1.0.0", optional = true, markers = "extra == \"fastapi\" or extra == \"all\""} loguru = ">=0.6.0,<1.0.0" pydantic = {version = ">=1.10.0,<2.0.0", extras = ["dotenv"]} pygtrie = ">=2.4.1,<3.0.0" tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.0.0,<5.0.0" -uvicorn = {version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true, markers = "extra == \"quart\" or extra == \"fastapi\" or extra == \"all\""} +typing-extensions = ">=4.4.0,<5.0.0" yarl = ">=1.7.2,<2.0.0" [package.extras] -aiohttp = ["aiohttp[speedups] (>=3.7.4,<4.0.0)"] -all = ["Quart (>=0.18.0,<1.0.0)", "aiohttp[speedups] (>=3.7.4,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.20.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=10.0)"] +aiohttp = ["aiohttp[speedups] (>=3.9.0b0,<4.0.0)"] +all = ["Quart (>=0.18.0,<1.0.0)", "aiohttp[speedups] (>=3.9.0b0,<4.0.0)", "fastapi (>=0.93.0,<1.0.0)", "httpx[http2] (>=0.20.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)", "websockets (>=10.0)"] fastapi = ["fastapi (>=0.93.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"] httpx = ["httpx[http2] (>=0.20.0,<1.0.0)"] quart = ["Quart (>=0.18.0,<1.0.0)", "uvicorn[standard] (>=0.20.0,<1.0.0)"] @@ -1907,109 +1339,18 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "numpy" -version = "1.24.4" -description = "Fundamental package for array computing in Python" -category = "main" +name = "noneprompt" +version = "0.1.9" +description = "Prompt toolkit for console interaction" optional = false -python-versions = ">=3.8" +python-versions = ">=3.8,<4.0" files = [ - {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, - {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, - {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, - {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, - {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, - {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, - {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, - {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, - {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, - {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, - {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, - {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, - {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, - {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, - {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, - {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, - {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, - {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, - {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "opencv-python" -version = "4.8.0.76" -description = "Wrapper package for OpenCV python bindings." -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "opencv-python-4.8.0.76.tar.gz", hash = "sha256:56d84c43ce800938b9b1ec74b33942b2edbcef3f70c2754eb9bfe5dff1ee3ace"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:67bce4b9aad307c98a9a07c6afb7de3a4e823c1f4991d6d8e88e229e7dfeee59"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:48eb3121d809a873086d6677565e3ac963e6946110d13cd115533fa70e2aa2eb"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93871871b1c9d6b125cddd45b0638a2fa01ee9fd37f5e428823f750e404f2f15"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcb4944211acf13742dbfd9d3a11dc4e36353ffa1746f2c7dcd6a01c32d1376"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-win32.whl", hash = "sha256:b2349dc9f97ed6c9ba163d0a7a24bcef9695a3e216cd143e92f1b9659c5d9a49"}, - {file = "opencv_python-4.8.0.76-cp37-abi3-win_amd64.whl", hash = "sha256:ba32cfa75a806abd68249699d34420737d27b5678553387fc5768747a6492147"}, + {file = "noneprompt-0.1.9-py3-none-any.whl", hash = "sha256:a54f1e6a19a3da2dedf7f365f80420e9ae49326a0ffe60a8a9c7afdee6b6eeb3"}, + {file = "noneprompt-0.1.9.tar.gz", hash = "sha256:338b8bb89a8d22ef35f1dedb3aa7c1b228cf139973bdc43c5ffc3eef64457db9"}, ] [package.dependencies] -numpy = [ - {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""}, - {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, - {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, - {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, - {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, - {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "packaging" -version = "23.1" -description = "Core utilities for Python packages" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "pathspec" -version = "0.11.2" -description = "Utility library for gitignore style pattern matching of file paths." -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, -] +prompt-toolkit = ">=3.0.19,<4.0.0" [package.source] type = "legacy" @@ -2020,7 +1361,6 @@ reference = "ali" name = "pillow" version = "9.5.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2103,14 +1443,13 @@ reference = "ali" [[package]] name = "platformdirs" -version = "3.10.0" +version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, - {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] @@ -2124,25 +1463,23 @@ reference = "ali" [[package]] name = "playwright" -version = "1.37.0" +version = "1.41.1" description = "A high-level API to automate web browsers" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "playwright-1.37.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b476f63251876f1625f490af8d58ec0db90b555c623b7f54105f91d33878c06d"}, - {file = "playwright-1.37.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:68d56efe5ce916bab349177e90726837a6f0cae77ebd6a5200f5333b787b25fb"}, - {file = "playwright-1.37.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:428fdf9bfff586b73f96df53692d50d422afb93ca4650624f61e8181f548fed2"}, - {file = "playwright-1.37.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:41f0280472af94c426e941f6a969ff6a7ea156dc15fd01d09ac4b8f092e2346e"}, - {file = "playwright-1.37.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b574889ef97b7f44a633aa10d72b8966a850a4354d915fd0bc7e8658e825dd63"}, - {file = "playwright-1.37.0-py3-none-win32.whl", hash = "sha256:8b5d96aae54289129ab19d3d0e2e431171ae3e5d88d49a10900dcbe569a27d43"}, - {file = "playwright-1.37.0-py3-none-win_amd64.whl", hash = "sha256:678b9926be2df06321d11a525d4bf08d9f4a5b151354a3b82fe2ac14476322d5"}, + {file = "playwright-1.41.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b456f25db38e4d93afc3c671e1093f3995afb374f14cee284152a30f84cfff02"}, + {file = "playwright-1.41.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53ff152506dbd8527aa815e92757be72f5df60810e8000e9419d29fd4445f53c"}, + {file = "playwright-1.41.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:70c432887b8b5e896fa804fb90ca2c8baf05b13a3590fb8bce8b3c3efba2842d"}, + {file = "playwright-1.41.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:f227a8d616fd3a02d45d68546ee69947dce4a058df134a9e7dc6167c543de3cd"}, + {file = "playwright-1.41.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:475130f879b4ba38b9db7232a043dd5bc3a8bd1a84567fbea7e21a02ee2fcb13"}, + {file = "playwright-1.41.1-py3-none-win32.whl", hash = "sha256:ef769414ea0ceb76085c67812ab6bc0cc6fac0adfc45aaa09d54ee161d7f637b"}, + {file = "playwright-1.41.1-py3-none-win_amd64.whl", hash = "sha256:316e1ba0854a712e9288b3fe49509438e648d43bade77bf724899de8c24848de"}, ] [package.dependencies] -greenlet = "2.0.2" -pyee = "9.0.4" -typing-extensions = {version = "*", markers = "python_version <= \"3.8\""} +greenlet = "3.0.3" +pyee = "11.0.1" [package.source] type = "legacy" @@ -2150,96 +1487,18 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "proces" -version = "0.1.6" -description = "text preprocess." -category = "main" +name = "prompt-toolkit" +version = "3.0.43" +description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7.0" files = [ - {file = "proces-0.1.6-py3-none-any.whl", hash = "sha256:6d6fefcb2a83bf26e1f0d35ac1d1c22b988d5161ccd01d83440544780a294d9e"}, - {file = "proces-0.1.6.tar.gz", hash = "sha256:a319bdf4b1724d080371f87febf4fd511d387dc78185d91687b607fa289cdce2"}, + {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, + {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, ] [package.dependencies] -"ruamel.yaml" = ">=0.16.5" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "protobuf" -version = "4.24.2" -description = "" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "protobuf-4.24.2-cp310-abi3-win32.whl", hash = "sha256:58e12d2c1aa428ece2281cef09bbaa6938b083bcda606db3da4e02e991a0d924"}, - {file = "protobuf-4.24.2-cp310-abi3-win_amd64.whl", hash = "sha256:77700b55ba41144fc64828e02afb41901b42497b8217b558e4a001f18a85f2e3"}, - {file = "protobuf-4.24.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:237b9a50bd3b7307d0d834c1b0eb1a6cd47d3f4c2da840802cd03ea288ae8880"}, - {file = "protobuf-4.24.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:25ae91d21e3ce8d874211110c2f7edd6384816fb44e06b2867afe35139e1fd1c"}, - {file = "protobuf-4.24.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:c00c3c7eb9ad3833806e21e86dca448f46035242a680f81c3fe068ff65e79c74"}, - {file = "protobuf-4.24.2-cp37-cp37m-win32.whl", hash = "sha256:4e69965e7e54de4db989289a9b971a099e626f6167a9351e9d112221fc691bc1"}, - {file = "protobuf-4.24.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c5cdd486af081bf752225b26809d2d0a85e575b80a84cde5172a05bbb1990099"}, - {file = "protobuf-4.24.2-cp38-cp38-win32.whl", hash = "sha256:6bd26c1fa9038b26c5c044ee77e0ecb18463e957fefbaeb81a3feb419313a54e"}, - {file = "protobuf-4.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb7aa97c252279da65584af0456f802bd4b2de429eb945bbc9b3d61a42a8cd16"}, - {file = "protobuf-4.24.2-cp39-cp39-win32.whl", hash = "sha256:2b23bd6e06445699b12f525f3e92a916f2dcf45ffba441026357dea7fa46f42b"}, - {file = "protobuf-4.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:839952e759fc40b5d46be319a265cf94920174d88de31657d5622b5d8d6be5cd"}, - {file = "protobuf-4.24.2-py3-none-any.whl", hash = "sha256:3b7b170d3491ceed33f723bbf2d5a260f8a4e23843799a3906f16ef736ef251e"}, - {file = "protobuf-4.24.2.tar.gz", hash = "sha256:7fda70797ddec31ddfa3576cbdcc3ddbb6b3078b737a1a87ab9136af0570cd6e"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "psutil" -version = "5.9.5" -description = "Cross-platform lib for process and system monitoring in Python." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"}, - {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"}, - {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"}, - {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"}, - {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"}, - {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"}, - {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"}, - {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"}, - {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"}, - {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"}, - {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "pyasn1" -version = "0.5.0" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, - {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, -] +wcwidth = "*" [package.source] type = "legacy" @@ -2248,48 +1507,47 @@ reference = "ali" [[package]] name = "pydantic" -version = "1.10.12" +version = "1.10.14" description = "Data validation and settings management using python type hints" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"}, - {file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"}, - {file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"}, - {file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"}, - {file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"}, - {file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"}, - {file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"}, - {file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"}, - {file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"}, - {file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"}, - {file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"}, - {file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"}, - {file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"}, - {file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"}, - {file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"}, - {file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"}, - {file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"}, - {file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"}, - {file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"}, - {file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"}, - {file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"}, - {file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"}, - {file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, + {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, + {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, + {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, + {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, + {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, + {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, + {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, + {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, ] [package.dependencies] @@ -2307,19 +1565,37 @@ reference = "ali" [[package]] name = "pyee" -version = "9.0.4" -description = "A port of node.js's EventEmitter to python." -category = "main" +version = "11.0.1" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "pyee-9.0.4-py2.py3-none-any.whl", hash = "sha256:9f066570130c554e9cc12de5a9d86f57c7ee47fece163bbdaa3e9c933cfbdfa5"}, - {file = "pyee-9.0.4.tar.gz", hash = "sha256:2770c4928abc721f46b705e6a72b0c59480c4a69c9a83ca0b00bb994f1ea4b32"}, + {file = "pyee-11.0.1-py3-none-any.whl", hash = "sha256:9bcc9647822234f42c228d88de63d0f9ffa881e87a87f9d36ddf5211f6ac977d"}, + {file = "pyee-11.0.1.tar.gz", hash = "sha256:a642c51e3885a33ead087286e35212783a4e9b8d6514a10a5db4e57ac57b2b29"}, ] [package.dependencies] typing-extensions = "*" +[package.extras] +dev = ["black", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "pyfiglet" +version = "1.0.2" +description = "Pure-python FIGlet implementation" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea"}, + {file = "pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab"}, +] + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -2327,18 +1603,18 @@ reference = "ali" [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [package.source] type = "legacy" @@ -2349,7 +1625,6 @@ reference = "ali" name = "pygtrie" version = "2.5.0" description = "A pure Python trie data structure implementation." -category = "main" optional = false python-versions = "*" files = [ @@ -2362,55 +1637,10 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "pymdown-extensions" -version = "10.3" -description = "Extension pack for Python Markdown." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pymdown_extensions-10.3-py3-none-any.whl", hash = "sha256:77a82c621c58a83efc49a389159181d570e370fff9f810d3a4766a75fc678b66"}, - {file = "pymdown_extensions-10.3.tar.gz", hash = "sha256:94a0d8a03246712b64698af223848fd80aaf1ae4c4be29c8c61939b0467b5722"}, -] - -[package.dependencies] -markdown = ">=3.2" -pyyaml = "*" - -[package.extras] -extra = ["pygments (>=2.12)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" -files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "pypika-tortoise" version = "0.1.6" description = "Forked from pypika and streamline just for tortoise-orm" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -2423,28 +1653,10 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "pypinyin" -version = "0.46.0" -description = "汉字拼音转换模块/工具." -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" -files = [ - {file = "pypinyin-0.46.0-py2.py3-none-any.whl", hash = "sha256:7251f4fa0b1e43ad91f6121d9a842e8acd72a6a34deea5e87d2a97621eadc11f"}, - {file = "pypinyin-0.46.0.tar.gz", hash = "sha256:0d2e41e95dbc20a232c0f5d3850654eebbfcba303d96358d2c46592725bb989c"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2462,14 +1674,13 @@ reference = "ali" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -2481,65 +1692,21 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "python-jose" -version = "3.3.0" -description = "JOSE implementation in Python" -category = "main" +name = "python-slugify" +version = "8.0.2" +description = "A Python slugify application that also handles Unicode" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, - {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, + {file = "python-slugify-8.0.2.tar.gz", hash = "sha256:a1a02b127a95c124fd84f8f88be730e557fd823774bf19b1cd5e8704e2ae0e5e"}, + {file = "python_slugify-8.0.2-py2.py3-none-any.whl", hash = "sha256:428ea9b00c977b8f6c097724398f190b2c18e2a6011094d1001285875ccacdbf"}, ] [package.dependencies] -ecdsa = "!=0.15" -pyasn1 = "*" -rsa = "*" +text-unidecode = ">=1.3" [package.extras] -cryptography = ["cryptography (>=3.4.0)"] -pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] -pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "python-markdown-math" -version = "0.8" -description = "Math extension for Python-Markdown" -category = "main" -optional = false -python-versions = ">=3.6" -files = [ - {file = "python-markdown-math-0.8.tar.gz", hash = "sha256:8564212af679fc18d53f38681f16080fcd3d186073f23825c7ce86fadd3e3635"}, - {file = "python_markdown_math-0.8-py3-none-any.whl", hash = "sha256:c685249d84b5b697e9114d7beb352bd8ca2e07fd268fd4057ffca888c14641e5"}, -] - -[package.dependencies] -Markdown = ">=3.0" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "python-multipart" -version = "0.0.5" -description = "A streaming multipart parser for Python" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, -] - -[package.dependencies] -six = ">=1.4.0" +unidecode = ["Unidecode (>=1.1.1)"] [package.source] type = "legacy" @@ -2550,7 +1717,6 @@ reference = "ali" name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -2563,189 +1729,64 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "pywavelets" -version = "1.4.1" -description = "PyWavelets, wavelet transform module" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c"}, - {file = "PyWavelets-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win32.whl", hash = "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd"}, - {file = "PyWavelets-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875"}, - {file = "PyWavelets-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e"}, - {file = "PyWavelets-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win32.whl", hash = "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1"}, - {file = "PyWavelets-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966"}, - {file = "PyWavelets-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc"}, - {file = "PyWavelets-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win32.whl", hash = "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd"}, - {file = "PyWavelets-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6"}, - {file = "PyWavelets-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b"}, - {file = "PyWavelets-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win32.whl", hash = "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c"}, - {file = "PyWavelets-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4"}, - {file = "PyWavelets-1.4.1.tar.gz", hash = "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93"}, -] - -[package.dependencies] -numpy = ">=1.17.3" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "pyyaml" -version = "5.4.1" +version = "6.0.1" description = "YAML parser and emitter for Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -files = [ - {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, - {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, - {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, - {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347"}, - {file = "PyYAML-5.4.1-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, - {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, - {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa"}, - {file = "PyYAML-5.4.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, - {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, - {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247"}, - {file = "PyYAML-5.4.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, - {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, - {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122"}, - {file = "PyYAML-5.4.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6"}, - {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, - {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, - {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "regex" -version = "2023.8.8" -description = "Alternative regular expression module, to replace re." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "regex-2023.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb"}, - {file = "regex-2023.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61"}, - {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db"}, - {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7"}, - {file = "regex-2023.8.8-cp310-cp310-win32.whl", hash = "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb"}, - {file = "regex-2023.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b"}, - {file = "regex-2023.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71"}, - {file = "regex-2023.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559"}, - {file = "regex-2023.8.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7"}, - {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd"}, - {file = "regex-2023.8.8-cp311-cp311-win32.whl", hash = "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8"}, - {file = "regex-2023.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb"}, - {file = "regex-2023.8.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61"}, - {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235"}, - {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b"}, - {file = "regex-2023.8.8-cp36-cp36m-win32.whl", hash = "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7"}, - {file = "regex-2023.8.8-cp36-cp36m-win_amd64.whl", hash = "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236"}, - {file = "regex-2023.8.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116"}, - {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684"}, - {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7"}, - {file = "regex-2023.8.8-cp37-cp37m-win32.whl", hash = "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3"}, - {file = "regex-2023.8.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921"}, - {file = "regex-2023.8.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675"}, - {file = "regex-2023.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9"}, - {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586"}, - {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882"}, - {file = "regex-2023.8.8-cp38-cp38-win32.whl", hash = "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7"}, - {file = "regex-2023.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be"}, - {file = "regex-2023.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3"}, - {file = "regex-2023.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033"}, - {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf"}, - {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6"}, - {file = "regex-2023.8.8-cp39-cp39-win32.whl", hash = "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e"}, - {file = "regex-2023.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb"}, - {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [package.source] @@ -2754,42 +1795,25 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "retrying" -version = "1.3.4" -description = "Retrying" -category = "main" +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, - {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, ] [package.dependencies] -six = ">=1.7.0" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" [package.extras] -idna2008 = ["idna"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [package.source] type = "legacy" @@ -2798,43 +1822,21 @@ reference = "ali" [[package]] name = "rich" -version = "12.6.0" +version = "13.7.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -category = "main" optional = false -python-versions = ">=3.6.3,<4.0.0" +python-versions = ">=3.7.0" files = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, ] [package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -category = "main" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" +jupyter = ["ipywidgets (>=7.5.1,<9)"] [package.source] type = "legacy" @@ -2843,21 +1845,20 @@ reference = "ali" [[package]] name = "ruamel-yaml" -version = "0.17.32" +version = "0.18.5" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -category = "main" optional = false -python-versions = ">=3" +python-versions = ">=3.7" files = [ - {file = "ruamel.yaml-0.17.32-py3-none-any.whl", hash = "sha256:23cd2ed620231677564646b0c6a89d138b6822a0d78656df7abda5879ec4f447"}, - {file = "ruamel.yaml-0.17.32.tar.gz", hash = "sha256:ec939063761914e14542972a5cba6d33c23b0859ab6342f61cf070cfc600efc2"}, + {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, + {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, ] [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.12\""} +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} [package.extras] -docs = ["ryd"] +docs = ["mercurial (>5.7)", "ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [package.source] @@ -2867,131 +1868,61 @@ reference = "ali" [[package]] name = "ruamel-yaml-clib" -version = "0.2.7" +version = "0.2.8" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, - {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:1a6391a7cabb7641c32517539ca42cf84b87b667bad38b78d4d42dd23e957c81"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:9c7617df90c1365638916b98cdd9be833d31d337dbcd722485597b43c4a215bf"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win32.whl", hash = "sha256:f6d3d39611ac2e4f62c3128a9eed45f19a6608670c5a2f4f07f24e8de3441d38"}, - {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:da538167284de58a52109a9b89b8f6a53ff8437dd6dc26d33b57bf6699153122"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, - {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, - {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, - {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, - {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, - {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "scipy" -version = "1.9.3" -description = "Fundamental algorithms for scientific computing in Python" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"}, - {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"}, - {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"}, - {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"}, - {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"}, - {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"}, - {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"}, - {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"}, - {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"}, - {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"}, - {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"}, - {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"}, - {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"}, - {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"}, -] - -[package.dependencies] -numpy = ">=1.18.5,<1.26.0" - -[package.extras] -dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"] -doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"] -test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "setuptools" -version = "68.1.2" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"}, - {file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5,<=7.1.2)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - -[[package]] -name = "sgmllib3k" -version = "1.0.0" -description = "Py3k port of sgmllib." -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, + {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, + {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, ] [package.source] @@ -3003,7 +1934,6 @@ reference = "ali" name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3020,7 +1950,6 @@ reference = "ali" name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3034,40 +1963,91 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "soupsieve" -version = "2.5" -description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" +name = "strenum" +version = "0.4.15" +description = "An Enum that inherits from str." optional = false -python-versions = ">=3.8" +python-versions = "*" files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, + {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, ] +[package.extras] +docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] +release = ["twine"] +test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "starlette" -version = "0.26.1" -description = "The little ASGI library that shines." -category = "main" +name = "tarina" +version = "0.4.2" +description = "A collection of common utils for Arclet" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "tarina-0.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c46b2b827a4d14f521c5f323e1cb8ed5350d3d9bf8e7828100265903526b9907"}, + {file = "tarina-0.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc78a3c653fdea3f2ae642584a6a55cf26856b4858875068a7cfca92b13bca6b"}, + {file = "tarina-0.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1943cabd1707e52b1bfc478c33c48c04d6c0d3ef9425ad808265d7965142c3b"}, + {file = "tarina-0.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872507bc155412ab71f202a9b28ee170bd395c7cf8dbee63bfe78845265717a2"}, + {file = "tarina-0.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61aab6935092373fd53565ec7ba894ff9567e4620535a26362aeb66826f6d0d7"}, + {file = "tarina-0.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5ae2ccd7aa409d33ea14944533b93f15cee71a1a7f4547f0cfef1ad6153ed142"}, + {file = "tarina-0.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ee72b6a55215ad6acc1d50fbd227d972138adacc9f7c6a3f1c63080780d968ed"}, + {file = "tarina-0.4.2-cp310-cp310-win32.whl", hash = "sha256:4f417bb80c18d5f87f27bcb1e70d5eaae125578440c6bbe4c5275c6c633a7475"}, + {file = "tarina-0.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:e54de934c6a754e27daf64a04d6ca287303f7b7e6f8ecee6cb8162577ffc2a6c"}, + {file = "tarina-0.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:30ec63fce2556f9e63d8d774a74ef688d9c46dd04f198d3b9a653cd5c539fd5e"}, + {file = "tarina-0.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03718dd5630daa7ee84f31ae258c979f168f4b58149a0647af706bff64268321"}, + {file = "tarina-0.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d49030aefd8486fdb5b91e72c188451de216fb71c636cbd33a7eee5e6c73daf"}, + {file = "tarina-0.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ad962153c9f63ad1e89acd3d9bbbcee21b5ea07ee0e8dac06c6d2471ee8337d"}, + {file = "tarina-0.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2a8a9bfd4a9a5907c2c64e163c374eaed24b1e0df3fe9d0bd7d52a9f730daa"}, + {file = "tarina-0.4.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:50a2bea62c32cf9a6cd89f7eb1b2a1a5a5ca3b7533960abdcc77956121267de1"}, + {file = "tarina-0.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c6c16b01783797cbd12eeef042a7ed96a5531b01b931e50ac5afb3d9d4d1634"}, + {file = "tarina-0.4.2-cp311-cp311-win32.whl", hash = "sha256:c4b54ebdfeb63f9a60ca4c70014784eb50c00ad892e9ad8211527e2d57108abb"}, + {file = "tarina-0.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b613afb8e3381f64e2ede5ad17b7c013515f601b8ce335dead5403aa5ac58281"}, + {file = "tarina-0.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a95da4a609cdf7e888e1688c0d332acf9c62633f81a40cf851ce3649c3fb9283"}, + {file = "tarina-0.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a3cb636ec94d5f7a6bb7a7bfc451e484c6e8ea7bc04144eb4662d692923ca8ca"}, + {file = "tarina-0.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1f4a76784f47a89e7e2bb44136fe8e07a636f50fbc4b24f0d8da63cc28ebf78"}, + {file = "tarina-0.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45348f4ecfa21b84c103ef021b161020637c7a6aa02e02440fbe3418cc4a5654"}, + {file = "tarina-0.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1848011cf5707aad0899236bcc972f86ef5554ec4799a8ddf15ef53aea784ff"}, + {file = "tarina-0.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:494ccf76d4428de18c59464f5a9e1c8b6d0bf598941a8b0d9f407729f81e0a95"}, + {file = "tarina-0.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0086c91358536549f61a6ac24861764ef1a607a91bd82a2dbd574392de26dff0"}, + {file = "tarina-0.4.2-cp38-cp38-win32.whl", hash = "sha256:c30618a7c8586719ff46c12b653d52e969c0a5105698f3fdbff8e5293b135b63"}, + {file = "tarina-0.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:6b7b3bff3cbf0901f55c87739af297fd6692f53d3d3a55d0de12e7e41e4e7ff4"}, + {file = "tarina-0.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af75e381a0ae7481741f37221fa721816d78c6e34791ac14a29d41f148bfae49"}, + {file = "tarina-0.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:938fe8e9fe950eb4ab1824f72d55973cae98aaeacba1bf6ce2f6e5729f9b8555"}, + {file = "tarina-0.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46737007a43cfef9ba20409230beb49a6754de498930eb1105edf0687ce4d6d8"}, + {file = "tarina-0.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbbbdd2aee142d3c0888ebce2481ad15e4d505c040fee1b9c7809bf3c9e83649"}, + {file = "tarina-0.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4781c783266e2d4eccc975ba79e1e0b4b465d267e616b5b44f32981a289a7a"}, + {file = "tarina-0.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01d1b3b0e0079e5f372111b149815d70d4a063a83f1a656d6acd91e67fa9c862"}, + {file = "tarina-0.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d493c5492a27964aa4f8fa65ac6eab172624b8d25718b1a5e2261102ac363b26"}, + {file = "tarina-0.4.2-cp39-cp39-win32.whl", hash = "sha256:df510f8d8a2cb5e684f2412d98a3f2654986e40c2a697be686f0b27f51ea7c36"}, + {file = "tarina-0.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:646f74e7426acf2644461a2077b72677d964b5c064f6cb2df8c60c77580e1f89"}, + {file = "tarina-0.4.2-py3-none-any.whl", hash = "sha256:08650c08b1950e7346b13f20ff695d3d3d0d7a9d240521a7544c433ba326a736"}, + {file = "tarina-0.4.2.tar.gz", hash = "sha256:a719c31c1e65c5fb68c12fbacffe802f160678d2e6a9745d1241b924e9283791"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +typing-extensions = ">=4.4.0" -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] [package.source] type = "legacy" @@ -3078,7 +2058,6 @@ reference = "ali" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3092,15 +2071,30 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "tortoise-orm" -version = "0.19.3" -description = "Easy async ORM for python, built with relations in mind" -category = "main" +name = "tomlkit" +version = "0.12.3" +description = "Style preserving TOML library" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.7" files = [ - {file = "tortoise_orm-0.19.3-py3-none-any.whl", hash = "sha256:9e368820c70a0866ef9c521d43aa5503485bd7a20a561edc0933b7b0f7036fbc"}, - {file = "tortoise_orm-0.19.3.tar.gz", hash = "sha256:ca574bca5191f55608f9013314b1f5d1c6ffd4165a1fcc2f60f6c902f529b3b6"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "tortoise-orm" +version = "0.20.0" +description = "Easy async ORM for python, built with relations in mind" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "tortoise_orm-0.20.0-py3-none-any.whl", hash = "sha256:1891ad935de689ddf002c5c65c864176d28659ab6069e45f0e2cde32359bb8d9"}, + {file = "tortoise_orm-0.20.0.tar.gz", hash = "sha256:283af584d685dcc58d6cc1da35b9115bb1e41c89075eae2a19c493b39b9b41f7"}, ] [package.dependencies] @@ -3113,10 +2107,26 @@ pytz = "*" [package.extras] accel = ["ciso8601", "orjson", "uvloop"] aiomysql = ["aiomysql"] -asyncmy = ["asyncmy (>=0.2.5,<0.3.0)"] +asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] asyncpg = ["asyncpg"] -psycopg = ["psycopg[binary,pool] (==3.0.12)"] +psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.20240106" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"}, + {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"}, +] [package.source] type = "legacy" @@ -3125,14 +2135,13 @@ reference = "ali" [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [package.source] @@ -3142,14 +2151,13 @@ reference = "ali" [[package]] name = "tzdata" -version = "2023.3" +version = "2023.4" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, ] [package.source] @@ -3159,22 +2167,20 @@ reference = "ali" [[package]] name = "tzlocal" -version = "5.0.1" +version = "5.2" description = "tzinfo object for the local timezone" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"}, - {file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"}, + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, ] [package.dependencies] -"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""} tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] [package.source] type = "legacy" @@ -3183,73 +2189,76 @@ reference = "ali" [[package]] name = "ujson" -version = "5.8.0" +version = "5.9.0" description = "Ultra fast JSON encoder and decoder for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "ujson-5.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4511560d75b15ecb367eef561554959b9d49b6ec3b8d5634212f9fed74a6df1"}, - {file = "ujson-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9399eaa5d1931a0ead49dce3ffacbea63f3177978588b956036bfe53cdf6af75"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4e7bb7eba0e1963f8b768f9c458ecb193e5bf6977090182e2b4f4408f35ac76"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40931d7c08c4ce99adc4b409ddb1bbb01635a950e81239c2382cfe24251b127a"}, - {file = "ujson-5.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d53039d39de65360e924b511c7ca1a67b0975c34c015dd468fca492b11caa8f7"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bdf04c6af3852161be9613e458a1fb67327910391de8ffedb8332e60800147a2"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a70f776bda2e5072a086c02792c7863ba5833d565189e09fabbd04c8b4c3abba"}, - {file = "ujson-5.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f26629ac531d712f93192c233a74888bc8b8212558bd7d04c349125f10199fcf"}, - {file = "ujson-5.8.0-cp310-cp310-win32.whl", hash = "sha256:7ecc33b107ae88405aebdb8d82c13d6944be2331ebb04399134c03171509371a"}, - {file = "ujson-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:3b27a8da7a080add559a3b73ec9ebd52e82cc4419f7c6fb7266e62439a055ed0"}, - {file = "ujson-5.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:193349a998cd821483a25f5df30b44e8f495423840ee11b3b28df092ddfd0f7f"}, - {file = "ujson-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ddeabbc78b2aed531f167d1e70387b151900bc856d61e9325fcdfefb2a51ad8"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ce24909a9c25062e60653073dd6d5e6ec9d6ad7ed6e0069450d5b673c854405"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27a2a3c7620ebe43641e926a1062bc04e92dbe90d3501687957d71b4bdddaec4"}, - {file = "ujson-5.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b852bdf920fe9f84e2a2c210cc45f1b64f763b4f7d01468b33f7791698e455e"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:20768961a6a706170497129960762ded9c89fb1c10db2989c56956b162e2a8a3"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e0147d41e9fb5cd174207c4a2895c5e24813204499fd0839951d4c8784a23bf5"}, - {file = "ujson-5.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3673053b036fd161ae7a5a33358ccae6793ee89fd499000204676baafd7b3aa"}, - {file = "ujson-5.8.0-cp311-cp311-win32.whl", hash = "sha256:a89cf3cd8bf33a37600431b7024a7ccf499db25f9f0b332947fbc79043aad879"}, - {file = "ujson-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3659deec9ab9eb19e8646932bfe6fe22730757c4addbe9d7d5544e879dc1b721"}, - {file = "ujson-5.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:102bf31c56f59538cccdfec45649780ae00657e86247c07edac434cb14d5388c"}, - {file = "ujson-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:299a312c3e85edee1178cb6453645217ba23b4e3186412677fa48e9a7f986de6"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e385a7679b9088d7bc43a64811a7713cc7c33d032d020f757c54e7d41931ae"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad24ec130855d4430a682c7a60ca0bc158f8253ec81feed4073801f6b6cb681b"}, - {file = "ujson-5.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16fde596d5e45bdf0d7de615346a102510ac8c405098e5595625015b0d4b5296"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6d230d870d1ce03df915e694dcfa3f4e8714369cce2346686dbe0bc8e3f135e7"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9571de0c53db5cbc265945e08f093f093af2c5a11e14772c72d8e37fceeedd08"}, - {file = "ujson-5.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7cba16b26efe774c096a5e822e4f27097b7c81ed6fb5264a2b3f5fd8784bab30"}, - {file = "ujson-5.8.0-cp312-cp312-win32.whl", hash = "sha256:48c7d373ff22366eecfa36a52b9b55b0ee5bd44c2b50e16084aa88b9de038916"}, - {file = "ujson-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:5ac97b1e182d81cf395ded620528c59f4177eee024b4b39a50cdd7b720fdeec6"}, - {file = "ujson-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64cc32bb4a436e5813b83f5aab0889927e5ea1788bf99b930fad853c5625cb"}, - {file = "ujson-5.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e54578fa8838ddc722539a752adfce9372474114f8c127bb316db5392d942f8b"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9721cd112b5e4687cb4ade12a7b8af8b048d4991227ae8066d9c4b3a6642a582"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9707e5aacf63fb919f6237d6490c4e0244c7f8d3dc2a0f84d7dec5db7cb54c"}, - {file = "ujson-5.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0be81bae295f65a6896b0c9030b55a106fb2dec69ef877253a87bc7c9c5308f7"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae7f4725c344bf437e9b881019c558416fe84ad9c6b67426416c131ad577df67"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9ab282d67ef3097105552bf151438b551cc4bedb3f24d80fada830f2e132aeb9"}, - {file = "ujson-5.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94c7bd9880fa33fcf7f6d7f4cc032e2371adee3c5dba2922b918987141d1bf07"}, - {file = "ujson-5.8.0-cp38-cp38-win32.whl", hash = "sha256:bf5737dbcfe0fa0ac8fa599eceafae86b376492c8f1e4b84e3adf765f03fb564"}, - {file = "ujson-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:11da6bed916f9bfacf13f4fc6a9594abd62b2bb115acfb17a77b0f03bee4cfd5"}, - {file = "ujson-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:69b3104a2603bab510497ceabc186ba40fef38ec731c0ccaa662e01ff94a985c"}, - {file = "ujson-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9249fdefeb021e00b46025e77feed89cd91ffe9b3a49415239103fc1d5d9c29a"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2873d196725a8193f56dde527b322c4bc79ed97cd60f1d087826ac3290cf9207"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4dafa9010c366589f55afb0fd67084acd8added1a51251008f9ff2c3e44042"}, - {file = "ujson-5.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a42baa647a50fa8bed53d4e242be61023bd37b93577f27f90ffe521ac9dc7a3"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f3554eaadffe416c6f543af442066afa6549edbc34fe6a7719818c3e72ebfe95"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fb87decf38cc82bcdea1d7511e73629e651bdec3a43ab40985167ab8449b769c"}, - {file = "ujson-5.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:407d60eb942c318482bbfb1e66be093308bb11617d41c613e33b4ce5be789adc"}, - {file = "ujson-5.8.0-cp39-cp39-win32.whl", hash = "sha256:0fe1b7edaf560ca6ab023f81cbeaf9946a240876a993b8c5a21a1c539171d903"}, - {file = "ujson-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f9b63530a5392eb687baff3989d0fb5f45194ae5b1ca8276282fb647f8dcdb3"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:efeddf950fb15a832376c0c01d8d7713479fbeceaed1eaecb2665aa62c305aec"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d8283ac5d03e65f488530c43d6610134309085b71db4f675e9cf5dff96a8282"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb0142f6f10f57598655340a3b2c70ed4646cbe674191da195eb0985a9813b83"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07d459aca895eb17eb463b00441986b021b9312c6c8cc1d06880925c7f51009c"}, - {file = "ujson-5.8.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d524a8c15cfc863705991d70bbec998456a42c405c291d0f84a74ad7f35c5109"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d6f84a7a175c75beecde53a624881ff618e9433045a69fcfb5e154b73cdaa377"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b748797131ac7b29826d1524db1cc366d2722ab7afacc2ce1287cdafccddbf1f"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e72ba76313d48a1a3a42e7dc9d1db32ea93fac782ad8dde6f8b13e35c229130"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f504117a39cb98abba4153bf0b46b4954cc5d62f6351a14660201500ba31fe7f"}, - {file = "ujson-5.8.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8c91b6f4bf23f274af9002b128d133b735141e867109487d17e344d38b87d94"}, - {file = "ujson-5.8.0.tar.gz", hash = "sha256:78e318def4ade898a461b3d92a79f9441e7e0e4d2ad5419abed4336d702c7425"}, + {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, + {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, + {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, + {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, + {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, + {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, + {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, + {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, + {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, + {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, + {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, + {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, + {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, + {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, + {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, + {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, + {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, + {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, + {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, + {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, + {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, ] [package.source] @@ -3258,31 +2267,20 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "uvicorn" -version = "0.23.2" -description = "The lightning-fast ASGI server." -category = "main" +name = "urllib3" +version = "2.1.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, + {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, + {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, ] -[package.dependencies] -click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} -h11 = ">=0.8" -httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} -python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} -typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} - [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [package.source] type = "legacy" @@ -3290,49 +2288,24 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "uvloop" -version = "0.17.0" -description = "Fast implementation of asyncio event loop on top of libuv" -category = "main" +name = "virtualenv" +version = "20.25.0" +description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718"}, - {file = "uvloop-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d"}, - {file = "uvloop-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa"}, - {file = "uvloop-0.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c"}, - {file = "uvloop-0.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539"}, - {file = "uvloop-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05"}, - {file = "uvloop-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376"}, - {file = "uvloop-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8"}, - {file = "uvloop-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d"}, - {file = "uvloop-0.17.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738"}, - {file = "uvloop-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f"}, - {file = "uvloop-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578"}, - {file = "uvloop-0.17.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b"}, - {file = "uvloop-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8"}, - {file = "uvloop-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9"}, - {file = "uvloop-0.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded"}, - {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + [package.extras] -dev = ["Cython (>=0.29.32,<0.30.0)", "Sphinx (>=4.1.2,<4.2.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)", "pytest (>=3.6.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=22.0.0,<22.1.0)", "pycodestyle (>=2.7.0,<2.8.0)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [package.source] type = "legacy" @@ -3341,34 +2314,86 @@ reference = "ali" [[package]] name = "watchfiles" -version = "0.20.0" +version = "0.21.0" description = "Simple, modern and high performance file watching and code reload in python." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "watchfiles-0.20.0-cp37-abi3-macosx_10_7_x86_64.whl", hash = "sha256:3796312bd3587e14926013612b23066912cf45a14af71cf2b20db1c12dadf4e9"}, - {file = "watchfiles-0.20.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:d0002d81c89a662b595645fb684a371b98ff90a9c7d8f8630c82f0fde8310458"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:570848706440373b4cd8017f3e850ae17f76dbdf1e9045fc79023b11e1afe490"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a0351d20d03c6f7ad6b2e8a226a5efafb924c7755ee1e34f04c77c3682417fa"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:007dcc4a401093010b389c044e81172c8a2520dba257c88f8828b3d460c6bb38"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d82dbc1832da83e441d112069833eedd4cf583d983fb8dd666fbefbea9d99c0"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99f4c65fd2fce61a571b2a6fcf747d6868db0bef8a934e8ca235cc8533944d95"}, - {file = "watchfiles-0.20.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5392dd327a05f538c56edb1c6ebba6af91afc81b40822452342f6da54907bbdf"}, - {file = "watchfiles-0.20.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:08dc702529bb06a2b23859110c214db245455532da5eaea602921687cfcd23db"}, - {file = "watchfiles-0.20.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7d4e66a857621584869cfbad87039e65dadd7119f0d9bb9dbc957e089e32c164"}, - {file = "watchfiles-0.20.0-cp37-abi3-win32.whl", hash = "sha256:a03d1e6feb7966b417f43c3e3783188167fd69c2063e86bad31e62c4ea794cc5"}, - {file = "watchfiles-0.20.0-cp37-abi3-win_amd64.whl", hash = "sha256:eccc8942bcdc7d638a01435d915b913255bbd66f018f1af051cd8afddb339ea3"}, - {file = "watchfiles-0.20.0-cp37-abi3-win_arm64.whl", hash = "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d97db179f7566dcf145c5179ddb2ae2a4450e3a634eb864b09ea04e68c252e8e"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:835df2da7a5df5464c4a23b2d963e1a9d35afa422c83bf4ff4380b3114603644"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:608cd94a8767f49521901aff9ae0c92cc8f5a24d528db7d6b0295290f9d41193"}, - {file = "watchfiles-0.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89d1de8218874925bce7bb2ae9657efc504411528930d7a83f98b1749864f2ef"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:13f995d5152a8ba4ed7c2bbbaeee4e11a5944defc7cacd0ccb4dcbdcfd78029a"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9b5c8d3be7b502f8c43a33c63166ada8828dbb0c6d49c8f9ce990a96de2f5a49"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e43af4464daa08723c04b43cf978ab86cc55c684c16172622bdac64b34e36af0"}, - {file = "watchfiles-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d9e1f75c4f86c93d73b5bd1ebe667558357548f11b4f8af4e0e272f79413ce"}, - {file = "watchfiles-0.20.0.tar.gz", hash = "sha256:728575b6b94c90dd531514677201e8851708e6e4b5fe7028ac506a200b622019"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, + {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, + {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, + {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, + {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, ] [package.dependencies] @@ -3380,83 +2405,14 @@ url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" [[package]] -name = "websockets" -version = "11.0.3" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d"}, - {file = "websockets-11.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11"}, - {file = "websockets-11.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4"}, - {file = "websockets-11.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526"}, - {file = "websockets-11.0.3-cp310-cp310-win32.whl", hash = "sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69"}, - {file = "websockets-11.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288"}, - {file = "websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b"}, - {file = "websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf"}, - {file = "websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd"}, - {file = "websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c"}, - {file = "websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8"}, - {file = "websockets-11.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b"}, - {file = "websockets-11.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0"}, - {file = "websockets-11.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af"}, - {file = "websockets-11.0.3-cp37-cp37m-win32.whl", hash = "sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f"}, - {file = "websockets-11.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae"}, - {file = "websockets-11.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86"}, - {file = "websockets-11.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e"}, - {file = "websockets-11.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788"}, - {file = "websockets-11.0.3-cp38-cp38-win32.whl", hash = "sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74"}, - {file = "websockets-11.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd"}, - {file = "websockets-11.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b"}, - {file = "websockets-11.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1"}, - {file = "websockets-11.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311"}, - {file = "websockets-11.0.3-cp39-cp39-win32.whl", hash = "sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128"}, - {file = "websockets-11.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b"}, - {file = "websockets-11.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280"}, - {file = "websockets-11.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4"}, - {file = "websockets-11.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602"}, - {file = "websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6"}, - {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] [package.source] @@ -3468,7 +2424,6 @@ reference = "ali" name = "win32-setctime" version = "1.1.0" description = "A small Python utility to set file creation time on Windows" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -3484,163 +2439,103 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "wordcloud" -version = "1.9.2" -description = "A little word cloud generator" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "wordcloud-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67a83ad05b7a08db64e99cf734ba4394855e78c9e3c6e3a6104f80c64f090d2f"}, - {file = "wordcloud-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d7614f46c8062fe2d3fa0325c747793b463215b0e9d97b4f9a3d6f31a60b7f"}, - {file = "wordcloud-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5cec4a76b3cde8a19ec5c7ed7356d92c7337af3f1e11eb8e0d8191459f318bba"}, - {file = "wordcloud-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f37673f17d772907b37d92102cb437dce408b1e12fc3f59a6d3920082318881b"}, - {file = "wordcloud-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1e0b346eb15f6deed7d4cb6caaa4d7abf4d488434cb9cdb91194720b524ea86d"}, - {file = "wordcloud-1.9.2-cp310-cp310-win32.whl", hash = "sha256:61816f1e548a5505789e6e42a7cf7c798312b77a30465c3b8a6049235bcf4649"}, - {file = "wordcloud-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:e03ec307901cab43baeda3416d880d56895ef4ffa560e25c7a9dd74e94ede5c6"}, - {file = "wordcloud-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e744a88bc463ca373472cc831d9a6a6469dceaa6471abdc124828adeb2355df"}, - {file = "wordcloud-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c25014af784bc75741ae6693bbbd98faa4865de397903eed5485dafb2eb356a4"}, - {file = "wordcloud-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f49ff8eb67982b22ed476c48a4b5728c4f638ebed2452b4760b68f15d62931f"}, - {file = "wordcloud-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7101f62967fd912e49cf10e58e09a5af0fc11095d9f8bf170b22d8e0b2ee8ca"}, - {file = "wordcloud-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:73f7571df267ea8f6765b4cb946d6b158c4fd36aca44533716ca634bf2fd75f9"}, - {file = "wordcloud-1.9.2-cp311-cp311-win32.whl", hash = "sha256:10ad4bbed83ad487b3592ad0417426ffff946f131422e9e6bab74129caf7a6e3"}, - {file = "wordcloud-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:76ebbbefd453fa3ea15e03e605437c88a721502e077bae2f49c676d84dd0437d"}, - {file = "wordcloud-1.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a0fefffa6ea013276a36489d28aaceed1ae2f99f884b6a2fa1d4107bd1b2df3a"}, - {file = "wordcloud-1.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da5b4b8ed29fe56039b30f86c1c5ef4b06ce7f2f3ba111af61ede2fc661b0c45"}, - {file = "wordcloud-1.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4210c7f2db7e825d670a8e31a118f15b333e7ace555e03df15c901ae7fa19219"}, - {file = "wordcloud-1.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:030d89ea934472eeda1beabd82a81cfb0f2d0de7c5703f220016742aa6911c3f"}, - {file = "wordcloud-1.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9750c4e641b96e02e7e50ba2cae9441d611ca26310de0628b1e6444dfba93554"}, - {file = "wordcloud-1.9.2-cp36-cp36m-win32.whl", hash = "sha256:4dad68777056a09b5c9d8bb10842fbd5c851ecca35a27ddcbb47f28b1d6b5921"}, - {file = "wordcloud-1.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a70f10c477db98a9850dec1f9fe4f8774c2afd902f29bbff887c625f6c92e5d1"}, - {file = "wordcloud-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2df1e7e74ec97e296bf1a1b0ac2764576b83c6c4d3b1d00e01d095326b89063b"}, - {file = "wordcloud-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cd52aaa52b1a802154e402c2063ee6805c6f7d7ee84b5503b5c11d6b501224"}, - {file = "wordcloud-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ec9ab90114a3afc050b4a3e94c86d6f81e7dbbf517f79291a89e39366459e24"}, - {file = "wordcloud-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1a1d0bac12025031594a76694e0b04b38f82172443ad197224ef6d51f540e28e"}, - {file = "wordcloud-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1e521afd56d86e7f6ced3567ac685c4342c81c47f52c32b414a408c2f00f7884"}, - {file = "wordcloud-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:6dfcf9a54a328c5547a6c08197d5999a8d2444390dcf35dfdba9774d65c992cb"}, - {file = "wordcloud-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9eb2a9469d1981f3d102225ca5b1b2e7022042761e824d803c802ae0ff5d3251"}, - {file = "wordcloud-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:349272c5156e196ff0c05d910595dcc56071a4e622707e12889f2ee21c360a0b"}, - {file = "wordcloud-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad0a2e4509cc620587746280e5cf667ec3db4e713c63f4c21e69adad41041be"}, - {file = "wordcloud-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baebbdd53b1b800c816ffc1c2d71f78feb3a9599f1197422a5995d112069d485"}, - {file = "wordcloud-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad41c4069a636452fe37754e0f3b008417ab3b29e45016caf9113fad911106c7"}, - {file = "wordcloud-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bf03002ea15f5ad2ba69d8a68071c96758d88e0c466a599a71324e4297d4d2e0"}, - {file = "wordcloud-1.9.2-cp38-cp38-win32.whl", hash = "sha256:fb25360f2c8d5b52703b8a92dd45cadc84fa901bc322b40c0883dc2f974c29fe"}, - {file = "wordcloud-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:785b8bcc00d953b752a220c9b4b1f8532983f26805f70b46f3c6ce2e6b130f54"}, - {file = "wordcloud-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:139f0dbf6b6aeb32a20ccb383320dde514297c9aeba9b6cc7a82f4bb2502b1f9"}, - {file = "wordcloud-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91e025b5e50d814601703aa4878dbafdceaa0a3e64d42cd1633317d9b829bb64"}, - {file = "wordcloud-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cdf7962555f9fed612c4d060a36851f2aefeb744bd0bf6b45adeaf1a37c43e1"}, - {file = "wordcloud-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce15d884fb2b1087303878b248f449035abe2a545e17016021c2b86fe916ba4a"}, - {file = "wordcloud-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aed191fb1a7cefc6556a290a26d4986246f0a71e93330f8cbca25341566c312c"}, - {file = "wordcloud-1.9.2-cp39-cp39-win32.whl", hash = "sha256:391b98c88ef2ce79171edee9be60b9a5eb864716452a1fbe47984252550e6a8d"}, - {file = "wordcloud-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:0115a5dd0fd7aafc1ea20738478ac01ba9cf4203bfd7b624e2ab1a311c33f80c"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6ecfde604fbb8a1096d1507edc6d75771f07751409293188e559efb6628fd9f6"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f6ba0cc325d99eb515a60cb89d4b34d546c5f1c9a0595accff4c783d92ce28"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22fb86e0101c46f139d353e8f3575c8c64a6c503f263d90cecc28cd14bc5032"}, - {file = "wordcloud-1.9.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:7dfcb202cd38a094add34261bbb63119e2897dacbde50cac6c7ff3fdb97c555f"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fa46c8d175c0b8dd9a1d599884bfb3dd7f626a9fc553834ae6522544d1ae0eca"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a602e9161b7a8d4e2d92f5a891a9aee92b97402aa75db51918afa6aa4da9e8f"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0625865763b1a6c27860431023ee5f5a402e199301b2dc671bea03734a612ee0"}, - {file = "wordcloud-1.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b98e7ff6fa4277ca40074cb4fac2f2e4dd792d530177cc7494d42777d53e96c6"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0351a0190f2f2abd8422a0f817d1f9689eec24f0563438626f8f9b25ba7d061"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47b6e8948350cfc6f55988f1398ccd405a49a14b2ca6dd6b76fb76089a8b9a03"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9b7524bd647077ea1b095b3f2669dba4e6a6314eb41641914a41b991ae56b2a"}, - {file = "wordcloud-1.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3132bf742fa403616201454ee90f1123e53f546b6e22bddda1b5ba14974457a7"}, - {file = "wordcloud-1.9.2.tar.gz", hash = "sha256:71062ba6bfeaf1a7f8b6f18f6a8a7a810ef10973ebd9aa10c04d9fff690363d3"}, -] - -[package.dependencies] -matplotlib = "*" -numpy = ">=1.6.1" -pillow = "*" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "yarl" -version = "1.9.2" +version = "1.9.4" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, ] [package.dependencies] @@ -3652,28 +2547,7 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "zipp" -version = "3.16.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.16.2-py3-none-any.whl", hash = "sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0"}, - {file = "zipp-3.16.2.tar.gz", hash = "sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "d2b5e1b19170350e5f955e1ca454f952e69c2229b06d6631ed05dc8c8531064a" +python-versions = "^3.10" +content-hash = "bc2932cc9955e05badaaf34f0bda8031edd80d3a832ccd05f9c079fadc4c5cdf" diff --git a/pyproject.toml b/pyproject.toml index 75e454ae..b79c47c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,42 +11,24 @@ default = true url = "https://mirrors.aliyun.com/pypi/simple/" [tool.poetry.dependencies] -python = "^3.8" -nonebot2 = "^2.0.0rc1" -aiofiles = "^0.8.0" -aiohttp = "3.7.4.post0" -beautifulsoup4 = "4.9.3" -feedparser = "^6.0.8" -httpx = "^0.23.0" -ImageHash = "^4.2.1" -jieba = "^0.42.1" -lxml = "4.6.5" -opencv-python = "^4.5.5" -Pillow = "^9.0.1" -playwright = "^1.18.2" -psutil = "^5.9.0" -PyYAML = "5.4.1" -retrying = "^1.3.3" -ujson = "^5.1.0" -"ruamel.yaml" = "^0.17.21" -matplotlib = "^3.5.1" -black = "^22.1.0" -pypinyin = "^0.46.0" -dateparser = "^1.1.0" -cn2an = "^0.5.16" -python-jose = "^3.3.0" -python-multipart = "^0.0.5" -bilireq = "^0.2.6" -emoji = "^1.7.0" -wordcloud = "^1.8.1" -rich = "^12.4.3" -nonebot-adapter-onebot = "^2.1.5" -nonebot-plugin-apscheduler = "^0.2.0" -nonebot-plugin-htmlrender = "^0.2.0" -cachetools = "^5.2.0" -tortoise-orm = {extras = ["asyncpg"], version = "^0.19.3"} -cattrs = "^22.2.0" -starlette = "^0.26.1" +python = "^3.10" +nonebot-plugin-alconna = "^0.36.0" +playwright = "^1.41.1" +nonebot-adapter-onebot = "^2.3.1" +nonebot-plugin-apscheduler = "^0.3.0" +tortoise-orm = {extras = ["asyncpg"], version = "^0.20.0"} +cattrs = "^23.2.3" +ruamel-yaml = "^0.18.5" +strenum = "^0.4.15" +nonebot-plugin-session = "^0.2.3" +nonebot-plugin-send-anything-anywhere = "^0.5.0" +ujson = "^5.9.0" +nonebot-adapter-kaiheila = "^0.3.0" +nb-cli = "^1.3.0" +nonebot2 = "^2.1.3" +nonebot-adapter-discord = "^0.1.3" +nonebot-adapter-dodo = "^0.1.4" +pillow = "9.5" [tool.poetry.dev-dependencies] diff --git a/resources/image/_icon/discode.png b/resources/image/_icon/discode.png new file mode 100644 index 0000000000000000000000000000000000000000..08e0a93731f471a6bfdfd6f8e95aad27a3d8bfb0 GIT binary patch literal 285478 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 R(yr7{YDkWjti;Sn{(q<3%}xLS literal 0 HcmV?d00001 diff --git a/resources/image/_icon/dodo.png b/resources/image/_icon/dodo.png new file mode 100644 index 0000000000000000000000000000000000000000..58fa20fcdf20df76a9ca7f2582a4053e306eabec GIT binary patch literal 229161 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 literal 0 HcmV?d00001 diff --git a/resources/image/_icon/qq.png b/resources/image/_icon/qq.png new file mode 100644 index 0000000000000000000000000000000000000000..42825456fef4e36855eda47132c37c157c6167d9 GIT binary patch literal 1058 zcmV+-1l{|IP)W(h*N=-3gQmz4(tw;4q{gTbztX~n*)rfF-?;c1z<_+ za^K7!&PW=H*iY;d0to?%q9}@@C`;rW%Ex-0d7akVPhO_=nXT7Ty?yYy_xfGGrbJ-9 zuDu?;o)fh3s<1On8A0XckQVeVg?+B@w&y4{7Q3|ZOsrr;1?3gLKp*(7)MT)5LS7Dm zc%DfP3pT-kCf6UUP~^|BFv6K%Lhu)B!Y#@P{^HWn3grcVuMLD_P;T(|T0<}n8JF9Nhym&QhI=eF8N}SNQwa> zo}o(Q95eC(@ZRJpsE?nJYrv0z_Y5YaZvf`%_iw;&Fm*_e zb=$6xc|ex427IZvLtT|C`!=o7-@~J(DPs)i`l4p)k!Zj}y$usE-_Sh~Gub?GyX)QJ z?SZ7O`(K)DtBsch?RsD_5e~z_M}07*2X?Hk(a)ywIfIE< zW8C`t4!i>0Co|B-kZ!Ng-%Dnt0U^=YVckJd9KB7EAtqOOGZ@vCO2%%9c z@EQ+Z?o3QP$H&LO~Kv;i|DEL-0e*`+DfdO=&rBKc9IK5{%G)`h|D zYR7t=;Pu~-f5Pze-4k9n_^GA8c=`KsH1>Pa+P7)!07*qoM6N<$g0ouI00000 literal 0 HcmV?d00001 diff --git a/services/log.py b/services/log.py deleted file mode 100755 index c656100d..00000000 --- a/services/log.py +++ /dev/null @@ -1,144 +0,0 @@ -from datetime import datetime, timedelta -from typing import Any, Dict, Optional, Union - -from loguru import logger as logger_ -from nonebot.log import default_filter, default_format - -from configs.path_config import LOG_PATH - -logger_.add( - LOG_PATH / f"{datetime.now().date()}.log", - level="INFO", - rotation="00:00", - format=default_format, - filter=default_filter, - retention=timedelta(days=30), -) - -logger_.add( - LOG_PATH / f"error_{datetime.now().date()}.log", - level="ERROR", - rotation="00:00", - format=default_format, - filter=default_filter, - retention=timedelta(days=30), -) - - -class logger: - - TEMPLATE_A = "{}" - TEMPLATE_B = "[{}]: {}" - TEMPLATE_C = "用户[{}] 触发 [{}]: {}" - TEMPLATE_D = "群聊[{}] 用户[{}] 触发 [{}]: {}" - TEMPLATE_E = "群聊[{}] 用户[{}] 触发 [{}] [Target]({}): {}" - - TEMPLATE_USER = "用户[{}] " - TEMPLATE_GROUP = "群聊[{}] " - TEMPLATE_COMMAND = "CMD[{}] " - TEMPLATE_TARGET = "[Target]([{}]) " - - SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" - - WARNING_TEMPLATE = "[{}]: {}" - - ERROR_TEMPLATE = "[{}]: {}" - - @classmethod - def info( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - logger_.opt(colors=True).info(template) - - @classmethod - def success( - cls, - info: str, - command: str, - param: Optional[Dict[str, Any]] = None, - result: Optional[str] = "", - ): - param_str = "" - if param: - param_str = ",".join([f"{k}:{v}" for k, v in param.items()]) - logger_.opt(colors=True).success( - cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) - ) - - @classmethod - def warning( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误{type(e)}: {e}" - logger_.opt(colors=True).warning(template) - - @classmethod - def error( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).error(template) - - @classmethod - def debug( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - e: Optional[Exception] = None, - ): - template = cls.__parser_template(info, command, user_id, group_id, target) - if e: - template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).debug(template) - - @classmethod - def __parser_template( - cls, - info: str, - command: Optional[str] = None, - user_id: Optional[Union[int, str]] = None, - group_id: Optional[Union[int, str]] = None, - target: Optional[Any] = None, - ) -> str: - arg_list = [] - template = "" - if group_id is not None: - template += cls.TEMPLATE_GROUP - arg_list.append(group_id) - if user_id is not None: - template += cls.TEMPLATE_USER - arg_list.append(user_id) - if command is not None: - template += cls.TEMPLATE_COMMAND - arg_list.append(command) - if target is not None: - template += cls.TEMPLATE_TARGET - arg_list.append(target) - arg_list.append(info) - template += "{}" - return template.format(*arg_list) diff --git a/update_info.json b/update_info.json deleted file mode 100644 index 140e2ea4..00000000 --- a/update_info.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "update_file": [ - "plugins", - "models", - "services", - "utils", - "basic_plugins", - "configs/path_config.py", - "configs/utils", - "poetry.lock", - "pyproject.toml" - ], - "add_file": ["resources/image/csgo_cases"], - "delete_file": [] -} \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100755 index e69de29b..00000000 diff --git a/utils/data_utils.py b/utils/data_utils.py deleted file mode 100755 index 6c6458c0..00000000 --- a/utils/data_utils.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio -import os -from typing import List, Union - -from configs.path_config import IMAGE_PATH -from models.group_member_info import GroupInfoUser -from utils.image_utils import BuildMat - - -async def init_rank( - title: str, - all_user_id: List[str], - all_user_data: List[Union[int, float]], - group_id: int, - total_count: int = 10, -) -> BuildMat: - """ - 说明: - 初始化通用的数据排行榜 - 参数: - :param title: 排行榜标题 - :param all_user_id: 所有用户的qq号 - :param all_user_data: 所有用户需要排行的对应数据 - :param group_id: 群号,用于从数据库中获取该用户在此群的昵称 - :param total_count: 获取人数总数 - """ - _uname_lst = [] - _num_lst = [] - for i in range(min(len(all_user_id), total_count)): - _max = max(all_user_data) - max_user_id = all_user_id[all_user_data.index(_max)] - all_user_id.remove(max_user_id) - all_user_data.remove(_max) - if user := await GroupInfoUser.get_or_none( - user_id=str(max_user_id), group_id=str(group_id) - ): - user_name = user.user_name - else: - user_name = f"{max_user_id}" - _uname_lst.append(user_name) - _num_lst.append(_max) - _uname_lst.reverse() - _num_lst.reverse() - return await asyncio.get_event_loop().run_in_executor( - None, _init_rank_graph, title, _uname_lst, _num_lst - ) - - -def _init_rank_graph( - title: str, _uname_lst: List[str], _num_lst: List[Union[int, float]] -) -> BuildMat: - """ - 生成排行榜统计图 - :param title: 排行榜标题 - :param _uname_lst: 用户名列表 - :param _num_lst: 数值列表 - """ - image = BuildMat( - y=_num_lst, - y_name="* 可以在命令后添加数字来指定排行人数 至多 50 *", - mat_type="barh", - title=title, - x_index=_uname_lst, - display_num=True, - x_rotate=30, - background=[ - f"{IMAGE_PATH}/background/create_mat/{x}" - for x in os.listdir(f"{IMAGE_PATH}/background/create_mat") - ], - bar_color=["*"], - ) - image.gen_graph() - return image diff --git a/utils/decorator/__init__.py b/utils/decorator/__init__.py deleted file mode 100644 index b91f2a8a..00000000 --- a/utils/decorator/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -class Singleton: - - """ - 单例注解 - """ - - def __init__(self, cls): - self._cls = cls - - def __call__(self, *args, **kw): - if not hasattr(self, "_instance"): - self._instance = self._cls(*args, **kw) - return self._instance diff --git a/utils/decorator/shop.py b/utils/decorator/shop.py deleted file mode 100644 index 5105e4c2..00000000 --- a/utils/decorator/shop.py +++ /dev/null @@ -1,204 +0,0 @@ -from typing import Callable, Union, Tuple, Optional -from nonebot.adapters.onebot.v11 import MessageSegment, Message -from nonebot.plugin import require - - -class ShopRegister(dict): - def __init__(self, *args, **kwargs): - super(ShopRegister, self).__init__(*args, **kwargs) - self._data = {} - self._flag = True - - def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用前检查方法 - 参数: - :param name: 道具名称 - :param load_status: 加载状态 - """ - def register_before_handle(name_list: Tuple[str, ...], func: Callable): - if load_status: - for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('before_handle'): - self._data[name_]['before_handle'] = [] - self._data[name]['before_handle'].append(func) - _name = (name,) if isinstance(name, str) else name - return lambda func: register_before_handle(_name, func) - - def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用后执行方法 - 参数: - :param name: 道具名称 - :param load_status: 加载状态 - """ - def register_after_handle(name_list: Tuple[str, ...], func: Callable): - if load_status: - for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('after_handle'): - self._data[name_]['after_handle'] = [] - self._data[name_]['after_handle'].append(func) - _name = (name,) if isinstance(name, str) else name - return lambda func: register_after_handle(_name, func) - - def register( - self, - name: Tuple[str, ...], - price: Tuple[float, ...], - des: Tuple[str, ...], - discount: Tuple[float, ...], - limit_time: Tuple[int, ...], - load_status: Tuple[bool, ...], - daily_limit: Tuple[int, ...], - is_passive: Tuple[bool, ...], - icon: Tuple[str, ...], - **kwargs, - ): - def add_register_item(func: Callable): - if name in self._data.keys(): - raise ValueError("该商品已注册,请替换其他名称!") - for n, p, d, dd, l, s, dl, pa, i in zip( - name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon - ): - if s: - _temp_kwargs = {} - for key, value in kwargs.items(): - if key.startswith(f"{n}_"): - _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value - else: - _temp_kwargs[key] = value - temp = self._data.get(n, {}) - temp.update({ - "price": p, - "des": d, - "discount": dd, - "limit_time": l, - "daily_limit": dl, - "icon": i, - "is_passive": pa, - "func": func, - "kwargs": _temp_kwargs, - }) - self._data[n] = temp - return func - - return lambda func: add_register_item(func) - - async def load_register(self): - require("use") - require("shop_handle") - from basic_plugins.shop.use.data_source import register_use, func_manager - from basic_plugins.shop.shop_handle.data_source import register_goods - # 统一进行注册 - if self._flag: - # 只进行一次注册 - self._flag = False - for name in self._data.keys(): - await register_goods( - name, - self._data[name]["price"], - self._data[name]["des"], - self._data[name]["discount"], - self._data[name]["limit_time"], - self._data[name]["daily_limit"], - self._data[name]["is_passive"], - self._data[name]["icon"], - ) - register_use( - name, self._data[name]["func"], **self._data[name]["kwargs"] - ) - func_manager.register_use_before_handle(name, self._data[name].get('before_handle', [])) - func_manager.register_use_after_handle(name, self._data[name].get('after_handle', [])) - - def __call__( - self, - name: Union[str, Tuple[str, ...]], # 名称 - price: Union[float, Tuple[float, ...]], # 价格 - des: Union[str, Tuple[str, ...]], # 简介 - discount: Union[float, Tuple[float, ...]] = 1, # 折扣 - limit_time: Union[int, Tuple[int, ...]] = 0, # 限时 - load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态 - daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购 - is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗) - icon: Union[str, Tuple[str, ...]] = False, # 图标 - **kwargs, - ): - _tuple_list = [] - _current_len = -1 - for x in [name, price, des, discount, limit_time, load_status]: - if isinstance(x, tuple): - if _current_len == -1: - _current_len = len(x) - if _current_len != len(x): - raise ValueError( - f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!" - ) - _current_len = _current_len if _current_len > -1 else 1 - _name = self.__get(name, _current_len) - _price = self.__get(price, _current_len) - _discount = self.__get(discount, _current_len) - _limit_time = self.__get(limit_time, _current_len) - _des = self.__get(des, _current_len) - _load_status = self.__get(load_status, _current_len) - _daily_limit = self.__get(daily_limit, _current_len) - _is_passive = self.__get(is_passive, _current_len) - _icon = self.__get(icon, _current_len) - return self.register( - _name, - _price, - _des, - _discount, - _limit_time, - _load_status, - _daily_limit, - _is_passive, - _icon, - **kwargs, - ) - - def __get(self, value, _current_len): - return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)]) - - def __setitem__(self, key, value): - self._data[key] = value - - def __getitem__(self, key): - return self._data[key] - - def __contains__(self, key): - return key in self._data - - def __str__(self): - return str(self._data) - - def keys(self): - return self._data.keys() - - def values(self): - return self._data.values() - - def items(self): - return self._data.items() - - -class NotMeetUseConditionsException(Exception): - - """ - 不满足条件异常类 - """ - - def __init__(self, info: Optional[Union[str, MessageSegment, Message]]): - super().__init__(self) - self._info = info - - def get_info(self): - return self._info - - -shop_register = ShopRegister() diff --git a/utils/depends/__init__.py b/utils/depends/__init__.py deleted file mode 100644 index 310df213..00000000 --- a/utils/depends/__init__.py +++ /dev/null @@ -1,215 +0,0 @@ -from typing import Any, Callable, List, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import GroupMessageEvent, MessageEvent -from nonebot.internal.matcher import Matcher -from nonebot.internal.params import Depends -from nonebot.params import Command - -from configs.config import Config -from models.bag_user import BagUser -# from models.bag_user import BagUser -from models.level_user import LevelUser -from models.user_shop_gold_log import UserShopGoldLog -# from models.user_shop_gold_log import UserShopGoldLog -from utils.manager import admin_manager -from utils.message_builder import at -from utils.utils import ( - get_message_at, - get_message_face, - get_message_img, - get_message_text, -) - - -def OneCommand(): - """ - 获取单个命令Command - """ - - async def dependency( - cmd: Tuple[str, ...] = Command(), - ): - return cmd[0] if cmd else None - - return Depends(dependency) - - -def AdminCheck(level: Optional[int] = None): - """ - 说明: - 权限检查 - 参数: - :param level: 等级 - """ - - async def dependency(matcher: Matcher, event: GroupMessageEvent): - if name := matcher.plugin_name: - plugin_level = admin_manager.get_plugin_level(name) - user_level = await LevelUser.get_user_level(event.user_id, event.group_id) - if level is None: - if user_level < plugin_level: - await matcher.finish( - at(event.user_id) + f"你的权限不足喔,该功能需要的权限等级:{plugin_level}" - ) - else: - if user_level < level: - await matcher.finish( - at(event.user_id) + f"你的权限不足喔,该功能需要的权限等级:{level}" - ) - - return Depends(dependency) - - -def CostGold(gold: int): - """ - 说明: - 插件方法调用使用金币 - 参数: - :param gold: 金币数量 - """ - - async def dependency(matcher: Matcher, event: GroupMessageEvent): - if (await BagUser.get_gold(event.user_id, event.group_id)) < gold: - await matcher.finish(at(event.user_id) + f"金币不足..该功能需要{gold}金币..") - await BagUser.spend_gold(event.user_id, event.group_id, gold) - await UserShopGoldLog.create( - user_id=str(event.user_id), - group_id=str(event.group_id), - type=2, - name=matcher.plugin_name, - num=1, - spend_gold=gold, - ) - - return Depends(dependency) - - -def GetConfig( - module: Optional[str] = None, - config: str = "", - default_value: Any = None, - prompt: Optional[str] = None, -): - """ - 说明: - 获取配置项 - 参数: - :param module: 模块名,为空时默认使用当前插件模块名 - :param config: 配置项名称 - :param default_value: 默认值 - :param prompt: 为空时提示 - """ - - async def dependency(matcher: Matcher): - module_ = module or matcher.plugin_name - if module_: - value = Config.get_config(module_, config, default_value) - if value is None and prompt: - # await matcher.finish(prompt or f"配置项 {config} 未填写!") - await matcher.finish(prompt) - return value - - return Depends(dependency) - - -def CheckConfig( - module: Optional[str] = None, - config: Union[str, List[str]] = "", - prompt: Optional[str] = None, -): - """ - 说明: - 检测配置项在配置文件中是否填写 - 参数: - :param module: 模块名,为空时默认使用当前插件模块名 - :param config: 需要检查的配置项名称 - :param prompt: 为空时提示 - """ - - async def dependency(matcher: Matcher): - module_ = module or matcher.plugin_name - if module_: - config_list = [config] if isinstance(config, str) else config - for c in config_list: - if Config.get_config(module_, c) is None: - await matcher.finish(prompt or f"配置项 {c} 未填写!") - - return Depends(dependency) - - -async def _match( - matcher: Matcher, - event: MessageEvent, - msg: Optional[str], - func: Callable, - contain_reply: bool, -): - _list = func(event.message) - if event.reply and contain_reply: - _list = func(event.reply.message) - if not _list and msg: - await matcher.finish(msg) - return _list - - -def ImageList(msg: Optional[str] = None, contain_reply: bool = True) -> List[str]: - """ - 说明: - 获取图片列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_img, contain_reply) - - return Depends(dependency) - - -def AtList(msg: Optional[str] = None, contain_reply: bool = True) -> List[int]: - """ - 说明: - 获取at列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return [ - int(x) - for x in await _match(matcher, event, msg, get_message_at, contain_reply) - ] - - return Depends(dependency) - - -def FaceList(msg: Optional[str] = None, contain_reply: bool = True) -> List[str]: - """ - 说明: - 获取face列表(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_face, contain_reply) - - return Depends(dependency) - - -def PlaintText(msg: Optional[str] = None, contain_reply: bool = True) -> str: - """ - 说明: - 获取纯文本且(包括回复时),含有msg时不能为空,为空时提示并结束事件 - 参数: - :param msg: 提示文本 - :param contain_reply: 包含回复内容 - """ - - async def dependency(matcher: Matcher, event: MessageEvent): - return await _match(matcher, event, msg, get_message_text, contain_reply) - - return Depends(dependency) diff --git a/utils/game_utils.py b/utils/game_utils.py deleted file mode 100644 index 2ce2eb86..00000000 --- a/utils/game_utils.py +++ /dev/null @@ -1,192 +0,0 @@ -from typing import Optional, Dict, Union - -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from pydantic import BaseModel -import time - - -class GameEntry(BaseModel): - game_name: str - module: str - default_msg: str - msg_data: Dict[int, Union[str, Message, MessageSegment]] - timeout: int # 超时时限 - anti_concurrency: bool # 是否阻断 - - -class GroupGameStatus(BaseModel): - game: GameEntry - status: int - time: time.time() # 创建时间 - - -class GameManager: - def __init__(self): - self._data = {} - self._status = {} - - def add_game( - self, - game_name: str, - module: str, - timeout: int, - default_msg: Optional[str] = "游戏还未结束!", - msg_data: Dict[int, Union[str, Message, MessageSegment]] = None, - anti_concurrency: bool = True, - **kwargs, - ): - """ - 参数: - 将游戏添加到游戏管理器 - 说明: - :param game_name: 游戏名称 - :param module: 模块名 - :param timeout: 超时时长 - :param default_msg: 默认回复消息 - :param msg_data: 不同状态回复的消息 - :param anti_concurrency: 是否阻断反并发 - """ - self._data[module] = GameEntry( - game_name=game_name, - module=module, - timeout=timeout, - default_msg=default_msg, - msg_data=msg_data or {}, - anti_concurrency=anti_concurrency, - **kwargs, - ) - - def start(self, group_id: int, module: str): - """ - 说明: - 游戏开始标记 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if not self._status.get(group_id): - self._status[group_id] = [] - if module not in [x.game.module for x in self._status[module]]: - self._status[group_id].append( - GroupGameStatus(game=self._data[module], status=0) - ) - - def end(self, group_id: int, module: str): - """ - 说明: - 游戏结束标记 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - for x in self._status[group_id]: - if self._status[group_id][x].game.module == module: - self._status[group_id].remove(x) - break - - def set_status(self, group_id: int, module: str, status: int): - """ - 说明: - 设置游戏状态,根据状态发送不同的提示消息 msg_data - 参数: - :param group_id: 群号 - :param module: 模块名 - :param status: 状态码 - """ - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - [x.game.module for x in self._status[group_id] if x.game.module == module][ - 0 - ].status = status - - def check(self, group_id, module: str) -> Optional[str]: - """ - 说明: - 检查群游戏当前状态并返回提示语句 - 参数: - :param group_id: 群号 - :param module: 模块名 - """ - if module in self._data and self._status.get(group_id): - for x in self._status[group_id]: - if x.game.anti_concurrency: - return f"{x.game.game_name} 还未结束,请等待 {x.game.game_name} 游戏结束!" - if self._status.get(group_id) and module in [ - x.game.module for x in self._status[group_id] - ]: - group_game_status = [ - x.game.module for x in self._status[group_id] if x.game.module == module - ][0] - if time.time() - group_game_status.time > group_game_status.game.timeout: - # 超时结束 - self.end(group_id, module) - else: - return ( - group_game_status.game.msg_data.get(group_game_status.status) - or group_game_status.game.default_msg - ) - - -game_manager = GameManager() - - -class Game: - """ - 反并发,游戏重复开始 - """ - def __init__( - self, - game_name: str, - module: str, - timeout: int = 60, - default_msg: Optional[str] = None, - msg_data: Dict[int, Union[str, Message, MessageSegment]] = None, - anti_concurrency: bool = True, - ): - """ - 参数: - 将游戏添加到游戏管理器 - 说明: - :param game_name: 游戏名称 - :param module: 模块名 - :param timeout: 超时时长 - :param default_msg: 默认回复消息 - :param msg_data: 不同状态回复的消息 - :param anti_concurrency: 是否阻断反并发 - """ - self.module = module - game_manager.add_game( - game_name, module, timeout, default_msg, msg_data, anti_concurrency - ) - - def start(self, group_id: int): - """ - 说明: - 游戏开始标记 - 参数: - :param group_id: 群号 - """ - game_manager.start(group_id, self.module) - - def end(self, group_id: int): - """ - 说明: - 游戏结束标记 - 参数: - :param group_id: 群号 - """ - game_manager.end(group_id, self.module) - - def set_status(self, group_id: int, status: int): - """ - 说明: - 设置游戏状态,根据状态发送不同的提示消息 msg_data - 参数: - :param group_id: 群号 - :param status: 状态码 - """ - game_manager.set_status(group_id, self.module, status) diff --git a/utils/http_utils.py b/utils/http_utils.py deleted file mode 100644 index ad542dcc..00000000 --- a/utils/http_utils.py +++ /dev/null @@ -1,379 +0,0 @@ -import asyncio -from asyncio.exceptions import TimeoutError -from contextlib import asynccontextmanager -from pathlib import Path -from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union - -import aiofiles -import httpx -import rich -from httpx import ConnectTimeout, Response -from nonebot.adapters.onebot.v11 import MessageSegment -from playwright.async_api import BrowserContext, Page -from retrying import retry - -from services.log import logger -from utils.user_agent import get_user_agent, get_user_agent_str - -from .browser import get_browser -from .message_builder import image -from .utils import get_local_proxy - - -class AsyncHttpx: - - proxy = {"http://": get_local_proxy(), "https://": get_local_proxy()} - - @classmethod - @retry(stop_max_attempt_number=3) - async def get( - cls, - url: str, - *, - params: Optional[Dict[str, Any]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - verify: bool = True, - use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, - **kwargs, - ) -> Response: - """ - 说明: - Get - 参数: - :param url: url - :param params: params - :param headers: 请求头 - :param cookies: cookies - :param verify: verify - :param use_proxy: 使用默认代理 - :param proxy: 指定代理 - :param timeout: 超时时间 - """ - if not headers: - headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None - async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client: - return await client.get( - url, - params=params, - headers=headers, - cookies=cookies, - timeout=timeout, - **kwargs, - ) - - @classmethod - async def post( - cls, - url: str, - *, - data: Optional[Dict[str, str]] = None, - content: Any = None, - files: Any = None, - verify: bool = True, - use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - json: Optional[Dict[str, Any]] = None, - params: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, - **kwargs, - ) -> Response: - """ - 说明: - Post - 参数: - :param url: url - :param data: data - :param content: content - :param files: files - :param use_proxy: 是否默认代理 - :param proxy: 指定代理 - :param json: json - :param params: params - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 - """ - if not headers: - headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None - async with httpx.AsyncClient(proxies=proxy_, verify=verify) as client: - return await client.post( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - timeout=timeout, - **kwargs, - ) - - @classmethod - async def download_file( - cls, - url: str, - path: Union[str, Path], - *, - params: Optional[Dict[str, str]] = None, - verify: bool = True, - use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, - stream: bool = False, - **kwargs, - ) -> bool: - """ - 说明: - 下载文件 - 参数: - :param url: url - :param path: 存储路径 - :param params: params - :param verify: verify - :param use_proxy: 使用代理 - :param proxy: 指定代理 - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 - :param stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件) - """ - if isinstance(path, str): - path = Path(path) - path.parent.mkdir(parents=True, exist_ok=True) - try: - for _ in range(3): - if not stream: - try: - content = ( - await cls.get( - url, - params=params, - headers=headers, - cookies=cookies, - use_proxy=use_proxy, - proxy=proxy, - timeout=timeout, - **kwargs, - ) - ).content - async with aiofiles.open(path, "wb") as wf: - await wf.write(content) - logger.info(f"下载 {url} 成功.. Path:{path.absolute()}") - return True - except (TimeoutError, ConnectTimeout): - pass - else: - if not headers: - headers = get_user_agent() - proxy_ = proxy if proxy else cls.proxy if use_proxy else None - try: - async with httpx.AsyncClient( - proxies=proxy_, verify=verify - ) as client: - async with client.stream( - "GET", - url, - params=params, - headers=headers, - cookies=cookies, - timeout=timeout, - **kwargs, - ) as response: - logger.info( - f"开始下载 {path.name}.. Path: {path.absolute()}" - ) - async with aiofiles.open(path, "wb") as wf: - total = int(response.headers["Content-Length"]) - with rich.progress.Progress( - rich.progress.TextColumn(path.name), - "[progress.percentage]{task.percentage:>3.0f}%", - rich.progress.BarColumn(bar_width=None), - rich.progress.DownloadColumn(), - rich.progress.TransferSpeedColumn(), - ) as progress: - download_task = progress.add_task( - "Download", total=total - ) - async for chunk in response.aiter_bytes(): - await wf.write(chunk) - await wf.flush() - progress.update( - download_task, - completed=response.num_bytes_downloaded, - ) - logger.info(f"下载 {url} 成功.. Path:{path.absolute()}") - return True - except (TimeoutError, ConnectTimeout): - pass - else: - logger.error(f"下载 {url} 下载超时.. Path:{path.absolute()}") - except Exception as e: - logger.error(f"下载 {url} 错误 Path:{path.absolute()}", e=e) - return False - - @classmethod - async def gather_download_file( - cls, - url_list: List[str], - path_list: List[Union[str, Path]], - *, - limit_async_number: Optional[int] = None, - params: Optional[Dict[str, str]] = None, - use_proxy: bool = True, - proxy: Optional[Dict[str, str]] = None, - headers: Optional[Dict[str, str]] = None, - cookies: Optional[Dict[str, str]] = None, - timeout: Optional[int] = 30, - **kwargs, - ) -> List[bool]: - """ - 说明: - 分组同时下载文件 - 参数: - :param url_list: url列表 - :param path_list: 存储路径列表 - :param limit_async_number: 限制同时请求数量 - :param params: params - :param use_proxy: 使用代理 - :param proxy: 指定代理 - :param headers: 请求头 - :param cookies: cookies - :param timeout: 超时时间 - """ - if n := len(url_list) != len(path_list): - raise UrlPathNumberNotEqual( - f"Url数量与Path数量不对等,Url:{len(url_list)},Path:{len(path_list)}" - ) - if limit_async_number and n > limit_async_number: - m = float(n) / limit_async_number - x = 0 - j = limit_async_number - _split_url_list = [] - _split_path_list = [] - for _ in range(int(m)): - _split_url_list.append(url_list[x:j]) - _split_path_list.append(path_list[x:j]) - x += limit_async_number - j += limit_async_number - if int(m) < m: - _split_url_list.append(url_list[j:]) - _split_path_list.append(path_list[j:]) - else: - _split_url_list = [url_list] - _split_path_list = [path_list] - tasks = [] - result_ = [] - for x, y in zip(_split_url_list, _split_path_list): - for url, path in zip(x, y): - tasks.append( - asyncio.create_task( - cls.download_file( - url, - path, - params=params, - headers=headers, - cookies=cookies, - use_proxy=use_proxy, - timeout=timeout, - proxy=proxy, - **kwargs, - ) - ) - ) - _x = await asyncio.gather(*tasks) - result_ = result_ + list(_x) - tasks.clear() - return result_ - - -class AsyncPlaywright: - @classmethod - @asynccontextmanager - async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]: - """ - 说明: - 获取一个新页面 - 参数: - :param user_agent: 请求头 - """ - browser = get_browser() - ctx = await browser.new_context(**kwargs) - page = await ctx.new_page() - try: - yield page - finally: - await page.close() - await ctx.close() - - @classmethod - async def screenshot( - cls, - url: str, - path: Union[Path, str], - element: Union[str, List[str]], - *, - wait_time: Optional[int] = None, - viewport_size: Optional[Dict[str, int]] = None, - wait_until: Optional[ - Literal["domcontentloaded", "load", "networkidle"] - ] = "networkidle", - timeout: Optional[float] = None, - type_: Optional[Literal["jpeg", "png"]] = None, - user_agent: Optional[str] = None, - **kwargs, - ) -> Optional[MessageSegment]: - """ - 说明: - 截图,该方法仅用于简单快捷截图,复杂截图请操作 page - 参数: - :param url: 网址 - :param path: 存储路径 - :param element: 元素选择 - :param wait_time: 等待截取超时时间 - :param viewport_size: 窗口大小 - :param wait_until: 等待类型 - :param timeout: 超时限制 - :param type_: 保存类型 - """ - if viewport_size is None: - viewport_size = dict(width=2560, height=1080) - if isinstance(path, str): - path = Path(path) - wait_time = wait_time * 1000 if wait_time else None - if isinstance(element, str): - element_list = [element] - else: - element_list = element - async with cls.new_page( - viewport=viewport_size, - user_agent=user_agent, - **kwargs, - ) as page: - await page.goto(url, timeout=timeout, wait_until=wait_until) - card = page - for e in element_list: - if not card: - return None - card = await card.wait_for_selector(e, timeout=wait_time) - if card: - await card.screenshot(path=path, timeout=timeout, type=type_) - return image(path) - return None - - -class UrlPathNumberNotEqual(Exception): - pass - - -class BrowserIsNone(Exception): - pass diff --git a/utils/image_template.py b/utils/image_template.py deleted file mode 100644 index caa93698..00000000 --- a/utils/image_template.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Tuple, List, Literal - -from utils.image_utils import BuildImage, text2image - - -async def help_template(title: str, usage: BuildImage) -> BuildImage: - """ - 说明: - 生成单个功能帮助模板 - 参数: - :param title: 标题 - :param usage: 说明图片 - """ - title_image = BuildImage( - 0, - 0, - font_size=35, - plain_text=title, - font_color=(255, 255, 255), - font="CJGaoDeGuo.otf", - ) - background_image = BuildImage( - max(title_image.w, usage.w) + 50, - max(title_image.h, usage.h) + 100, - color=(114, 138, 204), - ) - await background_image.apaste(usage, (25, 80), True) - await background_image.apaste(title_image, (25, 20), True) - await background_image.aline( - (25, title_image.h + 22, 25 + title_image.w, title_image.h + 22), - (204, 196, 151), - 3, - ) - return background_image - diff --git a/utils/image_utils.py b/utils/image_utils.py deleted file mode 100755 index d312f62f..00000000 --- a/utils/image_utils.py +++ /dev/null @@ -1,1778 +0,0 @@ -import asyncio -import base64 -import os -import random -import re -import uuid -from io import BytesIO -from math import ceil -from pathlib import Path -from typing import Any, Awaitable, Callable, List, Literal, Optional, Tuple, Union - -import cv2 -import imagehash -from imagehash import ImageHash -from matplotlib import pyplot as plt -from nonebot.utils import is_coroutine_callable -from PIL import Image, ImageDraw, ImageFile, ImageFilter, ImageFont -from PIL.ImageFont import FreeTypeFont - -from configs.path_config import FONT_PATH, IMAGE_PATH -from services import logger - -ImageFile.LOAD_TRUNCATED_IMAGES = True -Image.MAX_IMAGE_PIXELS = None - - -ModeType = Literal[ - "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr" -] - - -def compare_image_with_hash( - image_file1: str, image_file2: str, max_dif: float = 1.5 -) -> bool: - """ - 说明: - 比较两张图片的hash值是否相同 - 参数: - :param image_file1: 图片文件路径 - :param image_file2: 图片文件路径 - :param max_dif: 允许最大hash差值, 越小越精确,最小为0 - """ - ImageFile.LOAD_TRUNCATED_IMAGES = True - hash_1 = get_img_hash(image_file1) - hash_2 = get_img_hash(image_file2) - dif = hash_1 - hash_2 - if dif < 0: - dif = -dif - if dif <= max_dif: - return True - else: - return False - - -def get_img_hash(image_file: Union[str, Path]) -> ImageHash: - """ - 说明: - 获取图片的hash值 - 参数: - :param image_file: 图片文件路径 - """ - with open(image_file, "rb") as fp: - hash_value = imagehash.average_hash(Image.open(fp)) - return hash_value - - -def compressed_image( - in_file: Union[str, Path], - out_file: Optional[Union[str, Path]] = None, - ratio: float = 0.9, -): - """ - 说明: - 压缩图片 - 参数: - :param in_file: 被压缩的文件路径 - :param out_file: 压缩后输出的文件路径 - :param ratio: 压缩率,宽高 * 压缩率 - """ - in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file - if out_file: - out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file - else: - out_file = in_file - h, w, d = cv2.imread(str(in_file.absolute())).shape - img = cv2.resize( - cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) - ) - cv2.imwrite(str(out_file.absolute()), img) - - -def alpha2white_pil(pic: Image) -> Image: - """ - 说明: - 将图片透明背景转化为白色 - 参数: - :param pic: 通过PIL打开的图片文件 - """ - img = pic.convert("RGBA") - width, height = img.size - for yh in range(height): - for xw in range(width): - dot = (xw, yh) - color_d = img.getpixel(dot) - if color_d[3] == 0: - color_d = (255, 255, 255, 255) - img.putpixel(dot, color_d) - return img - - -def pic2b64(pic: Image) -> str: - """ - 说明: - PIL图片转base64 - 参数: - :param pic: 通过PIL打开的图片文件 - """ - buf = BytesIO() - pic.save(buf, format="PNG") - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - -def fig2b64(plt_: plt) -> str: - """ - 说明: - matplotlib图片转base64 - 参数: - :param plt_: matplotlib生成的图片 - """ - buf = BytesIO() - plt_.savefig(buf, format="PNG", dpi=100) - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - -def is_valid(file: Union[str, Path]) -> bool: - """ - 说明: - 判断图片是否损坏 - 参数: - :param file: 图片文件路径 - """ - valid = True - try: - Image.open(file).load() - except OSError: - valid = False - return valid - - -class BuildImage: - """ - 快捷生成图片与操作图片的工具类 - """ - - def __init__( - self, - w: int, - h: int, - paste_image_width: int = 0, - paste_image_height: int = 0, - paste_space: int = 0, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = None, - image_mode: ModeType = "RGBA", - font_size: int = 10, - background: Union[Optional[str], BytesIO, Path] = None, - font: str = "yz.ttf", - ratio: float = 1, - is_alpha: bool = False, - plain_text: Optional[str] = None, - font_color: Optional[Union[str, Tuple[int, int, int]]] = None, - **kwargs, - ): - """ - 参数: - :param w: 自定义图片的宽度,w=0时为图片原本宽度 - :param h: 自定义图片的高度,h=0时为图片原本高度 - :param paste_image_width: 当图片做为背景图时,设置贴图的宽度,用于贴图自动换行 - :param paste_image_height: 当图片做为背景图时,设置贴图的高度,用于贴图自动换行 - :param paste_space: 自动贴图间隔 - :param color: 生成图片的颜色 - :param image_mode: 图片的类型 - :param font_size: 文字大小 - :param background: 打开图片的路径 - :param font: 字体,默认在 resource/ttf/ 路径下 - :param ratio: 倍率压缩 - :param is_alpha: 是否背景透明 - :param plain_text: 纯文字文本 - """ - self.w = int(w) - self.h = int(h) - self.paste_image_width = int(paste_image_width) - self.paste_image_height = int(paste_image_height) - self.paste_space = int(paste_space) - self._current_w = 0 - self._current_h = 0 - self.uid = uuid.uuid1() - self.font_name = font - self.font_size = font_size - self.font = ImageFont.truetype(str(FONT_PATH / font), int(font_size)) - if not plain_text and not color: - color = (255, 255, 255) - self.background = background - if not background: - if plain_text: - if not color: - color = (255, 255, 255, 0) - ttf_w, ttf_h = self.getsize(str(plain_text)) - self.w = self.w if self.w > ttf_w else ttf_w - self.h = self.h if self.h > ttf_h else ttf_h - self.markImg = Image.new(image_mode, (self.w, self.h), color) - self.markImg.convert(image_mode) - else: - if not w and not h: - self.markImg = Image.open(background) - w, h = self.markImg.size - if ratio and ratio > 0 and ratio != 1: - self.w = int(ratio * w) - self.h = int(ratio * h) - self.markImg = self.markImg.resize( - (self.w, self.h), Image.ANTIALIAS - ) - else: - self.w = w - self.h = h - else: - self.markImg = Image.open(background).resize( - (self.w, self.h), Image.ANTIALIAS - ) - if is_alpha: - try: - if array := self.markImg.load(): - for i in range(w): - for j in range(h): - pos = array[i, j] - is_edit = sum([1 for x in pos[0:3] if x > 240]) == 3 - if is_edit: - array[i, j] = (255, 255, 255, 0) - except Exception as e: - logger.warning(f"背景透明化发生错误..{type(e)}:{e}") - self.draw = ImageDraw.Draw(self.markImg) - self.size = self.w, self.h - if plain_text: - fill = font_color if font_color else (0, 0, 0) - self.text((0, 0), str(plain_text), fill) - try: - self.loop = asyncio.get_event_loop() - except RuntimeError: - new_loop = asyncio.new_event_loop() - asyncio.set_event_loop(new_loop) - self.loop = asyncio.get_event_loop() - - @classmethod - def load_font(cls, font: str, font_size: Optional[int]) -> FreeTypeFont: - """ - 说明: - 加载字体 - 参数: - :param font: 字体名称 - :param font_size: 字体大小 - """ - return ImageFont.truetype(str(FONT_PATH / font), font_size or cls.font_size) - - async def apaste( - self, - img: "BuildImage" or Image, - pos: Optional[Tuple[int, int]] = None, - alpha: bool = False, - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - allow_negative: bool = False, - ): - """ - 说明: - 异步 贴图 - 参数: - :param img: 已打开的图片文件,可以为 BuildImage 或 Image - :param pos: 贴图位置(左上角) - :param alpha: 图片背景是否为透明 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param allow_negative: 允许使用负数作为坐标且不超出图片范围,从右侧开始计算 - """ - await self.loop.run_in_executor( - None, self.paste, img, pos, alpha, center_type, allow_negative - ) - - def paste( - self, - img: "BuildImage", - pos: Optional[Tuple[int, int]] = None, - alpha: bool = False, - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - allow_negative: bool = False, - ): - """ - 说明: - 贴图 - 参数: - :param img: 已打开的图片文件,可以为 BuildImage 或 Image - :param pos: 贴图位置(左上角) - :param alpha: 图片背景是否为透明 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param allow_negative: 允许使用负数作为坐标且不超出图片范围,从右侧开始计算 - """ - if center_type: - if center_type not in ["center", "by_height", "by_width"]: - raise ValueError( - "center_type must be 'center', 'by_width' or 'by_height'" - ) - width, height = 0, 0 - if not pos: - pos = (0, 0) - if center_type == "center": - width = int((self.w - img.w) / 2) - height = int((self.h - img.h) / 2) - elif center_type == "by_width": - width = int((self.w - img.w) / 2) - height = pos[1] - elif center_type == "by_height": - width = pos[0] - height = int((self.h - img.h) / 2) - pos = (width, height) - if pos and allow_negative: - if pos[0] < 0: - pos = (self.w + pos[0], pos[1]) - if pos[1] < 0: - pos = (pos[0], self.h + pos[1]) - if isinstance(img, BuildImage): - img = img.markImg - if self._current_w >= self.w: - self._current_w = 0 - self._current_h += self.paste_image_height + self.paste_space - if not pos: - pos = (self._current_w, self._current_h) - if alpha: - try: - self.markImg.paste(img, pos, img) - except ValueError: - img = img.convert("RGBA") - self.markImg.paste(img, pos, img) - else: - self.markImg.paste(img, pos) - self._current_w += self.paste_image_width + self.paste_space - - @classmethod - def get_text_size(cls, msg: str, font: str, font_size: int) -> Tuple[int, int]: - """ - 说明: - 获取文字在该图片 font_size 下所需要的空间 - 参数: - :param msg: 文字内容 - :param font: 字体 - :param font_size: 字体大小 - """ - font_ = cls.load_font(font, font_size) - return font_.getsize(msg) # type: ignore - - def getsize(self, msg: Any) -> Tuple[int, int]: - """ - 说明: - 获取文字在该图片 font_size 下所需要的空间 - 参数: - :param msg: 文字内容 - """ - return self.font.getsize(str(msg)) # type: ignore - - async def apoint( - self, pos: Tuple[int, int], fill: Optional[Tuple[int, int, int]] = None - ): - """ - 说明: - 异步 绘制多个或单独的像素 - 参数: - :param pos: 坐标 - :param fill: 填错颜色 - """ - await self.loop.run_in_executor(None, self.point, pos, fill) - - def point(self, pos: Tuple[int, int], fill: Optional[Tuple[int, int, int]] = None): - """ - 说明: - 绘制多个或单独的像素 - 参数: - :param pos: 坐标 - :param fill: 填错颜色 - """ - self.draw.point(pos, fill=fill) - - async def aellipse( - self, - pos: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[Tuple[int, int, int]] = None, - width: int = 1, - ): - """ - 说明: - 异步 绘制圆 - 参数: - :param pos: 坐标范围 - :param fill: 填充颜色 - :param outline: 描线颜色 - :param width: 描线宽度 - """ - await self.loop.run_in_executor(None, self.ellipse, pos, fill, outline, width) - - def ellipse( - self, - pos: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[Tuple[int, int, int]] = None, - width: int = 1, - ): - """ - 说明: - 绘制圆 - 参数: - :param pos: 坐标范围 - :param fill: 填充颜色 - :param outline: 描线颜色 - :param width: 描线宽度 - """ - self.draw.ellipse(pos, fill, outline, width) - - async def atext( - self, - pos: Union[Tuple[int, int], Tuple[float, float]], - text: str, - fill: Union[str, Tuple[int, int, int]] = (0, 0, 0), - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - font: Optional[Union[FreeTypeFont, str]] = None, - font_size: Optional[int] = None, - **kwargs, - ): - """ - 说明: - 异步 在图片上添加文字 - 参数: - :param pos: 文字位置 - :param text: 文字内容 - :param fill: 文字颜色 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param font: 字体 - :param font_size: 字体大小 - """ - await self.loop.run_in_executor( - None, self.text, pos, text, fill, center_type, font, font_size, **kwargs - ) - - def text( - self, - pos: Union[Tuple[int, int], Tuple[float, float]], - text: str, - fill: Union[str, Tuple[int, int, int]] = (0, 0, 0), - center_type: Optional[Literal["center", "by_height", "by_width"]] = None, - font: Optional[Union[FreeTypeFont, str]] = None, - font_size: Optional[int] = None, - **kwargs, - ): - """ - 说明: - 在图片上添加文字 - 参数: - :param pos: 文字位置(使用center_type中的center后会失效,使用by_width后x失效,使用by_height后y失效) - :param text: 文字内容 - :param fill: 文字颜色 - :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 - :param font: 字体 - :param font_size: 字体大小 - """ - if center_type: - if center_type not in ["center", "by_height", "by_width"]: - raise ValueError( - "center_type must be 'center', 'by_width' or 'by_height'" - ) - w, h = self.w, self.h - longgest_text = "" - sentence = text.split("\n") - for x in sentence: - longgest_text = x if len(x) > len(longgest_text) else longgest_text - ttf_w, ttf_h = self.getsize(longgest_text) - ttf_h = ttf_h * len(sentence) - if center_type == "center": - w = int((w - ttf_w) / 2) - h = int((h - ttf_h) / 2) - elif center_type == "by_width": - w = int((w - ttf_w) / 2) - h = pos[1] - elif center_type == "by_height": - h = int((h - ttf_h) / 2) - w = pos[0] - pos = (w, h) - if font: - if isinstance(font, str): - font = self.load_font(font, font_size) - elif font_size: - font = self.load_font(self.font_name, font_size) - self.draw.text(pos, text, fill=fill, font=font or self.font, **kwargs) - - async def asave(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 异步 保存图片 - 参数: - :param path: 图片路径 - """ - await self.loop.run_in_executor(None, self.save, path) - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存图片 - 参数: - :param path: 图片路径 - """ - self.markImg.save(path or self.background) # type: ignore - - def show(self): - """ - 说明: - 显示图片 - """ - self.markImg.show() - - async def aresize(self, ratio: float = 0, w: int = 0, h: int = 0): - """ - 说明: - 异步 压缩图片 - 参数: - :param ratio: 压缩倍率 - :param w: 压缩图片宽度至 w - :param h: 压缩图片高度至 h - """ - await self.loop.run_in_executor(None, self.resize, ratio, w, h) - - def resize(self, ratio: float = 0, w: int = 0, h: int = 0): - """ - 说明: - 压缩图片 - 参数: - :param ratio: 压缩倍率 - :param w: 压缩图片宽度至 w - :param h: 压缩图片高度至 h - """ - if not w and not h and not ratio: - raise Exception("缺少参数...") - if not w and not h and ratio: - w = int(self.w * ratio) - h = int(self.h * ratio) - self.markImg = self.markImg.resize((w, h), Image.ANTIALIAS) - self.w, self.h = self.markImg.size - self.size = self.w, self.h - self.draw = ImageDraw.Draw(self.markImg) - - async def acrop(self, box: Tuple[int, int, int, int]): - """ - 说明: - 异步 裁剪图片 - 参数: - :param box: 左上角坐标,右下角坐标 (left, upper, right, lower) - """ - await self.loop.run_in_executor(None, self.crop, box) - - def crop(self, box: Tuple[int, int, int, int]): - """ - 说明: - 裁剪图片 - 参数: - :param box: 左上角坐标,右下角坐标 (left, upper, right, lower) - """ - self.markImg = self.markImg.crop(box) - self.w, self.h = self.markImg.size - self.size = self.w, self.h - self.draw = ImageDraw.Draw(self.markImg) - - def check_font_size(self, word: str) -> bool: - """ - 说明: - 检查文本所需宽度是否大于图片宽度 - 参数: - :param word: 文本内容 - """ - return self.font.getsize(word)[0] > self.w - - async def atransparent(self, alpha_ratio: float = 1, n: int = 0): - """ - 说明: - 异步 图片透明化 - 参数: - :param alpha_ratio: 透明化程度 - :param n: 透明化大小内边距 - """ - await self.loop.run_in_executor(None, self.transparent, alpha_ratio, n) - - def transparent(self, alpha_ratio: float = 1, n: int = 0): - """ - 说明: - 图片透明化 - 参数: - :param alpha_ratio: 透明化程度 - :param n: 透明化大小内边距 - """ - self.markImg = self.markImg.convert("RGBA") - x, y = self.markImg.size - for i in range(n, x - n): - for k in range(n, y - n): - color = self.markImg.getpixel((i, k)) - color = color[:-1] + (int(100 * alpha_ratio),) - self.markImg.putpixel((i, k), color) - self.draw = ImageDraw.Draw(self.markImg) - - def pic2bs4(self) -> str: - """ - 说明: - BuildImage 转 base64 - """ - buf = BytesIO() - self.markImg.save(buf, format="PNG") - base64_str = base64.b64encode(buf.getvalue()).decode() - return "base64://" + base64_str - - def convert(self, type_: ModeType): - """ - 说明: - 修改图片类型 - 参数: - :param type_: 类型 - """ - self.markImg = self.markImg.convert(type_) - - async def arectangle( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[str] = None, - width: int = 1, - ): - """ - 说明: - 异步 画框 - 参数: - :param xy: 坐标 - :param fill: 填充颜色 - :param outline: 轮廓颜色 - :param width: 线宽 - """ - await self.loop.run_in_executor(None, self.rectangle, xy, fill, outline, width) - - def rectangle( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Tuple[int, int, int]] = None, - outline: Optional[str] = None, - width: int = 1, - ): - """ - 说明: - 画框 - 参数: - :param xy: 坐标 - :param fill: 填充颜色 - :param outline: 轮廓颜色 - :param width: 线宽 - """ - self.draw.rectangle(xy, fill, outline, width) - - async def apolygon( - self, - xy: List[Tuple[int, int]], - fill: Tuple[int, int, int] = (0, 0, 0), - outline: int = 1, - ): - """ - 说明: - 异步 画多边形 - 参数: - :param xy: 坐标 - :param fill: 颜色 - :param outline: 线宽 - """ - await self.loop.run_in_executor(None, self.polygon, xy, fill, outline) - - def polygon( - self, - xy: List[Tuple[int, int]], - fill: Tuple[int, int, int] = (0, 0, 0), - outline: int = 1, - ): - """ - 说明: - 画多边形 - 参数: - :param xy: 坐标 - :param fill: 颜色 - :param outline: 线宽 - """ - self.draw.polygon(xy, fill, outline) - - async def aline( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Union[str, Tuple[int, int, int]]] = None, - width: int = 1, - ): - """ - 说明: - 异步 画线 - 参数: - :param xy: 坐标 - :param fill: 填充 - :param width: 线宽 - """ - await self.loop.run_in_executor(None, self.line, xy, fill, width) - - def line( - self, - xy: Tuple[int, int, int, int], - fill: Optional[Union[Tuple[int, int, int], str]] = None, - width: int = 1, - ): - """ - 说明: - 画线 - 参数: - :param xy: 坐标 - :param fill: 填充 - :param width: 线宽 - """ - self.draw.line(xy, fill, width) - - async def acircle(self): - """ - 说明: - 异步 将 BuildImage 图片变为圆形 - """ - await self.loop.run_in_executor(None, self.circle) - - def circle(self): - """ - 说明: - 使图像变圆 - """ - self.markImg.convert("RGBA") - size = self.markImg.size - r2 = min(size[0], size[1]) - if size[0] != size[1]: - self.markImg = self.markImg.resize((r2, r2), Image.ANTIALIAS) - width = 1 - antialias = 4 - ellipse_box = [0, 0, r2 - 2, r2 - 2] - mask = Image.new( - size=[int(dim * antialias) for dim in self.markImg.size], - mode="L", - color="black", - ) - draw = ImageDraw.Draw(mask) - for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"): - left, top = [(value + offset) * antialias for value in ellipse_box[:2]] - right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]] - draw.ellipse([left, top, right, bottom], fill=fill) - mask = mask.resize(self.markImg.size, Image.LANCZOS) - try: - self.markImg.putalpha(mask) - except ValueError: - pass - - async def acircle_corner( - self, - radii: int = 30, - point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], - ): - """ - 说明: - 异步 矩形四角变圆 - 参数: - :param radii: 半径 - :param point_list: 需要变化的角 - """ - await self.loop.run_in_executor(None, self.circle_corner, radii, point_list) - - def circle_corner( - self, - radii: int = 30, - point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], - ): - """ - 说明: - 矩形四角变圆 - 参数: - :param radii: 半径 - :param point_list: 需要变化的角 - """ - # 画圆(用于分离4个角) - img = self.markImg.convert("RGBA") - alpha = img.split()[-1] - circle = Image.new("L", (radii * 2, radii * 2), 0) - draw = ImageDraw.Draw(circle) - draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 - w, h = img.size - if "lt" in point_list: - alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) - if "rt" in point_list: - alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) - if "lb" in point_list: - alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) - if "rb" in point_list: - alpha.paste( - circle.crop((radii, radii, radii * 2, radii * 2)), - (w - radii, h - radii), - ) - img.putalpha(alpha) - self.markImg = img - self.draw = ImageDraw.Draw(self.markImg) - - async def arotate(self, angle: int, expand: bool = False): - """ - 说明: - 异步 旋转图片 - 参数: - :param angle: 角度 - :param expand: 放大图片适应角度 - """ - await self.loop.run_in_executor(None, self.rotate, angle, expand) - - def rotate(self, angle: int, expand: bool = False): - """ - 说明: - 旋转图片 - 参数: - :param angle: 角度 - :param expand: 放大图片适应角度 - """ - self.markImg = self.markImg.rotate(angle, expand=expand) - - async def atranspose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]): - """ - 说明: - 异步 旋转图片(包括边框) - 参数: - :param angle: 角度 - """ - await self.loop.run_in_executor(None, self.transpose, angle) - - def transpose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]): - """ - 说明: - 旋转图片(包括边框) - 参数: - :param angle: 角度 - """ - self.markImg.transpose(angle) - - async def afilter(self, filter_: str, aud: Optional[int] = None): - """ - 说明: - 异步 图片变化 - 参数: - :param filter_: 变化效果 - :param aud: 利率 - """ - await self.loop.run_in_executor(None, self.filter, filter_, aud) - - def filter(self, filter_: str, aud: Optional[int] = None): - """ - 说明: - 图片变化 - 参数: - :param filter_: 变化效果 - :param aud: 利率 - """ - _x = None - if filter_ == "GaussianBlur": # 高斯模糊 - _x = ImageFilter.GaussianBlur - elif filter_ == "EDGE_ENHANCE": # 锐化效果 - _x = ImageFilter.EDGE_ENHANCE - elif filter_ == "BLUR": # 模糊效果 - _x = ImageFilter.BLUR - elif filter_ == "CONTOUR": # 铅笔滤镜 - _x = ImageFilter.CONTOUR - elif filter_ == "FIND_EDGES": # 边缘检测 - _x = ImageFilter.FIND_EDGES - if _x: - if aud: - self.markImg = self.markImg.filter(_x(aud)) - else: - self.markImg = self.markImg.filter(_x) - self.draw = ImageDraw.Draw(self.markImg) - - async def areplace_color_tran( - self, - src_color: Union[ - Tuple[int, int, int], Tuple[Tuple[int, int, int], Tuple[int, int, int]] - ], - replace_color: Tuple[int, int, int], - ): - """ - 说明: - 异步 颜色替换 - 参数: - :param src_color: 目标颜色,或者使用列表,设置阈值 - :param replace_color: 替换颜色 - """ - self.loop.run_in_executor( - None, self.replace_color_tran, src_color, replace_color - ) - - def replace_color_tran( - self, - src_color: Union[ - Tuple[int, int, int], Tuple[Tuple[int, int, int], Tuple[int, int, int]] - ], - replace_color: Tuple[int, int, int], - ): - """ - 说明: - 颜色替换 - 参数: - :param src_color: 目标颜色,或者使用元祖,设置阈值 - :param replace_color: 替换颜色 - """ - if isinstance(src_color, tuple): - start_ = src_color[0] - end_ = src_color[1] - else: - start_ = src_color - end_ = None - for i in range(self.w): - for j in range(self.h): - r, g, b = self.markImg.getpixel((i, j)) - if not end_: - if r == start_[0] and g == start_[1] and b == start_[2]: - self.markImg.putpixel((i, j), replace_color) - else: - if ( - start_[0] <= r <= end_[0] - and start_[1] <= g <= end_[1] - and start_[2] <= b <= end_[2] - ): - self.markImg.putpixel((i, j), replace_color) - - # - def getchannel(self, type_): - self.markImg = self.markImg.getchannel(type_) - - -class BuildMat: - """ - 针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具 - 目前仅支持 正整数 - """ - - def __init__( - self, - y: List[int], - mat_type: str = "line", - *, - x_name: Optional[str] = None, - y_name: Optional[str] = None, - x_index: List[Union[str, int, float]] = None, - y_index: List[Union[str, int, float]] = None, - x_min_spacing: Optional[int] = None, - x_rotate: int = 0, - title: Optional[str] = None, - size: Tuple[int, int] = (1000, 1000), - font: str = "msyh.ttf", - font_size: Optional[int] = None, - display_num: bool = False, - is_grid: bool = False, - background: Optional[List[str]] = None, - background_filler_type: Optional[str] = "center", - bar_color: Optional[List[Union[str, Tuple[int, int, int]]]] = None, - ): - """ - 说明: - 初始化 BuildMat - 参数: - :param y: 坐标值 - :param mat_type: 图像类型 可能的值:[line]: 折线图,[bar]: 柱状图,[barh]: 横向柱状图 - :param x_name: 横坐标名称 - :param y_name: 纵坐标名称 - :param x_index: 横坐标值 - :param y_index: 纵坐标值 - :param x_min_spacing: x轴最小间距 - :param x_rotate: 横坐标旋转角度 - :param title: 标题 - :param size: 图像大小,建议默认 - :param font: 字体 - :param font_size: 字体大小,建议默认 - :param display_num: 是否显示数值 - :param is_grid: 是否添加栅格 - :param background: 背景图片 - :param background_filler_type: 图像填充类型 - :param bar_color: 柱状图颜色,位 ['*'] 时替换位彩虹随机色 - """ - self.mat_type = mat_type - self.markImg = None - self._check_value(y, y_index) - self.w = size[0] - self.h = size[1] - self.y = y - self.x_name = x_name - self.y_name = y_name - self.x_index = x_index - self.y_index = y_index - self.x_min_spacing = x_min_spacing - self.x_rotate = x_rotate - self.title = title - self.font = font - self.display_num = display_num - self.is_grid = is_grid - self.background = background - self.background_filler_type = background_filler_type - self.bar_color = bar_color if bar_color else [(0, 0, 0)] - self.size = size - self.padding_w = 120 - self.padding_h = 120 - self.line_length = 760 - self._deviation = 0.905 - self._color = {} - if not font_size: - self.font_size = int(25 * (1 - len(x_index) / 100)) - else: - self.font_size = font_size - if self.bar_color == ["*"]: - self.bar_color = [ - "#FF0000", - "#FF7F00", - "#FFFF00", - "#00FF00", - "#00FFFF", - "#0000FF", - "#8B00FF", - ] - if not x_index: - raise ValueError("缺少 x_index [横坐标值]...") - if x_min_spacing: - self._x_interval = x_min_spacing - else: - self._x_interval = int((self.line_length - 70) / len(x_index)) - self._bar_width = int(30 * (1 - (len(x_index) + 10) / 100)) - # 没有 y_index 时自动生成 - if not y_index: - _y_index = [] - _max_value = int(max(y)) - _max_value = ceil( - _max_value / eval("1" + "0" * (len(str(_max_value)) - 1)) - ) * eval("1" + "0" * (len(str(_max_value)) - 1)) - _max_value = _max_value if _max_value >= 10 else 100 - _step = int(_max_value / 10) - for i in range(_step, _max_value + _step, _step): - _y_index.append(i) - self.y_index = _y_index - self._p = self.line_length / max(self.y_index) - self._y_interval = int((self.line_length - 70) / len(self.y_index)) - - def gen_graph(self): - """ - 说明: - 生成图像 - """ - self.markImg = self._init_graph( - x_name=self.x_name, - y_name=self.y_name, - x_index=self.x_index, - y_index=self.y_index, - font_size=self.font_size, - is_grid=self.is_grid, - ) - if self.mat_type == "line": - self._gen_line_graph(y=self.y, display_num=self.display_num) - elif self.mat_type == "bar": - self._gen_bar_graph(y=self.y, display_num=self.display_num) - elif self.mat_type == "barh": - self._gen_bar_graph(y=self.y, display_num=self.display_num, is_barh=True) - - def set_y(self, y: List[int]): - """ - 说明: - 给坐标点设置新值 - 参数: - :param y: 坐标点 - """ - self._check_value(y, self.y_index) - self.y = y - - def set_y_index(self, y_index: List[Union[str, int, float]]): - """ - 说明: - 设置y轴坐标值 - 参数: - :param y_index: y轴坐标值 - """ - self._check_value(self.y, y_index) - self.y_index = y_index - - def set_title(self, title: str, color: Optional[Union[str, Tuple[int, int, int]]]): - """ - 说明: - 设置标题 - 参数: - :param title: 标题 - :param color: 字体颜色 - """ - self.title = title - if color: - self._color["title"] = color - - def set_background( - self, background: Optional[List[str]], type_: Optional[str] = None - ): - """ - 说明: - 设置背景图片 - 参数: - :param background: 图片路径列表 - :param type_: 填充类型 - """ - self.background = background - self.background_filler_type = type_ if type_ else self.background_filler_type - - def show(self): - """ - 说明: - 展示图像 - """ - self.markImg.show() - - def pic2bs4(self) -> str: - """ - 说明: - 转base64 - """ - return self.markImg.pic2bs4() - - def resize(self, ratio: float = 0.9): - """ - 说明: - 调整图像大小 - 参数: - :param ratio: 比例 - """ - self.markImg.resize(ratio) - - def save(self, path: Union[str, Path]): - """ - 说明: - 保存图片 - 参数: - :param path: 路径 - """ - self.markImg.save(path) - - def _check_value( - self, - y: List[int], - y_index: List[Union[str, int, float]] = None, - x_index: List[Union[str, int, float]] = None, - ): - """ - 说明: - 检查值合法性 - 参数: - :param y: 坐标值 - :param y_index: y轴坐标值 - :param x_index: x轴坐标值 - """ - if y_index: - _value = x_index if self.mat_type == "barh" else y_index - if max(y) > max(y_index): - raise ValueError("坐标点的值必须小于y轴坐标的最大值...") - i = -9999999999 - for y in y_index: - if y > i: - i = y - else: - raise ValueError("y轴坐标值必须有序...") - - def _gen_line_graph( - self, - y: List[Union[int, float]], - display_num: bool = False, - ): - """ - 说明: - 生成折线图 - 参数: - :param y: 坐标点 - :param display_num: 显示该点的值 - """ - _black_point = BuildImage(11, 11, color=random.choice(self.bar_color)) - _black_point.circle() - x_interval = self._x_interval - current_w = self.padding_w + x_interval - current_h = self.padding_h + self.line_length - for i in range(len(y)): - if display_num: - w = int(self.markImg.getsize(str(y[i]))[0] / 2) - self.markImg.text( - ( - current_w - w, - current_h - int(y[i] * self._p * self._deviation) - 25 - 5, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - if i != len(y) - 1: - self.markImg.line( - ( - current_w, - current_h - int(y[i] * self._p * self._deviation), - current_w + x_interval, - current_h - int(y[i + 1] * self._p * self._deviation), - ), - fill=(0, 0, 0), - width=2, - ) - self.markImg.paste( - _black_point, - ( - current_w - 3, - current_h - int(y[i] * self._p * self._deviation) - 3, - ), - True, - ) - current_w += x_interval - - def _gen_bar_graph( - self, - y: List[Union[int, float]], - display_num: bool = False, - is_barh: bool = False, - ): - """ - 说明: - 生成柱状图 - 参数: - :param y: 坐标值 - :param display_num: 是否显示数值 - :param is_barh: 横柱状图 - """ - _interval = self._x_interval - if is_barh: - current_h = self.padding_h + self.line_length - _interval - current_w = self.padding_w - else: - current_w = self.padding_w + _interval - current_h = self.padding_h + self.line_length - for i in range(len(y)): - # 画出显示数字 - if display_num: - # 横柱状图 - if is_barh: - font_h = self.markImg.getsize(str(y[i]))[1] - self.markImg.text( - ( - self.padding_w - + int(y[i] * self._p * self._deviation) - + 2 - + 5, - current_h - int(font_h / 2) - 1, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - else: - w = int(self.markImg.getsize(str(y[i]))[0] / 2) - self.markImg.text( - ( - current_w - w, - current_h - int(y[i] * self._p * self._deviation) - 25, - ), - f"{y[i]:.2f}" if isinstance(y[i], float) else f"{y[i]}", - ) - if i != len(y): - bar_color = random.choice(self.bar_color) - if is_barh: - A = BuildImage( - int(y[i] * self._p * self._deviation), - self._bar_width, - color=bar_color, - ) - self.markImg.paste( - A, - ( - current_w + 2, - current_h - int(self._bar_width / 2), - ), - ) - else: - A = BuildImage( - self._bar_width, - int(y[i] * self._p * self._deviation), - color=bar_color, - ) - self.markImg.paste( - A, - ( - current_w - int(self._bar_width / 2), - current_h - int(y[i] * self._p * self._deviation), - ), - ) - if is_barh: - current_h -= _interval - else: - current_w += _interval - - def _init_graph( - self, - x_name: Optional[str] = None, - y_name: Optional[str] = None, - x_index: List[Union[str, int, float]] = None, - y_index: List[Union[str, int, float]] = None, - font_size: Optional[int] = None, - is_grid: bool = False, - ) -> BuildImage: - """ - 说明: - 初始化图像,生成xy轴 - 参数: - :param x_name: x轴名称 - :param y_name: y轴名称 - :param x_index: x轴坐标值 - :param y_index: y轴坐标值 - :param is_grid: 添加栅格 - """ - padding_w = self.padding_w - padding_h = self.padding_h - line_length = self.line_length - background = random.choice(self.background) if self.background else None - if self.x_min_spacing: - length = (len(self.x_index) + 1) * self.x_min_spacing - if 2 * padding_w + length > self.w: - self.w = 2 * padding_w + length - background = None - A = BuildImage( - self.w, self.h, font_size=font_size, font=self.font, background=background - ) - if background: - _tmp = BuildImage(self.w, self.h) - _tmp.transparent(2) - A.paste(_tmp, alpha=True) - if self.title: - title = BuildImage( - 0, - 0, - plain_text=self.title, - color=(255, 255, 255, 0), - font_size=35, - font_color=self._color.get("title"), - font=self.font, - ) - A.paste(title, (0, 25), True, "by_width") - A.line( - ( - padding_w, - padding_h + line_length, - self.w - padding_w, - padding_h + line_length, - ), - (0, 0, 0), - 2, - ) - A.line( - ( - padding_w, - padding_h, - padding_w, - padding_h + line_length, - ), - (0, 0, 0), - 2, - ) - _interval = self._x_interval - if self.mat_type == "barh": - tmp = x_index - x_index = y_index - y_index = tmp - _interval = self._y_interval - current_w = padding_w + _interval - _text_font = BuildImage(0, 0, font_size=self.font_size, font=self.font) - _grid = self.line_length if is_grid else 10 - x_rotate_height = 0 - for _x in x_index: - _p = BuildImage(1, _grid, color="#a9a9a9") - A.paste(_p, (current_w, padding_h + line_length - _grid)) - w = int(_text_font.getsize(f"{_x}")[0] / 2) - text = BuildImage( - 0, - 0, - plain_text=f"{_x}", - font_size=self.font_size, - color=(255, 255, 255, 0), - font=self.font, - ) - text.rotate(self.x_rotate, True) - A.paste(text, (current_w - w, padding_h + line_length + 10), alpha=True) - current_w += _interval - x_rotate_height = text.h - _interval = self._x_interval if self.mat_type == "barh" else self._y_interval - current_h = padding_h + line_length - _interval - _text_font = BuildImage(0, 0, font_size=self.font_size, font=self.font) - for _y in y_index: - _p = BuildImage(_grid, 1, color="#a9a9a9") - A.paste(_p, (padding_w + 2, current_h)) - w, h = _text_font.getsize(f"{_y}") - h = int(h / 2) - text = BuildImage( - 0, - 0, - plain_text=f"{_y}", - font_size=self.font_size, - color=(255, 255, 255, 0), - font=self.font, - ) - idx = 0 - while text.size[0] > self.padding_w - 10 and idx < 3: - text = BuildImage( - 0, - 0, - plain_text=f"{_y}", - font_size=int(self.font_size * 0.75), - color=(255, 255, 255, 0), - font=self.font, - ) - w, _ = text.getsize(f"{_y}") - idx += 1 - A.paste(text, (padding_w - w - 10, current_h - h), alpha=True) - current_h -= _interval - if x_name: - A.text((int(padding_w / 2), int(padding_w / 2)), x_name) - if y_name: - A.text( - ( - int(padding_w + line_length + 50 - A.getsize(y_name)[0]), - int(padding_h + line_length + 50 + x_rotate_height), - ), - y_name, - ) - return A - - -async def text2image( - text: str, - auto_parse: bool = True, - font_size: int = 20, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = "white", - font: str = "CJGaoDeGuo.otf", - font_color: Union[str, Tuple[int, int, int]] = "black", - padding: Union[int, Tuple[int, int, int, int]] = 0, - _add_height: float = 0, -) -> BuildImage: - """ - 说明: - 解析文本并转为图片 - 使用标签 - - 可选配置项 - font: str -> 特殊文本字体 - fs / font_size: int -> 特殊文本大小 - fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色 - 示例 - 在不在,HibiKi小姐, - 你最近还好吗,我非常想你,这段时间我非常不好过, - 抽卡抽不到金色,这让我很痛苦 - 参数: - :param text: 文本 - :param auto_parse: 是否自动解析,否则原样发送 - :param font_size: 普通字体大小 - :param color: 背景颜色 - :param font: 普通字体 - :param font_color: 普通字体颜色 - :param padding: 文本外边距,元组类型时为 (上,左,下,右) - :param _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度 - """ - pw = ph = top_padding = left_padding = 0 - if padding: - if isinstance(padding, int): - pw = padding * 2 - ph = padding * 2 - top_padding = left_padding = padding - elif isinstance(padding, tuple): - pw = padding[0] + padding[2] - ph = padding[1] + padding[3] - top_padding = padding[0] - left_padding = padding[1] - if auto_parse and re.search(r"(.*)", text): - _data = [] - new_text = "" - placeholder_index = 0 - for s in text.split(""): - r = re.search(r"(.*)", s) - if r: - start, end = r.span() - if start != 0 and (t := s[:start]): - new_text += t - _data.append( - [ - (start, end), - f"[placeholder_{placeholder_index}]", - r.group(1).strip(), - r.group(2), - ] - ) - new_text += f"[placeholder_{placeholder_index}]" - placeholder_index += 1 - new_text += text.split("")[-1] - image_list = [] - current_placeholder_index = 0 - # 切分换行,每行为单张图片 - for s in new_text.split("\n"): - _tmp_text = s - img_height = BuildImage(0, 0, font_size=font_size).getsize("正")[1] - img_width = 0 - _tmp_index = current_placeholder_index - for _ in range(s.count("[placeholder_")): - placeholder = _data[_tmp_index] - if "font_size" in placeholder[2]: - r = re.search(r"font_size=['\"]?(\d+)", placeholder[2]) - if r: - w, h = BuildImage(0, 0, font_size=int(r.group(1))).getsize( - placeholder[3] - ) - img_height = img_height if img_height > h else h - img_width += w - else: - img_width += BuildImage(0, 0, font_size=font_size).getsize( - placeholder[3] - )[0] - _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "") - _tmp_index += 1 - img_width += BuildImage(0, 0, font_size=font_size).getsize(_tmp_text)[0] - # img_width += len(_tmp_text) * font_size - # 开始画图 - A = BuildImage( - img_width, img_height, color=color, font=font, font_size=font_size - ) - basic_font_h = A.getsize("正")[1] - current_width = 0 - # 遍历占位符 - for _ in range(s.count("[placeholder_")): - if not s.startswith(f"[placeholder_{current_placeholder_index}]"): - slice_ = s.split(f"[placeholder_{current_placeholder_index}]") - await A.atext( - (current_width, A.h - basic_font_h - 1), slice_[0], font_color - ) - current_width += A.getsize(slice_[0])[0] - placeholder = _data[current_placeholder_index] - # 解析配置 - _font = font - _font_size = font_size - _font_color = font_color - for e in placeholder[2].split(): - if e.startswith("font="): - _font = e.split("=")[-1] - if e.startswith("font_size=") or e.startswith("fs="): - _font_size = int(e.split("=")[-1]) - if _font_size > 1000: - _font_size = 1000 - if _font_size < 1: - _font_size = 1 - if e.startswith("font_color") or e.startswith("fc="): - _font_color = e.split("=")[-1] - text_img = BuildImage( - 0, - 0, - plain_text=placeholder[3], - font_size=_font_size, - font_color=_font_color, - font=_font, - ) - _img_h = ( - int(A.h / 2 - text_img.h / 2) - if new_text == "[placeholder_0]" - else A.h - text_img.h - ) - await A.apaste(text_img, (current_width, _img_h - 1), True) - current_width += text_img.w - s = s[ - s.index(f"[placeholder_{current_placeholder_index}]") - + len(f"[placeholder_{current_placeholder_index}]") : - ] - current_placeholder_index += 1 - if s: - slice_ = s.split(f"[placeholder_{current_placeholder_index}]") - await A.atext((current_width, A.h - basic_font_h), slice_[0]) - current_width += A.getsize(slice_[0])[0] - A.crop((0, 0, current_width, A.h)) - # A.show() - image_list.append(A) - height = 0 - width = 0 - for img in image_list: - height += img.h - width = width if width > img.w else img.w - width += pw - height += ph - A = BuildImage(width + left_padding, height + top_padding, color=color) - current_height = top_padding - for img in image_list: - await A.apaste(img, (left_padding, current_height), True) - current_height += img.h - else: - width = 0 - height = 0 - _tmp = BuildImage(0, 0, font=font, font_size=font_size) - _, h = _tmp.getsize("正") - line_height = int(font_size / 3) - image_list = [] - for x in text.split("\n"): - w, _ = _tmp.getsize(x.strip() or "正") - height += h + line_height - width = width if width > w else w - image_list.append( - BuildImage( - w, - h, - font=font, - font_size=font_size, - plain_text=x.strip(), - color=color, - ) - ) - width += pw - height += ph - A = BuildImage( - width + left_padding, - height + top_padding + 2, - color=color, - ) - cur_h = ph - for img in image_list: - await A.apaste(img, (pw, cur_h), True) - cur_h += img.h + line_height - return A - - -def group_image(image_list: List[BuildImage]) -> Tuple[List[List[BuildImage]], int]: - """ - 说明: - 根据图片大小进行分组 - 参数: - :param image_list: 排序图片列表 - """ - image_list.sort(key=lambda x: x.h, reverse=True) - max_image = max(image_list, key=lambda x: x.h) - - image_list.remove(max_image) - max_h = max_image.h - total_w = 0 - - # 图片分组 - image_group = [[max_image]] - is_use = [] - surplus_list = image_list[:] - - for image in image_list: - if image.uid not in is_use: - group = [image] - is_use.append(image.uid) - curr_h = image.h - while True: - surplus_list = [x for x in surplus_list if x.uid not in is_use] - for tmp in surplus_list: - temp_h = curr_h + tmp.h + 10 - if temp_h < max_h or abs(max_h - temp_h) < 100: - curr_h += tmp.h + 15 - is_use.append(tmp.uid) - group.append(tmp) - break - else: - break - total_w += max([x.w for x in group]) + 15 - image_group.append(group) - while surplus_list: - surplus_list = [x for x in surplus_list if x.uid not in is_use] - if not surplus_list: - break - surplus_list.sort(key=lambda x: x.h, reverse=True) - for img in surplus_list: - if img.uid not in is_use: - _w = 0 - index = -1 - for i, ig in enumerate(image_group): - if s := sum([x.h for x in ig]) > _w: - _w = s - index = i - if index != -1: - image_group[index].append(img) - is_use.append(img.uid) - - max_h = 0 - max_w = 0 - for ig in image_group: - if (_h := sum([x.h + 15 for x in ig])) > max_h: - max_h = _h - max_w += max([x.w for x in ig]) + 30 - is_use.clear() - while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]): - for img in image_group[-1]: - _min_h = 999999 - _min_index = -1 - for i, ig in enumerate(image_group): - # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h: - if (_h := sum([x.h for x in ig]) + img.h) < _min_h: - _min_h = _h - _min_index = i - is_use.append(_min_index) - image_group[_min_index].append(img) - max_w -= max([x.w for x in image_group[-1]]) - 30 - image_group.pop(-1) - max_h = max([sum([x.h + 15 for x in ig]) for ig in image_group]) - return image_group, max(max_h + 250, max_w + 70) - - -async def build_sort_image( - image_group: List[List[BuildImage]], - h: Optional[int] = None, - padding_top: int = 200, - color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = ( - 255, - 255, - 255, - ), - background_path: Optional[Path] = None, - background_handle: Callable[[BuildImage], Optional[Awaitable]] = None, -) -> BuildImage: - """ - 说明: - 对group_image的图片进行组装 - 参数: - :param image_group: 分组图片列表 - :param h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形 - :param padding_top: 图像列表与最顶层间距 - :param color: 背景颜色 - :param background_path: 背景图片文件夹路径(随机) - :param background_handle: 背景图额外操作 - """ - bk_file = None - if background_path: - random_bk = os.listdir(background_path) - if random_bk: - bk_file = random.choice(random_bk) - image_w = 0 - image_h = 0 - if not h: - for ig in image_group: - _w = max([x.w + 30 for x in ig]) - image_w += _w + 30 - _h = sum([x.h + 10 for x in ig]) - if _h > image_h: - image_h = _h - image_h += padding_top - else: - image_w = h - image_h = h - A = BuildImage( - image_w, - image_h, - font_size=24, - font="CJGaoDeGuo.otf", - color=color, - background=(background_path / bk_file) if bk_file else None, - ) - if background_handle: - if is_coroutine_callable(background_handle): - await background_handle(A) - else: - background_handle(A) - curr_w = 50 - for ig in image_group: - curr_h = padding_top - 20 - for img in ig: - await A.apaste(img, (curr_w, curr_h), True) - curr_h += img.h + 10 - curr_w += max([x.w for x in ig]) + 30 - return A - - -if __name__ == "__main__": - pass diff --git a/utils/langconv.py b/utils/langconv.py deleted file mode 100755 index 4977161b..00000000 --- a/utils/langconv.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from copy import deepcopy -import re - -try: - import psyco - psyco.full() -except: - pass - -from .zh_wiki import zh2Hant, zh2Hans - -import sys -py3k = sys.version_info >= (3, 0, 0) - -if py3k: - UEMPTY = '' -else: - _zh2Hant, _zh2Hans = {}, {} - for old, new in ((zh2Hant, _zh2Hant), (zh2Hans, _zh2Hans)): - for k, v in old.items(): - new[k.decode('utf8')] = v.decode('utf8') - zh2Hant = _zh2Hant - zh2Hans = _zh2Hans - UEMPTY = ''.decode('utf8') - -# states -(START, END, FAIL, WAIT_TAIL) = list(range(4)) -# conditions -(TAIL, ERROR, MATCHED_SWITCH, UNMATCHED_SWITCH, CONNECTOR) = list(range(5)) - -MAPS = {} - -class Node(object): - def __init__(self, from_word, to_word=None, is_tail=True, - have_child=False): - self.from_word = from_word - if to_word is None: - self.to_word = from_word - self.data = (is_tail, have_child, from_word) - self.is_original = True - else: - self.to_word = to_word or from_word - self.data = (is_tail, have_child, to_word) - self.is_original = False - self.is_tail = is_tail - self.have_child = have_child - - def is_original_long_word(self): - return self.is_original and len(self.from_word)>1 - - def is_follow(self, chars): - return chars != self.from_word[:-1] - - def __str__(self): - return '' % (repr(self.from_word), - repr(self.to_word), self.is_tail, self.have_child) - - __repr__ = __str__ - -class ConvertMap(object): - def __init__(self, name, mapping=None): - self.name = name - self._map = {} - if mapping: - self.set_convert_map(mapping) - - def set_convert_map(self, mapping): - convert_map = {} - have_child = {} - max_key_length = 0 - for key in sorted(mapping.keys()): - if len(key)>1: - for i in range(1, len(key)): - parent_key = key[:i] - have_child[parent_key] = True - have_child[key] = False - max_key_length = max(max_key_length, len(key)) - for key in sorted(have_child.keys()): - convert_map[key] = (key in mapping, have_child[key], - mapping.get(key, UEMPTY)) - self._map = convert_map - self.max_key_length = max_key_length - - def __getitem__(self, k): - try: - is_tail, have_child, to_word = self._map[k] - return Node(k, to_word, is_tail, have_child) - except: - return Node(k) - - def __contains__(self, k): - return k in self._map - - def __len__(self): - return len(self._map) - -class StatesMachineException(Exception): pass - -class StatesMachine(object): - def __init__(self): - self.state = START - self.final = UEMPTY - self.len = 0 - self.pool = UEMPTY - - def clone(self, pool): - new = deepcopy(self) - new.state = WAIT_TAIL - new.pool = pool - return new - - def feed(self, char, map): - node = map[self.pool+char] - - if node.have_child: - if node.is_tail: - if node.is_original: - cond = UNMATCHED_SWITCH - else: - cond = MATCHED_SWITCH - else: - cond = CONNECTOR - else: - if node.is_tail: - cond = TAIL - else: - cond = ERROR - - new = None - if cond == ERROR: - self.state = FAIL - elif cond == TAIL: - if self.state == WAIT_TAIL and node.is_original_long_word(): - self.state = FAIL - else: - self.final += node.to_word - self.len += 1 - self.pool = UEMPTY - self.state = END - elif self.state == START or self.state == WAIT_TAIL: - if cond == MATCHED_SWITCH: - new = self.clone(node.from_word) - self.final += node.to_word - self.len += 1 - self.state = END - self.pool = UEMPTY - elif cond == UNMATCHED_SWITCH or cond == CONNECTOR: - if self.state == START: - new = self.clone(node.from_word) - self.final += node.to_word - self.len += 1 - self.state = END - else: - if node.is_follow(self.pool): - self.state = FAIL - else: - self.pool = node.from_word - elif self.state == END: - # END is a new START - self.state = START - new = self.feed(char, map) - elif self.state == FAIL: - raise StatesMachineException('Translate States Machine ' - 'have error with input data %s' % node) - return new - - def __len__(self): - return self.len + 1 - - def __str__(self): - return '' % ( - id(self), self.pool, self.state, self.final) - __repr__ = __str__ - -class Converter(object): - def __init__(self, to_encoding): - self.to_encoding = to_encoding - self.map = MAPS[to_encoding] - self.start() - - def feed(self, char): - branches = [] - for fsm in self.machines: - new = fsm.feed(char, self.map) - if new: - branches.append(new) - if branches: - self.machines.extend(branches) - self.machines = [fsm for fsm in self.machines if fsm.state != FAIL] - all_ok = True - for fsm in self.machines: - if fsm.state != END: - all_ok = False - if all_ok: - self._clean() - return self.get_result() - - def _clean(self): - if len(self.machines): - self.machines.sort(key=lambda x: len(x)) - # self.machines.sort(cmp=lambda x,y: cmp(len(x), len(y))) - self.final += self.machines[0].final - self.machines = [StatesMachine()] - - def start(self): - self.machines = [StatesMachine()] - self.final = UEMPTY - - def end(self): - self.machines = [fsm for fsm in self.machines - if fsm.state == FAIL or fsm.state == END] - self._clean() - - def convert(self, string): - self.start() - for char in string: - self.feed(char) - self.end() - return self.get_result() - - def get_result(self): - return self.final - - -def registery(name, mapping): - global MAPS - MAPS[name] = ConvertMap(name, mapping) - -registery('zh-hant', zh2Hant) -registery('zh-hans', zh2Hans) -del zh2Hant, zh2Hans - - -def run(): - import sys - from optparse import OptionParser - parser = OptionParser() - parser.add_option('-e', type='string', dest='encoding', - help='encoding') - parser.add_option('-f', type='string', dest='file_in', - help='input file (- for stdin)') - parser.add_option('-t', type='string', dest='file_out', - help='output file') - (options, args) = parser.parse_args() - if not options.encoding: - parser.error('encoding must be set') - if options.file_in: - if options.file_in == '-': - file_in = sys.stdin - else: - file_in = open(options.file_in) - else: - file_in = sys.stdin - if options.file_out: - if options.file_out == '-': - file_out = sys.stdout - else: - file_out = open(options.file_out, 'wb') - else: - file_out = sys.stdout - - c = Converter(options.encoding) - for line in file_in: - # print >> file_out, c.convert(line.rstrip('\n').decode( - file_out.write(c.convert(line.rstrip('\n').decode( - 'utf8')).encode('utf8')) - - -if __name__ == '__main__': - run() - diff --git a/utils/manager/__init__.py b/utils/manager/__init__.py deleted file mode 100755 index a3dc5ab0..00000000 --- a/utils/manager/__init__.py +++ /dev/null @@ -1,71 +0,0 @@ -from typing import Optional -from .group_manager import GroupManager -from .data_class import StaticData -from .plugin_data_manager import PluginDataManager -from .withdraw_message_manager import WithdrawMessageManager -from .plugins2cd_manager import Plugins2cdManager -from .plugins2block_manager import Plugins2blockManager -from .plugins2count_manager import Plugins2countManager -from .plugins2settings_manager import Plugins2settingsManager -from .plugins_manager import PluginsManager -from .resources_manager import ResourcesManager -from .admin_manager import AdminManager -from .none_plugin_count_manager import NonePluginCountManager -from .requests_manager import RequestManager -from configs.path_config import DATA_PATH - - -# 管理员命令管理器 -admin_manager = AdminManager() - -# 群功能开关 | 群被动技能 | 群权限 管理 -group_manager: GroupManager = GroupManager( - DATA_PATH / "manager" / "group_manager.json" -) - -# 撤回消息管理 -withdraw_message_manager: WithdrawMessageManager = WithdrawMessageManager() - -# 插件管理 -plugins_manager: PluginsManager = PluginsManager( - DATA_PATH / "manager" / "plugins_manager.json" -) - -# 插件基本设置管理 -plugins2settings_manager: Plugins2settingsManager = Plugins2settingsManager( - DATA_PATH / "configs" / "plugins2settings.yaml" -) - -# 插件命令 cd 管理 -plugins2cd_manager: Plugins2cdManager = Plugins2cdManager( - DATA_PATH / "configs" / "plugins2cd.yaml" -) - -# 插件命令 阻塞 管理 -plugins2block_manager: Plugins2blockManager = Plugins2blockManager( - DATA_PATH / "configs" / "plugins2block.yaml" -) - -# 插件命令 每次次数限制 管理 -plugins2count_manager: Plugins2countManager = Plugins2countManager( - DATA_PATH / "configs" / "plugins2count.yaml" -) - -# 资源管理 -resources_manager: ResourcesManager = ResourcesManager( - DATA_PATH / "manager" / "resources_manager.json" -) - -# 插件加载容忍管理 -none_plugin_count_manager: NonePluginCountManager = NonePluginCountManager( - DATA_PATH / "manager" / "none_plugin_count_manager.json" -) - -# 好友请求/群聊邀请 管理 -requests_manager: RequestManager = RequestManager( - DATA_PATH / "manager" / "requests_manager.json" -) - -# 全局插件数据 -plugin_data_manager: PluginDataManager = PluginDataManager() - diff --git a/utils/manager/admin_manager.py b/utils/manager/admin_manager.py deleted file mode 100644 index 9576582d..00000000 --- a/utils/manager/admin_manager.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Dict, List, Optional - -from utils.manager.data_class import StaticData - -from .models import AdminSetting - - -class AdminManager(StaticData): - """ - 管理员命令 管理器 - """ - - def __init__(self): - super().__init__(None) - self._data: Dict[str, AdminSetting] = {} - - def add_admin_plugin_settings(self, plugin: str, cmd: List[str], level: int): - """ - 说明: - 添加一个管理员命令 - 参数: - :param plugin: 模块 - :param cmd: 别名 - :param level: 等级 - """ - self._data[plugin] = AdminSetting(level=level, cmd=cmd) - - def set_admin_level(self, plugin: str, level: int): - """ - 说明: - 设置管理员命令等级 - 参数: - :param plugin: 模块名 - :param level: 权限等级 - """ - if plugin in self._data.keys(): - self._data[plugin].level = level - - def remove_admin_plugin_settings(self, plugin: str): - """ - 说明: - 删除一个管理员命令 - 参数: - :param plugin: 模块名 - """ - if plugin in self._data.keys(): - del self._data[plugin] - - def check(self, plugin: str, level: int) -> bool: - """ - 说明: - 检查是否满足权限 - 参数: - :param plugin: 模块名 - :param level: 权限等级 - """ - if plugin in self._data.keys(): - return level >= self._data[plugin].level - return True - - def get_plugin_level(self, plugin: str) -> int: - """ - 说明: - 获取插件权限 - 参数: - :param plugin: 模块名 - """ - if plugin in self._data.keys(): - return self._data[plugin].level - return 0 - - def get_plugin_module(self, cmd: str) -> Optional[str]: - """ - 说明: - 根据 cmd 获取功能 modules - 参数: - :param cmd: 命令 - """ - for key in self._data.keys(): - if data := self._data.get(key): - if data.cmd and cmd in data.cmd: - return key - return None diff --git a/utils/manager/configs_manager.py b/utils/manager/configs_manager.py deleted file mode 100644 index b6f106c2..00000000 --- a/utils/manager/configs_manager.py +++ /dev/null @@ -1,65 +0,0 @@ -# from typing import Optional, Any -# from .data_class import StaticData -# from pathlib import Path -# from ruamel.yaml import YAML -# -# yaml = YAML(typ="safe") -# -# -# class ConfigsManager(StaticData): -# """ -# 插件配置 与 资源 管理器 -# """ -# -# def __init__(self, file: Path): -# self.file = file -# super().__init__(file) -# self._resource_data = {} -# -# def add_plugin_config( -# self, -# modules: str, -# key: str, -# value: str, -# help_: Optional[str] = None, -# default_value: Optional[str] = None, -# ): -# """ -# 为插件添加一个配置 -# :param modules: 模块 -# :param key: 键 -# :param value: 值 -# :param help_: 配置注解 -# :param default_value: 默认值 -# """ -# if self._data.get(modules) is None: -# self._data[modules] = {} -# self._data[modules][key] = { -# "value": value, -# "help": help_, -# "default_value": default_value, -# } -# -# def remove_plugin_config(self, modules: str): -# """ -# 为插件删除一个配置 -# :param modules: 模块名 -# """ -# if modules in self._data.keys(): -# del self._data[modules] -# -# def get_config(self, modules: str, key: str) -> Optional[Any]: -# """ -# 获取指定配置值 -# :param modules: 模块名 -# :param key: 配置名称 -# """ -# if modules in self._data.keys(): -# if self._data[modules].get(key): -# if self._data[modules][key]["value"] is None: -# return self._data[modules][key]["default_value"] -# return self._data[modules][key]["value"] -# return None -# -# -# diff --git a/utils/manager/data_class.py b/utils/manager/data_class.py deleted file mode 100755 index b93a0ef5..00000000 --- a/utils/manager/data_class.py +++ /dev/null @@ -1,110 +0,0 @@ -import copy -from pathlib import Path -from typing import Any, Dict, Generic, NoReturn, Optional, TypeVar, Union - -import ujson as json -from ruamel import yaml -from ruamel.yaml import YAML - -from .models import * - -_yaml = YAML(typ="safe") - - -T = TypeVar("T") - - -class StaticData(Generic[T]): - """ - 静态数据共享类 - """ - - def __init__(self, file: Optional[Path], load_file: bool = True): - self._data: dict = {} - if file: - file.parent.mkdir(exist_ok=True, parents=True) - self.file = file - if file.exists() and load_file: - with open(file, "r", encoding="utf8") as f: - if file.name.endswith("json"): - try: - self._data: dict = json.load(f) - except ValueError: - if not f.read().strip(): - raise ValueError(f"{file} 文件加载错误,请检查文件内容格式.") - elif file.name.endswith("yaml"): - self._data = _yaml.load(f) - - def set(self, key, value): - self._data[key] = value - self.save() - - def set_module_data(self, module, key, value, auto_save: bool = True): - if module in self._data.keys(): - self._data[module][key] = value - if auto_save: - self.save() - - def get(self, key) -> T: - return self._data.get(key) - - def keys(self) -> List[str]: - return self._data.keys() - - def delete(self, key): - if self._data.get(key) is not None: - del self._data[key] - - def get_data(self) -> Dict[str, T]: - return copy.deepcopy(self._data) - - def dict(self) -> Dict[str, Any]: - temp = {} - for k, v in self._data.items(): - try: - temp[k] = v.dict() - except AttributeError: - temp[k] = copy.deepcopy(v) - return temp - - def save(self, path: Optional[Union[str, Path]] = None): - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - if path.name.endswith("yaml"): - yaml.dump( - self._data, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - else: - json.dump(self.dict(), f, ensure_ascii=False, indent=4) - - def reload(self): - if self.file.exists(): - if self.file.name.endswith("json"): - self._data: dict = json.load(open(self.file, "r", encoding="utf8")) - elif self.file.name.endswith("yaml"): - self._data: dict = _yaml.load(open(self.file, "r", encoding="utf8")) - - def is_exists(self) -> bool: - return self.file.exists() - - def is_empty(self) -> bool: - return bool(len(self._data)) - - def __str__(self) -> str: - return str(self._data) - - def __setitem__(self, key, value): - self._data[key] = value - - def __getitem__(self, key) -> T: - return self._data[key] - - def __len__(self) -> int: - return len(self._data) diff --git a/utils/manager/group_manager.py b/utils/manager/group_manager.py deleted file mode 100644 index 80f37337..00000000 --- a/utils/manager/group_manager.py +++ /dev/null @@ -1,443 +0,0 @@ -import copy -from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Union - -import nonebot -import ujson as json - -from configs.config import Config -from utils.manager.data_class import StaticData -from utils.utils import get_matchers, is_number - -from .models import BaseData, BaseGroup - -Config.add_plugin_config( - "group_manager", "DEFAULT_GROUP_LEVEL", 5, help_="默认群权限", default_value=5, type=int -) - -Config.add_plugin_config( - "group_manager", - "DEFAULT_GROUP_BOT_STATUS", - True, - help_="默认进群总开关状态", - default_value=True, - type=bool, -) - - -def init_group(func: Callable): - """ - 说明: - 初始化群数据 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - self = args[0] - if arg_list := list(filter(lambda x: is_number(x), args[1:])): - group_id = str(arg_list[0]) - if self is not None and group_id and not self._data.group_manager.get(group_id): - self._data.group_manager[group_id] = BaseGroup() - self.save() - return func(*args, **kwargs) - - return wrapper - - -def init_task(func: Callable): - """ - 说明: - 初始化群被动 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - self = args[0] - group_id = str(args[1]) - task = args[2] if len(args) > 1 else None - if ( - group_id - and task - and self._data.group_manager[group_id].group_task_status.get(task) is None - ): - for task in self._data.task: - if ( - self._data.group_manager[group_id].group_task_status.get(task) - is None - ): - self._data.group_manager[group_id].group_task_status[ - task - ] = Config.get_config("_task", f"DEFAULT_{task}", default=True) - for task in list(self._data.group_manager[group_id].group_task_status): - if task not in self._data.task: - del self._data.group_manager[group_id].group_task_status[task] - self.save() - return func(*args, **kwargs) - - return wrapper - - -class GroupManager(StaticData[BaseData]): - """ - 群权限 | 功能 | 总开关 | 聊天时间 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._data: BaseData = ( - BaseData.parse_file(file) if file.exists() else BaseData() - ) - - def get_data(self) -> BaseData: - return copy.deepcopy(self._data) - - def block_plugin( - self, module: str, group_id: Union[str, int], is_save: bool = True - ): - """ - 说明: - 锁定插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组,None时为超级用户禁用 - :param is_save: 是否保存文件 - """ - self._set_plugin_status(module, "block", group_id, is_save) - - def unblock_plugin( - self, module: str, group_id: Union[str, int], is_save: bool = True - ): - """ - 说明: - 解锁插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - :param is_save: 是否保存文件 - """ - self._set_plugin_status(module, "unblock", group_id, is_save) - - def turn_on_group_bot_status(self, group_id: Union[str, int]): - """ - 说明: - 开启群bot开关 - 参数: - :param group_id: 群号 - """ - self._set_group_bot_status(group_id, True) - - def shutdown_group_bot_status(self, group_id: Union[str, int]): - """ - 说明: - 关闭群bot开关 - 参数: - :param group_id: 群号 - """ - self._set_group_bot_status(group_id, False) - - @init_group - def check_group_bot_status(self, group_id: Union[str, int]) -> bool: - """ - 说明: - 检查群聊bot总开关状态 - 参数: - :param group_id: 说明 - """ - return self._data.group_manager[str(group_id)].status - - @init_group - def set_group_level(self, group_id: Union[str, int], level: int): - """ - 说明: - 设置群权限 - 参数: - :param group_id: 群组 - :param level: 权限等级 - """ - self._data.group_manager[str(group_id)].level = level - self.save() - - @init_group - def get_plugin_status(self, module: str, group_id: Union[str, int]) -> bool: - """ - 说明: - 获取插件状态 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - return module not in self._data.group_manager[str(group_id)].close_plugins - - def get_plugin_super_status(self, module: str, group_id: Union[str, int]) -> bool: - """ - 说明: - 获取插件是否被超级用户关闭 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - return ( - f"{module}:super" - not in self._data.group_manager[str(group_id)].close_plugins - ) - - @init_group - def get_group_level(self, group_id: Union[str, int]) -> int: - """ - 说明: - 获取群等级 - 参数: - :param group_id: 群号 - """ - return self._data.group_manager[str(group_id)].level - - def check_group_is_white(self, group_id: Union[str, int]) -> bool: - """ - 说明: - 检测群聊是否在白名单 - 参数: - :param group_id: 群号 - """ - return str(group_id) in self._data.white_group - - def add_group_white_list(self, group_id: Union[str, int]): - """ - 说明: - 将群聊加入白名单 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id not in self._data.white_group: - self._data.white_group.append(group_id) - - def delete_group_white_list(self, group_id: Union[str, int]): - """ - 说明: - 将群聊从白名单中删除 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id in self._data.white_group: - self._data.white_group.remove(group_id) - - def get_group_white_list(self) -> List[str]: - """ - 说明: - 获取所有群白名单 - """ - return self._data.white_group - - def load_task(self): - """ - 说明: - 加载被动技能 - """ - for matcher in get_matchers(True): - _plugin = nonebot.plugin.get_plugin(matcher.plugin_name) # type: ignore - try: - _module = _plugin.module - plugin_task = _module.__getattribute__("__plugin_task__") - for key in plugin_task.keys(): - if key in self._data.task.keys(): - raise ValueError(f"plugin_task:{key} 已存在!") - self._data.task[key] = plugin_task[key] - except AttributeError: - pass - - @init_group - def delete_group(self, group_id: Union[str, int]): - """ - 说明: - 删除群配置 - 参数: - :param group_id: 群号 - """ - group_id = str(group_id) - if group_id in self._data.white_group: - self._data.white_group.remove(group_id) - self.save() - - def open_group_task(self, group_id: Union[str, int], task: str): - """ - 说明: - 开启群被动技能 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - self._set_group_group_task_status(group_id, task, True) - - def close_global_task(self, task: str): - """ - 说明: - 关闭全局被动技能 - 参数: - :param task: 被动技能名称 - """ - if task not in self._data.close_task: - self._data.close_task.append(task) - - def open_global_task(self, task: str): - """ - 说明: - 开启全局被动技能 - 参数: - :param task: 被动技能名称 - """ - if task in self._data.close_task: - self._data.close_task.remove(task) - - def close_group_task(self, group_id: Union[str, int], task: str): - """ - 说明: - 关闭群被动技能 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - self._set_group_group_task_status(group_id, task, False) - - def check_task_status(self, task: str, group_id: Optional[str] = None) -> bool: - """ - 说明: - 检查该被动状态 - 参数: - :param task: 被动技能名称 - :param group_id: 群号 - """ - if group_id: - return self.check_group_task_status( - group_id, task - ) and self.check_task_super_status(task) - return self.check_task_super_status(task) - - @init_group - @init_task - def check_group_task_status(self, group_id: Union[str, int], task: str) -> bool: - """ - 说明: - 查看群被动技能状态 - 参数: - :param group_id: 群号 - :param task: 被动技能名称 - """ - return self._data.group_manager[str(group_id)].group_task_status.get( - task, False - ) - - def check_task_super_status(self, task: str) -> bool: - """ - 说明: - 查看群被动技能状态(超级用户设置的状态) - 参数: - :param task: 被动技能名称 - """ - return task not in self._data.close_task - - def get_task_data(self) -> Dict[str, str]: - """ - 说明: - 获取所有被动任务 - """ - return self._data.task - - @init_group - @init_task - def group_group_task_status(self, group_id: Union[str, int]) -> str: - """ - 说明: - 查看群被全部动技能状态 - 参数: - :param group_id: 群号 - """ - x = "[群被动技能]:\n" - group_id = str(group_id) - for key in self._data.group_manager[group_id].group_task_status.keys(): - x += f'{self._data.task[key]}:{"√" if self.check_group_task_status(group_id, key) else "×"}\n' - return x[:-1] - - @init_group - @init_task - def _set_group_group_task_status( - self, group_id: Union[str, int], task: str, status: bool - ): - """ - 说明: - 管理群被动技能状态 - 参数: - :param group_id: 群号 - :param task: 被动技能 - :param status: 状态 - """ - self._data.group_manager[str(group_id)].group_task_status[task] = status - self.save() - - @init_group - def _set_plugin_status( - self, module: str, status: str, group_id: Union[str, int], is_save: bool - ): - """ - 说明: - 设置功能开关状态 - 参数: - :param module: 功能模块名 - :param status: 功能状态 - :param group_id: 群组 - :param is_save: 是否保存 - """ - group_id = str(group_id) - if status == "block": - if module not in self._data.group_manager[group_id].close_plugins: - self._data.group_manager[group_id].close_plugins.append(module) - else: - if module in self._data.group_manager[group_id].close_plugins: - self._data.group_manager[group_id].close_plugins.remove(module) - if is_save: - self.save() - - @init_group - def _set_group_bot_status(self, group_id: Union[int, str], status: bool): - """ - 说明: - 设置群聊bot总开关 - 参数: - :param group_id: 群号 - :param status: 开关状态 - """ - self._data.group_manager[str(group_id)].status = status - self.save() - - def reload(self): - if self.file.exists(): - t = self._data.task - self._data = BaseData.parse_file(self.file) - self._data.task = t - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 路径文件 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - dict_data = self._data.dict() - del dict_data["task"] - with open(path, "w", encoding="utf8") as f: - json.dump(dict_data, f, ensure_ascii=False, indent=4) - - def get(self, key: str, default: Any = None) -> BaseGroup: - return self._data.group_manager.get(key, default) - - def __setitem__(self, key, value): - self._data.group_manager[key] = value - - def __getitem__(self, key) -> BaseGroup: - return self._data.group_manager[key] diff --git a/utils/manager/models.py b/utils/manager/models.py deleted file mode 100644 index 60b93661..00000000 --- a/utils/manager/models.py +++ /dev/null @@ -1,150 +0,0 @@ -from enum import Enum -from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Tuple, Union - -from pydantic import BaseModel - -from configs.config import Config -from configs.utils import Config as zConfig - - -class AdminSetting(BaseModel): - """ - 管理员设置 - """ - - level: int = 5 - cmd: Optional[List[str]] - - -class BaseGroup(BaseModel): - """ - 基础群聊信息 - """ - - level: int = Config.get_config("group_manager", "DEFAULT_GROUP_LEVEL") # 群等级 - status: bool = Config.get_config( - "group_manager", "DEFAULT_GROUP_BOT_STATUS" - ) # 总开关状态 - close_plugins: List[str] = [] # 已关闭插件 - group_task_status: Dict[str, bool] = {} # 被动状态 - - -class BaseData(BaseModel): - """ - 群基本信息 - """ - - white_group: List[str] = [] - """白名单""" - close_task: List[str] = [] - """全局关闭的被动任务""" - group_manager: Dict[str, BaseGroup] = {} - """群组管理""" - task: Dict[str, str] = {} - """被动任务 英文:中文名""" - - def __len__(self) -> int: - return len(self.group_manager) - - -class PluginBlock(BaseModel): - """ - 插件阻断 - """ - - status: bool = True # 限制状态 - check_type: Literal["private", "group", "all"] = "all" # 检查类型 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginCd(BaseModel): - """ - 插件阻断 - """ - - cd: int = 5 # cd - status: bool = True # 限制状态 - check_type: Literal["private", "group", "all"] = "all" # 检查类型 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginCount(BaseModel): - """ - 插件阻断 - """ - - max_count: int # 次数 - status: bool = True # 限制状态 - limit_type: Literal["user", "group"] = "user" # 监听对象 - rst: Optional[str] # 阻断时回复 - - -class PluginSetting(BaseModel): - """ - 插件设置 - """ - - cmd: List[str] = [] # 命令 或 命令别名 - default_status: bool = True # 默认开关状态 - level: int = 5 # 功能权限等级 - limit_superuser: bool = False # 功能状态是否限制超级用户 - plugin_type: Tuple[Union[str, int], ...] = ("normal",) # 插件类型 - cost_gold: int = 0 # 需要消费的金币 - - -class Plugin(BaseModel): - """ - 插件数据 - """ - - plugin_name: str # 模块名 - status: Optional[bool] = True # 开关状态 - error: Optional[bool] = False # 是否加载报错 - block_type: Optional[str] = None # 关闭类型 - author: Optional[str] = None # 作者 - version: Optional[Union[int, str]] = None # 版本 - - -class PluginType(Enum): - """ - 插件类型 - """ - - NORMAL = "normal" - ADMIN = "admin" - HIDDEN = "hidden" - SUPERUSER = "superuser" - - -class PluginData(BaseModel): - model: str - name: str - plugin_type: PluginType # 插件内部类型,根据name [Hidden] [Admin] [SUPERUSER] - usage: Optional[str] - superuser_usage: Optional[str] - des: Optional[str] - task: Optional[Dict[str, str]] - menu_type: Tuple[Union[str, int], ...] = ("normal",) # 菜单类型 - plugin_setting: Optional[PluginSetting] - plugin_cd: Optional[PluginCd] - plugin_block: Optional[PluginBlock] - plugin_count: Optional[PluginCount] - plugin_resources: Optional[Dict[str, Union[str, Path]]] - plugin_configs: Optional[Dict[str, zConfig]] - plugin_status: Plugin - - class Config: - arbitrary_types_allowed = True - - def __eq__(self, other: "PluginData"): - return ( - isinstance(other, PluginData) - and self.name == other.name - and self.menu_type == other.menu_type - ) - - def __hash__(self): - return hash(self.name + str(self.menu_type[0])) diff --git a/utils/manager/none_plugin_count_manager.py b/utils/manager/none_plugin_count_manager.py deleted file mode 100644 index 9aab831f..00000000 --- a/utils/manager/none_plugin_count_manager.py +++ /dev/null @@ -1,47 +0,0 @@ -from utils.manager.data_class import StaticData -from typing import Optional -from pathlib import Path - - -class NonePluginCountManager(StaticData): - """ - 插件加载容忍管理器,当连续 max_count 次插件加载,视为删除插件,清楚数据 - """ - - def __init__(self, file: Optional[Path], max_count: int = 5): - """ - :param file: 存储路径 - :param max_count: 容忍最大次数 - """ - super().__init__(file) - if not self._data: - self._data = {} - self._max_count = max_count - - def add_count(self, module: str, count: int = 1): - """ - 添加次数 - :param module: 模块 - :param count: 次数,无特殊情况均为 1 - """ - if module not in self._data.keys(): - self._data[module] = count - else: - self._data[module] += count - - def reset(self, module: str): - """ - 重置次数 - :param module: 模块 - """ - if module in self._data.keys(): - self._data[module] = 0 - - def check(self, module: str): - """ - 检查容忍次数是否到达最大值 - :param module: 模块 - """ - if module in self._data.keys(): - return self._data[module] >= self._max_count - return False diff --git a/utils/manager/plugin_data_manager.py b/utils/manager/plugin_data_manager.py deleted file mode 100644 index f94307dc..00000000 --- a/utils/manager/plugin_data_manager.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, Dict, Optional - -from . import StaticData -from .models import PluginData - - -class PluginDataManager(StaticData[PluginData]): - """ - 插件所有信息管理 - """ - - def __init__(self): - super().__init__(None) - self._data: Dict[str, PluginData] = {} - - def add_plugin_info(self, info: PluginData): - """ - 说明: - 添加插件信息 - 参数: - :param info: PluginInfo - """ - if info.model in self._data.keys() and self._data[info.model] == info: - raise ValueError(f"PluginInfoManager {info.model}:{info.name} 插件名称及类型已存在") - self._data[info.model] = info - - def get(self, item: str, default: Any = None) -> Optional[PluginData]: - return self._data.get(item, default) - - def __getitem__(self, item) -> Optional[PluginData]: - return self._data.get(item) - - def reload(self): - pass diff --git a/utils/manager/plugins2block_manager.py b/utils/manager/plugins2block_manager.py deleted file mode 100644 index 92259ffc..00000000 --- a/utils/manager/plugins2block_manager.py +++ /dev/null @@ -1,192 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from services.log import logger -from utils.utils import UserBlockLimiter -from pathlib import Path -from ruamel import yaml -from .models import PluginBlock - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2blockManager(StaticData[PluginBlock]): - """ - 插件命令阻塞 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._block_limiter: Dict[str, UserBlockLimiter] = {} - self.__load_file() - - @overload - def add_block_limit(self, plugin: str, plugin_block: PluginBlock): - ... - - @overload - def add_block_limit( - self, - plugin: str, - status: bool = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_block_limit( - self, - plugin: str, - status: Union[bool, PluginBlock] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 block 限制 - 参数: - :param plugin: 插件模块名称 - :param status: 默认开关状态 - :param check_type: 检查类型 'private'/'group'/'all',限制私聊/群聊/全部 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(status, PluginBlock): - self._data[plugin] = status - else: - if check_type not in ["all", "group", "private"]: - raise ValueError( - f"{plugin} 添加block限制错误,‘check_type‘ 必须为 'private'/'group'/'all'" - ) - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加block限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginBlock( - status=status, check_type=check_type, limit_type=limit_type, rst=rst - ) - - def get_plugin_block_data(self, plugin: str) -> Optional[PluginBlock]: - """ - 说明: - 获取插件block数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_block_status(plugin): - return self._data[plugin] - return None - - def check_plugin_block_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 block - 参数: - :param plugin: 模块名 - """ - return plugin in self._data.keys() and self._data[plugin].status - - def check(self, id_: int, plugin: str) -> bool: - """ - 说明: - 检查 block - 参数: - :param id_: 限制 id - :param plugin: 模块名 - """ - if self._block_limiter.get(plugin): - return self._block_limiter[plugin].check(id_) - return False - - def set_true(self, id_: int, plugin: str): - """ - 说明: - 对插件 block - 参数: - :param id_: 限制 id - :param plugin: 模块名 - """ - if self._block_limiter.get(plugin): - self._block_limiter[plugin].set_true(id_) - - def set_false(self, id_: int, plugin: str): - """ - 说明: - 对插件 unblock - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._block_limiter.get(plugin): - self._block_limiter[plugin].set_false(id_) - - def reload_block_limit(self): - """ - 说明: - 加载 block 限制器 - """ - for plugin in self._data: - if self.check_plugin_block_status(plugin): - self._block_limiter[plugin] = UserBlockLimiter() - logger.info(f"已成功加载 {len(self._block_limiter)} 个Block限制.") - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - self.reload_block_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginBlockLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginBlockLimit"].yaml_set_start_comment( - """# 用户调用阻塞 -# 即 当用户调用此功能还未结束时 -# 用发送消息阻止用户重复调用此命令直到该命令结束 -# key:模块名称 -# status:此限制的开关状态 -# check_type:'private'/'group'/'all',限制私聊/群聊/全部 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':阻塞用户,'group':阻塞群聊 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginBlock] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = yaml.round_trip_load(f) - if "PluginBlockLimit" in temp.keys(): - for k, v in temp["PluginBlockLimit"].items(): - self._data[k] = PluginBlock.parse_obj(v) diff --git a/utils/manager/plugins2cd_manager.py b/utils/manager/plugins2cd_manager.py deleted file mode 100644 index 40aa668a..00000000 --- a/utils/manager/plugins2cd_manager.py +++ /dev/null @@ -1,199 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from utils.utils import FreqLimiter -from services.log import logger -from pathlib import Path -from ruamel import yaml -from .models import PluginCd - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2cdManager(StaticData[PluginCd]): - """ - 插件命令 cd 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._freq_limiter: Dict[str, FreqLimiter] = {} - self.__load_file() - - @overload - def add_cd_limit(self, plugin: str, plugin_cd: PluginCd): - ... - - @overload - def add_cd_limit( - self, - plugin: str, - cd: Union[int, PluginCd] = 5, - status: Optional[bool] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_cd_limit( - self, - plugin: str, - cd: Union[int, PluginCd] = 5, - status: Optional[bool] = True, - check_type: Literal["private", "group", "all"] = "all", - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 cd 限制 - 参数: - :param plugin: 插件模块名称 - :param cd: cd 时长 - :param status: 默认开关状态 - :param check_type: 检查类型 'private'/'group'/'all',限制私聊/群聊/全部 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(cd, PluginCd): - self._data[plugin] = cd - else: - if check_type not in ["all", "group", "private"]: - raise ValueError( - f"{plugin} 添加cd限制错误,‘check_type‘ 必须为 'private'/'group'/'all'" - ) - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加cd限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginCd(cd=cd, status=status, check_type=check_type, limit_type=limit_type, rst=rst) - - def get_plugin_cd_data(self, plugin: str) -> Optional[PluginCd]: - """ - 说明: - 获取插件cd数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_cd_status(plugin): - return self._data[plugin] - return None - - def check_plugin_cd_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 cd - 参数: - :param plugin: 模块名 - """ - return ( - plugin in self._data.keys() - and self._data[plugin].cd > 0 - and self._data[plugin].status - ) - - def check(self, plugin: str, id_: int) -> bool: - """ - 说明: - 检查 cd - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._freq_limiter.get(plugin): - return self._freq_limiter[plugin].check(id_) - return False - - def start_cd(self, plugin: str, id_: int, cd: int = 0): - """ - 说明: - 开始cd - 参数: - :param plugin: 模块名 - :param id_: cd 限制类型 - :param cd: cd 时长 - """ - if self._freq_limiter.get(plugin): - self._freq_limiter[plugin].start_cd(id_, cd) - - def get_plugin_data(self, plugin: str) -> Optional[PluginCd]: - """ - 说明: - 获取单个模块限制数据 - 参数: - :param plugin: 模块名 - """ - if self._data.get(plugin): - return self._data.get(plugin) - - def reload_cd_limit(self): - """ - 说明: - 加载 cd 限制器 - """ - for plugin in self._data: - if self.check_plugin_cd_status(plugin): - self._freq_limiter[plugin] = FreqLimiter( - self.get_plugin_cd_data(plugin).cd - ) - logger.info(f"已成功加载 {len(self._freq_limiter)} 个Cd限制.") - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - self.reload_cd_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginCdLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginCdLimit"].yaml_set_start_comment( - """# 需要cd的功能 -# 自定义的功能需要cd也可以在此配置 -# key:模块名称 -# cd:cd 时长(秒) -# status:此限制的开关状态 -# check_type:'private'/'group'/'all',限制私聊/群聊/全部 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':用户N秒内触发1次,'group':群N秒内触发1次 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginCd] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = _yaml.load(f) - if "PluginCdLimit" in temp.keys(): - for k, v in temp["PluginCdLimit"].items(): - self._data[k] = PluginCd.parse_obj(v) diff --git a/utils/manager/plugins2count_manager.py b/utils/manager/plugins2count_manager.py deleted file mode 100644 index 09b305df..00000000 --- a/utils/manager/plugins2count_manager.py +++ /dev/null @@ -1,191 +0,0 @@ -from typing import Optional, Dict, Literal, Union, overload -from utils.manager.data_class import StaticData -from utils.utils import DailyNumberLimiter -from services.log import logger -from pathlib import Path -from ruamel import yaml -from .models import PluginCount - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2countManager(StaticData[PluginCount]): - """ - 插件命令 次数 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self._daily_limiter: Dict[str, DailyNumberLimiter] = {} - self.__load_file() - - @overload - def add_count_limit(self, plugin: str, plugin_count: PluginCount): - ... - - @overload - def add_count_limit( - self, - plugin: str, - max_count: int = 5, - status: Optional[bool] = True, - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - ... - - def add_count_limit( - self, - plugin: str, - max_count: Union[int, PluginCount] = 5, - status: Optional[bool] = True, - limit_type: Literal["user", "group"] = "user", - rst: Optional[str] = None, - ): - """ - 说明: - 添加插件调用 次数 限制 - 参数: - :param plugin: 插件模块名称 - :param max_count: 最大次数限制 - :param status: 默认开关状态 - :param limit_type: 限制类型 监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id - :param rst: 回复的话,为空则不回复 - """ - if isinstance(max_count, PluginCount): - self._data[plugin] = max_count - else: - if limit_type not in ["user", "group"]: - raise ValueError(f"{plugin} 添加count限制错误,‘limit_type‘ 必须为 'user'/'group'") - self._data[plugin] = PluginCount(max_count=max_count, status=status, limit_type=limit_type, rst=rst) - - def get_plugin_count_data(self, plugin: str) -> Optional[PluginCount]: - """ - 说明: - 获取插件次数数据 - 参数: - :param plugin: 模块名 - """ - if self.check_plugin_count_status(plugin): - return self._data[plugin] - return None - - def get_plugin_data(self, plugin: str) -> Optional[PluginCount]: - """ - 说明: - 获取单个模块限制数据 - 参数: - :param plugin: 模块名 - """ - if self._data.get(plugin) is not None: - return self._data.get(plugin) - - def check_plugin_count_status(self, plugin: str) -> bool: - """ - 说明: - 检测插件是否有 次数 限制 - 参数: - :param plugin: 模块名 - """ - return ( - plugin in self._data.keys() - and self._data[plugin].status - and self._data[plugin].max_count > 0 - ) - - def check(self, plugin: str, id_: int) -> bool: - """ - 说明: - 检查 count - 参数: - :param plugin: 模块名 - :param id_: 限制 id - """ - if self._daily_limiter.get(plugin): - return self._daily_limiter[plugin].check(id_) - return True - - def increase(self, plugin: str, id_: int, num: int = 1): - """ - 说明: - 增加次数 - 参数: - :param plugin: 模块名 - :param id_: cd 限制类型 - :param num: 增加次数 - """ - if self._daily_limiter.get(plugin): - self._daily_limiter[plugin].increase(id_, num) - - def reload_count_limit(self): - """ - 说明: - 加载 cd 限制器 - """ - for plugin in self._data: - if self.check_plugin_count_status(plugin): - self._daily_limiter[plugin] = DailyNumberLimiter( - self.get_plugin_count_data(plugin).max_count - ) - logger.info(f"已成功加载 {len(self._daily_limiter)} 个Count限制.") - - def reload(self): - """ - 重载本地数据 - """ - self.__load_file() - self.reload_count_limit() - - def save(self, path: Union[str, Path] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - yaml.dump( - {"PluginCountLimit": self.dict()}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginCountLimit"].yaml_set_start_comment( - """# 命令每日次数限制 -# 即 用户/群聊 每日可调用命令的次数 [数据内存存储,重启将会重置] -# 每日调用直到 00:00 刷新 -# key:模块名称 -# max_count: 每日调用上限 -# status:此限制的开关状态 -# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id -# 示例:'user':用户上限,'group':群聊上限 -# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 -# rst 为 "" 或 None 时则不回复 -# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" -# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" -# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginCount] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - temp = _yaml.load(f) - if "PluginCountLimit" in temp.keys(): - for k, v in temp["PluginCountLimit"].items(): - self._data[k] = PluginCount.parse_obj(v) diff --git a/utils/manager/plugins2settings_manager.py b/utils/manager/plugins2settings_manager.py deleted file mode 100644 index b58c4644..00000000 --- a/utils/manager/plugins2settings_manager.py +++ /dev/null @@ -1,171 +0,0 @@ -from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union, overload - -from ruamel import yaml - -from utils.manager.data_class import StaticData - -from .models import PluginSetting, PluginType - -_yaml = yaml.YAML(typ="safe") - - -class Plugins2settingsManager(StaticData[PluginSetting]): - """ - 插件命令阻塞 管理器 - """ - - def __init__(self, file: Path): - super().__init__(file, False) - self.__load_file() - - @overload - def add_plugin_settings(self, plugin: str, plugin_settings: PluginSetting): - ... - - @overload - def add_plugin_settings( - self, - plugin: str, - cmd: Optional[List[str]] = None, - default_status: bool = True, - level: int = 5, - limit_superuser: bool = False, - plugin_type: Tuple[Union[str, int]] = ("normal",), - cost_gold: int = 0, - ): - ... - - def add_plugin_settings( - self, - plugin: str, - cmd: Optional[Union[List[str], PluginSetting]] = None, - default_status: bool = True, - level: int = 5, - limit_superuser: bool = False, - plugin_type: Tuple[Union[str, int]] = ("normal",), - cost_gold: int = 0, - ): - """ - 说明: - 添加一个插件设置 - 参数: - :param plugin: 插件模块名称 - :param cmd: 命令 或 命令别名 - :param default_status: 默认开关状态 - :param level: 功能权限等级 - :param limit_superuser: 功能状态是否限制超级用户 - :param plugin_type: 插件类型 - :param cost_gold: 需要消费的金币 - """ - if isinstance(cmd, PluginSetting): - self._data[plugin] = cmd - else: - self._data[plugin] = PluginSetting( - cmd=cmd, - level=level, - default_status=default_status, - limit_superuser=limit_superuser, - plugin_type=plugin_type, - cost_gold=cost_gold, - ) - - def get_plugin_data(self, module: str) -> Optional[PluginSetting]: - """ - 说明: - 通过模块名获取数据 - 参数: - :param module: 模块名称 - """ - return self._data.get(module) - - @overload - def get_plugin_module(self, cmd: str) -> str: - ... - - @overload - def get_plugin_module(self, cmd: str, is_all: bool = True) -> List[str]: - ... - - def get_plugin_module( - self, cmd: str, is_all: bool = False - ) -> Union[str, List[str]]: - """ - 说明: - 根据 cmd 获取功能 modules - 参数: - :param cmd: 命令 - :param is_all: 获取全部包含cmd的模块 - """ - keys = [] - for key in self._data.keys(): - if cmd in self._data[key].cmd: - if is_all: - keys.append(key) - else: - return key - return keys - - def reload(self): - """ - 说明: - 重载本地数据 - """ - self.__load_file() - - def save(self, path: Optional[Union[str, Path]] = None): - """ - 说明: - 保存文件 - 参数: - :param path: 文件路径 - """ - path = path or self.file - if isinstance(path, str): - path = Path(path) - if path: - with open(path, "w", encoding="utf8") as f: - self_dict = self.dict() - for key in self_dict.keys(): - if self_dict[key].get("plugin_type") and isinstance( - self_dict[key].get("plugin_type"), PluginType - ): - self_dict[key]["plugin_type"] = self_dict[key][ - "plugin_type" - ].value - yaml.dump( - {"PluginSettings": self_dict}, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) - _data = yaml.round_trip_load(open(path, encoding="utf8")) - _data["PluginSettings"].yaml_set_start_comment( - """# 模块与对应命令和对应群权限 -# 用于生成帮助图片 和 开关功能 -# key:模块名称 -# level:需要的群等级 -# default_status:加入群时功能的默认开关状态 -# limit_superuser: 功能状态是否限制超级用户 -# cmd: 关闭[cmd] 都会触发命令 关闭对应功能,cmd列表第一个词为统计的功能名称 -# plugin_type: 帮助类别 示例:('原神相关',) 或 ('原神相关', 1),1代表帮助命令列向排列,否则为横向排列""", - indent=2, - ) - with open(path, "w", encoding="utf8") as wf: - yaml.round_trip_dump( - _data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) - - def __load_file(self): - """ - 说明: - 读取配置文件 - """ - self._data: Dict[str, PluginSetting] = {} - if self.file.exists(): - with open(self.file, "r", encoding="utf8") as f: - if temp := _yaml.load(f): - if "PluginSettings" in temp.keys(): - for k, v in temp["PluginSettings"].items(): - self._data[k] = PluginSetting.parse_obj(v) diff --git a/utils/manager/plugins_manager.py b/utils/manager/plugins_manager.py deleted file mode 100644 index 51aa725a..00000000 --- a/utils/manager/plugins_manager.py +++ /dev/null @@ -1,169 +0,0 @@ -from pathlib import Path -from typing import Callable, Dict, Optional, Union - -from utils.manager import group_manager -from utils.manager.data_class import StaticData - -from .models import Plugin - - -def init_plugin(func: Callable): - """ - 说明: - 初始化群数据 - 参数: - :param func: func - """ - - def wrapper(*args, **kwargs): - try: - self = args[0] - module = args[1] - if module not in self._data.keys(): - self._data[module] = Plugin( - plugin_name=module, - status=True, - error=False, - block_type=None, - author=None, - version=None, - ) - except Exception as e: - pass - return func(*args, **kwargs) - - return wrapper - - -class PluginsManager(StaticData[Plugin]): - """ - 插件 管理器 - """ - - def __init__(self, file: Path): - self._data: Dict[str, Plugin] - super().__init__(file) - for k, v in self._data.items(): - self._data[k] = Plugin.parse_obj(v) - - def add_plugin_data( - self, - module: str, - plugin_name: str, - *, - status: Optional[bool] = True, - error: Optional[bool] = False, - block_type: Optional[str] = None, - author: Optional[str] = None, - version: Optional[int] = None, - ): - """ - 说明: - 添加插件数据 - 参数: - :param module: 模块名称 - :param plugin_name: 插件名称 - :param status: 插件开关状态 - :param error: 加载状态 - :param block_type: 限制类型 - :param author: 作者 - :param version: 版本 - """ - self._data[module] = Plugin( - plugin_name=plugin_name, - status=status, - error=error, - block_type=block_type, - author=author, - version=version, - ) - - def block_plugin( - self, module: str, group_id: Optional[str] = None, block_type: str = "all" - ): - """ - 说明: - 锁定插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组,None时为超级用户禁用 - :param block_type: 限制类型 - """ - self._set_plugin_status(module, "block", group_id, block_type) - - def unblock_plugin(self, module: str, group_id: Optional[str] = None): - """ - 说明: - 解锁插件 - 参数: - :param module: 功能模块名 - :param group_id: 群组 - """ - self._set_plugin_status(module, "unblock", group_id) - - def get_plugin_status(self, module: str, block_type: str = "all") -> bool: - """ - 说明: - 获取插件状态 - 参数: - :param module: 功能模块名 - :param block_type: 限制类型 - """ - if module in self._data.keys(): - if self._data[module].block_type == "all" and block_type == "all": - return False - return not self._data[module].block_type == block_type - return True - - def get_plugin_block_type(self, module: str) -> Optional[str]: - """ - 说明: - 获取功能限制类型 - 参数: - :param module: 模块名称 - """ - if module in self._data.keys(): - return self._data[module].block_type - - @init_plugin - def get_plugin_error_status(self, module: str) -> Optional[bool]: - """ - 说明: - 插件是否成功加载 - 参数: - :param module: 模块名称 - """ - return self._data[module].error - - @init_plugin - def _set_plugin_status( - self, - module: str, - status: str, - group_id: Optional[str], - block_type: str = "all", - ): - """ - 说明: - 设置功能开关状态 - 参数: - :param module: 功能模块名 - :param status: 功能状态 - :param group_id: 群组 - :param block_type: 限制类型 - """ - if module: - if group_id: - if status == "block": - group_manager.block_plugin(f"{module}:super", group_id) - else: - group_manager.unblock_plugin(f"{module}:super", group_id) - else: - if status == "block": - self._data[module].status = False - self._data[module].block_type = block_type - else: - if module in self._data.keys(): - self._data[module].status = True - self._data[module].block_type = None - self.save() diff --git a/utils/manager/requests_manager.py b/utils/manager/requests_manager.py deleted file mode 100644 index d9b5175c..00000000 --- a/utils/manager/requests_manager.py +++ /dev/null @@ -1,347 +0,0 @@ -from io import BytesIO -from pathlib import Path -from typing import Optional, Union, overload - -from nonebot.adapters.onebot.v11 import ActionFailed, Bot - -from services.log import logger -from utils.image_utils import BuildImage -from utils.manager.data_class import StaticData -from utils.utils import get_user_avatar - - -class RequestManager(StaticData): - - """ - 好友请求/邀请请求 管理 - """ - - def __init__(self, file: Optional[Path]): - super().__init__(file) - if not self._data: - self._data = {"private": {}, "group": {}} - - def add_request( - self, - bot_id: str, - id_: int, - type_: str, - flag: str, - *, - nickname: Optional[str] = None, - level: Optional[int] = None, - sex: Optional[str] = None, - age: Optional[str] = None, - from_: Optional[str] = "", - comment: Optional[str] = None, - invite_group: Optional[int] = None, - group_name: Optional[str] = None, - ): - """ - 添加一个请求 - :param bot_id: bot_id - :param id_: id,用户id或群id - :param type_: 类型,private 或 group - :param flag: event.flag - :param nickname: 用户昵称 - :param level: 等级 - :param sex: 性别 - :param age: 年龄 - :param from_: 请求来自 - :param comment: 附加消息 - :param invite_group: 邀请群聊 - :param group_name: 群聊名称 - """ - self._data[type_][str(len(self._data[type_].keys()))] = { - "bot_id": bot_id, - "id": id_, - "flag": flag, - "nickname": nickname, - "level": level, - "sex": sex, - "age": age, - "from": from_, - "comment": comment, - "invite_group": invite_group, - "group_name": group_name, - } - self.save() - - @overload - def remove_request(self, type_: str, flag: str): - ... - - @overload - def remove_request(self, type_: str, id_: int): - ... - - def remove_request(self, type_: str, id_: Union[int, str]): - """ - 删除一个请求数据 - :param type_: 类型 - :param id_: id,user_id 或 group_id - """ - for x in self._data[type_].keys(): - a_id = self._data[type_][x].get("id") - a_flag = self._data[type_][x].get("flag") - if a_id == id_ or a_flag == id_: - del self._data[type_][x] - break - self.save() - - def get_group_id(self, id_: int) -> Optional[int]: - """ - 通过id获取群号 - :param id_: id - """ - data = self._data["group"].get(str(id_)) - if data: - return data["invite_group"] - return None - - @overload - async def approve(self, bot: Bot, id_: int, type_: str) -> int: - ... - - @overload - async def approve(self, bot: Bot, flag: str, type_: str) -> int: - ... - - - async def approve(self, bot: Bot, id_: Union[int, str], type_: str) -> int: - """ - 同意请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - """ - return await self._set_add_request(bot, id_, type_, True) - - @overload - async def refused(self, bot: Bot, id_: int, type_: str) -> int: - ... - - @overload - async def refused(self, bot: Bot, flag: str, type_: str) -> int: - ... - - async def refused(self, bot: Bot, id_: Union[int, str], type_: str) -> Optional[int]: - """ - 拒绝请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - """ - return await self._set_add_request(bot, id_, type_, False) - - def clear( - self, type_: Optional[str] = None - ): # type_: Optional[Literal["group", "private"]] = None - """ - 清空所有请求信息,无视请求 - :param type_: 类型 - """ - if type_: - self._data[type_] = {} - else: - self._data = {"private": {}, "group": {}} - self.save() - - @overload - async def delete_request(self, id_: int, type_: str) -> int: - ... - - @overload - async def delete_request(self, flag: str, type_: str) -> int: - ... - - def delete_request( - self, id_: Union[str, int], type_: str - ): # type_: Literal["group", "private"] - """ - 删除请求 - :param id_: id - :param type_: 类型 - """ - if type(id_) == int: - if self._data[type_].get(id_): - del self._data[type_][id_] - self.save() - else: - for k, item in self._data[type_].items(): - if item['flag'] == id_: - del self._data[type_][k] - self.save() - break - - def set_group_name(self, group_name: str, group_id: int): - """ - 设置群聊名称 - :param group_name: 名称 - :param group_id: id - """ - for id_ in self._data["group"].keys(): - if self._data["group"][id_]["invite_group"] == group_id: - self._data["group"][id_]["group_name"] = group_name - break - self.save() - - async def show(self, type_: str) -> Optional[str]: - """ - 请求可视化 - """ - data = self._data[type_] - if not data: - return None - img_list = [] - id_list = list(data.keys()) - id_list.reverse() - for id_ in id_list: - age = data[id_]["age"] - nickname = data[id_]["nickname"] - comment = data[id_]["comment"] if type_ == "private" else "" - from_ = data[id_]["from"] - sex = data[id_]["sex"] - ava = BuildImage( - 80, 80, background=BytesIO(await get_user_avatar(data[id_]["id"])) - ) - ava.circle() - age_bk = BuildImage( - len(str(age)) * 6 + 6, - 15, - color="#04CAF7" if sex == "male" else "#F983C1", - ) - age_bk.text((3, 1), f"{age}", fill=(255, 255, 255)) - x = BuildImage( - 90, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" - ) - x.text((0, 0), "同意/拒绝", center_type="center") - x.circle_corner(10) - A = BuildImage(500, 100, font_size=24, font="msyh.ttf") - A.paste(ava, (15, 0), alpha=True, center_type="by_height") - A.text((120, 15), nickname) - A.paste(age_bk, (120, 50), True) - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"对方留言:{comment}", - font_color=(140, 140, 143), - ), - (120 + age_bk.w + 10, 49), - True, - ) - if type_ == "private": - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"来源:{from_}", - font_color=(140, 140, 143), - ), - (120, 70), - True, - ) - else: - A.paste( - BuildImage( - 200, - 0, - font_size=12, - plain_text=f"邀请你加入:{data[id_]['group_name']}({data[id_]['invite_group']})", - font_color=(140, 140, 143), - ), - (120, 70), - True, - ) - A.paste(x, (380, 35), True) - A.paste( - BuildImage( - 0, - 0, - plain_text=f"id:{id_}", - font_size=13, - font_color=(140, 140, 143), - ), - (400, 10), - True, - ) - img_list.append(A) - A = BuildImage(500, len(img_list) * 100, 500, 100) - for img in img_list: - A.paste(img) - bk = BuildImage(A.w, A.h + 50, color="#F8F9FB", font_size=20) - bk.paste(A, (0, 50)) - bk.text( - (15, 13), "好友请求" if type_ == "private" else "群聊请求", fill=(140, 140, 143) - ) - return bk.pic2bs4() - - async def _set_add_request( - self, bot: Bot, idx: Union[str, int], type_: str, approve: bool - ) -> int: - """ - 处理请求 - :param bot: Bot - :param id_: id - :param type_: 类型,private 或 group - :param approve: 是否同意 - """ - flag = None - id_ = None - if type(idx) == str: - flag = idx - else: - id_ = str(idx) - if id_ and id_ in self._data[type_].keys(): - try: - if type_ == "private": - await bot.set_friend_add_request( - flag=self._data[type_][id_]["flag"], approve=approve - ) - rid = self._data[type_][id_]["id"] - else: - await bot.set_group_add_request( - flag=self._data[type_][id_]["flag"], - sub_type="invite", - approve=approve, - ) - rid = self._data[type_][id_]["invite_group"] - except ActionFailed: - logger.info( - f"同意{self._data[type_][id_]['nickname']}({self._data[type_][id_]['id']})" - f"的{'好友' if type_ == 'private' else '入群'}请求失败了..." - ) - return 1 # flag失效 - else: - logger.info( - f"{'同意' if approve else '拒绝'}{self._data[type_][id_]['nickname']}({self._data[type_][id_]['id']})" - f"的{'好友' if type_ == 'private' else '入群'}请求..." - ) - del self._data[type_][id_] - self.save() - return rid - if flag: - rm_id = None - for k, item in self._data[type_].items(): - if item['flag'] == flag: - rm_id = k - if type_ == 'private': - await bot.set_friend_add_request( - flag=item['flag'], approve=approve - ) - rid = item["id"] - else: - await bot.set_group_add_request( - flag=item['flag'], - sub_type="invite", - approve=approve, - ) - rid = item["invite_group"] - if rm_id is not None: - del self._data[type_][rm_id] - self.save() - return rid - return 2 # 未找到id diff --git a/utils/manager/resources_manager.py b/utils/manager/resources_manager.py deleted file mode 100644 index 649223fc..00000000 --- a/utils/manager/resources_manager.py +++ /dev/null @@ -1,113 +0,0 @@ -from typing import Union, List, Optional - -from configs.path_config import IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH -from utils.manager.data_class import StaticData -from pathlib import Path -from ruamel.yaml import YAML -from services.log import logger -import shutil - -yaml = YAML(typ="safe") - - -class ResourcesManager(StaticData): - """ - 插件配置 与 资源 管理器 - """ - - def __init__(self, file: Path): - self.file = file - super().__init__(file) - self._temp_dir = [] - self._abspath = Path() - - def add_resource( - self, module: str, source_file: Union[str, Path], move_file: Union[str, Path] - ): - """ - 添加一个资源移动路劲 - :param module: 模块名 - :param source_file: 源文件路径 - :param move_file: 移动路径 - """ - if isinstance(source_file, Path): - source_file = str(source_file.absolute()) - if isinstance(move_file, Path): - move_file = str(move_file.absolute()) - if module not in self._data.keys(): - self._data[module] = {source_file: move_file} - else: - self._data[module][source_file] = move_file - - def remove_resource(self, module: str, source_file: Optional[Union[str, Path]] = None): - """ - 删除一个资源路径 - :param module: 模块 - :param source_file: 源文件路径 - """ - if not source_file: - if module in self._data.keys(): - for x in self._data[module].keys(): - move_file = Path(self._data[module][x]) - if move_file not in [IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH]: - if move_file.exists(): - shutil.rmtree(move_file.absolute(), ignore_errors=True) - logger.info(f"已清除插件 {module} 资源路径:{self._data[module][x]}") - del self._data[module][x] - else: - if isinstance(source_file, Path): - source_file = str(source_file.absolute()) - if source_file: - if module in self._data.keys() and source_file in self._data[module].keys(): - move_file = Path(self._data[module][source_file]) - if move_file not in [IMAGE_PATH, DATA_PATH, RECORD_PATH, TEXT_PATH, FONT_PATH, LOG_PATH]: - if move_file.exists(): - shutil.rmtree(move_file.absolute(), ignore_errors=True) - del self._data[module][source_file] - self.save() - - def start_move(self): - """ - 开始移动路径 - """ - for module in self._data.keys(): - for source_path in self._data[module].keys(): - move_path = Path(self._data[module][source_path]) - try: - source_path = Path(source_path) - file_name = source_path.name - move_path = move_path / file_name - move_path.mkdir(exist_ok=True, parents=True) - if source_path.exists(): - if move_path.exists(): - shutil.rmtree(str(move_path.absolute()), ignore_errors=True) - shutil.move(str(source_path.absolute()), str(move_path.absolute())) - logger.info( - f"移动资源文件路径 {source_path.absolute()} >>> {move_path.absolute()}" - ) - elif not move_path.exists(): - logger.warning( - f"移动资源路径文件{source_path.absolute()} >>>" - f" {move_path.absolute()} 失败,源文件不存在.." - ) - except Exception as e: - logger.error( - f"移动资源路径文件{source_path.absolute()} >>>" - f" {move_path.absolute()}失败,{type(e)}:{e}" - ) - self.save() - - def add_temp_dir(self, path: Union[str, Path]): - """ - 添加临时清理文件夹 - :param path: 路径 - """ - if isinstance(path, str): - path = Path(path) - self._temp_dir.append(path) - - def get_temp_data_dir(self) -> List[Path]: - """ - 获取临时文件文件夹 - """ - return self._temp_dir diff --git a/utils/manager/withdraw_message_manager.py b/utils/manager/withdraw_message_manager.py deleted file mode 100644 index 59548f4f..00000000 --- a/utils/manager/withdraw_message_manager.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Dict, Optional, Tuple, Union - -from nonebot.adapters.onebot.v11 import ( - GroupMessageEvent, - MessageEvent, - PrivateMessageEvent, -) - - -class WithdrawMessageManager: - def __init__(self): - self.data = [] - - def append(self, message_data: Tuple[Union[int, Dict[str, int]], int]): - """ - 说明: - 添加一个撤回消息id和时间 - 参数: - :param message_data: 撤回消息id和时间 - """ - if isinstance(message_data[0], dict): - message_data = (message_data[0]["message_id"], message_data[1]) - self.data.append(message_data) - - def remove(self, message_data: Tuple[int, int]): - """ - 说明: - 删除一个数据 - 参数: - :param message_data: 消息id和时间 - """ - self.data.remove(message_data) - - def withdraw_message( - self, - event: MessageEvent, - id_: Union[int, Dict[str, int]], - conditions: Optional[Tuple[int, int]], - ): - """ - 便捷判断消息撤回 - :param event: event - :param id_: 消息id 或 send 返回的字典 - :param conditions: 判断条件 - """ - if conditions and conditions[0]: - if ( - (conditions[1] == 0 and isinstance(event, PrivateMessageEvent)) - or (conditions[1] == 1 and isinstance(event, GroupMessageEvent)) - or conditions[1] == 2 - ): - self.append((id_, conditions[0])) diff --git a/utils/message_builder.py b/utils/message_builder.py deleted file mode 100755 index f19876da..00000000 --- a/utils/message_builder.py +++ /dev/null @@ -1,212 +0,0 @@ -import io -from pathlib import Path -from typing import List, Optional, Union - -from nonebot.adapters.onebot.v11.message import Message, MessageSegment - -from configs.config import NICKNAME -from configs.path_config import IMAGE_PATH, RECORD_PATH -from services.log import logger -from utils.image_utils import BuildImage, BuildMat - - -def image( - file: Optional[Union[str, Path, bytes, BuildImage, io.BytesIO, BuildMat]] = None, - b64: Optional[str] = None, -) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.image 消息 - 生成顺序:绝对路径(abspath) > base64(b64) > img_name - 参数: - :param file: 图片文件 - :param b64: 图片base64(兼容旧方法) - """ - if b64: - file = b64 if b64.startswith("base64://") else ("base64://" + b64) - if isinstance(file, str): - if file.startswith(("http", "base64://")): - return MessageSegment.image(file) - else: - if (IMAGE_PATH / file).exists(): - return MessageSegment.image(IMAGE_PATH / file) - logger.warning(f"图片 {(IMAGE_PATH / file).absolute()}缺失...") - return "" - if isinstance(file, Path): - if file.exists(): - return MessageSegment.image(file) - logger.warning(f"图片 {file.absolute()}缺失...") - if isinstance(file, (bytes, io.BytesIO)): - return MessageSegment.image(file) - if isinstance(file, (BuildImage, BuildMat)): - return MessageSegment.image(file.pic2bs4()) - return MessageSegment.image("") - - -def at(qq: Union[int, str]) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.at 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment.at(qq) - - -def record(file: Union[Path, str, bytes, io.BytesIO]) -> Union[MessageSegment, str]: - """ - 说明: - 生成一个 MessageSegment.record 消息 - 参数: - :param file: 音频文件名称,默认在 resource/voice 目录下 - """ - if isinstance(file, Path): - if file.exists(): - return MessageSegment.record(file) - logger.warning(f"音频 {file.absolute()}缺失...") - if isinstance(file, (bytes, io.BytesIO)): - return MessageSegment.record(file) - if isinstance(file, str): - if "http" in file: - return MessageSegment.record(file) - else: - return MessageSegment.record(RECORD_PATH / file) - return "" - - -def text(msg: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.text 消息 - 参数: - :param msg: 消息文本 - """ - return MessageSegment.text(msg) - - -def contact_user(qq: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.contact_user 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment.contact_user(qq) - - -def share( - url: str, title: str, content: Optional[str] = None, image_url: Optional[str] = None -) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.share 消息 - 参数: - :param url: 自定义分享的链接 - :param title: 自定义分享的包体 - :param content: 自定义分享的内容 - :param image_url: 自定义分享的展示图片 - """ - return MessageSegment.share(url, title, content, image_url) - - -def xml(data: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.xml 消息 - 参数: - :param data: 数据文本 - """ - return MessageSegment.xml(data) - - -def json(data: str) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.json 消息 - 参数: - :param data: 消息数据 - """ - return MessageSegment.json(data) - - -def face(id_: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.face 消息 - 参数: - :param id_: 表情id - """ - return MessageSegment.face(id_) - - -def poke(qq: int) -> MessageSegment: - """ - 说明: - 生成一个 MessageSegment.poke 消息 - 参数: - :param qq: qq号 - """ - return MessageSegment("poke", {"qq": qq}) - - -def music(type_: str, id_: int) -> MessageSegment: - return MessageSegment.music(type_, id_) - - -def custom_forward_msg( - msg_list: List[Union[str, Message]], - uin: Union[int, str], - name: str = f"这里是{NICKNAME}", -) -> List[dict]: - """ - 说明: - 生成自定义合并消息 - 参数: - :param msg_list: 消息列表 - :param uin: 发送者 QQ - :param name: 自定义名称 - """ - uin = int(uin) - mes_list = [] - for _message in msg_list: - data = { - "type": "node", - "data": { - "name": name, - "uin": f"{uin}", - "content": _message, - }, - } - mes_list.append(data) - return mes_list - - -class MessageBuilder: - """ - MessageSegment构建工具 - """ - - def __init__(self, msg: Union[str, MessageSegment, Message]): - if msg: - if isinstance(msg, str): - self._msg = text(msg) - else: - self._msg = msg - else: - self._msg = text("") - - def text(self, msg: str): - return MessageBuilder(self._msg + text(msg)) - - def image( - self, - file: Optional[Union[str, Path, bytes]] = None, - b64: Optional[str] = None, - ): - return MessageBuilder(self._msg + image(file, b64)) - - def at(self, qq: int): - return MessageBuilder(self._msg + at(qq)) - - def face(self, id_: int): - return MessageBuilder(self._msg + face(id_)) diff --git a/utils/models/__init__.py b/utils/models/__init__.py deleted file mode 100644 index 0b217fcf..00000000 --- a/utils/models/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Any - -from nonebot.adapters.onebot.v11 import Message, MessageEvent -from pydantic import BaseModel - - -class ShopParam(BaseModel): - - - goods_name: str - """商品名称""" - user_id: int - """用户id""" - group_id: int - """群聊id""" - bot: Any - """bot""" - event: MessageEvent - """event""" - num: int - """道具单次使用数量""" - message: Message - """message""" - text: str - """text""" - send_success_msg: bool = True - """是否发送使用成功信息""" - max_num_limit: int = 1 - """单次使用最大次数""" - - -class CommonSql(BaseModel): - - sql: str - """sql语句""" - remark: str - """备注""" \ No newline at end of file diff --git a/utils/text_utils.py b/utils/text_utils.py deleted file mode 100644 index 01e46bf5..00000000 --- a/utils/text_utils.py +++ /dev/null @@ -1,12 +0,0 @@ - - -def prompt2cn(text: str, count: int, s: str = "#") -> str: - """ - 格式化中文提示 - :param text: 文本 - :param count: 个数 - :param s: # - """ - return s * count + "\n" + s * 6 + " " + text + " " + s * 6 + "\n" + s * count - - diff --git a/utils/typing.py b/utils/typing.py deleted file mode 100644 index cac8ca09..00000000 --- a/utils/typing.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Literal - -BLOCK_TYPE = Literal["all", "private", "group"] -"""禁用类型""" diff --git a/utils/user_agent.py b/utils/user_agent.py deleted file mode 100755 index 3047da33..00000000 --- a/utils/user_agent.py +++ /dev/null @@ -1,50 +0,0 @@ -import random - -user_agent = [ - "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", - "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", - "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", - "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko", - "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", - "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)", - "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", - "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11", - "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)", - "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", - "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", - "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", - "Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", - "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - "MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", - "Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10", - "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13", - "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+", - "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0", - "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124", - "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)", - "UCWEB7.0.2.37/28/999", - "NOKIA5700/ UCWEB7.0.2.37/28/999", - "Openwave/ UCWEB7.0.2.37/28/999", - "Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999", - # iPhone 6: - "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25", -] - - -def get_user_agent(): - return {"User-Agent": random.choice(user_agent)} - - -def get_user_agent_str(): - return random.choice(user_agent) diff --git a/utils/utils.py b/utils/utils.py deleted file mode 100755 index aca0abbc..00000000 --- a/utils/utils.py +++ /dev/null @@ -1,629 +0,0 @@ -import time -from collections import defaultdict -from datetime import datetime -from pathlib import Path -from typing import Any, Callable, List, Optional, Set, Type, Union - -import httpx -import nonebot -import pypinyin -import pytz -from nonebot import require -from nonebot.adapters import Bot -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot.matcher import Matcher, matchers - -from configs.config import SYSTEM_PROXY, Config -from services.log import logger - -try: - import ujson as json -except ModuleNotFoundError: - import json - -require("nonebot_plugin_apscheduler") -from nonebot_plugin_apscheduler import scheduler - -scheduler = scheduler - -# 全局字典 -GDict = { - "run_sql": [], # 需要启动前运行的sql语句 - "_shop_before_handle": {}, # 商品使用前函数 - "_shop_after_handle": {}, # 商品使用后函数 -} - - -CN2NUM = { - "一": 1, - "二": 2, - "三": 3, - "四": 4, - "五": 5, - "六": 6, - "七": 7, - "八": 8, - "九": 9, - "十": 10, - "十一": 11, - "十二": 12, - "十三": 13, - "十四": 14, - "十五": 15, - "十六": 16, - "十七": 17, - "十八": 18, - "十九": 19, - "二十": 20, - "二十一": 21, - "二十二": 22, - "二十三": 23, - "二十四": 24, - "二十五": 25, - "二十六": 26, - "二十七": 27, - "二十八": 28, - "二十九": 29, - "三十": 30, -} - - -class CountLimiter: - """ - 次数检测工具,检测调用次数是否超过设定值 - """ - - def __init__(self, max_count: int): - self.count = defaultdict(int) - self.max_count = max_count - - def add(self, key: Any): - self.count[key] += 1 - - def check(self, key: Any) -> bool: - if self.count[key] >= self.max_count: - self.count[key] = 0 - return True - return False - - -class UserBlockLimiter: - """ - 检测用户是否正在调用命令 - """ - - def __init__(self): - self.flag_data = defaultdict(bool) - self.time = time.time() - - def set_true(self, key: Any): - self.time = time.time() - self.flag_data[key] = True - - def set_false(self, key: Any): - self.flag_data[key] = False - - def check(self, key: Any) -> bool: - if time.time() - self.time > 30: - self.set_false(key) - return False - return self.flag_data[key] - - -class FreqLimiter: - """ - 命令冷却,检测用户是否处于冷却状态 - """ - - def __init__(self, default_cd_seconds: int): - self.next_time = defaultdict(float) - self.default_cd = default_cd_seconds - - def check(self, key: Any) -> bool: - return time.time() >= self.next_time[key] - - def start_cd(self, key: Any, cd_time: int = 0): - self.next_time[key] = time.time() + ( - cd_time if cd_time > 0 else self.default_cd - ) - - def left_time(self, key: Any) -> float: - return self.next_time[key] - time.time() - - -static_flmt = FreqLimiter(15) - - -class BanCheckLimiter: - """ - 恶意命令触发检测 - """ - - def __init__(self, default_check_time: float = 5, default_count: int = 4): - self.mint = defaultdict(int) - self.mtime = defaultdict(float) - self.default_check_time = default_check_time - self.default_count = default_count - - def add(self, key: Union[str, int, float]): - if self.mint[key] == 1: - self.mtime[key] = time.time() - self.mint[key] += 1 - - def check(self, key: Union[str, int, float]) -> bool: - if time.time() - self.mtime[key] > self.default_check_time: - self.mtime[key] = time.time() - self.mint[key] = 0 - return False - if ( - self.mint[key] >= self.default_count - and time.time() - self.mtime[key] < self.default_check_time - ): - self.mtime[key] = time.time() - self.mint[key] = 0 - return True - return False - - -class DailyNumberLimiter: - """ - 每日调用命令次数限制 - """ - - tz = pytz.timezone("Asia/Shanghai") - - def __init__(self, max_num): - self.today = -1 - self.count = defaultdict(int) - self.max = max_num - - def check(self, key) -> bool: - day = datetime.now(self.tz).day - if day != self.today: - self.today = day - self.count.clear() - return bool(self.count[key] < self.max) - - def get_num(self, key): - return self.count[key] - - def increase(self, key, num=1): - self.count[key] += num - - def reset(self, key): - self.count[key] = 0 - - -def is_number(s: Union[int, str]) -> bool: - """ - 说明: - 检测 s 是否为数字 - 参数: - :param s: 文本 - """ - if isinstance(s, int): - return True - try: - float(s) - return True - except ValueError: - pass - try: - import unicodedata - - unicodedata.numeric(s) - return True - except (TypeError, ValueError): - pass - return False - - -def get_bot(id_: Optional[str] = None) -> Optional[Bot]: - """ - 说明: - 获取 bot 对象 - """ - try: - return nonebot.get_bot(id_) - except ValueError: - return None - - -def get_matchers(distinct: bool = False) -> List[Type[Matcher]]: - """ - 说明: - 获取所有matcher - 参数: - distinct: 去重 - """ - _matchers = [] - temp = [] - for i in matchers.keys(): - for matcher in matchers[i]: - if distinct and matcher.plugin_name in temp: - continue - temp.append(matcher.plugin_name) - _matchers.append(matcher) - return _matchers - - -def get_message_at(data: Union[str, Message]) -> List[int]: - """ - 说明: - 获取消息中所有的 at 对象的 qq - 参数: - :param data: event.json(), event.message - """ - qq_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg and msg.get("type") == "at": - qq_list.append(int(msg["data"]["qq"])) - else: - for seg in data: - if seg.type == "at": - qq_list.append(seg.data["qq"]) - return qq_list - - -def get_message_img(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 图片 的链接 - 参数: - :param data: event.json() - """ - img_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "image": - img_list.append(msg["data"]["url"]) - else: - for seg in data["image"]: - img_list.append(seg.data["url"]) - return img_list - - -def get_message_face(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 face Id - 参数: - :param data: event.json() - """ - face_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "face": - face_list.append(msg["data"]["id"]) - else: - for seg in data["face"]: - face_list.append(seg.data["id"]) - return face_list - - -def get_message_img_file(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有的 图片file - 参数: - :param data: event.json() - """ - file_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "image": - file_list.append(msg["data"]["file"]) - else: - for seg in data["image"]: - file_list.append(seg.data["file"]) - return file_list - - -def get_message_text(data: Union[str, Message]) -> str: - """ - 说明: - 获取消息中 纯文本 的信息 - 参数: - :param data: event.json() - """ - result = "" - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - if isinstance(message, str): - return message.strip() - for msg in message: - if msg["type"] == "text": - result += msg["data"]["text"].strip() + " " - return result.strip() - else: - for seg in data["text"]: - result += seg.data["text"] + " " - return result.strip() - - -def get_message_record(data: Union[str, Message]) -> List[str]: - """ - 说明: - 获取消息中所有 语音 的链接 - 参数: - :param data: event.json() - """ - record_list = [] - if isinstance(data, str): - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "record": - record_list.append(msg["data"]["url"]) - else: - for seg in data["record"]: - record_list.append(seg.data["url"]) - return record_list - - -def get_message_json(data: str) -> List[dict]: - """ - 说明: - 获取消息中所有 json - 参数: - :param data: event.json() - """ - try: - json_list = [] - event = json.loads(data) - if data and (message := event.get("message")): - for msg in message: - if msg["type"] == "json": - json_list.append(msg["data"]) - return json_list - except KeyError: - return [] - - -def get_local_proxy() -> Optional[str]: - """ - 说明: - 获取 config.py 中设置的代理 - """ - return SYSTEM_PROXY or None - - -def is_chinese(word: str) -> bool: - """ - 说明: - 判断字符串是否为纯中文 - 参数: - :param word: 文本 - """ - for ch in word: - if not "\u4e00" <= ch <= "\u9fff": - return False - return True - - -async def get_user_avatar(qq: Union[int, str]) -> Optional[bytes]: - """ - 说明: - 快捷获取用户头像 - 参数: - :param qq: qq号 - """ - url = f"http://q1.qlogo.cn/g?b=qq&nk={qq}&s=160" - async with httpx.AsyncClient() as client: - for _ in range(3): - try: - return (await client.get(url)).content - except Exception as e: - logger.error("获取用户头像错误", "Util", target=qq) - return None - - -async def get_group_avatar(group_id: int) -> Optional[bytes]: - """ - 说明: - 快捷获取用群头像 - 参数: - :param group_id: 群号 - """ - url = f"http://p.qlogo.cn/gh/{group_id}/{group_id}/640/" - async with httpx.AsyncClient() as client: - for _ in range(3): - try: - return (await client.get(url)).content - except Exception as e: - logger.error("获取群头像错误", "Util", target=group_id) - return None - - -def cn2py(word: str) -> str: - """ - 说明: - 将字符串转化为拼音 - 参数: - :param word: 文本 - """ - temp = "" - for i in pypinyin.pinyin(word, style=pypinyin.NORMAL): - temp += "".join(i) - return temp - - -def change_pixiv_image_links( - url: str, size: Optional[str] = None, nginx_url: Optional[str] = None -): - """ - 说明: - 根据配置改变图片大小和反代链接 - 参数: - :param url: 图片原图链接 - :param size: 模式 - :param nginx_url: 反代 - """ - if size == "master": - img_sp = url.rsplit(".", maxsplit=1) - url = img_sp[0] - img_type = img_sp[1] - url = url.replace("original", "master") + f"_master1200.{img_type}" - if not nginx_url: - nginx_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") - if nginx_url: - url = ( - url.replace("i.pximg.net", nginx_url) - .replace("i.pixiv.cat", nginx_url) - .replace("_webp", "") - ) - return url - - -def change_img_md5(path_file: Union[str, Path]) -> bool: - """ - 说明: - 改变图片MD5 - 参数: - :param path_file: 图片路径 - """ - try: - with open(path_file, "a") as f: - f.write(str(int(time.time() * 1000))) - return True - except Exception as e: - logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e) - return False - - -async def broadcast_group( - message: Union[str, Message, MessageSegment], - bot: Optional[Union[Bot, List[Bot]]] = None, - bot_id: Optional[Union[str, Set[str]]] = None, - ignore_group: Optional[Set[int]] = None, - check_func: Optional[Callable[[int], bool]] = None, - log_cmd: Optional[str] = None, -): - """获取所有Bot或指定Bot对象广播群聊 - - Args: - message (Any): 广播消息内容 - bot (Optional[Bot], optional): 指定bot对象. Defaults to None. - bot_id (Optional[str], optional): 指定bot id. Defaults to None. - ignore_group (Optional[List[int]], optional): 忽略群聊列表. Defaults to None. - check_func (Optional[Callable[[int], bool]], optional): 发送前对群聊检测方法,判断是否发送. Defaults to None. - log_cmd (Optional[str], optional): 日志标记. Defaults to None. - """ - if not message: - raise ValueError("群聊广播消息不能为空") - bot_dict = nonebot.get_bots() - bot_list: List[Bot] = [] - if bot: - if isinstance(bot, list): - bot_list = bot - else: - bot_list.append(bot) - elif bot_id: - _bot_id_list = bot_id - if isinstance(bot_id, str): - _bot_id_list = [bot_id] - for id_ in _bot_id_list: - if bot_id in bot_dict: - bot_list.append(bot_dict[bot_id]) - else: - logger.warning(f"Bot:{id_} 对象未连接或不存在") - else: - bot_list = list(bot_dict.values()) - _used_group = [] - for _bot in bot_list: - try: - if _group_list := await _bot.get_group_list(): - group_id_list = [g["group_id"] for g in _group_list] - for group_id in set(group_id_list): - try: - if ( - ignore_group and group_id in ignore_group - ) or group_id in _used_group: - continue - if check_func and not check_func(group_id): - continue - _used_group.append(group_id) - await _bot.send_group_msg(group_id=group_id, message=message) - except Exception as e: - logger.error( - f"广播群发消息失败: {message}", - command=log_cmd, - group_id=group_id, - e=e, - ) - except Exception as e: - logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) - - -async def broadcast_superuser( - message: Union[str, Message, MessageSegment], - bot: Optional[Union[Bot, List[Bot]]] = None, - bot_id: Optional[Union[str, Set[str]]] = None, - ignore_superuser: Optional[Set[int]] = None, - check_func: Optional[Callable[[int], bool]] = None, - log_cmd: Optional[str] = None, -): - """获取所有Bot或指定Bot对象广播超级用户 - - Args: - message (Any): 广播消息内容 - bot (Optional[Bot], optional): 指定bot对象. Defaults to None. - bot_id (Optional[str], optional): 指定bot id. Defaults to None. - ignore_superuser (Optional[List[int]], optional): 忽略的超级用户id. Defaults to None. - check_func (Optional[Callable[[int], bool]], optional): 发送前对群聊检测方法,判断是否发送. Defaults to None. - log_cmd (Optional[str], optional): 日志标记. Defaults to None. - """ - if not message: - raise ValueError("超级用户广播消息不能为空") - bot_dict = nonebot.get_bots() - bot_list: List[Bot] = [] - if bot: - if isinstance(bot, list): - bot_list = bot - else: - bot_list.append(bot) - elif bot_id: - _bot_id_list = bot_id - if isinstance(bot_id, str): - _bot_id_list = [bot_id] - for id_ in _bot_id_list: - if bot_id in bot_dict: - bot_list.append(bot_dict[bot_id]) - else: - logger.warning(f"Bot:{id_} 对象未连接或不存在") - else: - bot_list = list(bot_dict.values()) - _used_user = [] - for _bot in bot_list: - try: - for user_id in _bot.config.superusers: - try: - if ( - ignore_superuser and int(user_id) in ignore_superuser - ) or user_id in _used_user: - continue - if check_func and not check_func(int(user_id)): - continue - _used_user.append(user_id) - await _bot.send_private_message( - user_id=int(user_id), message=message - ) - except Exception as e: - logger.error( - f"广播超级用户发消息失败: {message}", - command=log_cmd, - user_id=user_id, - e=e, - ) - except Exception as e: - logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) diff --git a/utils/zh_wiki.py b/utils/zh_wiki.py deleted file mode 100755 index 4f5720f7..00000000 --- a/utils/zh_wiki.py +++ /dev/null @@ -1,8275 +0,0 @@ -# -*- coding: utf-8 -*- -# copy fom wikipedia - -zh2Hant = { -'呆': '獃', -"打印机": "印表機", -'帮助文件': '說明檔案', -"画": "畫", -"龙": "竜", -"板": "板", -"表": "表", -"才": "才", -"丑": "醜", -"出": "出", -"淀": "澱", -"冬": "冬", -"范": "範", -"丰": "豐", -"刮": "刮", -"后": "後", -"胡": "胡", -"回": "回", -"伙": "夥", -"姜": "薑", -"借": "借", -"克": "克", -"困": "困", -"漓": "漓", -"里": "里", -"帘": "簾", -"霉": "霉", -"面": "面", -"蔑": "蔑", -"千": "千", -"秋": "秋", -"松": "松", -"咸": "咸", -"向": "向", -"余": "餘", -"郁": "鬱", -"御": "御", -"愿": "願", -"云": "雲", -"芸": "芸", -"沄": "沄", -"致": "致", -"制": "制", -"朱": "朱", -"筑": "築", -"准": "準", -"厂": "廠", -"广": "廣", -"辟": "闢", -"别": "別", -"卜": "卜", -"沈": "沈", -"冲": "沖", -"种": "種", -"虫": "蟲", -"担": "擔", -"党": "黨", -"斗": "鬥", -"儿": "兒", -"干": "乾", -"谷": "谷", -"柜": "櫃", -"合": "合", -"划": "劃", -"坏": "壞", -"几": "幾", -"系": "系", -"家": "家", -"价": "價", -"据": "據", -"卷": "捲", -"适": "適", -"蜡": "蠟", -"腊": "臘", -"了": "了", -"累": "累", -"么": "麽", -"蒙": "蒙", -"万": "萬", -"宁": "寧", -"朴": "樸", -"苹": "蘋", -"仆": "僕", -"曲": "曲", -"确": "確", -"舍": "舍", -"胜": "勝", -"术": "術", -"台": "台", -"体": "體", -"涂": "塗", -"叶": "葉", -"吁": "吁", -"旋": "旋", -"佣": "傭", -"与": "與", -"折": "折", -"征": "徵", -"症": "症", -"恶": "惡", -"发": "發", -"复": "復", -"汇": "匯", -"获": "獲", -"饥": "飢", -"尽": "盡", -"历": "歷", -"卤": "滷", -"弥": "彌", -"签": "簽", -"纤": "纖", -"苏": "蘇", -"坛": "壇", -"团": "團", -"须": "須", -"脏": "臟", -"只": "只", -"钟": "鐘", -"药": "藥", -"同": "同", -"志": "志", -"杯": "杯", -"岳": "岳", -"布": "布", -"当": "當", -"吊": "弔", -"仇": "仇", -"蕴": "蘊", -"线": "線", -"为": "為", -"产": "產", -"众": "眾", -"伪": "偽", -"凫": "鳧", -"厕": "廁", -"启": "啟", -"墙": "牆", -"壳": "殼", -"奖": "獎", -"妫": "媯", -"并": "並", -"录": "錄", -"悫": "愨", -"极": "極", -"沩": "溈", -"瘘": "瘺", -"硷": "鹼", -"竖": "豎", -"绝": "絕", -"绣": "繡", -"绦": "絛", -"绱": "緔", -"绷": "綳", -"绿": "綠", -"缰": "韁", -"苧": "苎", -"莼": "蒓", -"说": "說", -"谣": "謠", -"谫": "譾", -"赃": "贓", -"赍": "齎", -"赝": "贗", -"酝": "醞", -"采": "採", -"钩": "鉤", -"钵": "缽", -"锈": "銹", -"锐": "銳", -"锨": "杴", -"镌": "鐫", -"镢": "钁", -"阅": "閱", -"颓": "頹", -"颜": "顏", -"骂": "罵", -"鲇": "鯰", -"鲞": "鯗", -"鳄": "鱷", -"鸡": "雞", -"鹚": "鶿", -"荡": "盪", -"锤": "錘", -"㟆": "㠏", -"㛟": "𡞵", -"专": "專", -"业": "業", -"丛": "叢", -"东": "東", -"丝": "絲", -"丢": "丟", -"两": "兩", -"严": "嚴", -"丧": "喪", -"个": "個", -"临": "臨", -"丽": "麗", -"举": "舉", -"义": "義", -"乌": "烏", -"乐": "樂", -"乔": "喬", -"习": "習", -"乡": "鄉", -"书": "書", -"买": "買", -"乱": "亂", -"争": "爭", -"于": "於", -"亏": "虧", -"亚": "亞", -"亩": "畝", -"亲": "親", -"亵": "褻", -"亸": "嚲", -"亿": "億", -"仅": "僅", -"从": "從", -"仑": "侖", -"仓": "倉", -"仪": "儀", -"们": "們", -"优": "優", -"会": "會", -"伛": "傴", -"伞": "傘", -"伟": "偉", -"传": "傳", -"伣": "俔", -"伤": "傷", -"伥": "倀", -"伦": "倫", -"伧": "傖", -"伫": "佇", -"佥": "僉", -"侠": "俠", -"侣": "侶", -"侥": "僥", -"侦": "偵", -"侧": "側", -"侨": "僑", -"侩": "儈", -"侪": "儕", -"侬": "儂", -"俣": "俁", -"俦": "儔", -"俨": "儼", -"俩": "倆", -"俪": "儷", -"俫": "倈", -"俭": "儉", -"债": "債", -"倾": "傾", -"偬": "傯", -"偻": "僂", -"偾": "僨", -"偿": "償", -"傥": "儻", -"傧": "儐", -"储": "儲", -"傩": "儺", -"㑩": "儸", -"兑": "兌", -"兖": "兗", -"兰": "蘭", -"关": "關", -"兴": "興", -"兹": "茲", -"养": "養", -"兽": "獸", -"冁": "囅", -"内": "內", -"冈": "岡", -"册": "冊", -"写": "寫", -"军": "軍", -"农": "農", -"冯": "馮", -"决": "決", -"况": "況", -"冻": "凍", -"净": "凈", -"凉": "涼", -"减": "減", -"凑": "湊", -"凛": "凜", -"凤": "鳳", -"凭": "憑", -"凯": "凱", -"击": "擊", -"凿": "鑿", -"刍": "芻", -"刘": "劉", -"则": "則", -"刚": "剛", -"创": "創", -"删": "刪", -"刬": "剗", -"刭": "剄", -"刹": "剎", -"刽": "劊", -"刿": "劌", -"剀": "剴", -"剂": "劑", -"剐": "剮", -"剑": "劍", -"剥": "剝", -"剧": "劇", -"㓥": "劏", -"㔉": "劚", -"劝": "勸", -"办": "辦", -"务": "務", -"劢": "勱", -"动": "動", -"励": "勵", -"劲": "勁", -"劳": "勞", -"势": "勢", -"勋": "勛", -"勚": "勩", -"匀": "勻", -"匦": "匭", -"匮": "匱", -"区": "區", -"医": "醫", -"华": "華", -"协": "協", -"单": "單", -"卖": "賣", -"卢": "盧", -"卫": "衛", -"却": "卻", -"厅": "廳", -"厉": "厲", -"压": "壓", -"厌": "厭", -"厍": "厙", -"厐": "龎", -"厘": "釐", -"厢": "廂", -"厣": "厴", -"厦": "廈", -"厨": "廚", -"厩": "廄", -"厮": "廝", -"县": "縣", -"叁": "叄", -"参": "參", -"双": "雙", -"变": "變", -"叙": "敘", -"叠": "疊", -"号": "號", -"叹": "嘆", -"叽": "嘰", -"吓": "嚇", -"吕": "呂", -"吗": "嗎", -"吣": "唚", -"吨": "噸", -"听": "聽", -"吴": "吳", -"呐": "吶", -"呒": "嘸", -"呓": "囈", -"呕": "嘔", -"呖": "嚦", -"呗": "唄", -"员": "員", -"呙": "咼", -"呛": "嗆", -"呜": "嗚", -"咏": "詠", -"咙": "嚨", -"咛": "嚀", -"咝": "噝", -"响": "響", -"哑": "啞", -"哒": "噠", -"哓": "嘵", -"哔": "嗶", -"哕": "噦", -"哗": "嘩", -"哙": "噲", -"哜": "嚌", -"哝": "噥", -"哟": "喲", -"唛": "嘜", -"唝": "嗊", -"唠": "嘮", -"唡": "啢", -"唢": "嗩", -"唤": "喚", -"啧": "嘖", -"啬": "嗇", -"啭": "囀", -"啮": "嚙", -"啴": "嘽", -"啸": "嘯", -"㖞": "喎", -"喷": "噴", -"喽": "嘍", -"喾": "嚳", -"嗫": "囁", -"嗳": "噯", -"嘘": "噓", -"嘤": "嚶", -"嘱": "囑", -"㖊": "噚", -"噜": "嚕", -"嚣": "囂", -"园": "園", -"囱": "囪", -"围": "圍", -"囵": "圇", -"国": "國", -"图": "圖", -"圆": "圓", -"圣": "聖", -"圹": "壙", -"场": "場", -"坂": "阪", -"块": "塊", -"坚": "堅", -"坜": "壢", -"坝": "壩", -"坞": "塢", -"坟": "墳", -"坠": "墜", -"垄": "壟", -"垅": "壠", -"垆": "壚", -"垒": "壘", -"垦": "墾", -"垩": "堊", -"垫": "墊", -"垭": "埡", -"垱": "壋", -"垲": "塏", -"垴": "堖", -"埘": "塒", -"埙": "塤", -"埚": "堝", -"埯": "垵", -"堑": "塹", -"堕": "墮", -"𡒄": "壈", -"壮": "壯", -"声": "聲", -"壶": "壺", -"壸": "壼", -"处": "處", -"备": "備", -"够": "夠", -"头": "頭", -"夸": "誇", -"夹": "夾", -"夺": "奪", -"奁": "奩", -"奂": "奐", -"奋": "奮", -"奥": "奧", -"奸": "姦", -"妆": "妝", -"妇": "婦", -"妈": "媽", -"妩": "嫵", -"妪": "嫗", -"姗": "姍", -"姹": "奼", -"娄": "婁", -"娅": "婭", -"娆": "嬈", -"娇": "嬌", -"娈": "孌", -"娱": "娛", -"娲": "媧", -"娴": "嫻", -"婳": "嫿", -"婴": "嬰", -"婵": "嬋", -"婶": "嬸", -"媪": "媼", -"嫒": "嬡", -"嫔": "嬪", -"嫱": "嬙", -"嬷": "嬤", -"孙": "孫", -"学": "學", -"孪": "孿", -"宝": "寶", -"实": "實", -"宠": "寵", -"审": "審", -"宪": "憲", -"宫": "宮", -"宽": "寬", -"宾": "賓", -"寝": "寢", -"对": "對", -"寻": "尋", -"导": "導", -"寿": "壽", -"将": "將", -"尔": "爾", -"尘": "塵", -"尝": "嘗", -"尧": "堯", -"尴": "尷", -"尸": "屍", -"层": "層", -"屃": "屓", -"屉": "屜", -"届": "屆", -"属": "屬", -"屡": "屢", -"屦": "屨", -"屿": "嶼", -"岁": "歲", -"岂": "豈", -"岖": "嶇", -"岗": "崗", -"岘": "峴", -"岙": "嶴", -"岚": "嵐", -"岛": "島", -"岭": "嶺", -"岽": "崬", -"岿": "巋", -"峄": "嶧", -"峡": "峽", -"峣": "嶢", -"峤": "嶠", -"峥": "崢", -"峦": "巒", -"崂": "嶗", -"崃": "崍", -"崄": "嶮", -"崭": "嶄", -"嵘": "嶸", -"嵚": "嶔", -"嵝": "嶁", -"巅": "巔", -"巩": "鞏", -"巯": "巰", -"币": "幣", -"帅": "帥", -"师": "師", -"帏": "幃", -"帐": "帳", -"帜": "幟", -"带": "帶", -"帧": "幀", -"帮": "幫", -"帱": "幬", -"帻": "幘", -"帼": "幗", -"幂": "冪", -"庄": "莊", -"庆": "慶", -"庐": "廬", -"庑": "廡", -"库": "庫", -"应": "應", -"庙": "廟", -"庞": "龐", -"废": "廢", -"廪": "廩", -"开": "開", -"异": "異", -"弃": "棄", -"弑": "弒", -"张": "張", -"弪": "弳", -"弯": "彎", -"弹": "彈", -"强": "強", -"归": "歸", -"彝": "彞", -"彦": "彥", -"彻": "徹", -"径": "徑", -"徕": "徠", -"忆": "憶", -"忏": "懺", -"忧": "憂", -"忾": "愾", -"怀": "懷", -"态": "態", -"怂": "慫", -"怃": "憮", -"怄": "慪", -"怅": "悵", -"怆": "愴", -"怜": "憐", -"总": "總", -"怼": "懟", -"怿": "懌", -"恋": "戀", -"恒": "恆", -"恳": "懇", -"恸": "慟", -"恹": "懨", -"恺": "愷", -"恻": "惻", -"恼": "惱", -"恽": "惲", -"悦": "悅", -"悬": "懸", -"悭": "慳", -"悮": "悞", -"悯": "憫", -"惊": "驚", -"惧": "懼", -"惨": "慘", -"惩": "懲", -"惫": "憊", -"惬": "愜", -"惭": "慚", -"惮": "憚", -"惯": "慣", -"愠": "慍", -"愤": "憤", -"愦": "憒", -"慑": "懾", -"懑": "懣", -"懒": "懶", -"懔": "懍", -"戆": "戇", -"戋": "戔", -"戏": "戲", -"戗": "戧", -"战": "戰", -"戬": "戩", -"戯": "戱", -"户": "戶", -"扑": "撲", -"执": "執", -"扩": "擴", -"扪": "捫", -"扫": "掃", -"扬": "揚", -"扰": "擾", -"抚": "撫", -"抛": "拋", -"抟": "摶", -"抠": "摳", -"抡": "掄", -"抢": "搶", -"护": "護", -"报": "報", -"拟": "擬", -"拢": "攏", -"拣": "揀", -"拥": "擁", -"拦": "攔", -"拧": "擰", -"拨": "撥", -"择": "擇", -"挂": "掛", -"挚": "摯", -"挛": "攣", -"挜": "掗", -"挝": "撾", -"挞": "撻", -"挟": "挾", -"挠": "撓", -"挡": "擋", -"挢": "撟", -"挣": "掙", -"挤": "擠", -"挥": "揮", -"挦": "撏", -"挽": "輓", -"捝": "挩", -"捞": "撈", -"损": "損", -"捡": "撿", -"换": "換", -"捣": "搗", -"掳": "擄", -"掴": "摑", -"掷": "擲", -"掸": "撣", -"掺": "摻", -"掼": "摜", -"揽": "攬", -"揾": "搵", -"揿": "撳", -"搀": "攙", -"搁": "擱", -"搂": "摟", -"搅": "攪", -"携": "攜", -"摄": "攝", -"摅": "攄", -"摆": "擺", -"摇": "搖", -"摈": "擯", -"摊": "攤", -"撄": "攖", -"撑": "撐", -"㧑": "撝", -"撵": "攆", -"撷": "擷", -"撸": "擼", -"撺": "攛", -"㧟": "擓", -"擞": "擻", -"攒": "攢", -"敌": "敵", -"敛": "斂", -"数": "數", -"斋": "齋", -"斓": "斕", -"斩": "斬", -"断": "斷", -"无": "無", -"旧": "舊", -"时": "時", -"旷": "曠", -"旸": "暘", -"昙": "曇", -"昼": "晝", -"昽": "曨", -"显": "顯", -"晋": "晉", -"晒": "曬", -"晓": "曉", -"晔": "曄", -"晕": "暈", -"晖": "暉", -"暂": "暫", -"暧": "曖", -"机": "機", -"杀": "殺", -"杂": "雜", -"权": "權", -"杆": "桿", -"条": "條", -"来": "來", -"杨": "楊", -"杩": "榪", -"杰": "傑", -"构": "構", -"枞": "樅", -"枢": "樞", -"枣": "棗", -"枥": "櫪", -"枧": "梘", -"枨": "棖", -"枪": "槍", -"枫": "楓", -"枭": "梟", -"柠": "檸", -"柽": "檉", -"栀": "梔", -"栅": "柵", -"标": "標", -"栈": "棧", -"栉": "櫛", -"栊": "櫳", -"栋": "棟", -"栌": "櫨", -"栎": "櫟", -"栏": "欄", -"树": "樹", -"栖": "棲", -"栗": "慄", -"样": "樣", -"栾": "欒", -"桠": "椏", -"桡": "橈", -"桢": "楨", -"档": "檔", -"桤": "榿", -"桥": "橋", -"桦": "樺", -"桧": "檜", -"桨": "槳", -"桩": "樁", -"梦": "夢", -"梼": "檮", -"梾": "棶", -"梿": "槤", -"检": "檢", -"棁": "梲", -"棂": "欞", -"椁": "槨", -"椟": "櫝", -"椠": "槧", -"椤": "欏", -"椭": "橢", -"楼": "樓", -"榄": "欖", -"榅": "榲", -"榇": "櫬", -"榈": "櫚", -"榉": "櫸", -"槚": "檟", -"槛": "檻", -"槟": "檳", -"槠": "櫧", -"横": "橫", -"樯": "檣", -"樱": "櫻", -"橥": "櫫", -"橱": "櫥", -"橹": "櫓", -"橼": "櫞", -"檩": "檁", -"欢": "歡", -"欤": "歟", -"欧": "歐", -"歼": "殲", -"殁": "歿", -"殇": "殤", -"残": "殘", -"殒": "殞", -"殓": "殮", -"殚": "殫", -"殡": "殯", -"㱮": "殨", -"㱩": "殰", -"殴": "毆", -"毁": "毀", -"毂": "轂", -"毕": "畢", -"毙": "斃", -"毡": "氈", -"毵": "毿", -"氇": "氌", -"气": "氣", -"氢": "氫", -"氩": "氬", -"氲": "氳", -"汉": "漢", -"汤": "湯", -"汹": "洶", -"沟": "溝", -"没": "沒", -"沣": "灃", -"沤": "漚", -"沥": "瀝", -"沦": "淪", -"沧": "滄", -"沪": "滬", -"泞": "濘", -"注": "註", -"泪": "淚", -"泶": "澩", -"泷": "瀧", -"泸": "瀘", -"泺": "濼", -"泻": "瀉", -"泼": "潑", -"泽": "澤", -"泾": "涇", -"洁": "潔", -"洒": "灑", -"洼": "窪", -"浃": "浹", -"浅": "淺", -"浆": "漿", -"浇": "澆", -"浈": "湞", -"浊": "濁", -"测": "測", -"浍": "澮", -"济": "濟", -"浏": "瀏", -"浐": "滻", -"浑": "渾", -"浒": "滸", -"浓": "濃", -"浔": "潯", -"涛": "濤", -"涝": "澇", -"涞": "淶", -"涟": "漣", -"涠": "潿", -"涡": "渦", -"涣": "渙", -"涤": "滌", -"润": "潤", -"涧": "澗", -"涨": "漲", -"涩": "澀", -"渊": "淵", -"渌": "淥", -"渍": "漬", -"渎": "瀆", -"渐": "漸", -"渑": "澠", -"渔": "漁", -"渖": "瀋", -"渗": "滲", -"温": "溫", -"湾": "灣", -"湿": "濕", -"溃": "潰", -"溅": "濺", -"溆": "漵", -"滗": "潷", -"滚": "滾", -"滞": "滯", -"滟": "灧", -"滠": "灄", -"满": "滿", -"滢": "瀅", -"滤": "濾", -"滥": "濫", -"滦": "灤", -"滨": "濱", -"滩": "灘", -"滪": "澦", -"漤": "灠", -"潆": "瀠", -"潇": "瀟", -"潋": "瀲", -"潍": "濰", -"潜": "潛", -"潴": "瀦", -"澜": "瀾", -"濑": "瀨", -"濒": "瀕", -"㲿": "瀇", -"灏": "灝", -"灭": "滅", -"灯": "燈", -"灵": "靈", -"灶": "竈", -"灾": "災", -"灿": "燦", -"炀": "煬", -"炉": "爐", -"炖": "燉", -"炜": "煒", -"炝": "熗", -"点": "點", -"炼": "煉", -"炽": "熾", -"烁": "爍", -"烂": "爛", -"烃": "烴", -"烛": "燭", -"烟": "煙", -"烦": "煩", -"烧": "燒", -"烨": "燁", -"烩": "燴", -"烫": "燙", -"烬": "燼", -"热": "熱", -"焕": "煥", -"焖": "燜", -"焘": "燾", -"㶽": "煱", -"煴": "熅", -"㶶": "燶", -"爱": "愛", -"爷": "爺", -"牍": "牘", -"牦": "氂", -"牵": "牽", -"牺": "犧", -"犊": "犢", -"状": "狀", -"犷": "獷", -"犸": "獁", -"犹": "猶", -"狈": "狽", -"狝": "獮", -"狞": "獰", -"独": "獨", -"狭": "狹", -"狮": "獅", -"狯": "獪", -"狰": "猙", -"狱": "獄", -"狲": "猻", -"猃": "獫", -"猎": "獵", -"猕": "獼", -"猡": "玀", -"猪": "豬", -"猫": "貓", -"猬": "蝟", -"献": "獻", -"獭": "獺", -"㺍": "獱", -"玑": "璣", -"玚": "瑒", -"玛": "瑪", -"玮": "瑋", -"环": "環", -"现": "現", -"玱": "瑲", -"玺": "璽", -"珐": "琺", -"珑": "瓏", -"珰": "璫", -"珲": "琿", -"琏": "璉", -"琐": "瑣", -"琼": "瓊", -"瑶": "瑤", -"瑷": "璦", -"璎": "瓔", -"瓒": "瓚", -"瓯": "甌", -"电": "電", -"画": "畫", -"畅": "暢", -"畴": "疇", -"疖": "癤", -"疗": "療", -"疟": "瘧", -"疠": "癘", -"疡": "瘍", -"疬": "癧", -"疭": "瘲", -"疮": "瘡", -"疯": "瘋", -"疱": "皰", -"疴": "痾", -"痈": "癰", -"痉": "痙", -"痒": "癢", -"痖": "瘂", -"痨": "癆", -"痪": "瘓", -"痫": "癇", -"瘅": "癉", -"瘆": "瘮", -"瘗": "瘞", -"瘪": "癟", -"瘫": "癱", -"瘾": "癮", -"瘿": "癭", -"癞": "癩", -"癣": "癬", -"癫": "癲", -"皑": "皚", -"皱": "皺", -"皲": "皸", -"盏": "盞", -"盐": "鹽", -"监": "監", -"盖": "蓋", -"盗": "盜", -"盘": "盤", -"眍": "瞘", -"眦": "眥", -"眬": "矓", -"睁": "睜", -"睐": "睞", -"睑": "瞼", -"瞆": "瞶", -"瞒": "瞞", -"䁖": "瞜", -"瞩": "矚", -"矫": "矯", -"矶": "磯", -"矾": "礬", -"矿": "礦", -"砀": "碭", -"码": "碼", -"砖": "磚", -"砗": "硨", -"砚": "硯", -"砜": "碸", -"砺": "礪", -"砻": "礱", -"砾": "礫", -"础": "礎", -"硁": "硜", -"硕": "碩", -"硖": "硤", -"硗": "磽", -"硙": "磑", -"碍": "礙", -"碛": "磧", -"碜": "磣", -"碱": "鹼", -"礼": "禮", -"祃": "禡", -"祎": "禕", -"祢": "禰", -"祯": "禎", -"祷": "禱", -"祸": "禍", -"禀": "稟", -"禄": "祿", -"禅": "禪", -"离": "離", -"秃": "禿", -"秆": "稈", -"积": "積", -"称": "稱", -"秽": "穢", -"秾": "穠", -"稆": "穭", -"税": "稅", -"䅉": "稏", -"稣": "穌", -"稳": "穩", -"穑": "穡", -"穷": "窮", -"窃": "竊", -"窍": "竅", -"窎": "窵", -"窑": "窯", -"窜": "竄", -"窝": "窩", -"窥": "窺", -"窦": "竇", -"窭": "窶", -"竞": "競", -"笃": "篤", -"笋": "筍", -"笔": "筆", -"笕": "筧", -"笺": "箋", -"笼": "籠", -"笾": "籩", -"筚": "篳", -"筛": "篩", -"筜": "簹", -"筝": "箏", -"䇲": "筴", -"筹": "籌", -"筼": "篔", -"简": "簡", -"箓": "籙", -"箦": "簀", -"箧": "篋", -"箨": "籜", -"箩": "籮", -"箪": "簞", -"箫": "簫", -"篑": "簣", -"篓": "簍", -"篮": "籃", -"篱": "籬", -"簖": "籪", -"籁": "籟", -"籴": "糴", -"类": "類", -"籼": "秈", -"粜": "糶", -"粝": "糲", -"粤": "粵", -"粪": "糞", -"粮": "糧", -"糁": "糝", -"糇": "餱", -"紧": "緊", -"䌷": "紬", -"䌹": "絅", -"絷": "縶", -"䌼": "綐", -"䌽": "綵", -"䌸": "縳", -"䍁": "繸", -"䍀": "繿", -"纟": "糹", -"纠": "糾", -"纡": "紆", -"红": "紅", -"纣": "紂", -"纥": "紇", -"约": "約", -"级": "級", -"纨": "紈", -"纩": "纊", -"纪": "紀", -"纫": "紉", -"纬": "緯", -"纭": "紜", -"纮": "紘", -"纯": "純", -"纰": "紕", -"纱": "紗", -"纲": "綱", -"纳": "納", -"纴": "紝", -"纵": "縱", -"纶": "綸", -"纷": "紛", -"纸": "紙", -"纹": "紋", -"纺": "紡", -"纻": "紵", -"纼": "紖", -"纽": "紐", -"纾": "紓", -"绀": "紺", -"绁": "紲", -"绂": "紱", -"练": "練", -"组": "組", -"绅": "紳", -"细": "細", -"织": "織", -"终": "終", -"绉": "縐", -"绊": "絆", -"绋": "紼", -"绌": "絀", -"绍": "紹", -"绎": "繹", -"经": "經", -"绐": "紿", -"绑": "綁", -"绒": "絨", -"结": "結", -"绔": "絝", -"绕": "繞", -"绖": "絰", -"绗": "絎", -"绘": "繪", -"给": "給", -"绚": "絢", -"绛": "絳", -"络": "絡", -"绞": "絞", -"统": "統", -"绠": "綆", -"绡": "綃", -"绢": "絹", -"绤": "綌", -"绥": "綏", -"继": "繼", -"绨": "綈", -"绩": "績", -"绪": "緒", -"绫": "綾", -"绬": "緓", -"续": "續", -"绮": "綺", -"绯": "緋", -"绰": "綽", -"绲": "緄", -"绳": "繩", -"维": "維", -"绵": "綿", -"绶": "綬", -"绸": "綢", -"绹": "綯", -"绺": "綹", -"绻": "綣", -"综": "綜", -"绽": "綻", -"绾": "綰", -"缀": "綴", -"缁": "緇", -"缂": "緙", -"缃": "緗", -"缄": "緘", -"缅": "緬", -"缆": "纜", -"缇": "緹", -"缈": "緲", -"缉": "緝", -"缊": "縕", -"缋": "繢", -"缌": "緦", -"缍": "綞", -"缎": "緞", -"缏": "緶", -"缑": "緱", -"缒": "縋", -"缓": "緩", -"缔": "締", -"缕": "縷", -"编": "編", -"缗": "緡", -"缘": "緣", -"缙": "縉", -"缚": "縛", -"缛": "縟", -"缜": "縝", -"缝": "縫", -"缞": "縗", -"缟": "縞", -"缠": "纏", -"缡": "縭", -"缢": "縊", -"缣": "縑", -"缤": "繽", -"缥": "縹", -"缦": "縵", -"缧": "縲", -"缨": "纓", -"缩": "縮", -"缪": "繆", -"缫": "繅", -"缬": "纈", -"缭": "繚", -"缮": "繕", -"缯": "繒", -"缱": "繾", -"缲": "繰", -"缳": "繯", -"缴": "繳", -"缵": "纘", -"罂": "罌", -"网": "網", -"罗": "羅", -"罚": "罰", -"罢": "罷", -"罴": "羆", -"羁": "羈", -"羟": "羥", -"翘": "翹", -"耢": "耮", -"耧": "耬", -"耸": "聳", -"耻": "恥", -"聂": "聶", -"聋": "聾", -"职": "職", -"聍": "聹", -"联": "聯", -"聩": "聵", -"聪": "聰", -"肃": "肅", -"肠": "腸", -"肤": "膚", -"肮": "骯", -"肴": "餚", -"肾": "腎", -"肿": "腫", -"胀": "脹", -"胁": "脅", -"胆": "膽", -"胧": "朧", -"胨": "腖", -"胪": "臚", -"胫": "脛", -"胶": "膠", -"脉": "脈", -"脍": "膾", -"脐": "臍", -"脑": "腦", -"脓": "膿", -"脔": "臠", -"脚": "腳", -"脱": "脫", -"脶": "腡", -"脸": "臉", -"腭": "齶", -"腻": "膩", -"腼": "靦", -"腽": "膃", -"腾": "騰", -"膑": "臏", -"臜": "臢", -"舆": "輿", -"舣": "艤", -"舰": "艦", -"舱": "艙", -"舻": "艫", -"艰": "艱", -"艳": "艷", -"艺": "藝", -"节": "節", -"芈": "羋", -"芗": "薌", -"芜": "蕪", -"芦": "蘆", -"苁": "蓯", -"苇": "葦", -"苈": "藶", -"苋": "莧", -"苌": "萇", -"苍": "蒼", -"苎": "苧", -"茎": "莖", -"茏": "蘢", -"茑": "蔦", -"茔": "塋", -"茕": "煢", -"茧": "繭", -"荆": "荊", -"荐": "薦", -"荙": "薘", -"荚": "莢", -"荛": "蕘", -"荜": "蓽", -"荞": "蕎", -"荟": "薈", -"荠": "薺", -"荣": "榮", -"荤": "葷", -"荥": "滎", -"荦": "犖", -"荧": "熒", -"荨": "蕁", -"荩": "藎", -"荪": "蓀", -"荫": "蔭", -"荬": "蕒", -"荭": "葒", -"荮": "葤", -"莅": "蒞", -"莱": "萊", -"莲": "蓮", -"莳": "蒔", -"莴": "萵", -"莶": "薟", -"莸": "蕕", -"莹": "瑩", -"莺": "鶯", -"萝": "蘿", -"萤": "螢", -"营": "營", -"萦": "縈", -"萧": "蕭", -"萨": "薩", -"葱": "蔥", -"蒇": "蕆", -"蒉": "蕢", -"蒋": "蔣", -"蒌": "蔞", -"蓝": "藍", -"蓟": "薊", -"蓠": "蘺", -"蓣": "蕷", -"蓥": "鎣", -"蓦": "驀", -"蔂": "虆", -"蔷": "薔", -"蔹": "蘞", -"蔺": "藺", -"蔼": "藹", -"蕰": "薀", -"蕲": "蘄", -"薮": "藪", -"䓕": "薳", -"藓": "蘚", -"蘖": "櫱", -"虏": "虜", -"虑": "慮", -"虚": "虛", -"虬": "虯", -"虮": "蟣", -"虽": "雖", -"虾": "蝦", -"虿": "蠆", -"蚀": "蝕", -"蚁": "蟻", -"蚂": "螞", -"蚕": "蠶", -"蚬": "蜆", -"蛊": "蠱", -"蛎": "蠣", -"蛏": "蟶", -"蛮": "蠻", -"蛰": "蟄", -"蛱": "蛺", -"蛲": "蟯", -"蛳": "螄", -"蛴": "蠐", -"蜕": "蛻", -"蜗": "蝸", -"蝇": "蠅", -"蝈": "蟈", -"蝉": "蟬", -"蝼": "螻", -"蝾": "蠑", -"螀": "螿", -"螨": "蟎", -"䗖": "螮", -"蟏": "蠨", -"衅": "釁", -"衔": "銜", -"补": "補", -"衬": "襯", -"衮": "袞", -"袄": "襖", -"袅": "裊", -"袆": "褘", -"袜": "襪", -"袭": "襲", -"袯": "襏", -"装": "裝", -"裆": "襠", -"裈": "褌", -"裢": "褳", -"裣": "襝", -"裤": "褲", -"裥": "襇", -"褛": "褸", -"褴": "襤", -"䙓": "襬", -"见": "見", -"观": "觀", -"觃": "覎", -"规": "規", -"觅": "覓", -"视": "視", -"觇": "覘", -"览": "覽", -"觉": "覺", -"觊": "覬", -"觋": "覡", -"觌": "覿", -"觍": "覥", -"觎": "覦", -"觏": "覯", -"觐": "覲", -"觑": "覷", -"觞": "觴", -"触": "觸", -"觯": "觶", -"訚": "誾", -"䜣": "訢", -"誉": "譽", -"誊": "謄", -"䜧": "譅", -"讠": "訁", -"计": "計", -"订": "訂", -"讣": "訃", -"认": "認", -"讥": "譏", -"讦": "訐", -"讧": "訌", -"讨": "討", -"让": "讓", -"讪": "訕", -"讫": "訖", -"讬": "託", -"训": "訓", -"议": "議", -"讯": "訊", -"记": "記", -"讱": "訒", -"讲": "講", -"讳": "諱", -"讴": "謳", -"讵": "詎", -"讶": "訝", -"讷": "訥", -"许": "許", -"讹": "訛", -"论": "論", -"讻": "訩", -"讼": "訟", -"讽": "諷", -"设": "設", -"访": "訪", -"诀": "訣", -"证": "證", -"诂": "詁", -"诃": "訶", -"评": "評", -"诅": "詛", -"识": "識", -"诇": "詗", -"诈": "詐", -"诉": "訴", -"诊": "診", -"诋": "詆", -"诌": "謅", -"词": "詞", -"诎": "詘", -"诏": "詔", -"诐": "詖", -"译": "譯", -"诒": "詒", -"诓": "誆", -"诔": "誄", -"试": "試", -"诖": "詿", -"诗": "詩", -"诘": "詰", -"诙": "詼", -"诚": "誠", -"诛": "誅", -"诜": "詵", -"话": "話", -"诞": "誕", -"诟": "詬", -"诠": "詮", -"诡": "詭", -"询": "詢", -"诣": "詣", -"诤": "諍", -"该": "該", -"详": "詳", -"诧": "詫", -"诨": "諢", -"诩": "詡", -"诪": "譸", -"诫": "誡", -"诬": "誣", -"语": "語", -"诮": "誚", -"误": "誤", -"诰": "誥", -"诱": "誘", -"诲": "誨", -"诳": "誑", -"诵": "誦", -"诶": "誒", -"请": "請", -"诸": "諸", -"诹": "諏", -"诺": "諾", -"读": "讀", -"诼": "諑", -"诽": "誹", -"课": "課", -"诿": "諉", -"谀": "諛", -"谁": "誰", -"谂": "諗", -"调": "調", -"谄": "諂", -"谅": "諒", -"谆": "諄", -"谇": "誶", -"谈": "談", -"谊": "誼", -"谋": "謀", -"谌": "諶", -"谍": "諜", -"谎": "謊", -"谏": "諫", -"谐": "諧", -"谑": "謔", -"谒": "謁", -"谓": "謂", -"谔": "諤", -"谕": "諭", -"谖": "諼", -"谗": "讒", -"谘": "諮", -"谙": "諳", -"谚": "諺", -"谛": "諦", -"谜": "謎", -"谝": "諞", -"谞": "諝", -"谟": "謨", -"谠": "讜", -"谡": "謖", -"谢": "謝", -"谤": "謗", -"谥": "謚", -"谦": "謙", -"谧": "謐", -"谨": "謹", -"谩": "謾", -"谪": "謫", -"谬": "謬", -"谭": "譚", -"谮": "譖", -"谯": "譙", -"谰": "讕", -"谱": "譜", -"谲": "譎", -"谳": "讞", -"谴": "譴", -"谵": "譫", -"谶": "讖", -"豮": "豶", -"䝙": "貙", -"䞐": "賰", -"贝": "貝", -"贞": "貞", -"负": "負", -"贠": "貟", -"贡": "貢", -"财": "財", -"责": "責", -"贤": "賢", -"败": "敗", -"账": "賬", -"货": "貨", -"质": "質", -"贩": "販", -"贪": "貪", -"贫": "貧", -"贬": "貶", -"购": "購", -"贮": "貯", -"贯": "貫", -"贰": "貳", -"贱": "賤", -"贲": "賁", -"贳": "貰", -"贴": "貼", -"贵": "貴", -"贶": "貺", -"贷": "貸", -"贸": "貿", -"费": "費", -"贺": "賀", -"贻": "貽", -"贼": "賊", -"贽": "贄", -"贾": "賈", -"贿": "賄", -"赀": "貲", -"赁": "賃", -"赂": "賂", -"资": "資", -"赅": "賅", -"赆": "贐", -"赇": "賕", -"赈": "賑", -"赉": "賚", -"赊": "賒", -"赋": "賦", -"赌": "賭", -"赎": "贖", -"赏": "賞", -"赐": "賜", -"赑": "贔", -"赒": "賙", -"赓": "賡", -"赔": "賠", -"赕": "賧", -"赖": "賴", -"赗": "賵", -"赘": "贅", -"赙": "賻", -"赚": "賺", -"赛": "賽", -"赜": "賾", -"赞": "贊", -"赟": "贇", -"赠": "贈", -"赡": "贍", -"赢": "贏", -"赣": "贛", -"赪": "赬", -"赵": "趙", -"赶": "趕", -"趋": "趨", -"趱": "趲", -"趸": "躉", -"跃": "躍", -"跄": "蹌", -"跞": "躒", -"践": "踐", -"跶": "躂", -"跷": "蹺", -"跸": "蹕", -"跹": "躚", -"跻": "躋", -"踊": "踴", -"踌": "躊", -"踪": "蹤", -"踬": "躓", -"踯": "躑", -"蹑": "躡", -"蹒": "蹣", -"蹰": "躕", -"蹿": "躥", -"躏": "躪", -"躜": "躦", -"躯": "軀", -"车": "車", -"轧": "軋", -"轨": "軌", -"轩": "軒", -"轪": "軑", -"轫": "軔", -"转": "轉", -"轭": "軛", -"轮": "輪", -"软": "軟", -"轰": "轟", -"轱": "軲", -"轲": "軻", -"轳": "轤", -"轴": "軸", -"轵": "軹", -"轶": "軼", -"轷": "軤", -"轸": "軫", -"轹": "轢", -"轺": "軺", -"轻": "輕", -"轼": "軾", -"载": "載", -"轾": "輊", -"轿": "轎", -"辀": "輈", -"辁": "輇", -"辂": "輅", -"较": "較", -"辄": "輒", -"辅": "輔", -"辆": "輛", -"辇": "輦", -"辈": "輩", -"辉": "輝", -"辊": "輥", -"辋": "輞", -"辌": "輬", -"辍": "輟", -"辎": "輜", -"辏": "輳", -"辐": "輻", -"辑": "輯", -"辒": "轀", -"输": "輸", -"辔": "轡", -"辕": "轅", -"辖": "轄", -"辗": "輾", -"辘": "轆", -"辙": "轍", -"辚": "轔", -"辞": "辭", -"辩": "辯", -"辫": "辮", -"边": "邊", -"辽": "遼", -"达": "達", -"迁": "遷", -"过": "過", -"迈": "邁", -"运": "運", -"还": "還", -"这": "這", -"进": "進", -"远": "遠", -"违": "違", -"连": "連", -"迟": "遲", -"迩": "邇", -"迳": "逕", -"迹": "跡", -"选": "選", -"逊": "遜", -"递": "遞", -"逦": "邐", -"逻": "邏", -"遗": "遺", -"遥": "遙", -"邓": "鄧", -"邝": "鄺", -"邬": "鄔", -"邮": "郵", -"邹": "鄒", -"邺": "鄴", -"邻": "鄰", -"郏": "郟", -"郐": "鄶", -"郑": "鄭", -"郓": "鄆", -"郦": "酈", -"郧": "鄖", -"郸": "鄲", -"酂": "酇", -"酦": "醱", -"酱": "醬", -"酽": "釅", -"酾": "釃", -"酿": "釀", -"释": "釋", -"鉴": "鑒", -"銮": "鑾", -"錾": "鏨", -"𨱏": "鎝", -"钅": "釒", -"钆": "釓", -"钇": "釔", -"针": "針", -"钉": "釘", -"钊": "釗", -"钋": "釙", -"钌": "釕", -"钍": "釷", -"钎": "釺", -"钏": "釧", -"钐": "釤", -"钑": "鈒", -"钒": "釩", -"钓": "釣", -"钔": "鍆", -"钕": "釹", -"钖": "鍚", -"钗": "釵", -"钘": "鈃", -"钙": "鈣", -"钚": "鈈", -"钛": "鈦", -"钜": "鉅", -"钝": "鈍", -"钞": "鈔", -"钠": "鈉", -"钡": "鋇", -"钢": "鋼", -"钣": "鈑", -"钤": "鈐", -"钥": "鑰", -"钦": "欽", -"钧": "鈞", -"钨": "鎢", -"钪": "鈧", -"钫": "鈁", -"钬": "鈥", -"钭": "鈄", -"钮": "鈕", -"钯": "鈀", -"钰": "鈺", -"钱": "錢", -"钲": "鉦", -"钳": "鉗", -"钴": "鈷", -"钶": "鈳", -"钷": "鉕", -"钸": "鈽", -"钹": "鈸", -"钺": "鉞", -"钻": "鑽", -"钼": "鉬", -"钽": "鉭", -"钾": "鉀", -"钿": "鈿", -"铀": "鈾", -"铁": "鐵", -"铂": "鉑", -"铃": "鈴", -"铄": "鑠", -"铅": "鉛", -"铆": "鉚", -"铇": "鉋", -"铈": "鈰", -"铉": "鉉", -"铊": "鉈", -"铋": "鉍", -"铌": "鈮", -"铍": "鈹", -"铎": "鐸", -"铏": "鉶", -"铐": "銬", -"铑": "銠", -"铒": "鉺", -"铓": "鋩", -"铔": "錏", -"铕": "銪", -"铖": "鋮", -"铗": "鋏", -"铘": "鋣", -"铙": "鐃", -"铚": "銍", -"铛": "鐺", -"铜": "銅", -"铝": "鋁", -"铞": "銱", -"铟": "銦", -"铠": "鎧", -"铡": "鍘", -"铢": "銖", -"铣": "銑", -"铤": "鋌", -"铥": "銩", -"铦": "銛", -"铧": "鏵", -"铨": "銓", -"铩": "鎩", -"铪": "鉿", -"铫": "銚", -"铬": "鉻", -"铭": "銘", -"铮": "錚", -"铯": "銫", -"铰": "鉸", -"铱": "銥", -"铲": "鏟", -"铳": "銃", -"铴": "鐋", -"铵": "銨", -"银": "銀", -"铷": "銣", -"铸": "鑄", -"铹": "鐒", -"铺": "鋪", -"铻": "鋙", -"铼": "錸", -"铽": "鋱", -"链": "鏈", -"铿": "鏗", -"销": "銷", -"锁": "鎖", -"锂": "鋰", -"锃": "鋥", -"锄": "鋤", -"锅": "鍋", -"锆": "鋯", -"锇": "鋨", -"锉": "銼", -"锊": "鋝", -"锋": "鋒", -"锌": "鋅", -"锍": "鋶", -"锎": "鐦", -"锏": "鐧", -"锑": "銻", -"锒": "鋃", -"锓": "鋟", -"锔": "鋦", -"锕": "錒", -"锖": "錆", -"锗": "鍺", -"锘": "鍩", -"错": "錯", -"锚": "錨", -"锛": "錛", -"锜": "錡", -"锝": "鍀", -"锞": "錁", -"锟": "錕", -"锠": "錩", -"锡": "錫", -"锢": "錮", -"锣": "鑼", -"锥": "錐", -"锦": "錦", -"锧": "鑕", -"锩": "錈", -"锪": "鍃", -"锫": "錇", -"锬": "錟", -"锭": "錠", -"键": "鍵", -"锯": "鋸", -"锰": "錳", -"锱": "錙", -"锲": "鍥", -"锳": "鍈", -"锴": "鍇", -"锵": "鏘", -"锶": "鍶", -"锷": "鍔", -"锸": "鍤", -"锹": "鍬", -"锺": "鍾", -"锻": "鍛", -"锼": "鎪", -"锽": "鍠", -"锾": "鍰", -"锿": "鎄", -"镀": "鍍", -"镁": "鎂", -"镂": "鏤", -"镃": "鎡", -"镄": "鐨", -"镅": "鎇", -"镆": "鏌", -"镇": "鎮", -"镈": "鎛", -"镉": "鎘", -"镊": "鑷", -"镋": "鎲", -"镍": "鎳", -"镎": "鎿", -"镏": "鎦", -"镐": "鎬", -"镑": "鎊", -"镒": "鎰", -"镓": "鎵", -"镔": "鑌", -"镕": "鎔", -"镖": "鏢", -"镗": "鏜", -"镘": "鏝", -"镙": "鏍", -"镚": "鏰", -"镛": "鏞", -"镜": "鏡", -"镝": "鏑", -"镞": "鏃", -"镟": "鏇", -"镠": "鏐", -"镡": "鐔", -"镣": "鐐", -"镤": "鏷", -"镥": "鑥", -"镦": "鐓", -"镧": "鑭", -"镨": "鐠", -"镩": "鑹", -"镪": "鏹", -"镫": "鐙", -"镬": "鑊", -"镭": "鐳", -"镮": "鐶", -"镯": "鐲", -"镰": "鐮", -"镱": "鐿", -"镲": "鑔", -"镳": "鑣", -"镴": "鑞", -"镵": "鑱", -"镶": "鑲", -"长": "長", -"门": "門", -"闩": "閂", -"闪": "閃", -"闫": "閆", -"闬": "閈", -"闭": "閉", -"问": "問", -"闯": "闖", -"闰": "閏", -"闱": "闈", -"闲": "閑", -"闳": "閎", -"间": "間", -"闵": "閔", -"闶": "閌", -"闷": "悶", -"闸": "閘", -"闹": "鬧", -"闺": "閨", -"闻": "聞", -"闼": "闥", -"闽": "閩", -"闾": "閭", -"闿": "闓", -"阀": "閥", -"阁": "閣", -"阂": "閡", -"阃": "閫", -"阄": "鬮", -"阆": "閬", -"阇": "闍", -"阈": "閾", -"阉": "閹", -"阊": "閶", -"阋": "鬩", -"阌": "閿", -"阍": "閽", -"阎": "閻", -"阏": "閼", -"阐": "闡", -"阑": "闌", -"阒": "闃", -"阓": "闠", -"阔": "闊", -"阕": "闋", -"阖": "闔", -"阗": "闐", -"阘": "闒", -"阙": "闕", -"阚": "闞", -"阛": "闤", -"队": "隊", -"阳": "陽", -"阴": "陰", -"阵": "陣", -"阶": "階", -"际": "際", -"陆": "陸", -"陇": "隴", -"陈": "陳", -"陉": "陘", -"陕": "陝", -"陧": "隉", -"陨": "隕", -"险": "險", -"随": "隨", -"隐": "隱", -"隶": "隸", -"隽": "雋", -"难": "難", -"雏": "雛", -"雠": "讎", -"雳": "靂", -"雾": "霧", -"霁": "霽", -"霡": "霢", -"霭": "靄", -"靓": "靚", -"静": "靜", -"靥": "靨", -"䩄": "靦", -"鞑": "韃", -"鞒": "鞽", -"鞯": "韉", -"韦": "韋", -"韧": "韌", -"韨": "韍", -"韩": "韓", -"韪": "韙", -"韫": "韞", -"韬": "韜", -"韵": "韻", -"页": "頁", -"顶": "頂", -"顷": "頃", -"顸": "頇", -"项": "項", -"顺": "順", -"顼": "頊", -"顽": "頑", -"顾": "顧", -"顿": "頓", -"颀": "頎", -"颁": "頒", -"颂": "頌", -"颃": "頏", -"预": "預", -"颅": "顱", -"领": "領", -"颇": "頗", -"颈": "頸", -"颉": "頡", -"颊": "頰", -"颋": "頲", -"颌": "頜", -"颍": "潁", -"颎": "熲", -"颏": "頦", -"颐": "頤", -"频": "頻", -"颒": "頮", -"颔": "頷", -"颕": "頴", -"颖": "穎", -"颗": "顆", -"题": "題", -"颙": "顒", -"颚": "顎", -"颛": "顓", -"额": "額", -"颞": "顳", -"颟": "顢", -"颠": "顛", -"颡": "顙", -"颢": "顥", -"颤": "顫", -"颥": "顬", -"颦": "顰", -"颧": "顴", -"风": "風", -"飏": "颺", -"飐": "颭", -"飑": "颮", -"飒": "颯", -"飓": "颶", -"飔": "颸", -"飕": "颼", -"飖": "颻", -"飗": "飀", -"飘": "飄", -"飙": "飆", -"飚": "飈", -"飞": "飛", -"飨": "饗", -"餍": "饜", -"饣": "飠", -"饤": "飣", -"饦": "飥", -"饧": "餳", -"饨": "飩", -"饩": "餼", -"饪": "飪", -"饫": "飫", -"饬": "飭", -"饭": "飯", -"饮": "飲", -"饯": "餞", -"饰": "飾", -"饱": "飽", -"饲": "飼", -"饳": "飿", -"饴": "飴", -"饵": "餌", -"饶": "饒", -"饷": "餉", -"饸": "餄", -"饹": "餎", -"饺": "餃", -"饻": "餏", -"饼": "餅", -"饽": "餑", -"饾": "餖", -"饿": "餓", -"馀": "餘", -"馁": "餒", -"馂": "餕", -"馃": "餜", -"馄": "餛", -"馅": "餡", -"馆": "館", -"馇": "餷", -"馈": "饋", -"馉": "餶", -"馊": "餿", -"馋": "饞", -"馌": "饁", -"馍": "饃", -"馎": "餺", -"馏": "餾", -"馐": "饈", -"馑": "饉", -"馒": "饅", -"馓": "饊", -"馔": "饌", -"馕": "饢", -"䯄": "騧", -"马": "馬", -"驭": "馭", -"驮": "馱", -"驯": "馴", -"驰": "馳", -"驱": "驅", -"驲": "馹", -"驳": "駁", -"驴": "驢", -"驵": "駔", -"驶": "駛", -"驷": "駟", -"驸": "駙", -"驹": "駒", -"驺": "騶", -"驻": "駐", -"驼": "駝", -"驽": "駑", -"驾": "駕", -"驿": "驛", -"骀": "駘", -"骁": "驍", -"骃": "駰", -"骄": "驕", -"骅": "驊", -"骆": "駱", -"骇": "駭", -"骈": "駢", -"骉": "驫", -"骊": "驪", -"骋": "騁", -"验": "驗", -"骍": "騂", -"骎": "駸", -"骏": "駿", -"骐": "騏", -"骑": "騎", -"骒": "騍", -"骓": "騅", -"骔": "騌", -"骕": "驌", -"骖": "驂", -"骗": "騙", -"骘": "騭", -"骙": "騤", -"骚": "騷", -"骛": "騖", -"骜": "驁", -"骝": "騮", -"骞": "騫", -"骟": "騸", -"骠": "驃", -"骡": "騾", -"骢": "驄", -"骣": "驏", -"骤": "驟", -"骥": "驥", -"骦": "驦", -"骧": "驤", -"髅": "髏", -"髋": "髖", -"髌": "髕", -"鬓": "鬢", -"魇": "魘", -"魉": "魎", -"鱼": "魚", -"鱽": "魛", -"鱾": "魢", -"鱿": "魷", -"鲀": "魨", -"鲁": "魯", -"鲂": "魴", -"鲃": "䰾", -"鲄": "魺", -"鲅": "鮁", -"鲆": "鮃", -"鲈": "鱸", -"鲉": "鮋", -"鲊": "鮓", -"鲋": "鮒", -"鲌": "鮊", -"鲍": "鮑", -"鲎": "鱟", -"鲏": "鮍", -"鲐": "鮐", -"鲑": "鮭", -"鲒": "鮚", -"鲓": "鮳", -"鲔": "鮪", -"鲕": "鮞", -"鲖": "鮦", -"鲗": "鰂", -"鲘": "鮜", -"鲙": "鱠", -"鲚": "鱭", -"鲛": "鮫", -"鲜": "鮮", -"鲝": "鮺", -"鲟": "鱘", -"鲠": "鯁", -"鲡": "鱺", -"鲢": "鰱", -"鲣": "鰹", -"鲤": "鯉", -"鲥": "鰣", -"鲦": "鰷", -"鲧": "鯀", -"鲨": "鯊", -"鲩": "鯇", -"鲪": "鮶", -"鲫": "鯽", -"鲬": "鯒", -"鲭": "鯖", -"鲮": "鯪", -"鲯": "鯕", -"鲰": "鯫", -"鲱": "鯡", -"鲲": "鯤", -"鲳": "鯧", -"鲴": "鯝", -"鲵": "鯢", -"鲶": "鯰", -"鲷": "鯛", -"鲸": "鯨", -"鲹": "鰺", -"鲺": "鯴", -"鲻": "鯔", -"鲼": "鱝", -"鲽": "鰈", -"鲾": "鰏", -"鲿": "鱨", -"鳀": "鯷", -"鳁": "鰮", -"鳂": "鰃", -"鳃": "鰓", -"鳅": "鰍", -"鳆": "鰒", -"鳇": "鰉", -"鳈": "鰁", -"鳉": "鱂", -"鳊": "鯿", -"鳋": "鰠", -"鳌": "鰲", -"鳍": "鰭", -"鳎": "鰨", -"鳏": "鰥", -"鳐": "鰩", -"鳑": "鰟", -"鳒": "鰜", -"鳓": "鰳", -"鳔": "鰾", -"鳕": "鱈", -"鳖": "鱉", -"鳗": "鰻", -"鳘": "鰵", -"鳙": "鱅", -"鳚": "䲁", -"鳛": "鰼", -"鳜": "鱖", -"鳝": "鱔", -"鳞": "鱗", -"鳟": "鱒", -"鳠": "鱯", -"鳡": "鱤", -"鳢": "鱧", -"鳣": "鱣", -"䴓": "鳾", -"䴕": "鴷", -"䴔": "鵁", -"䴖": "鶄", -"䴗": "鶪", -"䴘": "鷈", -"䴙": "鷿", -"㶉": "鸂", -"鸟": "鳥", -"鸠": "鳩", -"鸢": "鳶", -"鸣": "鳴", -"鸤": "鳲", -"鸥": "鷗", -"鸦": "鴉", -"鸧": "鶬", -"鸨": "鴇", -"鸩": "鴆", -"鸪": "鴣", -"鸫": "鶇", -"鸬": "鸕", -"鸭": "鴨", -"鸮": "鴞", -"鸯": "鴦", -"鸰": "鴒", -"鸱": "鴟", -"鸲": "鴝", -"鸳": "鴛", -"鸴": "鷽", -"鸵": "鴕", -"鸶": "鷥", -"鸷": "鷙", -"鸸": "鴯", -"鸹": "鴰", -"鸺": "鵂", -"鸻": "鴴", -"鸼": "鵃", -"鸽": "鴿", -"鸾": "鸞", -"鸿": "鴻", -"鹀": "鵐", -"鹁": "鵓", -"鹂": "鸝", -"鹃": "鵑", -"鹄": "鵠", -"鹅": "鵝", -"鹆": "鵒", -"鹇": "鷳", -"鹈": "鵜", -"鹉": "鵡", -"鹊": "鵲", -"鹋": "鶓", -"鹌": "鵪", -"鹍": "鵾", -"鹎": "鵯", -"鹏": "鵬", -"鹐": "鵮", -"鹑": "鶉", -"鹒": "鶊", -"鹓": "鵷", -"鹔": "鷫", -"鹕": "鶘", -"鹖": "鶡", -"鹗": "鶚", -"鹘": "鶻", -"鹙": "鶖", -"鹛": "鶥", -"鹜": "鶩", -"鹝": "鷊", -"鹞": "鷂", -"鹟": "鶲", -"鹠": "鶹", -"鹡": "鶺", -"鹢": "鷁", -"鹣": "鶼", -"鹤": "鶴", -"鹥": "鷖", -"鹦": "鸚", -"鹧": "鷓", -"鹨": "鷚", -"鹩": "鷯", -"鹪": "鷦", -"鹫": "鷲", -"鹬": "鷸", -"鹭": "鷺", -"鹯": "鸇", -"鹰": "鷹", -"鹱": "鸌", -"鹲": "鸏", -"鹳": "鸛", -"鹴": "鸘", -"鹾": "鹺", -"麦": "麥", -"麸": "麩", -"黄": "黃", -"黉": "黌", -"黡": "黶", -"黩": "黷", -"黪": "黲", -"黾": "黽", -"鼋": "黿", -"鼍": "鼉", -"鼗": "鞀", -"鼹": "鼴", -"齐": "齊", -"齑": "齏", -"齿": "齒", -"龀": "齔", -"龁": "齕", -"龂": "齗", -"龃": "齟", -"龄": "齡", -"龅": "齙", -"龆": "齠", -"龇": "齜", -"龈": "齦", -"龉": "齬", -"龊": "齪", -"龋": "齲", -"龌": "齷", -"龙": "龍", -"龚": "龔", -"龛": "龕", -"龟": "龜", -"一伙": "一伙", -"一并": "一併", -"一准": "一准", -"一划": "一划", -"一地里": "一地裡", -"一干": "一干", -"一树百获": "一樹百穫", -"一台": "一臺", -"一冲": "一衝", -"一只": "一隻", -"一发千钧": "一髮千鈞", -"一出": "一齣", -"七只": "七隻", -"三元里": "三元裡", -"三国志": "三國誌", -"三复": "三複", -"三只": "三隻", -"上吊": "上吊", -"上台": "上臺", -"下不了台": "下不了臺", -"下台": "下臺", -"下面": "下麵", -"不准": "不准", -"不吊": "不吊", -"不知就里": "不知就裡", -"不知所云": "不知所云", -"不锈钢": "不鏽鋼", -"丑剧": "丑劇", -"丑旦": "丑旦", -"丑角": "丑角", -"并存着": "並存著", -"中岳": "中嶽", -"中台医专": "中臺醫專", -"丰南": "丰南", -"丰台": "丰台", -"丰姿": "丰姿", -"丰采": "丰采", -"丰韵": "丰韻", -"主干": "主幹", -"么么唱唱": "么么唱唱", -"么儿": "么兒", -"么喝": "么喝", -"么妹": "么妹", -"么弟": "么弟", -"么爷": "么爺", -"九世之雠": "九世之讎", -"九只": "九隻", -"干丝": "乾絲", -"干着急": "乾著急", -"乱发": "亂髮", -"云云": "云云", -"云尔": "云爾", -"五岳": "五嶽", -"五斗柜": "五斗櫃", -"五斗橱": "五斗櫥", -"五谷": "五穀", -"五行生克": "五行生剋", -"五只": "五隻", -"五出": "五齣", -"交卷": "交卷", -"人云亦云": "人云亦云", -"人物志": "人物誌", -"什锦面": "什錦麵", -"什么": "什麼", -"仆倒": "仆倒", -"介系词": "介係詞", -"介系词": "介繫詞", -"仿制": "仿製", -"伙伕": "伙伕", -"伙伴": "伙伴", -"伙同": "伙同", -"伙夫": "伙夫", -"伙房": "伙房", -"伙计": "伙計", -"伙食": "伙食", -"布下": "佈下", -"布告": "佈告", -"布哨": "佈哨", -"布局": "佈局", -"布岗": "佈崗", -"布施": "佈施", -"布景": "佈景", -"布满": "佈滿", -"布线": "佈線", -"布置": "佈置", -"布署": "佈署", -"布道": "佈道", -"布达": "佈達", -"布防": "佈防", -"布阵": "佈陣", -"布雷": "佈雷", -"体育锻鍊": "体育鍛鍊", -"何干": "何干", -"作准": "作准", -"佣人": "佣人", -"佣工": "佣工", -"佣金": "佣金", -"并入": "併入", -"并列": "併列", -"并到": "併到", -"并合": "併合", -"并吞": "併吞", -"并在": "併在", -"并成": "併成", -"并排": "併排", -"并拢": "併攏", -"并案": "併案", -"并为": "併為", -"并发": "併發", -"并科": "併科", -"并购": "併購", -"并进": "併進", -"来复": "來複", -"供制": "供製", -"依依不舍": "依依不捨", -"侵并": "侵併", -"便辟": "便辟", -"系数": "係數", -"系为": "係為", -"保险柜": "保險柜", -"信号台": "信號臺", -"修复": "修複", -"修胡刀": "修鬍刀", -"俯冲": "俯衝", -"个里": "個裡", -"借着": "借著", -"假发": "假髮", -"停制": "停製", -"偷鸡不着": "偷雞不著", -"家伙": "傢伙", -"家俱": "傢俱", -"家具": "傢具", -"传布": "傳佈", -"债台高筑": "債臺高築", -"傻里傻气": "傻裡傻氣", -"倾家荡产": "傾家蕩產", -"倾覆": "傾複", -"倾覆": "傾覆", -"僱佣": "僱佣", -"仪表": "儀錶", -"亿只": "億隻", -"尽尽": "儘儘", -"尽先": "儘先", -"尽其所有": "儘其所有", -"尽力": "儘力", -"尽快": "儘快", -"尽早": "儘早", -"尽是": "儘是", -"尽管": "儘管", -"尽速": "儘速", -"尽量": "儘量", -"允准": "允准", -"兄台": "兄臺", -"充饥": "充饑", -"光采": "光采", -"克里": "克裡", -"克复": "克複", -"入伙": "入伙", -"内制": "內製", -"两只": "兩隻", -"八字胡": "八字鬍", -"八只": "八隻", -"公布": "公佈", -"公干": "公幹", -"公斗": "公斗", -"公历": "公曆", -"六只": "六隻", -"六出": "六齣", -"兼并": "兼併", -"冤雠": "冤讎", -"准予": "准予", -"准假": "准假", -"准将": "准將", -"准考证": "准考證", -"准许": "准許", -"几几": "几几", -"几案": "几案", -"几丝": "几絲", -"凹洞里": "凹洞裡", -"出征": "出征", -"出锤": "出鎚", -"刀削面": "刀削麵", -"刁斗": "刁斗", -"分布": "分佈", -"切面": "切麵", -"刊布": "刊佈", -"划上": "划上", -"划下": "划下", -"划不来": "划不來", -"划了": "划了", -"划具": "划具", -"划出": "划出", -"划到": "划到", -"划动": "划動", -"划去": "划去", -"划子": "划子", -"划得来": "划得來", -"划拳": "划拳", -"划桨": "划槳", -"划水": "划水", -"划算": "划算", -"划船": "划船", -"划艇": "划艇", -"划着": "划著", -"划着走": "划著走", -"划行": "划行", -"划走": "划走", -"划起": "划起", -"划进": "划進", -"划过": "划過", -"初征": "初征", -"别致": "別緻", -"别着": "別著", -"别只": "別隻", -"利比里亚": "利比裡亞", -"刮着": "刮著", -"刮胡刀": "刮鬍刀", -"剃发": "剃髮", -"剃须": "剃鬚", -"削发": "削髮", -"克制": "剋制", -"克星": "剋星", -"克服": "剋服", -"克死": "剋死", -"克薄": "剋薄", -"前仆后继": "前仆後繼", -"前台": "前臺", -"前车之复": "前車之覆", -"刚才": "剛纔", -"剪发": "剪髮", -"割舍": "割捨", -"创制": "創製", -"加里宁": "加裡寧", -"动荡": "動蕩", -"劳力士表": "勞力士錶", -"包准": "包准", -"包谷": "包穀", -"北斗": "北斗", -"北回": "北迴", -"匡复": "匡複", -"匪干": "匪幹", -"十卷": "十卷", -"十台": "十臺", -"十只": "十隻", -"十出": "十齣", -"千丝万缕": "千絲萬縷", -"千回百折": "千迴百折", -"千回百转": "千迴百轉", -"千钧一发": "千鈞一髮", -"千只": "千隻", -"升斗小民": "升斗小民", -"半只": "半隻", -"南岳": "南嶽", -"南征": "南征", -"南台": "南臺", -"南回": "南迴", -"卡里": "卡裡", -"印制": "印製", -"卷入": "卷入", -"卷取": "卷取", -"卷土重来": "卷土重來", -"卷子": "卷子", -"卷宗": "卷宗", -"卷尺": "卷尺", -"卷层云": "卷層雲", -"卷帙": "卷帙", -"卷扬机": "卷揚機", -"卷曲": "卷曲", -"卷染": "卷染", -"卷烟": "卷煙", -"卷筒": "卷筒", -"卷纬": "卷緯", -"卷绕": "卷繞", -"卷装": "卷裝", -"卷轴": "卷軸", -"卷云": "卷雲", -"卷领": "卷領", -"卷发": "卷髮", -"卷须": "卷鬚", -"参与": "參与", -"参与者": "參与者", -"参合": "參合", -"参考价值": "參考價值", -"参与": "參與", -"参与人员": "參與人員", -"参与制": "參與制", -"参与感": "參與感", -"参与者": "參與者", -"参观团": "參觀團", -"参观团体": "參觀團體", -"参阅": "參閱", -"反冲": "反衝", -"反复": "反複", -"反复": "反覆", -"取舍": "取捨", -"口里": "口裡", -"只准": "只准", -"只冲": "只衝", -"叮当": "叮噹", -"可怜虫": "可憐虫", -"可紧可松": "可緊可鬆", -"台制": "台製", -"司令台": "司令臺", -"吃着不尽": "吃著不盡", -"吃里扒外": "吃裡扒外", -"吃里爬外": "吃裡爬外", -"各吊": "各吊", -"合伙": "合伙", -"合并": "合併", -"吊上": "吊上", -"吊下": "吊下", -"吊了": "吊了", -"吊个": "吊個", -"吊儿郎当": "吊兒郎當", -"吊到": "吊到", -"吊去": "吊去", -"吊取": "吊取", -"吊吊": "吊吊", -"吊嗓": "吊嗓", -"吊好": "吊好", -"吊子": "吊子", -"吊带": "吊帶", -"吊带裤": "吊帶褲", -"吊床": "吊床", -"吊得": "吊得", -"吊挂": "吊掛", -"吊挂着": "吊掛著", -"吊杆": "吊杆", -"吊架": "吊架", -"吊桶": "吊桶", -"吊杆": "吊桿", -"吊桥": "吊橋", -"吊死": "吊死", -"吊灯": "吊燈", -"吊环": "吊環", -"吊盘": "吊盤", -"吊索": "吊索", -"吊着": "吊著", -"吊装": "吊裝", -"吊裤": "吊褲", -"吊裤带": "吊褲帶", -"吊袜": "吊襪", -"吊走": "吊走", -"吊起": "吊起", -"吊车": "吊車", -"吊钩": "吊鉤", -"吊销": "吊銷", -"吊钟": "吊鐘", -"同伙": "同伙", -"名表": "名錶", -"后冠": "后冠", -"后土": "后土", -"后妃": "后妃", -"后座": "后座", -"后稷": "后稷", -"后羿": "后羿", -"后里": "后里", -"向着": "向著", -"吞并": "吞併", -"吹发": "吹髮", -"吕后": "呂后", -"獃里獃气": "呆裡呆氣", -"周而复始": "周而複始", -"呼吁": "呼籲", -"和面": "和麵", -"哪里": "哪裡", -"哭脏": "哭髒", -"问卷": "問卷", -"喝采": "喝采", -"单干": "單干", -"单只": "單隻", -"嘴里": "嘴裡", -"恶心": "噁心", -"当啷": "噹啷", -"当当": "噹噹", -"噜苏": "嚕囌", -"向导": "嚮導", -"向往": "嚮往", -"向应": "嚮應", -"向日": "嚮日", -"向迩": "嚮邇", -"严丝合缝": "嚴絲合縫", -"严复": "嚴複", -"四舍五入": "四捨五入", -"四只": "四隻", -"四出": "四齣", -"回丝": "回絲", -"回着": "回著", -"回荡": "回蕩", -"回复": "回覆", -"回采": "回采", -"圈子里": "圈子裡", -"圈里": "圈裡", -"国历": "國曆", -"国雠": "國讎", -"园里": "園裡", -"图里": "圖裡", -"土里": "土裡", -"土制": "土製", -"地志": "地誌", -"坍台": "坍臺", -"坑里": "坑裡", -"坦荡": "坦蕩", -"垂发": "垂髮", -"垮台": "垮臺", -"埋布": "埋佈", -"城里": "城裡", -"基干": "基幹", -"报复": "報複", -"塌台": "塌臺", -"塔台": "塔臺", -"涂着": "塗著", -"墓志": "墓誌", -"墨斗": "墨斗", -"墨索里尼": "墨索裡尼", -"垦复": "墾複", -"垄断价格": "壟斷價格", -"垄断资产": "壟斷資產", -"垄断集团": "壟斷集團", -"壶里": "壺裡", -"寿面": "壽麵", -"夏天里": "夏天裡", -"夏历": "夏曆", -"外制": "外製", -"多冲": "多衝", -"多采多姿": "多采多姿", -"多么": "多麼", -"夜光表": "夜光錶", -"夜里": "夜裡", -"梦里": "夢裡", -"大伙": "大伙", -"大卷": "大卷", -"大干": "大干", -"大干": "大幹", -"大锤": "大鎚", -"大只": "大隻", -"天后": "天后", -"天干": "天干", -"天文台": "天文臺", -"太后": "太后", -"奏折": "奏摺", -"女丑": "女丑", -"女佣": "女佣", -"好家夥": "好傢夥", -"好戏连台": "好戲連臺", -"如法泡制": "如法泡製", -"妆台": "妝臺", -"姜太公": "姜太公", -"姜子牙": "姜子牙", -"姜丝": "姜絲", -"字汇": "字彙", -"字里行间": "字裡行間", -"存折": "存摺", -"孟姜女": "孟姜女", -"宇宙志": "宇宙誌", -"定准": "定准", -"定制": "定製", -"宣布": "宣佈", -"宫里": "宮裡", -"家伙": "家伙", -"家里": "家裡", -"密布": "密佈", -"寇雠": "寇讎", -"实干": "實幹", -"写字台": "寫字檯", -"写字台": "寫字臺", -"宽松": "寬鬆", -"封面里": "封面裡", -"射干": "射干", -"对表": "對錶", -"小丑": "小丑", -"小伙": "小伙", -"小只": "小隻", -"少吊": "少吊", -"尺布斗粟": "尺布斗粟", -"尼克松": "尼克鬆", -"尼采": "尼采", -"尿斗": "尿斗", -"局里": "局裡", -"居里": "居裡", -"屋子里": "屋子裡", -"屋里": "屋裡", -"展布": "展佈", -"屡仆屡起": "屢仆屢起", -"屯里": "屯裡", -"山岳": "山嶽", -"山里": "山裡", -"峰回": "峰迴", -"巡回": "巡迴", -"巧干": "巧幹", -"巴尔干": "巴爾幹", -"巴里": "巴裡", -"巷里": "巷裡", -"市里": "市裡", -"布谷": "布穀", -"希腊": "希腊", -"帘子": "帘子", -"帘布": "帘布", -"席卷": "席卷", -"带团参加": "帶團參加", -"带发修行": "帶髮修行", -"干休": "干休", -"干系": "干係", -"干卿何事": "干卿何事", -"干将": "干將", -"干戈": "干戈", -"干挠": "干撓", -"干扰": "干擾", -"干支": "干支", -"干政": "干政", -"干时": "干時", -"干涉": "干涉", -"干犯": "干犯", -"干与": "干與", -"干着急": "干著急", -"干贝": "干貝", -"干预": "干預", -"平台": "平臺", -"年历": "年曆", -"年里": "年裡", -"干上": "幹上", -"干下去": "幹下去", -"干了": "幹了", -"干事": "幹事", -"干些": "幹些", -"干个": "幹個", -"干劲": "幹勁", -"干员": "幹員", -"干吗": "幹嗎", -"干嘛": "幹嘛", -"干坏事": "幹壞事", -"干完": "幹完", -"干得": "幹得", -"干性油": "幹性油", -"干才": "幹才", -"干掉": "幹掉", -"干校": "幹校", -"干活": "幹活", -"干流": "幹流", -"干球温度": "幹球溫度", -"干线": "幹線", -"干练": "幹練", -"干警": "幹警", -"干起来": "幹起來", -"干路": "幹路", -"干道": "幹道", -"干部": "幹部", -"干么": "幹麼", -"几丝": "幾絲", -"几只": "幾隻", -"几出": "幾齣", -"底里": "底裡", -"康采恩": "康采恩", -"庙里": "廟裡", -"建台": "建臺", -"弄脏": "弄髒", -"弔卷": "弔卷", -"弘历": "弘曆", -"别扭": "彆扭", -"别拗": "彆拗", -"别气": "彆氣", -"别脚": "彆腳", -"别着": "彆著", -"弹子台": "彈子檯", -"弹药": "彈葯", -"汇报": "彙報", -"汇整": "彙整", -"汇编": "彙編", -"汇总": "彙總", -"汇纂": "彙纂", -"汇辑": "彙輯", -"汇集": "彙集", -"形单影只": "形單影隻", -"影后": "影后", -"往里": "往裡", -"往复": "往複", -"征伐": "征伐", -"征兵": "征兵", -"征尘": "征塵", -"征夫": "征夫", -"征战": "征戰", -"征收": "征收", -"征服": "征服", -"征求": "征求", -"征发": "征發", -"征衣": "征衣", -"征讨": "征討", -"征途": "征途", -"后台": "後臺", -"从里到外": "從裡到外", -"从里向外": "從裡向外", -"复雠": "復讎", -"复辟": "復辟", -"德干高原": "德干高原", -"心愿": "心愿", -"心荡神驰": "心蕩神馳", -"心里": "心裡", -"忙里": "忙裡", -"快干": "快幹", -"快冲": "快衝", -"怎么": "怎麼", -"怎么着": "怎麼著", -"怒发冲冠": "怒髮衝冠", -"急冲而下": "急衝而下", -"怪里怪气": "怪裡怪氣", -"恩准": "恩准", -"情有所钟": "情有所鍾", -"意面": "意麵", -"慌里慌张": "慌裡慌張", -"慰借": "慰藉", -"忧郁": "憂郁", -"凭吊": "憑吊", -"凭借": "憑藉", -"凭借着": "憑藉著", -"蒙懂": "懞懂", -"怀里": "懷裡", -"怀表": "懷錶", -"悬吊": "懸吊", -"恋恋不舍": "戀戀不捨", -"戏台": "戲臺", -"戴表": "戴錶", -"戽斗": "戽斗", -"房里": "房裡", -"手不释卷": "手不釋卷", -"手卷": "手卷", -"手折": "手摺", -"手里": "手裡", -"手表": "手錶", -"手松": "手鬆", -"才干": "才幹", -"才高八斗": "才高八斗", -"打谷": "打穀", -"扞御": "扞禦", -"批准": "批准", -"批复": "批複", -"批复": "批覆", -"承制": "承製", -"抗御": "抗禦", -"折冲": "折衝", -"披复": "披覆", -"披发": "披髮", -"抱朴": "抱朴", -"抵御": "抵禦", -"拆伙": "拆伙", -"拆台": "拆臺", -"拈须": "拈鬚", -"拉纤": "拉縴", -"拉面": "拉麵", -"拖吊": "拖吊", -"拗别": "拗彆", -"拮据": "拮据", -"振荡": "振蕩", -"捍御": "捍禦", -"舍不得": "捨不得", -"舍出": "捨出", -"舍去": "捨去", -"舍命": "捨命", -"舍己从人": "捨己從人", -"舍己救人": "捨己救人", -"舍己为人": "捨己為人", -"舍己为公": "捨己為公", -"舍己为国": "捨己為國", -"舍得": "捨得", -"舍我其谁": "捨我其誰", -"舍本逐末": "捨本逐末", -"舍弃": "捨棄", -"舍死忘生": "捨死忘生", -"舍生": "捨生", -"舍短取长": "捨短取長", -"舍身": "捨身", -"舍车保帅": "捨車保帥", -"舍近求远": "捨近求遠", -"捲发": "捲髮", -"捵面": "捵麵", -"扫荡": "掃蕩", -"掌柜": "掌柜", -"排骨面": "排骨麵", -"挂帘": "掛帘", -"挂面": "掛麵", -"接着说": "接著說", -"提心吊胆": "提心吊膽", -"插图卷": "插圖卷", -"换吊": "換吊", -"换只": "換隻", -"换发": "換髮", -"摇荡": "搖蕩", -"搭伙": "搭伙", -"折合": "摺合", -"折奏": "摺奏", -"折子": "摺子", -"折尺": "摺尺", -"折扇": "摺扇", -"折梯": "摺梯", -"折椅": "摺椅", -"折叠": "摺疊", -"折痕": "摺痕", -"折篷": "摺篷", -"折纸": "摺紙", -"折裙": "摺裙", -"撒布": "撒佈", -"撚须": "撚鬚", -"撞球台": "撞球檯", -"擂台": "擂臺", -"担仔面": "擔仔麵", -"担担面": "擔擔麵", -"担着": "擔著", -"担负着": "擔負著", -"据云": "據云", -"擢发难数": "擢髮難數", -"摆布": "擺佈", -"摄制": "攝製", -"支干": "支幹", -"收获": "收穫", -"改制": "改製", -"攻克": "攻剋", -"放荡": "放蕩", -"放松": "放鬆", -"叙说着": "敘說著", -"散伙": "散伙", -"散布": "散佈", -"散荡": "散蕩", -"散发": "散髮", -"整只": "整隻", -"整出": "整齣", -"文采": "文采", -"斗六": "斗六", -"斗南": "斗南", -"斗大": "斗大", -"斗子": "斗子", -"斗室": "斗室", -"斗方": "斗方", -"斗栱": "斗栱", -"斗笠": "斗笠", -"斗箕": "斗箕", -"斗篷": "斗篷", -"斗胆": "斗膽", -"斗转参横": "斗轉參橫", -"斗量": "斗量", -"斗门": "斗門", -"料斗": "料斗", -"斯里兰卡": "斯裡蘭卡", -"新历": "新曆", -"断头台": "斷頭臺", -"方才": "方纔", -"施舍": "施捨", -"旋绕着": "旋繞著", -"旋回": "旋迴", -"族里": "族裡", -"日历": "日曆", -"日志": "日誌", -"日进斗金": "日進斗金", -"明了": "明瞭", -"明窗净几": "明窗淨几", -"明里": "明裡", -"星斗": "星斗", -"星历": "星曆", -"星移斗换": "星移斗換", -"星移斗转": "星移斗轉", -"星罗棋布": "星羅棋佈", -"星辰表": "星辰錶", -"春假里": "春假裡", -"春天里": "春天裡", -"晃荡": "晃蕩", -"景致": "景緻", -"暗地里": "暗地裡", -"暗沟里": "暗溝裡", -"暗里": "暗裡", -"历数": "曆數", -"历书": "曆書", -"历法": "曆法", -"书卷": "書卷", -"会干": "會幹", -"会里": "會裡", -"月历": "月曆", -"月台": "月臺", -"有只": "有隻", -"木制": "木製", -"本台": "本臺", -"朴子": "朴子", -"朴实": "朴實", -"朴硝": "朴硝", -"朴素": "朴素", -"朴资茅斯": "朴資茅斯", -"村里": "村裡", -"束发": "束髮", -"东岳": "東嶽", -"东征": "東征", -"松赞干布": "松贊干布", -"板着脸": "板著臉", -"板荡": "板蕩", -"枕借": "枕藉", -"林宏岳": "林宏嶽", -"枝干": "枝幹", -"枯干": "枯幹", -"某只": "某隻", -"染发": "染髮", -"柜上": "柜上", -"柜台": "柜台", -"柜子": "柜子", -"查卷": "查卷", -"查号台": "查號臺", -"校雠学": "校讎學", -"核准": "核准", -"核复": "核覆", -"格里": "格裡", -"案卷": "案卷", -"条干": "條幹", -"棉卷": "棉卷", -"棉制": "棉製", -"植发": "植髮", -"楼台": "樓臺", -"标志着": "標志著", -"标致": "標緻", -"标志": "標誌", -"模制": "模製", -"树干": "樹幹", -"横征暴敛": "橫征暴斂", -"横冲": "橫衝", -"档卷": "檔卷", -"检复": "檢覆", -"台子": "檯子", -"台布": "檯布", -"台灯": "檯燈", -"台球": "檯球", -"台面": "檯面", -"柜台": "櫃檯", -"柜台": "櫃臺", -"栏干": "欄干", -"欺蒙": "欺矇", -"歌后": "歌后", -"欧几里得": "歐幾裡得", -"正当着": "正當著", -"武后": "武后", -"武松": "武鬆", -"归并": "歸併", -"死里求生": "死裡求生", -"死里逃生": "死裡逃生", -"残卷": "殘卷", -"杀虫药": "殺虫藥", -"壳里": "殼裡", -"母后": "母后", -"每只": "每隻", -"比干": "比干", -"毛卷": "毛卷", -"毛发": "毛髮", -"毫发": "毫髮", -"气冲牛斗": "氣沖牛斗", -"气象台": "氣象臺", -"氯霉素": "氯黴素", -"水斗": "水斗", -"水里": "水裡", -"水表": "水錶", -"永历": "永曆", -"污蔑": "汙衊", -"池里": "池裡", -"污蔑": "污衊", -"沈着": "沈著", -"没事干": "沒事幹", -"没精打采": "沒精打采", -"冲着": "沖著", -"沙里淘金": "沙裡淘金", -"河里": "河裡", -"油面": "油麵", -"泡面": "泡麵", -"泰斗": "泰斗", -"洗手不干": "洗手不幹", -"洗发精": "洗髮精", -"派团参加": "派團參加", -"流荡": "流蕩", -"浩荡": "浩蕩", -"浪琴表": "浪琴錶", -"浪荡": "浪蕩", -"浮荡": "浮蕩", -"海里": "海裡", -"涂着": "涂著", -"液晶表": "液晶錶", -"凉面": "涼麵", -"淡朱": "淡硃", -"淫荡": "淫蕩", -"测验卷": "測驗卷", -"港制": "港製", -"游荡": "游蕩", -"凑合着": "湊合著", -"湖里": "湖裡", -"汤团": "湯糰", -"汤面": "湯麵", -"卤制": "滷製", -"卤面": "滷麵", -"满布": "滿佈", -"漂荡": "漂蕩", -"漏斗": "漏斗", -"演奏台": "演奏臺", -"潭里": "潭裡", -"激荡": "激蕩", -"浓郁": "濃郁", -"浓发": "濃髮", -"湿地松": "濕地鬆", -"蒙蒙": "濛濛", -"蒙雾": "濛霧", -"瀛台": "瀛臺", -"弥漫": "瀰漫", -"弥漫着": "瀰漫著", -"火并": "火併", -"灰蒙": "灰濛", -"炒面": "炒麵", -"炮制": "炮製", -"炸药": "炸葯", -"炸酱面": "炸醬麵", -"为着": "為著", -"乌干达": "烏干達", -"乌苏里江": "烏蘇裡江", -"乌发": "烏髮", -"乌龙面": "烏龍麵", -"烘制": "烘製", -"烽火台": "烽火臺", -"无干": "無干", -"无精打采": "無精打采", -"炼制": "煉製", -"烟卷儿": "煙卷兒", -"烟斗": "煙斗", -"烟斗丝": "煙斗絲", -"烟台": "煙臺", -"照准": "照准", -"熨斗": "熨斗", -"灯台": "燈臺", -"燎发": "燎髮", -"烫发": "燙髮", -"烫面": "燙麵", -"烛台": "燭臺", -"炉台": "爐臺", -"爽荡": "爽蕩", -"片言只语": "片言隻語", -"牛肉面": "牛肉麵", -"牛只": "牛隻", -"特准": "特准", -"特征": "特征", -"特里": "特裡", -"特制": "特製", -"牵系": "牽繫", -"狼借": "狼藉", -"猛冲": "猛衝", -"奖杯": "獎盃", -"获准": "獲准", -"率团参加": "率團參加", -"王侯后": "王侯后", -"王后": "王后", -"班里": "班裡", -"理发": "理髮", -"瑶台": "瑤臺", -"甚么": "甚麼", -"甜面酱": "甜麵醬", -"生力面": "生力麵", -"生锈": "生鏽", -"生发": "生髮", -"田里": "田裡", -"由馀": "由余", -"男佣": "男佣", -"男用表": "男用錶", -"留发": "留髮", -"畚斗": "畚斗", -"当着": "當著", -"疏松": "疏鬆", -"疲困": "疲睏", -"病症": "病癥", -"症候": "癥候", -"症状": "癥狀", -"症结": "癥結", -"登台": "登臺", -"发布": "發佈", -"发着": "發著", -"发面": "發麵", -"发霉": "發黴", -"白卷": "白卷", -"白干儿": "白干兒", -"白发": "白髮", -"白面": "白麵", -"百里": "百裡", -"百只": "百隻", -"皇后": "皇后", -"皇历": "皇曆", -"皓发": "皓髮", -"皮里阳秋": "皮裏陽秋", -"皮里春秋": "皮裡春秋", -"皮制": "皮製", -"皱折": "皺摺", -"盒里": "盒裡", -"监制": "監製", -"盘里": "盤裡", -"盘回": "盤迴", -"直接参与": "直接參与", -"直冲": "直衝", -"相克": "相剋", -"相干": "相干", -"相冲": "相衝", -"看台": "看臺", -"眼帘": "眼帘", -"眼眶里": "眼眶裡", -"眼里": "眼裡", -"困乏": "睏乏", -"睡着了": "睡著了", -"了如": "瞭如", -"了望": "瞭望", -"了然": "瞭然", -"了若指掌": "瞭若指掌", -"了解": "瞭解", -"蒙住": "矇住", -"蒙昧无知": "矇昧無知", -"蒙混": "矇混", -"蒙蒙": "矇矇", -"蒙眬": "矇矓", -"蒙蔽": "矇蔽", -"蒙骗": "矇騙", -"短发": "短髮", -"石英表": "石英錶", -"研制": "研製", -"砰当": "砰噹", -"砲台": "砲臺", -"朱唇皓齿": "硃唇皓齒", -"朱批": "硃批", -"朱砂": "硃砂", -"朱笔": "硃筆", -"朱红色": "硃紅色", -"朱色": "硃色", -"硬干": "硬幹", -"砚台": "硯臺", -"碑志": "碑誌", -"磁制": "磁製", -"磨制": "磨製", -"示复": "示覆", -"社里": "社裡", -"神采": "神采", -"御侮": "禦侮", -"御寇": "禦寇", -"御寒": "禦寒", -"御敌": "禦敵", -"秃发": "禿髮", -"秀发": "秀髮", -"私下里": "私下裡", -"秋天里": "秋天裡", -"秋裤": "秋褲", -"秒表": "秒錶", -"稀松": "稀鬆", -"禀复": "稟覆", -"稻谷": "稻穀", -"稽征": "稽征", -"谷仓": "穀倉", -"谷场": "穀場", -"谷子": "穀子", -"谷壳": "穀殼", -"谷物": "穀物", -"谷皮": "穀皮", -"谷神": "穀神", -"谷粒": "穀粒", -"谷舱": "穀艙", -"谷苗": "穀苗", -"谷草": "穀草", -"谷贱伤农": "穀賤傷農", -"谷道": "穀道", -"谷雨": "穀雨", -"谷类": "穀類", -"积极参与": "積极參与", -"积极参加": "積极參加", -"空荡": "空蕩", -"窗帘": "窗帘", -"窗明几净": "窗明几淨", -"窗台": "窗檯", -"窗台": "窗臺", -"窝里": "窩裡", -"窝阔台": "窩闊臺", -"穷追不舍": "窮追不捨", -"笆斗": "笆斗", -"笑里藏刀": "笑裡藏刀", -"第一卷": "第一卷", -"筋斗": "筋斗", -"答卷": "答卷", -"答复": "答複", -"答复": "答覆", -"筵几": "筵几", -"箕斗": "箕斗", -"签着": "簽著", -"吁求": "籲求", -"吁请": "籲請", -"粗制": "粗製", -"粗卤": "粗鹵", -"精干": "精幹", -"精明强干": "精明強幹", -"精致": "精緻", -"精制": "精製", -"精辟": "精辟", -"精采": "精采", -"糊里糊涂": "糊裡糊塗", -"团子": "糰子", -"系着": "系著", -"纪历": "紀曆", -"红发": "紅髮", -"红霉素": "紅黴素", -"纡回": "紆迴", -"纳采": "納采", -"素食面": "素食麵", -"素面": "素麵", -"紫微斗数": "紫微斗數", -"细致": "細緻", -"组里": "組裡", -"结发": "結髮", -"绝对参照": "絕對參照", -"丝来线去": "絲來線去", -"丝布": "絲布", -"丝板": "絲板", -"丝瓜布": "絲瓜布", -"丝绒布": "絲絨布", -"丝线": "絲線", -"丝织厂": "絲織廠", -"丝虫": "絲蟲", -"綑吊": "綑吊", -"经卷": "經卷", -"绿霉素": "綠黴素", -"维系": "維繫", -"绾发": "綰髮", -"网里": "網裡", -"紧绷": "緊繃", -"紧绷着": "緊繃著", -"紧追不舍": "緊追不捨", -"编制": "編製", -"编发": "編髮", -"缓冲": "緩衝", -"致密": "緻密", -"萦回": "縈迴", -"县里": "縣裡", -"县志": "縣誌", -"缝里": "縫裡", -"缝制": "縫製", -"纤夫": "縴夫", -"繁复": "繁複", -"绷住": "繃住", -"绷子": "繃子", -"绷带": "繃帶", -"绷紧": "繃緊", -"绷脸": "繃臉", -"绷着": "繃著", -"绷着脸": "繃著臉", -"绷着脸儿": "繃著臉兒", -"绷开": "繃開", -"绘制": "繪製", -"系上": "繫上", -"系到": "繫到", -"系囚": "繫囚", -"系心": "繫心", -"系念": "繫念", -"系怀": "繫懷", -"系数": "繫數", -"系于": "繫於", -"系系": "繫系", -"系紧": "繫緊", -"系绳": "繫繩", -"系着": "繫著", -"系辞": "繫辭", -"缴卷": "繳卷", -"累囚": "纍囚", -"累累": "纍纍", -"坛子": "罈子", -"坛坛罐罐": "罈罈罐罐", -"骂着": "罵著", -"美制": "美製", -"美发": "美髮", -"翻来覆去": "翻來覆去", -"翻云覆雨": "翻雲覆雨", -"老么": "老么", -"老板": "老闆", -"考卷": "考卷", -"耕获": "耕穫", -"聊斋志异": "聊齋誌異", -"联系": "聯係", -"联系": "聯繫", -"肉丝面": "肉絲麵", -"肉羹面": "肉羹麵", -"肉松": "肉鬆", -"肢体": "肢体", -"背向着": "背向著", -"背地里": "背地裡", -"胡里胡涂": "胡裡胡塗", -"能干": "能幹", -"脉冲": "脈衝", -"脱发": "脫髮", -"腊味": "腊味", -"腊笔": "腊筆", -"腊肉": "腊肉", -"脑子里": "腦子裡", -"腰里": "腰裡", -"胶卷": "膠卷", -"自制": "自製", -"自觉自愿": "自覺自愿", -"台上": "臺上", -"台下": "臺下", -"台中": "臺中", -"台北": "臺北", -"台南": "臺南", -"台地": "臺地", -"台塑": "臺塑", -"台大": "臺大", -"台币": "臺幣", -"台座": "臺座", -"台东": "臺東", -"台柱": "臺柱", -"台榭": "臺榭", -"台汽": "臺汽", -"台海": "臺海", -"台澎金马": "臺澎金馬", -"台湾": "臺灣", -"台灯": "臺燈", -"台球": "臺球", -"台省": "臺省", -"台端": "臺端", -"台糖": "臺糖", -"台肥": "臺肥", -"台航": "臺航", -"台视": "臺視", -"台词": "臺詞", -"台车": "臺車", -"台铁": "臺鐵", -"台阶": "臺階", -"台电": "臺電", -"台面": "臺面", -"舂谷": "舂穀", -"兴致": "興緻", -"兴高采烈": "興高采烈", -"旧历": "舊曆", -"舒卷": "舒卷", -"舞台": "舞臺", -"航海历": "航海曆", -"船只": "船隻", -"舰只": "艦隻", -"芬郁": "芬郁", -"花卷": "花卷", -"花盆里": "花盆裡", -"花采": "花采", -"苑里": "苑裡", -"若干": "若干", -"苦干": "苦幹", -"苦里": "苦裏", -"苦卤": "苦鹵", -"范仲淹": "范仲淹", -"范蠡": "范蠡", -"范阳": "范陽", -"茅台": "茅臺", -"茶几": "茶几", -"草丛里": "草叢裡", -"庄里": "莊裡", -"茎干": "莖幹", -"莽荡": "莽蕩", -"菌丝体": "菌絲体", -"菌丝体": "菌絲體", -"华里": "華裡", -"华发": "華髮", -"万卷": "萬卷", -"万历": "萬曆", -"万只": "萬隻", -"落发": "落髮", -"着儿": "著兒", -"着书立说": "著書立說", -"着色软体": "著色軟體", -"着重指出": "著重指出", -"着录": "著錄", -"着录规则": "著錄規則", -"蓄发": "蓄髮", -"蓄须": "蓄鬚", -"蓬发": "蓬髮", -"蓬松": "蓬鬆", -"莲台": "蓮臺", -"荡来荡去": "蕩來蕩去", -"荡女": "蕩女", -"荡妇": "蕩婦", -"荡寇": "蕩寇", -"荡平": "蕩平", -"荡涤": "蕩滌", -"荡漾": "蕩漾", -"荡然": "蕩然", -"荡舟": "蕩舟", -"荡船": "蕩船", -"荡荡": "蕩蕩", -"薑丝": "薑絲", -"薙发": "薙髮", -"借以": "藉以", -"借口": "藉口", -"借故": "藉故", -"借机": "藉機", -"借此": "藉此", -"借由": "藉由", -"借端": "藉端", -"借着": "藉著", -"借借": "藉藉", -"借词": "藉詞", -"借资": "藉資", -"借酒浇愁": "藉酒澆愁", -"藤制": "藤製", -"蕴含着": "蘊含著", -"蕴涵着": "蘊涵著", -"蕴借": "蘊藉", -"萝卜": "蘿蔔", -"虎须": "虎鬚", -"号志": "號誌", -"蜂后": "蜂后", -"蛮干": "蠻幹", -"行事历": "行事曆", -"胡同": "衚衕", -"冲上": "衝上", -"冲下": "衝下", -"冲来": "衝來", -"冲倒": "衝倒", -"冲出": "衝出", -"冲到": "衝到", -"冲刺": "衝刺", -"冲克": "衝剋", -"冲力": "衝力", -"冲劲": "衝勁", -"冲动": "衝動", -"冲去": "衝去", -"冲口": "衝口", -"冲垮": "衝垮", -"冲堂": "衝堂", -"冲压": "衝壓", -"冲天": "衝天", -"冲掉": "衝掉", -"冲撞": "衝撞", -"冲击": "衝擊", -"冲散": "衝散", -"冲决": "衝決", -"冲浪": "衝浪", -"冲激": "衝激", -"冲破": "衝破", -"冲程": "衝程", -"冲突": "衝突", -"冲线": "衝線", -"冲着": "衝著", -"冲冲": "衝衝", -"冲要": "衝要", -"冲起": "衝起", -"冲进": "衝進", -"冲过": "衝過", -"冲锋": "衝鋒", -"表里": "表裡", -"袖里": "袖裡", -"被里": "被裡", -"被复": "被複", -"被复": "被覆", -"被复着": "被覆著", -"被发": "被髮", -"裁并": "裁併", -"裁制": "裁製", -"里面": "裏面", -"里人": "裡人", -"里加": "裡加", -"里外": "裡外", -"里子": "裡子", -"里屋": "裡屋", -"里层": "裡層", -"里布": "裡布", -"里带": "裡帶", -"里弦": "裡弦", -"里应外合": "裡應外合", -"里拉": "裡拉", -"里斯": "裡斯", -"里海": "裡海", -"里脊": "裡脊", -"里衣": "裡衣", -"里里": "裡裡", -"里通外国": "裡通外國", -"里通外敌": "裡通外敵", -"里边": "裡邊", -"里间": "裡間", -"里面": "裡面", -"里头": "裡頭", -"制件": "製件", -"制作": "製作", -"制做": "製做", -"制备": "製備", -"制冰": "製冰", -"制冷": "製冷", -"制剂": "製劑", -"制品": "製品", -"制图": "製圖", -"制成": "製成", -"制法": "製法", -"制为": "製為", -"制片": "製片", -"制版": "製版", -"制程": "製程", -"制糖": "製糖", -"制纸": "製紙", -"制药": "製藥", -"制表": "製表", -"制裁": "製裁", -"制造": "製造", -"制革": "製革", -"制鞋": "製鞋", -"制盐": "製鹽", -"复仞年如": "複仞年如", -"复以百万": "複以百萬", -"复位": "複位", -"复信": "複信", -"复分数": "複分數", -"复列": "複列", -"复利": "複利", -"复印": "複印", -"复原": "複原", -"复句": "複句", -"复合": "複合", -"复名": "複名", -"复员": "複員", -"复壁": "複壁", -"复壮": "複壯", -"复姓": "複姓", -"复字键": "複字鍵", -"复审": "複審", -"复写": "複寫", -"复式": "複式", -"复复": "複復", -"复数": "複數", -"复本": "複本", -"复查": "複查", -"复核": "複核", -"复检": "複檢", -"复次": "複次", -"复比": "複比", -"复决": "複決", -"复活": "複活", -"复测": "複測", -"复亩珍": "複畝珍", -"复发": "複發", -"复目": "複目", -"复眼": "複眼", -"复种": "複種", -"复线": "複線", -"复习": "複習", -"复兴社": "複興社", -"复旧": "複舊", -"复色": "複色", -"复叶": "複葉", -"复盖": "複蓋", -"复苏": "複蘇", -"复制": "複製", -"复诊": "複診", -"复词": "複詞", -"复试": "複試", -"复课": "複課", -"复议": "複議", -"复变函数": "複變函數", -"复赛": "複賽", -"复述": "複述", -"复选": "複選", -"复钱": "複錢", -"复杂": "複雜", -"复电": "複電", -"复音": "複音", -"复韵": "複韻", -"衬里": "襯裡", -"西岳": "西嶽", -"西征": "西征", -"西历": "西曆", -"要冲": "要衝", -"要么": "要麼", -"复上": "覆上", -"复亡": "覆亡", -"复住": "覆住", -"复信": "覆信", -"复命": "覆命", -"复在": "覆在", -"复审": "覆審", -"复成": "覆成", -"复败": "覆敗", -"复文": "覆文", -"复校": "覆校", -"复核": "覆核", -"覆水难收": "覆水難收", -"复没": "覆沒", -"复灭": "覆滅", -"复盆": "覆盆", -"复舟": "覆舟", -"复着": "覆著", -"复盖": "覆蓋", -"复盖着": "覆蓋著", -"复试": "覆試", -"复议": "覆議", -"复车": "覆車", -"复载": "覆載", -"复辙": "覆轍", -"复电": "覆電", -"见复": "見覆", -"亲征": "親征", -"观众台": "觀眾臺", -"观台": "觀臺", -"观象台": "觀象臺", -"角落里": "角落裡", -"觔斗": "觔斗", -"触须": "觸鬚", -"订制": "訂製", -"诉说着": "訴說著", -"词汇": "詞彙", -"试卷": "試卷", -"诗卷": "詩卷", -"话里有话": "話裡有話", -"志哀": "誌哀", -"志喜": "誌喜", -"志庆": "誌慶", -"语云": "語云", -"语汇": "語彙", -"诬蔑": "誣衊", -"诵经台": "誦經臺", -"说着": "說著", -"课征": "課征", -"调制": "調製", -"调频台": "調頻臺", -"请参阅": "請參閱", -"讲台": "講臺", -"谢绝参观": "謝絕參觀", -"护发": "護髮", -"雠隙": "讎隙", -"豆腐干": "豆腐干", -"竖着": "豎著", -"丰富多采": "豐富多采", -"丰滨": "豐濱", -"丰滨乡": "豐濱鄉", -"丰采": "豐采", -"象征着": "象徵著", -"贵干": "貴幹", -"贾后": "賈后", -"赈饥": "賑饑", -"贤后": "賢后", -"质朴": "質朴", -"赌台": "賭檯", -"购并": "購併", -"赤松": "赤鬆", -"起吊": "起吊", -"起复": "起複", -"赶制": "趕製", -"跌荡": "跌蕩", -"跟斗": "跟斗", -"跳荡": "跳蕩", -"跳表": "跳錶", -"踬仆": "躓仆", -"躯干": "軀幹", -"车库里": "車庫裡", -"车站里": "車站裡", -"车里": "車裡", -"轻松": "輕鬆", -"轮回": "輪迴", -"转台": "轉檯", -"辛丑": "辛丑", -"辟邪": "辟邪", -"办伙": "辦伙", -"办公台": "辦公檯", -"辞汇": "辭彙", -"农历": "農曆", -"迂回": "迂迴", -"近日里": "近日裡", -"迥然回异": "迥然迴異", -"回光返照": "迴光返照", -"回向": "迴向", -"回圈": "迴圈", -"回廊": "迴廊", -"回形夹": "迴形夾", -"回文": "迴文", -"回旋": "迴旋", -"回流": "迴流", -"回环": "迴環", -"回荡": "迴盪", -"回纹针": "迴紋針", -"回绕": "迴繞", -"回肠": "迴腸", -"回荡": "迴蕩", -"回诵": "迴誦", -"回路": "迴路", -"回转": "迴轉", -"回递性": "迴遞性", -"回避": "迴避", -"回响": "迴響", -"回风": "迴風", -"回首": "迴首", -"迷蒙": "迷濛", -"退伙": "退伙", -"这么着": "這么著", -"这里": "這裏", -"这里": "這裡", -"这只": "這隻", -"这么": "這麼", -"这么着": "這麼著", -"通心面": "通心麵", -"速食面": "速食麵", -"连系": "連繫", -"连台好戏": "連臺好戲", -"游荡": "遊蕩", -"遍布": "遍佈", -"递回": "遞迴", -"远征": "遠征", -"适才": "適纔", -"遮复": "遮覆", -"还冲": "還衝", -"邋里邋遢": "邋裡邋遢", -"那里": "那裡", -"那只": "那隻", -"那么": "那麼", -"那么着": "那麼著", -"邪辟": "邪辟", -"郁烈": "郁烈", -"郁穆": "郁穆", -"郁郁": "郁郁", -"郁闭": "郁閉", -"郁馥": "郁馥", -"乡愿": "鄉愿", -"乡里": "鄉裡", -"邻里": "鄰裡", -"配合着": "配合著", -"配制": "配製", -"酒杯": "酒盃", -"酒坛": "酒罈", -"酥松": "酥鬆", -"醋坛": "醋罈", -"酝借": "醞藉", -"酝酿着": "醞釀著", -"医药": "醫葯", -"醲郁": "醲郁", -"酿制": "釀製", -"采地": "采地", -"采女": "采女", -"采声": "采聲", -"采色": "采色", -"采邑": "采邑", -"里程表": "里程錶", -"重折": "重摺", -"重复": "重複", -"重复": "重覆", -"重锤": "重鎚", -"野台戏": "野臺戲", -"金斗": "金斗", -"金表": "金錶", -"金发": "金髮", -"金霉素": "金黴素", -"钉锤": "釘鎚", -"银朱": "銀硃", -"银发": "銀髮", -"铜制": "銅製", -"铝制": "鋁製", -"钢制": "鋼製", -"录着": "錄著", -"录制": "錄製", -"表带": "錶帶", -"表店": "錶店", -"表厂": "錶廠", -"表壳": "錶殼", -"表链": "錶鏈", -"表面": "錶面", -"锅台": "鍋臺", -"锻鍊出": "鍛鍊出", -"锻鍊身体": "鍛鍊身体", -"锲而不舍": "鍥而不捨", -"锤儿": "鎚兒", -"锤子": "鎚子", -"锤头": "鎚頭", -"链霉素": "鏈黴素", -"镜台": "鏡臺", -"锈病": "鏽病", -"锈菌": "鏽菌", -"锈蚀": "鏽蝕", -"钟表": "鐘錶", -"铁锤": "鐵鎚", -"铁锈": "鐵鏽", -"长征": "長征", -"长发": "長髮", -"长须鲸": "長鬚鯨", -"门帘": "門帘", -"门斗": "門斗", -"门里": "門裡", -"开伙": "開伙", -"开卷": "開卷", -"开诚布公": "開誠佈公", -"开采": "開采", -"閒情逸致": "閒情逸緻", -"閒荡": "閒蕩", -"间不容发": "間不容髮", -"闵采尔": "閔采爾", -"阅卷": "閱卷", -"阑干": "闌干", -"关系": "關係", -"关系着": "關係著", -"防御": "防禦", -"防锈": "防鏽", -"防台": "防颱", -"阿斗": "阿斗", -"阿里": "阿裡", -"除旧布新": "除舊佈新", -"阴干": "陰干", -"阴历": "陰曆", -"阴郁": "陰郁", -"陆征祥": "陸征祥", -"阳春面": "陽春麵", -"阳历": "陽曆", -"阳台": "陽臺", -"只字": "隻字", -"只影": "隻影", -"只手遮天": "隻手遮天", -"只眼": "隻眼", -"只言片语": "隻言片語", -"只身": "隻身", -"雅致": "雅緻", -"雇佣": "雇佣", -"双折": "雙摺", -"杂志": "雜誌", -"鸡丝": "雞絲", -"鸡丝面": "雞絲麵", -"鸡腿面": "雞腿麵", -"鸡只": "雞隻", -"难舍": "難捨", -"雪里": "雪裡", -"云须": "雲鬚", -"电子表": "電子錶", -"电台": "電臺", -"电冲": "電衝", -"电复": "電覆", -"电视台": "電視臺", -"电表": "電錶", -"震荡": "震蕩", -"雾里": "霧裡", -"露台": "露臺", -"灵台": "靈臺", -"青瓦台": "青瓦臺", -"青霉": "青黴", -"面朝着": "面朝著", -"面临着": "面臨著", -"鞋里": "鞋裡", -"鞣制": "鞣製", -"秋千": "鞦韆", -"鞭辟入里": "鞭辟入裡", -"韩国制": "韓國製", -"韩制": "韓製", -"预制": "預製", -"颁布": "頒佈", -"头里": "頭裡", -"头发": "頭髮", -"颊须": "頰鬚", -"颠仆": "顛仆", -"颠复": "顛複", -"颠复": "顛覆", -"显着标志": "顯著標志", -"风土志": "風土誌", -"风斗": "風斗", -"风物志": "風物誌", -"风里": "風裡", -"风采": "風采", -"台风": "颱風", -"刮了": "颳了", -"刮倒": "颳倒", -"刮去": "颳去", -"刮得": "颳得", -"刮着": "颳著", -"刮走": "颳走", -"刮起": "颳起", -"刮风": "颳風", -"飘荡": "飄蕩", -"饭团": "飯糰", -"饼干": "餅干", -"馄饨面": "餛飩麵", -"饥不择食": "饑不擇食", -"饥寒": "饑寒", -"饥民": "饑民", -"饥渴": "饑渴", -"饥溺": "饑溺", -"饥荒": "饑荒", -"饥饱": "饑飽", -"饥饿": "饑餓", -"饥馑": "饑饉", -"首当其冲": "首當其衝", -"香郁": "香郁", -"馥郁": "馥郁", -"马里": "馬裡", -"马表": "馬錶", -"骀荡": "駘蕩", -"腾冲": "騰衝", -"骨子里": "骨子裡", -"骨干": "骨幹", -"骨灰坛": "骨灰罈", -"肮脏": "骯髒", -"脏乱": "髒亂", -"脏兮兮": "髒兮兮", -"脏字": "髒字", -"脏得": "髒得", -"脏东西": "髒東西", -"脏水": "髒水", -"脏的": "髒的", -"脏话": "髒話", -"脏钱": "髒錢", -"高干": "高幹", -"高台": "高臺", -"髭须": "髭鬚", -"发型": "髮型", -"发夹": "髮夾", -"发妻": "髮妻", -"发姐": "髮姐", -"发带": "髮帶", -"发廊": "髮廊", -"发式": "髮式", -"发指": "髮指", -"发捲": "髮捲", -"发根": "髮根", -"发毛": "髮毛", -"发油": "髮油", -"发状": "髮狀", -"发短心长": "髮短心長", -"发端": "髮端", -"发结": "髮結", -"发丝": "髮絲", -"发网": "髮網", -"发肤": "髮膚", -"发胶": "髮膠", -"发菜": "髮菜", -"发蜡": "髮蠟", -"发辫": "髮辮", -"发针": "髮針", -"发长": "髮長", -"发际": "髮際", -"发霜": "髮霜", -"发髻": "髮髻", -"发鬓": "髮鬢", -"鬅松": "鬅鬆", -"松了": "鬆了", -"松些": "鬆些", -"松劲": "鬆勁", -"松动": "鬆動", -"松口": "鬆口", -"松土": "鬆土", -"松弛": "鬆弛", -"松快": "鬆快", -"松懈": "鬆懈", -"松手": "鬆手", -"松散": "鬆散", -"松林": "鬆林", -"松柔": "鬆柔", -"松毛虫": "鬆毛蟲", -"松浮": "鬆浮", -"松涛": "鬆濤", -"松科": "鬆科", -"松节油": "鬆節油", -"松绑": "鬆綁", -"松紧": "鬆緊", -"松缓": "鬆緩", -"松脆": "鬆脆", -"松脱": "鬆脫", -"松起": "鬆起", -"松软": "鬆軟", -"松通": "鬆通", -"松开": "鬆開", -"松饼": "鬆餅", -"松松": "鬆鬆", -"鬈发": "鬈髮", -"胡子": "鬍子", -"胡梢": "鬍梢", -"胡渣": "鬍渣", -"胡髭": "鬍髭", -"胡须": "鬍鬚", -"须根": "鬚根", -"须毛": "鬚毛", -"须生": "鬚生", -"须眉": "鬚眉", -"须发": "鬚髮", -"须须": "鬚鬚", -"鬓发": "鬢髮", -"斗着": "鬥著", -"闹着玩儿": "鬧著玩儿", -"闹着玩儿": "鬧著玩兒", -"郁郁": "鬱郁", -"鱼松": "魚鬆", -"鲸须": "鯨鬚", -"鲇鱼": "鯰魚", -"鹤发": "鶴髮", -"卤化": "鹵化", -"卤味": "鹵味", -"卤族": "鹵族", -"卤水": "鹵水", -"卤汁": "鹵汁", -"卤簿": "鹵簿", -"卤素": "鹵素", -"卤莽": "鹵莽", -"卤钝": "鹵鈍", -"咸味": "鹹味", -"咸土": "鹹土", -"咸度": "鹹度", -"咸得": "鹹得", -"咸水": "鹹水", -"咸海": "鹹海", -"咸淡": "鹹淡", -"咸湖": "鹹湖", -"咸汤": "鹹湯", -"咸的": "鹹的", -"咸肉": "鹹肉", -"咸菜": "鹹菜", -"咸蛋": "鹹蛋", -"咸猪肉": "鹹豬肉", -"咸类": "鹹類", -"咸鱼": "鹹魚", -"咸鸭蛋": "鹹鴨蛋", -"咸卤": "鹹鹵", -"咸咸": "鹹鹹", -"盐卤": "鹽鹵", -"面价": "麵價", -"面包": "麵包", -"面团": "麵團", -"面店": "麵店", -"面厂": "麵廠", -"面杖": "麵杖", -"面条": "麵條", -"面灰": "麵灰", -"面皮": "麵皮", -"面筋": "麵筋", -"面粉": "麵粉", -"面糊": "麵糊", -"面线": "麵線", -"面茶": "麵茶", -"面食": "麵食", -"面饺": "麵餃", -"面饼": "麵餅", -"麻酱面": "麻醬麵", -"黄历": "黃曆", -"黄发垂髫": "黃髮垂髫", -"黑发": "黑髮", -"黑松": "黑鬆", -"霉毒": "黴毒", -"霉菌": "黴菌", -"鼓里": "鼓裡", -"冬冬": "鼕鼕", -"龙卷": "龍卷", -"龙须": "龍鬚", -} - -zh2Hans = { -'顯著': '显著', -'土著': '土著', -'印表機': '打印机', -'說明檔案': '帮助文件', -"瀋": "沈", -"畫": "划", -"鍾": "钟", -"靦": "腼", -"餘": "余", -"鯰": "鲇", -"鹼": "碱", -"㠏": "㟆", -"𡞵": "㛟", -"万": "万", -"与": "与", -"丑": "丑", -"丟": "丢", -"並": "并", -"丰": "丰", -"么": "么", -"乾": "干", -"乾坤": "乾坤", -"乾隆": "乾隆", -"亂": "乱", -"云": "云", -"亙": "亘", -"亞": "亚", -"仆": "仆", -"价": "价", -"伙": "伙", -"佇": "伫", -"佈": "布", -"体": "体", -"余": "余", -"佣": "佣", -"併": "并", -"來": "来", -"侖": "仑", -"侶": "侣", -"俁": "俣", -"係": "系", -"俔": "伣", -"俠": "侠", -"倀": "伥", -"倆": "俩", -"倈": "俫", -"倉": "仓", -"個": "个", -"們": "们", -"倫": "伦", -"偉": "伟", -"側": "侧", -"偵": "侦", -"偽": "伪", -"傑": "杰", -"傖": "伧", -"傘": "伞", -"備": "备", -"傢": "家", -"傭": "佣", -"傯": "偬", -"傳": "传", -"傴": "伛", -"債": "债", -"傷": "伤", -"傾": "倾", -"僂": "偻", -"僅": "仅", -"僉": "佥", -"僑": "侨", -"僕": "仆", -"僞": "伪", -"僥": "侥", -"僨": "偾", -"價": "价", -"儀": "仪", -"儂": "侬", -"億": "亿", -"儈": "侩", -"儉": "俭", -"儐": "傧", -"儔": "俦", -"儕": "侪", -"儘": "尽", -"償": "偿", -"優": "优", -"儲": "储", -"儷": "俪", -"儸": "㑩", -"儺": "傩", -"儻": "傥", -"儼": "俨", -"儿": "儿", -"兇": "凶", -"兌": "兑", -"兒": "儿", -"兗": "兖", -"党": "党", -"內": "内", -"兩": "两", -"冊": "册", -"冪": "幂", -"准": "准", -"凈": "净", -"凍": "冻", -"凜": "凛", -"几": "几", -"凱": "凯", -"划": "划", -"別": "别", -"刪": "删", -"剄": "刭", -"則": "则", -"剋": "克", -"剎": "刹", -"剗": "刬", -"剛": "刚", -"剝": "剥", -"剮": "剐", -"剴": "剀", -"創": "创", -"劃": "划", -"劇": "剧", -"劉": "刘", -"劊": "刽", -"劌": "刿", -"劍": "剑", -"劏": "㓥", -"劑": "剂", -"劚": "㔉", -"勁": "劲", -"動": "动", -"務": "务", -"勛": "勋", -"勝": "胜", -"勞": "劳", -"勢": "势", -"勩": "勚", -"勱": "劢", -"勵": "励", -"勸": "劝", -"勻": "匀", -"匭": "匦", -"匯": "汇", -"匱": "匮", -"區": "区", -"協": "协", -"卷": "卷", -"卻": "却", -"厂": "厂", -"厙": "厍", -"厠": "厕", -"厭": "厌", -"厲": "厉", -"厴": "厣", -"參": "参", -"叄": "叁", -"叢": "丛", -"台": "台", -"叶": "叶", -"吊": "吊", -"后": "后", -"吳": "吴", -"吶": "呐", -"呂": "吕", -"獃": "呆", -"咼": "呙", -"員": "员", -"唄": "呗", -"唚": "吣", -"問": "问", -"啓": "启", -"啞": "哑", -"啟": "启", -"啢": "唡", -"喎": "㖞", -"喚": "唤", -"喪": "丧", -"喬": "乔", -"單": "单", -"喲": "哟", -"嗆": "呛", -"嗇": "啬", -"嗊": "唝", -"嗎": "吗", -"嗚": "呜", -"嗩": "唢", -"嗶": "哔", -"嘆": "叹", -"嘍": "喽", -"嘔": "呕", -"嘖": "啧", -"嘗": "尝", -"嘜": "唛", -"嘩": "哗", -"嘮": "唠", -"嘯": "啸", -"嘰": "叽", -"嘵": "哓", -"嘸": "呒", -"嘽": "啴", -"噁": "恶", -"噓": "嘘", -"噚": "㖊", -"噝": "咝", -"噠": "哒", -"噥": "哝", -"噦": "哕", -"噯": "嗳", -"噲": "哙", -"噴": "喷", -"噸": "吨", -"噹": "当", -"嚀": "咛", -"嚇": "吓", -"嚌": "哜", -"嚕": "噜", -"嚙": "啮", -"嚥": "咽", -"嚦": "呖", -"嚨": "咙", -"嚮": "向", -"嚲": "亸", -"嚳": "喾", -"嚴": "严", -"嚶": "嘤", -"囀": "啭", -"囁": "嗫", -"囂": "嚣", -"囅": "冁", -"囈": "呓", -"囌": "苏", -"囑": "嘱", -"囪": "囱", -"圇": "囵", -"國": "国", -"圍": "围", -"園": "园", -"圓": "圆", -"圖": "图", -"團": "团", -"坏": "坏", -"垵": "埯", -"埡": "垭", -"埰": "采", -"執": "执", -"堅": "坚", -"堊": "垩", -"堖": "垴", -"堝": "埚", -"堯": "尧", -"報": "报", -"場": "场", -"塊": "块", -"塋": "茔", -"塏": "垲", -"塒": "埘", -"塗": "涂", -"塚": "冢", -"塢": "坞", -"塤": "埙", -"塵": "尘", -"塹": "堑", -"墊": "垫", -"墜": "坠", -"墮": "堕", -"墳": "坟", -"墻": "墙", -"墾": "垦", -"壇": "坛", -"壈": "𡒄", -"壋": "垱", -"壓": "压", -"壘": "垒", -"壙": "圹", -"壚": "垆", -"壞": "坏", -"壟": "垄", -"壠": "垅", -"壢": "坜", -"壩": "坝", -"壯": "壮", -"壺": "壶", -"壼": "壸", -"壽": "寿", -"夠": "够", -"夢": "梦", -"夾": "夹", -"奐": "奂", -"奧": "奥", -"奩": "奁", -"奪": "夺", -"奬": "奖", -"奮": "奋", -"奼": "姹", -"妝": "妆", -"姍": "姗", -"姜": "姜", -"姦": "奸", -"娛": "娱", -"婁": "娄", -"婦": "妇", -"婭": "娅", -"媧": "娲", -"媯": "妫", -"媼": "媪", -"媽": "妈", -"嫗": "妪", -"嫵": "妩", -"嫻": "娴", -"嫿": "婳", -"嬀": "妫", -"嬈": "娆", -"嬋": "婵", -"嬌": "娇", -"嬙": "嫱", -"嬡": "嫒", -"嬤": "嬷", -"嬪": "嫔", -"嬰": "婴", -"嬸": "婶", -"孌": "娈", -"孫": "孙", -"學": "学", -"孿": "孪", -"宁": "宁", -"宮": "宫", -"寢": "寝", -"實": "实", -"寧": "宁", -"審": "审", -"寫": "写", -"寬": "宽", -"寵": "宠", -"寶": "宝", -"將": "将", -"專": "专", -"尋": "寻", -"對": "对", -"導": "导", -"尷": "尴", -"屆": "届", -"屍": "尸", -"屓": "屃", -"屜": "屉", -"屢": "屡", -"層": "层", -"屨": "屦", -"屬": "属", -"岡": "冈", -"峴": "岘", -"島": "岛", -"峽": "峡", -"崍": "崃", -"崗": "岗", -"崢": "峥", -"崬": "岽", -"嵐": "岚", -"嶁": "嵝", -"嶄": "崭", -"嶇": "岖", -"嶔": "嵚", -"嶗": "崂", -"嶠": "峤", -"嶢": "峣", -"嶧": "峄", -"嶮": "崄", -"嶴": "岙", -"嶸": "嵘", -"嶺": "岭", -"嶼": "屿", -"嶽": "岳", -"巋": "岿", -"巒": "峦", -"巔": "巅", -"巰": "巯", -"帘": "帘", -"帥": "帅", -"師": "师", -"帳": "帐", -"帶": "带", -"幀": "帧", -"幃": "帏", -"幗": "帼", -"幘": "帻", -"幟": "帜", -"幣": "币", -"幫": "帮", -"幬": "帱", -"幹": "干", -"幺": "么", -"幾": "几", -"广": "广", -"庫": "库", -"廁": "厕", -"廂": "厢", -"廄": "厩", -"廈": "厦", -"廚": "厨", -"廝": "厮", -"廟": "庙", -"廠": "厂", -"廡": "庑", -"廢": "废", -"廣": "广", -"廩": "廪", -"廬": "庐", -"廳": "厅", -"弒": "弑", -"弳": "弪", -"張": "张", -"強": "强", -"彆": "别", -"彈": "弹", -"彌": "弥", -"彎": "弯", -"彙": "汇", -"彞": "彝", -"彥": "彦", -"征": "征", -"後": "后", -"徑": "径", -"從": "从", -"徠": "徕", -"復": "复", -"徵": "征", -"徹": "彻", -"志": "志", -"恆": "恒", -"恥": "耻", -"悅": "悦", -"悞": "悮", -"悵": "怅", -"悶": "闷", -"惡": "恶", -"惱": "恼", -"惲": "恽", -"惻": "恻", -"愛": "爱", -"愜": "惬", -"愨": "悫", -"愴": "怆", -"愷": "恺", -"愾": "忾", -"愿": "愿", -"慄": "栗", -"態": "态", -"慍": "愠", -"慘": "惨", -"慚": "惭", -"慟": "恸", -"慣": "惯", -"慤": "悫", -"慪": "怄", -"慫": "怂", -"慮": "虑", -"慳": "悭", -"慶": "庆", -"憂": "忧", -"憊": "惫", -"憐": "怜", -"憑": "凭", -"憒": "愦", -"憚": "惮", -"憤": "愤", -"憫": "悯", -"憮": "怃", -"憲": "宪", -"憶": "忆", -"懇": "恳", -"應": "应", -"懌": "怿", -"懍": "懔", -"懞": "蒙", -"懟": "怼", -"懣": "懑", -"懨": "恹", -"懲": "惩", -"懶": "懒", -"懷": "怀", -"懸": "悬", -"懺": "忏", -"懼": "惧", -"懾": "慑", -"戀": "恋", -"戇": "戆", -"戔": "戋", -"戧": "戗", -"戩": "戬", -"戰": "战", -"戱": "戯", -"戲": "戏", -"戶": "户", -"担": "担", -"拋": "抛", -"挩": "捝", -"挾": "挟", -"捨": "舍", -"捫": "扪", -"据": "据", -"掃": "扫", -"掄": "抡", -"掗": "挜", -"掙": "挣", -"掛": "挂", -"採": "采", -"揀": "拣", -"揚": "扬", -"換": "换", -"揮": "挥", -"損": "损", -"搖": "摇", -"搗": "捣", -"搵": "揾", -"搶": "抢", -"摑": "掴", -"摜": "掼", -"摟": "搂", -"摯": "挚", -"摳": "抠", -"摶": "抟", -"摺": "折", -"摻": "掺", -"撈": "捞", -"撏": "挦", -"撐": "撑", -"撓": "挠", -"撝": "㧑", -"撟": "挢", -"撣": "掸", -"撥": "拨", -"撫": "抚", -"撲": "扑", -"撳": "揿", -"撻": "挞", -"撾": "挝", -"撿": "捡", -"擁": "拥", -"擄": "掳", -"擇": "择", -"擊": "击", -"擋": "挡", -"擓": "㧟", -"擔": "担", -"據": "据", -"擠": "挤", -"擬": "拟", -"擯": "摈", -"擰": "拧", -"擱": "搁", -"擲": "掷", -"擴": "扩", -"擷": "撷", -"擺": "摆", -"擻": "擞", -"擼": "撸", -"擾": "扰", -"攄": "摅", -"攆": "撵", -"攏": "拢", -"攔": "拦", -"攖": "撄", -"攙": "搀", -"攛": "撺", -"攜": "携", -"攝": "摄", -"攢": "攒", -"攣": "挛", -"攤": "摊", -"攪": "搅", -"攬": "揽", -"敗": "败", -"敘": "叙", -"敵": "敌", -"數": "数", -"斂": "敛", -"斃": "毙", -"斕": "斓", -"斗": "斗", -"斬": "斩", -"斷": "断", -"於": "于", -"時": "时", -"晉": "晋", -"晝": "昼", -"暈": "晕", -"暉": "晖", -"暘": "旸", -"暢": "畅", -"暫": "暂", -"曄": "晔", -"曆": "历", -"曇": "昙", -"曉": "晓", -"曏": "向", -"曖": "暧", -"曠": "旷", -"曨": "昽", -"曬": "晒", -"書": "书", -"會": "会", -"朧": "胧", -"朮": "术", -"术": "术", -"朴": "朴", -"東": "东", -"杴": "锨", -"极": "极", -"柜": "柜", -"柵": "栅", -"桿": "杆", -"梔": "栀", -"梘": "枧", -"條": "条", -"梟": "枭", -"梲": "棁", -"棄": "弃", -"棖": "枨", -"棗": "枣", -"棟": "栋", -"棧": "栈", -"棲": "栖", -"棶": "梾", -"椏": "桠", -"楊": "杨", -"楓": "枫", -"楨": "桢", -"業": "业", -"極": "极", -"榪": "杩", -"榮": "荣", -"榲": "榅", -"榿": "桤", -"構": "构", -"槍": "枪", -"槤": "梿", -"槧": "椠", -"槨": "椁", -"槳": "桨", -"樁": "桩", -"樂": "乐", -"樅": "枞", -"樓": "楼", -"標": "标", -"樞": "枢", -"樣": "样", -"樸": "朴", -"樹": "树", -"樺": "桦", -"橈": "桡", -"橋": "桥", -"機": "机", -"橢": "椭", -"橫": "横", -"檁": "檩", -"檉": "柽", -"檔": "档", -"檜": "桧", -"檟": "槚", -"檢": "检", -"檣": "樯", -"檮": "梼", -"檯": "台", -"檳": "槟", -"檸": "柠", -"檻": "槛", -"櫃": "柜", -"櫓": "橹", -"櫚": "榈", -"櫛": "栉", -"櫝": "椟", -"櫞": "橼", -"櫟": "栎", -"櫥": "橱", -"櫧": "槠", -"櫨": "栌", -"櫪": "枥", -"櫫": "橥", -"櫬": "榇", -"櫱": "蘖", -"櫳": "栊", -"櫸": "榉", -"櫻": "樱", -"欄": "栏", -"權": "权", -"欏": "椤", -"欒": "栾", -"欖": "榄", -"欞": "棂", -"欽": "钦", -"歐": "欧", -"歟": "欤", -"歡": "欢", -"歲": "岁", -"歷": "历", -"歸": "归", -"歿": "殁", -"殘": "残", -"殞": "殒", -"殤": "殇", -"殨": "㱮", -"殫": "殚", -"殮": "殓", -"殯": "殡", -"殰": "㱩", -"殲": "歼", -"殺": "杀", -"殻": "壳", -"殼": "壳", -"毀": "毁", -"毆": "殴", -"毿": "毵", -"氂": "牦", -"氈": "毡", -"氌": "氇", -"氣": "气", -"氫": "氢", -"氬": "氩", -"氳": "氲", -"汙": "污", -"決": "决", -"沒": "没", -"沖": "冲", -"況": "况", -"洶": "汹", -"浹": "浃", -"涂": "涂", -"涇": "泾", -"涼": "凉", -"淀": "淀", -"淒": "凄", -"淚": "泪", -"淥": "渌", -"淨": "净", -"淩": "凌", -"淪": "沦", -"淵": "渊", -"淶": "涞", -"淺": "浅", -"渙": "涣", -"減": "减", -"渦": "涡", -"測": "测", -"渾": "浑", -"湊": "凑", -"湞": "浈", -"湯": "汤", -"溈": "沩", -"準": "准", -"溝": "沟", -"溫": "温", -"滄": "沧", -"滅": "灭", -"滌": "涤", -"滎": "荥", -"滬": "沪", -"滯": "滞", -"滲": "渗", -"滷": "卤", -"滸": "浒", -"滻": "浐", -"滾": "滚", -"滿": "满", -"漁": "渔", -"漚": "沤", -"漢": "汉", -"漣": "涟", -"漬": "渍", -"漲": "涨", -"漵": "溆", -"漸": "渐", -"漿": "浆", -"潁": "颍", -"潑": "泼", -"潔": "洁", -"潙": "沩", -"潛": "潜", -"潤": "润", -"潯": "浔", -"潰": "溃", -"潷": "滗", -"潿": "涠", -"澀": "涩", -"澆": "浇", -"澇": "涝", -"澐": "沄", -"澗": "涧", -"澠": "渑", -"澤": "泽", -"澦": "滪", -"澩": "泶", -"澮": "浍", -"澱": "淀", -"濁": "浊", -"濃": "浓", -"濕": "湿", -"濘": "泞", -"濛": "蒙", -"濟": "济", -"濤": "涛", -"濫": "滥", -"濰": "潍", -"濱": "滨", -"濺": "溅", -"濼": "泺", -"濾": "滤", -"瀅": "滢", -"瀆": "渎", -"瀇": "㲿", -"瀉": "泻", -"瀋": "沈", -"瀏": "浏", -"瀕": "濒", -"瀘": "泸", -"瀝": "沥", -"瀟": "潇", -"瀠": "潆", -"瀦": "潴", -"瀧": "泷", -"瀨": "濑", -"瀰": "弥", -"瀲": "潋", -"瀾": "澜", -"灃": "沣", -"灄": "滠", -"灑": "洒", -"灕": "漓", -"灘": "滩", -"灝": "灏", -"灠": "漤", -"灣": "湾", -"灤": "滦", -"灧": "滟", -"災": "灾", -"為": "为", -"烏": "乌", -"烴": "烃", -"無": "无", -"煉": "炼", -"煒": "炜", -"煙": "烟", -"煢": "茕", -"煥": "焕", -"煩": "烦", -"煬": "炀", -"煱": "㶽", -"熅": "煴", -"熒": "荧", -"熗": "炝", -"熱": "热", -"熲": "颎", -"熾": "炽", -"燁": "烨", -"燈": "灯", -"燉": "炖", -"燒": "烧", -"燙": "烫", -"燜": "焖", -"營": "营", -"燦": "灿", -"燭": "烛", -"燴": "烩", -"燶": "㶶", -"燼": "烬", -"燾": "焘", -"爍": "烁", -"爐": "炉", -"爛": "烂", -"爭": "争", -"爲": "为", -"爺": "爷", -"爾": "尔", -"牆": "墙", -"牘": "牍", -"牽": "牵", -"犖": "荦", -"犢": "犊", -"犧": "牺", -"狀": "状", -"狹": "狭", -"狽": "狈", -"猙": "狰", -"猶": "犹", -"猻": "狲", -"獁": "犸", -"獄": "狱", -"獅": "狮", -"獎": "奖", -"獨": "独", -"獪": "狯", -"獫": "猃", -"獮": "狝", -"獰": "狞", -"獱": "㺍", -"獲": "获", -"獵": "猎", -"獷": "犷", -"獸": "兽", -"獺": "獭", -"獻": "献", -"獼": "猕", -"玀": "猡", -"現": "现", -"琺": "珐", -"琿": "珲", -"瑋": "玮", -"瑒": "玚", -"瑣": "琐", -"瑤": "瑶", -"瑩": "莹", -"瑪": "玛", -"瑲": "玱", -"璉": "琏", -"璣": "玑", -"璦": "瑷", -"璫": "珰", -"環": "环", -"璽": "玺", -"瓊": "琼", -"瓏": "珑", -"瓔": "璎", -"瓚": "瓒", -"甌": "瓯", -"產": "产", -"産": "产", -"畝": "亩", -"畢": "毕", -"異": "异", -"畵": "画", -"當": "当", -"疇": "畴", -"疊": "叠", -"痙": "痉", -"痾": "疴", -"瘂": "痖", -"瘋": "疯", -"瘍": "疡", -"瘓": "痪", -"瘞": "瘗", -"瘡": "疮", -"瘧": "疟", -"瘮": "瘆", -"瘲": "疭", -"瘺": "瘘", -"瘻": "瘘", -"療": "疗", -"癆": "痨", -"癇": "痫", -"癉": "瘅", -"癘": "疠", -"癟": "瘪", -"癢": "痒", -"癤": "疖", -"癥": "症", -"癧": "疬", -"癩": "癞", -"癬": "癣", -"癭": "瘿", -"癮": "瘾", -"癰": "痈", -"癱": "瘫", -"癲": "癫", -"發": "发", -"皚": "皑", -"皰": "疱", -"皸": "皲", -"皺": "皱", -"盃": "杯", -"盜": "盗", -"盞": "盏", -"盡": "尽", -"監": "监", -"盤": "盘", -"盧": "卢", -"盪": "荡", -"眥": "眦", -"眾": "众", -"睏": "困", -"睜": "睁", -"睞": "睐", -"瞘": "眍", -"瞜": "䁖", -"瞞": "瞒", -"瞭": "了", -"瞶": "瞆", -"瞼": "睑", -"矇": "蒙", -"矓": "眬", -"矚": "瞩", -"矯": "矫", -"硃": "朱", -"硜": "硁", -"硤": "硖", -"硨": "砗", -"确": "确", -"硯": "砚", -"碩": "硕", -"碭": "砀", -"碸": "砜", -"確": "确", -"碼": "码", -"磑": "硙", -"磚": "砖", -"磣": "碜", -"磧": "碛", -"磯": "矶", -"磽": "硗", -"礆": "硷", -"礎": "础", -"礙": "碍", -"礦": "矿", -"礪": "砺", -"礫": "砾", -"礬": "矾", -"礱": "砻", -"祿": "禄", -"禍": "祸", -"禎": "祯", -"禕": "祎", -"禡": "祃", -"禦": "御", -"禪": "禅", -"禮": "礼", -"禰": "祢", -"禱": "祷", -"禿": "秃", -"秈": "籼", -"种": "种", -"稅": "税", -"稈": "秆", -"稏": "䅉", -"稟": "禀", -"種": "种", -"稱": "称", -"穀": "谷", -"穌": "稣", -"積": "积", -"穎": "颖", -"穠": "秾", -"穡": "穑", -"穢": "秽", -"穩": "稳", -"穫": "获", -"穭": "稆", -"窩": "窝", -"窪": "洼", -"窮": "穷", -"窯": "窑", -"窵": "窎", -"窶": "窭", -"窺": "窥", -"竄": "窜", -"竅": "窍", -"竇": "窦", -"竈": "灶", -"竊": "窃", -"竪": "竖", -"競": "竞", -"筆": "笔", -"筍": "笋", -"筑": "筑", -"筧": "笕", -"筴": "䇲", -"箋": "笺", -"箏": "筝", -"節": "节", -"範": "范", -"築": "筑", -"篋": "箧", -"篔": "筼", -"篤": "笃", -"篩": "筛", -"篳": "筚", -"簀": "箦", -"簍": "篓", -"簞": "箪", -"簡": "简", -"簣": "篑", -"簫": "箫", -"簹": "筜", -"簽": "签", -"簾": "帘", -"籃": "篮", -"籌": "筹", -"籖": "签", -"籙": "箓", -"籜": "箨", -"籟": "籁", -"籠": "笼", -"籩": "笾", -"籪": "簖", -"籬": "篱", -"籮": "箩", -"籲": "吁", -"粵": "粤", -"糝": "糁", -"糞": "粪", -"糧": "粮", -"糰": "团", -"糲": "粝", -"糴": "籴", -"糶": "粜", -"糹": "纟", -"糾": "纠", -"紀": "纪", -"紂": "纣", -"約": "约", -"紅": "红", -"紆": "纡", -"紇": "纥", -"紈": "纨", -"紉": "纫", -"紋": "纹", -"納": "纳", -"紐": "纽", -"紓": "纾", -"純": "纯", -"紕": "纰", -"紖": "纼", -"紗": "纱", -"紘": "纮", -"紙": "纸", -"級": "级", -"紛": "纷", -"紜": "纭", -"紝": "纴", -"紡": "纺", -"紬": "䌷", -"細": "细", -"紱": "绂", -"紲": "绁", -"紳": "绅", -"紵": "纻", -"紹": "绍", -"紺": "绀", -"紼": "绋", -"紿": "绐", -"絀": "绌", -"終": "终", -"組": "组", -"絅": "䌹", -"絆": "绊", -"絎": "绗", -"結": "结", -"絕": "绝", -"絛": "绦", -"絝": "绔", -"絞": "绞", -"絡": "络", -"絢": "绚", -"給": "给", -"絨": "绒", -"絰": "绖", -"統": "统", -"絲": "丝", -"絳": "绛", -"絶": "绝", -"絹": "绢", -"綁": "绑", -"綃": "绡", -"綆": "绠", -"綈": "绨", -"綉": "绣", -"綌": "绤", -"綏": "绥", -"綐": "䌼", -"經": "经", -"綜": "综", -"綞": "缍", -"綠": "绿", -"綢": "绸", -"綣": "绻", -"綫": "线", -"綬": "绶", -"維": "维", -"綯": "绹", -"綰": "绾", -"綱": "纲", -"網": "网", -"綳": "绷", -"綴": "缀", -"綵": "䌽", -"綸": "纶", -"綹": "绺", -"綺": "绮", -"綻": "绽", -"綽": "绰", -"綾": "绫", -"綿": "绵", -"緄": "绲", -"緇": "缁", -"緊": "紧", -"緋": "绯", -"緑": "绿", -"緒": "绪", -"緓": "绬", -"緔": "绱", -"緗": "缃", -"緘": "缄", -"緙": "缂", -"線": "线", -"緝": "缉", -"緞": "缎", -"締": "缔", -"緡": "缗", -"緣": "缘", -"緦": "缌", -"編": "编", -"緩": "缓", -"緬": "缅", -"緯": "纬", -"緱": "缑", -"緲": "缈", -"練": "练", -"緶": "缏", -"緹": "缇", -"緻": "致", -"縈": "萦", -"縉": "缙", -"縊": "缢", -"縋": "缒", -"縐": "绉", -"縑": "缣", -"縕": "缊", -"縗": "缞", -"縛": "缚", -"縝": "缜", -"縞": "缟", -"縟": "缛", -"縣": "县", -"縧": "绦", -"縫": "缝", -"縭": "缡", -"縮": "缩", -"縱": "纵", -"縲": "缧", -"縳": "䌸", -"縴": "纤", -"縵": "缦", -"縶": "絷", -"縷": "缕", -"縹": "缥", -"總": "总", -"績": "绩", -"繃": "绷", -"繅": "缫", -"繆": "缪", -"繒": "缯", -"織": "织", -"繕": "缮", -"繚": "缭", -"繞": "绕", -"繡": "绣", -"繢": "缋", -"繩": "绳", -"繪": "绘", -"繫": "系", -"繭": "茧", -"繮": "缰", -"繯": "缳", -"繰": "缲", -"繳": "缴", -"繸": "䍁", -"繹": "绎", -"繼": "继", -"繽": "缤", -"繾": "缱", -"繿": "䍀", -"纈": "缬", -"纊": "纩", -"續": "续", -"纍": "累", -"纏": "缠", -"纓": "缨", -"纔": "才", -"纖": "纤", -"纘": "缵", -"纜": "缆", -"缽": "钵", -"罈": "坛", -"罌": "罂", -"罰": "罚", -"罵": "骂", -"罷": "罢", -"羅": "罗", -"羆": "罴", -"羈": "羁", -"羋": "芈", -"羥": "羟", -"義": "义", -"習": "习", -"翹": "翘", -"耬": "耧", -"耮": "耢", -"聖": "圣", -"聞": "闻", -"聯": "联", -"聰": "聪", -"聲": "声", -"聳": "耸", -"聵": "聩", -"聶": "聂", -"職": "职", -"聹": "聍", -"聽": "听", -"聾": "聋", -"肅": "肃", -"胜": "胜", -"脅": "胁", -"脈": "脉", -"脛": "胫", -"脫": "脱", -"脹": "胀", -"腊": "腊", -"腎": "肾", -"腖": "胨", -"腡": "脶", -"腦": "脑", -"腫": "肿", -"腳": "脚", -"腸": "肠", -"膃": "腽", -"膚": "肤", -"膠": "胶", -"膩": "腻", -"膽": "胆", -"膾": "脍", -"膿": "脓", -"臉": "脸", -"臍": "脐", -"臏": "膑", -"臘": "腊", -"臚": "胪", -"臟": "脏", -"臠": "脔", -"臢": "臜", -"臥": "卧", -"臨": "临", -"臺": "台", -"與": "与", -"興": "兴", -"舉": "举", -"舊": "旧", -"艙": "舱", -"艤": "舣", -"艦": "舰", -"艫": "舻", -"艱": "艰", -"艷": "艳", -"芻": "刍", -"苧": "苎", -"苹": "苹", -"范": "范", -"茲": "兹", -"荊": "荆", -"莊": "庄", -"莖": "茎", -"莢": "荚", -"莧": "苋", -"華": "华", -"萇": "苌", -"萊": "莱", -"萬": "万", -"萵": "莴", -"葉": "叶", -"葒": "荭", -"著名": "著名", -"葤": "荮", -"葦": "苇", -"葯": "药", -"葷": "荤", -"蒓": "莼", -"蒔": "莳", -"蒞": "莅", -"蒼": "苍", -"蓀": "荪", -"蓋": "盖", -"蓮": "莲", -"蓯": "苁", -"蓴": "莼", -"蓽": "荜", -"蔔": "卜", -"蔞": "蒌", -"蔣": "蒋", -"蔥": "葱", -"蔦": "茑", -"蔭": "荫", -"蕁": "荨", -"蕆": "蒇", -"蕎": "荞", -"蕒": "荬", -"蕓": "芸", -"蕕": "莸", -"蕘": "荛", -"蕢": "蒉", -"蕩": "荡", -"蕪": "芜", -"蕭": "萧", -"蕷": "蓣", -"薀": "蕰", -"薈": "荟", -"薊": "蓟", -"薌": "芗", -"薔": "蔷", -"薘": "荙", -"薟": "莶", -"薦": "荐", -"薩": "萨", -"薳": "䓕", -"薴": "苧", -"薺": "荠", -"藉": "借", -"藍": "蓝", -"藎": "荩", -"藝": "艺", -"藥": "药", -"藪": "薮", -"藴": "蕴", -"藶": "苈", -"藹": "蔼", -"藺": "蔺", -"蘄": "蕲", -"蘆": "芦", -"蘇": "苏", -"蘊": "蕴", -"蘋": "苹", -"蘚": "藓", -"蘞": "蔹", -"蘢": "茏", -"蘭": "兰", -"蘺": "蓠", -"蘿": "萝", -"虆": "蔂", -"處": "处", -"虛": "虚", -"虜": "虏", -"號": "号", -"虧": "亏", -"虫": "虫", -"虯": "虬", -"蛺": "蛱", -"蛻": "蜕", -"蜆": "蚬", -"蜡": "蜡", -"蝕": "蚀", -"蝟": "猬", -"蝦": "虾", -"蝸": "蜗", -"螄": "蛳", -"螞": "蚂", -"螢": "萤", -"螮": "䗖", -"螻": "蝼", -"螿": "螀", -"蟄": "蛰", -"蟈": "蝈", -"蟎": "螨", -"蟣": "虮", -"蟬": "蝉", -"蟯": "蛲", -"蟲": "虫", -"蟶": "蛏", -"蟻": "蚁", -"蠅": "蝇", -"蠆": "虿", -"蠐": "蛴", -"蠑": "蝾", -"蠟": "蜡", -"蠣": "蛎", -"蠨": "蟏", -"蠱": "蛊", -"蠶": "蚕", -"蠻": "蛮", -"衆": "众", -"衊": "蔑", -"術": "术", -"衕": "同", -"衚": "胡", -"衛": "卫", -"衝": "冲", -"衹": "只", -"袞": "衮", -"裊": "袅", -"裏": "里", -"補": "补", -"裝": "装", -"裡": "里", -"製": "制", -"複": "复", -"褌": "裈", -"褘": "袆", -"褲": "裤", -"褳": "裢", -"褸": "褛", -"褻": "亵", -"襇": "裥", -"襏": "袯", -"襖": "袄", -"襝": "裣", -"襠": "裆", -"襤": "褴", -"襪": "袜", -"襬": "䙓", -"襯": "衬", -"襲": "袭", -"覆蓋": "覆盖", -"翻來覆去": "翻来覆去", -"見": "见", -"覎": "觃", -"規": "规", -"覓": "觅", -"視": "视", -"覘": "觇", -"覡": "觋", -"覥": "觍", -"覦": "觎", -"親": "亲", -"覬": "觊", -"覯": "觏", -"覲": "觐", -"覷": "觑", -"覺": "觉", -"覽": "览", -"覿": "觌", -"觀": "观", -"觴": "觞", -"觶": "觯", -"觸": "触", -"訁": "讠", -"訂": "订", -"訃": "讣", -"計": "计", -"訊": "讯", -"訌": "讧", -"討": "讨", -"訐": "讦", -"訒": "讱", -"訓": "训", -"訕": "讪", -"訖": "讫", -"託": "讬", -"記": "记", -"訛": "讹", -"訝": "讶", -"訟": "讼", -"訢": "䜣", -"訣": "诀", -"訥": "讷", -"訩": "讻", -"訪": "访", -"設": "设", -"許": "许", -"訴": "诉", -"訶": "诃", -"診": "诊", -"註": "注", -"詁": "诂", -"詆": "诋", -"詎": "讵", -"詐": "诈", -"詒": "诒", -"詔": "诏", -"評": "评", -"詖": "诐", -"詗": "诇", -"詘": "诎", -"詛": "诅", -"詞": "词", -"詠": "咏", -"詡": "诩", -"詢": "询", -"詣": "诣", -"試": "试", -"詩": "诗", -"詫": "诧", -"詬": "诟", -"詭": "诡", -"詮": "诠", -"詰": "诘", -"話": "话", -"該": "该", -"詳": "详", -"詵": "诜", -"詼": "诙", -"詿": "诖", -"誄": "诔", -"誅": "诛", -"誆": "诓", -"誇": "夸", -"誌": "志", -"認": "认", -"誑": "诳", -"誒": "诶", -"誕": "诞", -"誘": "诱", -"誚": "诮", -"語": "语", -"誠": "诚", -"誡": "诫", -"誣": "诬", -"誤": "误", -"誥": "诰", -"誦": "诵", -"誨": "诲", -"說": "说", -"説": "说", -"誰": "谁", -"課": "课", -"誶": "谇", -"誹": "诽", -"誼": "谊", -"誾": "訚", -"調": "调", -"諂": "谄", -"諄": "谆", -"談": "谈", -"諉": "诿", -"請": "请", -"諍": "诤", -"諏": "诹", -"諑": "诼", -"諒": "谅", -"論": "论", -"諗": "谂", -"諛": "谀", -"諜": "谍", -"諝": "谞", -"諞": "谝", -"諢": "诨", -"諤": "谔", -"諦": "谛", -"諧": "谐", -"諫": "谏", -"諭": "谕", -"諮": "谘", -"諱": "讳", -"諳": "谙", -"諶": "谌", -"諷": "讽", -"諸": "诸", -"諺": "谚", -"諼": "谖", -"諾": "诺", -"謀": "谋", -"謁": "谒", -"謂": "谓", -"謄": "誊", -"謅": "诌", -"謊": "谎", -"謎": "谜", -"謐": "谧", -"謔": "谑", -"謖": "谡", -"謗": "谤", -"謙": "谦", -"謚": "谥", -"講": "讲", -"謝": "谢", -"謠": "谣", -"謡": "谣", -"謨": "谟", -"謫": "谪", -"謬": "谬", -"謭": "谫", -"謳": "讴", -"謹": "谨", -"謾": "谩", -"譅": "䜧", -"證": "证", -"譎": "谲", -"譏": "讥", -"譖": "谮", -"識": "识", -"譙": "谯", -"譚": "谭", -"譜": "谱", -"譫": "谵", -"譯": "译", -"議": "议", -"譴": "谴", -"護": "护", -"譸": "诪", -"譽": "誉", -"譾": "谫", -"讀": "读", -"變": "变", -"讎": "仇", -"讎": "雠", -"讒": "谗", -"讓": "让", -"讕": "谰", -"讖": "谶", -"讜": "谠", -"讞": "谳", -"豈": "岂", -"豎": "竖", -"豐": "丰", -"豬": "猪", -"豶": "豮", -"貓": "猫", -"貙": "䝙", -"貝": "贝", -"貞": "贞", -"貟": "贠", -"負": "负", -"財": "财", -"貢": "贡", -"貧": "贫", -"貨": "货", -"販": "贩", -"貪": "贪", -"貫": "贯", -"責": "责", -"貯": "贮", -"貰": "贳", -"貲": "赀", -"貳": "贰", -"貴": "贵", -"貶": "贬", -"買": "买", -"貸": "贷", -"貺": "贶", -"費": "费", -"貼": "贴", -"貽": "贻", -"貿": "贸", -"賀": "贺", -"賁": "贲", -"賂": "赂", -"賃": "赁", -"賄": "贿", -"賅": "赅", -"資": "资", -"賈": "贾", -"賊": "贼", -"賑": "赈", -"賒": "赊", -"賓": "宾", -"賕": "赇", -"賙": "赒", -"賚": "赉", -"賜": "赐", -"賞": "赏", -"賠": "赔", -"賡": "赓", -"賢": "贤", -"賣": "卖", -"賤": "贱", -"賦": "赋", -"賧": "赕", -"質": "质", -"賫": "赍", -"賬": "账", -"賭": "赌", -"賰": "䞐", -"賴": "赖", -"賵": "赗", -"賺": "赚", -"賻": "赙", -"購": "购", -"賽": "赛", -"賾": "赜", -"贄": "贽", -"贅": "赘", -"贇": "赟", -"贈": "赠", -"贊": "赞", -"贋": "赝", -"贍": "赡", -"贏": "赢", -"贐": "赆", -"贓": "赃", -"贔": "赑", -"贖": "赎", -"贗": "赝", -"贛": "赣", -"贜": "赃", -"赬": "赪", -"趕": "赶", -"趙": "赵", -"趨": "趋", -"趲": "趱", -"跡": "迹", -"踐": "践", -"踴": "踊", -"蹌": "跄", -"蹕": "跸", -"蹣": "蹒", -"蹤": "踪", -"蹺": "跷", -"躂": "跶", -"躉": "趸", -"躊": "踌", -"躋": "跻", -"躍": "跃", -"躑": "踯", -"躒": "跞", -"躓": "踬", -"躕": "蹰", -"躚": "跹", -"躡": "蹑", -"躥": "蹿", -"躦": "躜", -"躪": "躏", -"軀": "躯", -"車": "车", -"軋": "轧", -"軌": "轨", -"軍": "军", -"軑": "轪", -"軒": "轩", -"軔": "轫", -"軛": "轭", -"軟": "软", -"軤": "轷", -"軫": "轸", -"軲": "轱", -"軸": "轴", -"軹": "轵", -"軺": "轺", -"軻": "轲", -"軼": "轶", -"軾": "轼", -"較": "较", -"輅": "辂", -"輇": "辁", -"輈": "辀", -"載": "载", -"輊": "轾", -"輒": "辄", -"輓": "挽", -"輔": "辅", -"輕": "轻", -"輛": "辆", -"輜": "辎", -"輝": "辉", -"輞": "辋", -"輟": "辍", -"輥": "辊", -"輦": "辇", -"輩": "辈", -"輪": "轮", -"輬": "辌", -"輯": "辑", -"輳": "辏", -"輸": "输", -"輻": "辐", -"輾": "辗", -"輿": "舆", -"轀": "辒", -"轂": "毂", -"轄": "辖", -"轅": "辕", -"轆": "辘", -"轉": "转", -"轍": "辙", -"轎": "轿", -"轔": "辚", -"轟": "轰", -"轡": "辔", -"轢": "轹", -"轤": "轳", -"辟": "辟", -"辦": "办", -"辭": "辞", -"辮": "辫", -"辯": "辩", -"農": "农", -"迴": "回", -"适": "适", -"逕": "迳", -"這": "这", -"連": "连", -"週": "周", -"進": "进", -"遊": "游", -"運": "运", -"過": "过", -"達": "达", -"違": "违", -"遙": "遥", -"遜": "逊", -"遞": "递", -"遠": "远", -"適": "适", -"遲": "迟", -"遷": "迁", -"選": "选", -"遺": "遗", -"遼": "辽", -"邁": "迈", -"還": "还", -"邇": "迩", -"邊": "边", -"邏": "逻", -"邐": "逦", -"郁": "郁", -"郟": "郏", -"郵": "邮", -"鄆": "郓", -"鄉": "乡", -"鄒": "邹", -"鄔": "邬", -"鄖": "郧", -"鄧": "邓", -"鄭": "郑", -"鄰": "邻", -"鄲": "郸", -"鄴": "邺", -"鄶": "郐", -"鄺": "邝", -"酇": "酂", -"酈": "郦", -"醖": "酝", -"醜": "丑", -"醞": "酝", -"醫": "医", -"醬": "酱", -"醱": "酦", -"釀": "酿", -"釁": "衅", -"釃": "酾", -"釅": "酽", -"采": "采", -"釋": "释", -"釐": "厘", -"釒": "钅", -"釓": "钆", -"釔": "钇", -"釕": "钌", -"釗": "钊", -"釘": "钉", -"釙": "钋", -"針": "针", -"釣": "钓", -"釤": "钐", -"釧": "钏", -"釩": "钒", -"釵": "钗", -"釷": "钍", -"釹": "钕", -"釺": "钎", -"鈀": "钯", -"鈁": "钫", -"鈃": "钘", -"鈄": "钭", -"鈈": "钚", -"鈉": "钠", -"鈍": "钝", -"鈎": "钩", -"鈐": "钤", -"鈑": "钣", -"鈒": "钑", -"鈔": "钞", -"鈕": "钮", -"鈞": "钧", -"鈣": "钙", -"鈥": "钬", -"鈦": "钛", -"鈧": "钪", -"鈮": "铌", -"鈰": "铈", -"鈳": "钶", -"鈴": "铃", -"鈷": "钴", -"鈸": "钹", -"鈹": "铍", -"鈺": "钰", -"鈽": "钸", -"鈾": "铀", -"鈿": "钿", -"鉀": "钾", -"鉅": "钜", -"鉈": "铊", -"鉉": "铉", -"鉋": "铇", -"鉍": "铋", -"鉑": "铂", -"鉕": "钷", -"鉗": "钳", -"鉚": "铆", -"鉛": "铅", -"鉞": "钺", -"鉢": "钵", -"鉤": "钩", -"鉦": "钲", -"鉬": "钼", -"鉭": "钽", -"鉶": "铏", -"鉸": "铰", -"鉺": "铒", -"鉻": "铬", -"鉿": "铪", -"銀": "银", -"銃": "铳", -"銅": "铜", -"銍": "铚", -"銑": "铣", -"銓": "铨", -"銖": "铢", -"銘": "铭", -"銚": "铫", -"銛": "铦", -"銜": "衔", -"銠": "铑", -"銣": "铷", -"銥": "铱", -"銦": "铟", -"銨": "铵", -"銩": "铥", -"銪": "铕", -"銫": "铯", -"銬": "铐", -"銱": "铞", -"銳": "锐", -"銷": "销", -"銹": "锈", -"銻": "锑", -"銼": "锉", -"鋁": "铝", -"鋃": "锒", -"鋅": "锌", -"鋇": "钡", -"鋌": "铤", -"鋏": "铗", -"鋒": "锋", -"鋙": "铻", -"鋝": "锊", -"鋟": "锓", -"鋣": "铘", -"鋤": "锄", -"鋥": "锃", -"鋦": "锔", -"鋨": "锇", -"鋩": "铓", -"鋪": "铺", -"鋭": "锐", -"鋮": "铖", -"鋯": "锆", -"鋰": "锂", -"鋱": "铽", -"鋶": "锍", -"鋸": "锯", -"鋼": "钢", -"錁": "锞", -"錄": "录", -"錆": "锖", -"錇": "锫", -"錈": "锩", -"錏": "铔", -"錐": "锥", -"錒": "锕", -"錕": "锟", -"錘": "锤", -"錙": "锱", -"錚": "铮", -"錛": "锛", -"錟": "锬", -"錠": "锭", -"錡": "锜", -"錢": "钱", -"錦": "锦", -"錨": "锚", -"錩": "锠", -"錫": "锡", -"錮": "锢", -"錯": "错", -"録": "录", -"錳": "锰", -"錶": "表", -"錸": "铼", -"鍀": "锝", -"鍁": "锨", -"鍃": "锪", -"鍆": "钔", -"鍇": "锴", -"鍈": "锳", -"鍋": "锅", -"鍍": "镀", -"鍔": "锷", -"鍘": "铡", -"鍚": "钖", -"鍛": "锻", -"鍠": "锽", -"鍤": "锸", -"鍥": "锲", -"鍩": "锘", -"鍬": "锹", -"鍰": "锾", -"鍵": "键", -"鍶": "锶", -"鍺": "锗", -"鍾": "钟", -"鎂": "镁", -"鎄": "锿", -"鎇": "镅", -"鎊": "镑", -"鎔": "镕", -"鎖": "锁", -"鎘": "镉", -"鎚": "锤", -"鎛": "镈", -"鎝": "𨱏", -"鎡": "镃", -"鎢": "钨", -"鎣": "蓥", -"鎦": "镏", -"鎧": "铠", -"鎩": "铩", -"鎪": "锼", -"鎬": "镐", -"鎮": "镇", -"鎰": "镒", -"鎲": "镋", -"鎳": "镍", -"鎵": "镓", -"鎸": "镌", -"鎿": "镎", -"鏃": "镞", -"鏇": "镟", -"鏈": "链", -"鏌": "镆", -"鏍": "镙", -"鏐": "镠", -"鏑": "镝", -"鏗": "铿", -"鏘": "锵", -"鏜": "镗", -"鏝": "镘", -"鏞": "镛", -"鏟": "铲", -"鏡": "镜", -"鏢": "镖", -"鏤": "镂", -"鏨": "錾", -"鏰": "镚", -"鏵": "铧", -"鏷": "镤", -"鏹": "镪", -"鏽": "锈", -"鐃": "铙", -"鐋": "铴", -"鐐": "镣", -"鐒": "铹", -"鐓": "镦", -"鐔": "镡", -"鐘": "钟", -"鐙": "镫", -"鐝": "镢", -"鐠": "镨", -"鐦": "锎", -"鐧": "锏", -"鐨": "镄", -"鐫": "镌", -"鐮": "镰", -"鐲": "镯", -"鐳": "镭", -"鐵": "铁", -"鐶": "镮", -"鐸": "铎", -"鐺": "铛", -"鐿": "镱", -"鑄": "铸", -"鑊": "镬", -"鑌": "镔", -"鑒": "鉴", -"鑔": "镲", -"鑕": "锧", -"鑞": "镴", -"鑠": "铄", -"鑣": "镳", -"鑥": "镥", -"鑭": "镧", -"鑰": "钥", -"鑱": "镵", -"鑲": "镶", -"鑷": "镊", -"鑹": "镩", -"鑼": "锣", -"鑽": "钻", -"鑾": "銮", -"鑿": "凿", -"钁": "镢", -"镟": "旋", -"長": "长", -"門": "门", -"閂": "闩", -"閃": "闪", -"閆": "闫", -"閈": "闬", -"閉": "闭", -"開": "开", -"閌": "闶", -"閎": "闳", -"閏": "闰", -"閑": "闲", -"間": "间", -"閔": "闵", -"閘": "闸", -"閡": "阂", -"閣": "阁", -"閤": "合", -"閥": "阀", -"閨": "闺", -"閩": "闽", -"閫": "阃", -"閬": "阆", -"閭": "闾", -"閱": "阅", -"閲": "阅", -"閶": "阊", -"閹": "阉", -"閻": "阎", -"閼": "阏", -"閽": "阍", -"閾": "阈", -"閿": "阌", -"闃": "阒", -"闆": "板", -"闈": "闱", -"闊": "阔", -"闋": "阕", -"闌": "阑", -"闍": "阇", -"闐": "阗", -"闒": "阘", -"闓": "闿", -"闔": "阖", -"闕": "阙", -"闖": "闯", -"關": "关", -"闞": "阚", -"闠": "阓", -"闡": "阐", -"闤": "阛", -"闥": "闼", -"阪": "坂", -"陘": "陉", -"陝": "陕", -"陣": "阵", -"陰": "阴", -"陳": "陈", -"陸": "陆", -"陽": "阳", -"隉": "陧", -"隊": "队", -"階": "阶", -"隕": "陨", -"際": "际", -"隨": "随", -"險": "险", -"隱": "隐", -"隴": "陇", -"隸": "隶", -"隻": "只", -"雋": "隽", -"雖": "虽", -"雙": "双", -"雛": "雏", -"雜": "杂", -"雞": "鸡", -"離": "离", -"難": "难", -"雲": "云", -"電": "电", -"霢": "霡", -"霧": "雾", -"霽": "霁", -"靂": "雳", -"靄": "霭", -"靈": "灵", -"靚": "靓", -"靜": "静", -"靨": "靥", -"鞀": "鼗", -"鞏": "巩", -"鞝": "绱", -"鞦": "秋", -"鞽": "鞒", -"韁": "缰", -"韃": "鞑", -"韆": "千", -"韉": "鞯", -"韋": "韦", -"韌": "韧", -"韍": "韨", -"韓": "韩", -"韙": "韪", -"韜": "韬", -"韞": "韫", -"韻": "韵", -"響": "响", -"頁": "页", -"頂": "顶", -"頃": "顷", -"項": "项", -"順": "顺", -"頇": "顸", -"須": "须", -"頊": "顼", -"頌": "颂", -"頎": "颀", -"頏": "颃", -"預": "预", -"頑": "顽", -"頒": "颁", -"頓": "顿", -"頗": "颇", -"領": "领", -"頜": "颌", -"頡": "颉", -"頤": "颐", -"頦": "颏", -"頭": "头", -"頮": "颒", -"頰": "颊", -"頲": "颋", -"頴": "颕", -"頷": "颔", -"頸": "颈", -"頹": "颓", -"頻": "频", -"頽": "颓", -"顆": "颗", -"題": "题", -"額": "额", -"顎": "颚", -"顏": "颜", -"顒": "颙", -"顓": "颛", -"顔": "颜", -"願": "愿", -"顙": "颡", -"顛": "颠", -"類": "类", -"顢": "颟", -"顥": "颢", -"顧": "顾", -"顫": "颤", -"顬": "颥", -"顯": "显", -"顰": "颦", -"顱": "颅", -"顳": "颞", -"顴": "颧", -"風": "风", -"颭": "飐", -"颮": "飑", -"颯": "飒", -"颱": "台", -"颳": "刮", -"颶": "飓", -"颸": "飔", -"颺": "飏", -"颻": "飖", -"颼": "飕", -"飀": "飗", -"飄": "飘", -"飆": "飙", -"飈": "飚", -"飛": "飞", -"飠": "饣", -"飢": "饥", -"飣": "饤", -"飥": "饦", -"飩": "饨", -"飪": "饪", -"飫": "饫", -"飭": "饬", -"飯": "饭", -"飲": "饮", -"飴": "饴", -"飼": "饲", -"飽": "饱", -"飾": "饰", -"飿": "饳", -"餃": "饺", -"餄": "饸", -"餅": "饼", -"餉": "饷", -"養": "养", -"餌": "饵", -"餎": "饹", -"餏": "饻", -"餑": "饽", -"餒": "馁", -"餓": "饿", -"餕": "馂", -"餖": "饾", -"餚": "肴", -"餛": "馄", -"餜": "馃", -"餞": "饯", -"餡": "馅", -"館": "馆", -"餱": "糇", -"餳": "饧", -"餶": "馉", -"餷": "馇", -"餺": "馎", -"餼": "饩", -"餾": "馏", -"餿": "馊", -"饁": "馌", -"饃": "馍", -"饅": "馒", -"饈": "馐", -"饉": "馑", -"饊": "馓", -"饋": "馈", -"饌": "馔", -"饑": "饥", -"饒": "饶", -"饗": "飨", -"饜": "餍", -"饞": "馋", -"饢": "馕", -"馬": "马", -"馭": "驭", -"馮": "冯", -"馱": "驮", -"馳": "驰", -"馴": "驯", -"馹": "驲", -"駁": "驳", -"駐": "驻", -"駑": "驽", -"駒": "驹", -"駔": "驵", -"駕": "驾", -"駘": "骀", -"駙": "驸", -"駛": "驶", -"駝": "驼", -"駟": "驷", -"駡": "骂", -"駢": "骈", -"駭": "骇", -"駰": "骃", -"駱": "骆", -"駸": "骎", -"駿": "骏", -"騁": "骋", -"騂": "骍", -"騅": "骓", -"騌": "骔", -"騍": "骒", -"騎": "骑", -"騏": "骐", -"騖": "骛", -"騙": "骗", -"騤": "骙", -"騧": "䯄", -"騫": "骞", -"騭": "骘", -"騮": "骝", -"騰": "腾", -"騶": "驺", -"騷": "骚", -"騸": "骟", -"騾": "骡", -"驀": "蓦", -"驁": "骜", -"驂": "骖", -"驃": "骠", -"驄": "骢", -"驅": "驱", -"驊": "骅", -"驌": "骕", -"驍": "骁", -"驏": "骣", -"驕": "骄", -"驗": "验", -"驚": "惊", -"驛": "驿", -"驟": "骤", -"驢": "驴", -"驤": "骧", -"驥": "骥", -"驦": "骦", -"驪": "骊", -"驫": "骉", -"骯": "肮", -"髏": "髅", -"髒": "脏", -"體": "体", -"髕": "髌", -"髖": "髋", -"髮": "发", -"鬆": "松", -"鬍": "胡", -"鬚": "须", -"鬢": "鬓", -"鬥": "斗", -"鬧": "闹", -"鬩": "阋", -"鬮": "阄", -"鬱": "郁", -"魎": "魉", -"魘": "魇", -"魚": "鱼", -"魛": "鱽", -"魢": "鱾", -"魨": "鲀", -"魯": "鲁", -"魴": "鲂", -"魷": "鱿", -"魺": "鲄", -"鮁": "鲅", -"鮃": "鲆", -"鮊": "鲌", -"鮋": "鲉", -"鮍": "鲏", -"鮎": "鲇", -"鮐": "鲐", -"鮑": "鲍", -"鮒": "鲋", -"鮓": "鲊", -"鮚": "鲒", -"鮜": "鲘", -"鮝": "鲞", -"鮞": "鲕", -"鮦": "鲖", -"鮪": "鲔", -"鮫": "鲛", -"鮭": "鲑", -"鮮": "鲜", -"鮳": "鲓", -"鮶": "鲪", -"鮺": "鲝", -"鯀": "鲧", -"鯁": "鲠", -"鯇": "鲩", -"鯉": "鲤", -"鯊": "鲨", -"鯒": "鲬", -"鯔": "鲻", -"鯕": "鲯", -"鯖": "鲭", -"鯗": "鲞", -"鯛": "鲷", -"鯝": "鲴", -"鯡": "鲱", -"鯢": "鲵", -"鯤": "鲲", -"鯧": "鲳", -"鯨": "鲸", -"鯪": "鲮", -"鯫": "鲰", -"鯴": "鲺", -"鯷": "鳀", -"鯽": "鲫", -"鯿": "鳊", -"鰁": "鳈", -"鰂": "鲗", -"鰃": "鳂", -"鰈": "鲽", -"鰉": "鳇", -"鰍": "鳅", -"鰏": "鲾", -"鰐": "鳄", -"鰒": "鳆", -"鰓": "鳃", -"鰜": "鳒", -"鰟": "鳑", -"鰠": "鳋", -"鰣": "鲥", -"鰥": "鳏", -"鰨": "鳎", -"鰩": "鳐", -"鰭": "鳍", -"鰮": "鳁", -"鰱": "鲢", -"鰲": "鳌", -"鰳": "鳓", -"鰵": "鳘", -"鰷": "鲦", -"鰹": "鲣", -"鰺": "鲹", -"鰻": "鳗", -"鰼": "鳛", -"鰾": "鳔", -"鱂": "鳉", -"鱅": "鳙", -"鱈": "鳕", -"鱉": "鳖", -"鱒": "鳟", -"鱔": "鳝", -"鱖": "鳜", -"鱗": "鳞", -"鱘": "鲟", -"鱝": "鲼", -"鱟": "鲎", -"鱠": "鲙", -"鱣": "鳣", -"鱤": "鳡", -"鱧": "鳢", -"鱨": "鲿", -"鱭": "鲚", -"鱯": "鳠", -"鱷": "鳄", -"鱸": "鲈", -"鱺": "鲡", -"䰾": "鲃", -"䲁": "鳚", -"鳥": "鸟", -"鳧": "凫", -"鳩": "鸠", -"鳬": "凫", -"鳲": "鸤", -"鳳": "凤", -"鳴": "鸣", -"鳶": "鸢", -"鳾": "䴓", -"鴆": "鸩", -"鴇": "鸨", -"鴉": "鸦", -"鴒": "鸰", -"鴕": "鸵", -"鴛": "鸳", -"鴝": "鸲", -"鴞": "鸮", -"鴟": "鸱", -"鴣": "鸪", -"鴦": "鸯", -"鴨": "鸭", -"鴯": "鸸", -"鴰": "鸹", -"鴴": "鸻", -"鴷": "䴕", -"鴻": "鸿", -"鴿": "鸽", -"鵁": "䴔", -"鵂": "鸺", -"鵃": "鸼", -"鵐": "鹀", -"鵑": "鹃", -"鵒": "鹆", -"鵓": "鹁", -"鵜": "鹈", -"鵝": "鹅", -"鵠": "鹄", -"鵡": "鹉", -"鵪": "鹌", -"鵬": "鹏", -"鵮": "鹐", -"鵯": "鹎", -"鵲": "鹊", -"鵷": "鹓", -"鵾": "鹍", -"鶄": "䴖", -"鶇": "鸫", -"鶉": "鹑", -"鶊": "鹒", -"鶓": "鹋", -"鶖": "鹙", -"鶘": "鹕", -"鶚": "鹗", -"鶡": "鹖", -"鶥": "鹛", -"鶩": "鹜", -"鶪": "䴗", -"鶬": "鸧", -"鶯": "莺", -"鶲": "鹟", -"鶴": "鹤", -"鶹": "鹠", -"鶺": "鹡", -"鶻": "鹘", -"鶼": "鹣", -"鶿": "鹚", -"鷀": "鹚", -"鷁": "鹢", -"鷂": "鹞", -"鷄": "鸡", -"鷈": "䴘", -"鷊": "鹝", -"鷓": "鹧", -"鷖": "鹥", -"鷗": "鸥", -"鷙": "鸷", -"鷚": "鹨", -"鷥": "鸶", -"鷦": "鹪", -"鷫": "鹔", -"鷯": "鹩", -"鷲": "鹫", -"鷳": "鹇", -"鷸": "鹬", -"鷹": "鹰", -"鷺": "鹭", -"鷽": "鸴", -"鷿": "䴙", -"鸂": "㶉", -"鸇": "鹯", -"鸌": "鹱", -"鸏": "鹲", -"鸕": "鸬", -"鸘": "鹴", -"鸚": "鹦", -"鸛": "鹳", -"鸝": "鹂", -"鸞": "鸾", -"鹵": "卤", -"鹹": "咸", -"鹺": "鹾", -"鹽": "盐", -"麗": "丽", -"麥": "麦", -"麩": "麸", -"麯": "曲", -"麵": "面", -"麼": "么", -"麽": "么", -"黃": "黄", -"黌": "黉", -"點": "点", -"黨": "党", -"黲": "黪", -"黴": "霉", -"黶": "黡", -"黷": "黩", -"黽": "黾", -"黿": "鼋", -"鼉": "鼍", -"鼕": "冬", -"鼴": "鼹", -"齊": "齐", -"齋": "斋", -"齎": "赍", -"齏": "齑", -"齒": "齿", -"齔": "龀", -"齕": "龁", -"齗": "龂", -"齙": "龅", -"齜": "龇", -"齟": "龃", -"齠": "龆", -"齡": "龄", -"齣": "出", -"齦": "龈", -"齪": "龊", -"齬": "龉", -"齲": "龋", -"齶": "腭", -"齷": "龌", -"龍": "龙", -"龎": "厐", -"龐": "庞", -"龔": "龚", -"龕": "龛", -"龜": "龟", - -"幾畫": "几画", -"賣畫": "卖画", -"滷鹼": "卤碱", -"原畫": "原画", -"口鹼": "口碱", -"古畫": "古画", -"名畫": "名画", -"奇畫": "奇画", -"如畫": "如画", -"弱鹼": "弱碱", -"彩畫": "彩画", -"所畫": "所画", -"扉畫": "扉画", -"教畫": "教画", -"水鹼": "水碱", -"洋鹼": "洋碱", -"炭畫": "炭画", -"畫一": "画一", -"畫上": "画上", -"畫下": "画下", -"畫中": "画中", -"畫供": "画供", -"畫兒": "画儿", -"畫具": "画具", -"畫出": "画出", -"畫史": "画史", -"畫品": "画品", -"畫商": "画商", -"畫圈": "画圈", -"畫境": "画境", -"畫工": "画工", -"畫帖": "画帖", -"畫幅": "画幅", -"畫意": "画意", -"畫成": "画成", -"畫景": "画景", -"畫本": "画本", -"畫架": "画架", -"畫框": "画框", -"畫法": "画法", -"畫王": "画王", -"畫界": "画界", -"畫符": "画符", -"畫紙": "画纸", -"畫線": "画线", -"畫航": "画航", -"畫舫": "画舫", -"畫虎": "画虎", -"畫論": "画论", -"畫譜": "画谱", -"畫象": "画象", -"畫質": "画质", -"畫貼": "画贴", -"畫軸": "画轴", -"畫頁": "画页", -"鹽鹼": "盐碱", -"鹼": "碱", -"鹼基": "碱基", -"鹼度": "碱度", -"鹼水": "碱水", -"鹼熔": "碱熔", -"磁畫": "磁画", -"策畫": "策画", -"組畫": "组画", -"絹畫": "绢画", -"耐鹼": "耐碱", -"肉鹼": "肉碱", -"膠畫": "胶画", -"茶鹼": "茶碱", -"西畫": "西画", -"貼畫": "贴画", -"返鹼": "返碱", -"鍾鍛": "锺锻", -"鍛鍾": "锻锺", -"雕畫": "雕画", -"鯰": "鲶", -"三聯畫": "三联画", -"中國畫": "中国画", -"書畫": "书画", -"書畫社": "书画社", -"五筆畫": "五笔画", -"作畫": "作画", -"入畫": "入画", -"寫生畫": "写生画", -"刻畫": "刻画", -"動畫": "动画", -"勾畫": "勾画", -"單色畫": "单色画", -"卡通畫": "卡通画", -"國畫": "国画", -"圖畫": "图画", -"壁畫": "壁画", -"字畫": "字画", -"宣傳畫": "宣传画", -"工筆畫": "工笔画", -"年畫": "年画", -"幽默畫": "幽默画", -"指畫": "指画", -"描畫": "描画", -"插畫": "插画", -"擘畫": "擘画", -"春畫": "春画", -"木刻畫": "木刻画", -"機械畫": "机械画", -"比畫": "比画", -"毛筆畫": "毛笔画", -"水粉畫": "水粉画", -"油畫": "油画", -"海景畫": "海景画", -"漫畫": "漫画", -"點畫": "点画", -"版畫": "版画", -"畫": "画", -"畫像": "画像", -"畫冊": "画册", -"畫刊": "画刊", -"畫匠": "画匠", -"畫捲": "画卷", -"畫圖": "画图", -"畫壇": "画坛", -"畫室": "画室", -"畫家": "画家", -"畫屏": "画屏", -"畫展": "画展", -"畫布": "画布", -"畫師": "画师", -"畫廊": "画廊", -"畫報": "画报", -"畫押": "画押", -"畫板": "画板", -"畫片": "画片", -"畫畫": "画画", -"畫皮": "画皮", -"畫眉鳥": "画眉鸟", -"畫稿": "画稿", -"畫筆": "画笔", -"畫院": "画院", -"畫集": "画集", -"畫面": "画面", -"筆畫": "笔画", -"細密畫": "细密画", -"繪畫": "绘画", -"自畫像": "自画像", -"蠟筆畫": "蜡笔画", -"裸體畫": "裸体画", -"西洋畫": "西洋画", -"透視畫": "透视画", -"銅版畫": "铜版画", -"鍾": "锺", -"靜物畫": "静物画", -"餘": "馀", -} - -zh2TW = { -"缺省": "預設", -"串行": "串列", -"以太网": "乙太網", -"位图": "點陣圖", -"例程": "常式", -"信道": "通道", -"光标": "游標", -"光盘": "光碟", -"光驱": "光碟機", -"全角": "全形", -"加载": "載入", -"半角": "半形", -"变量": "變數", -"噪声": "雜訊", -"脱机": "離線", -"声卡": "音效卡", -"老字号": "老字號", -"字号": "字型大小", -"字库": "字型檔", -"字段": "欄位", -"字符": "字元", -"存盘": "存檔", -"寻址": "定址", -"尾注": "章節附註", -"异步": "非同步", -"总线": "匯流排", -"括号": "括弧", -"接口": "介面", -"控件": "控制項", -"权限": "許可權", -"盘片": "碟片", -"硅片": "矽片", -"硅谷": "矽谷", -"硬盘": "硬碟", -"磁盘": "磁碟", -"磁道": "磁軌", -"程控": "程式控制", -"端口": "埠", -"算子": "運算元", -"算法": "演算法", -"芯片": "晶片", -"芯片": "晶元", -"词组": "片語", -"译码": "解碼", -"软驱": "軟碟機", -"快闪存储器": "快閃記憶體", -"闪存": "快閃記憶體", -"鼠标": "滑鼠", -"进制": "進位", -"交互式": "互動式", -"仿真": "模擬", -"优先级": "優先順序", -"传感": "感測", -"便携式": "攜帶型", -"信息论": "資訊理論", -"写保护": "防寫", -"分布式": "分散式", -"分辨率": "解析度", -"服务器": "伺服器", -"等于": "等於", -"局域网": "區域網", -"计算机": "電腦", -"扫瞄仪": "掃瞄器", -"宽带": "寬頻", -"数据库": "資料庫", -"奶酪": "乳酪", -"巨商": "鉅賈", -"手电": "手電筒", -"万历": "萬曆", -"永历": "永曆", -"词汇": "辭彙", -"习用": "慣用", -"元音": "母音", -"任意球": "自由球", -"头球": "頭槌", -"入球": "進球", -"粒入球": "顆進球", -"打门": "射門", -"火锅盖帽": "蓋火鍋", -"打印机": "印表機", -"打印機": "印表機", -"字节": "位元組", -"字節": "位元組", -"打印": "列印", -"打印": "列印", -"硬件": "硬體", -"硬件": "硬體", -"二极管": "二極體", -"二極管": "二極體", -"三极管": "三極體", -"三極管": "三極體", -"软件": "軟體", -"軟件": "軟體", -"网络": "網路", -"網絡": "網路", -"人工智能": "人工智慧", -"航天飞机": "太空梭", -"穿梭機": "太空梭", -"因特网": "網際網路", -"互聯網": "網際網路", -"机器人": "機器人", -"機械人": "機器人", -"移动电话": "行動電話", -"流動電話": "行動電話", -"调制解调器": "數據機", -"調制解調器": "數據機", -"短信": "簡訊", -"短訊": "簡訊", -"乌兹别克斯坦": "烏茲別克", -"乍得": "查德", -"乍得": "查德", -"也门": "葉門", -"也門": "葉門", -"伯利兹": "貝里斯", -"伯利茲": "貝里斯", -"佛得角": "維德角", -"佛得角": "維德角", -"克罗地亚": "克羅埃西亞", -"克羅地亞": "克羅埃西亞", -"冈比亚": "甘比亞", -"岡比亞": "甘比亞", -"几内亚比绍": "幾內亞比索", -"幾內亞比紹": "幾內亞比索", -"列支敦士登": "列支敦斯登", -"列支敦士登": "列支敦斯登", -"利比里亚": "賴比瑞亞", -"利比里亞": "賴比瑞亞", -"加纳": "迦納", -"加納": "迦納", -"加蓬": "加彭", -"加蓬": "加彭", -"博茨瓦纳": "波札那", -"博茨瓦納": "波札那", -"卡塔尔": "卡達", -"卡塔爾": "卡達", -"卢旺达": "盧安達", -"盧旺達": "盧安達", -"危地马拉": "瓜地馬拉", -"危地馬拉": "瓜地馬拉", -"厄瓜多尔": "厄瓜多", -"厄瓜多爾": "厄瓜多", -"厄立特里亚": "厄利垂亞", -"厄立特里亞": "厄利垂亞", -"吉布提": "吉布地", -"吉布堤": "吉布地", -"哈萨克斯坦": "哈薩克", -"哥斯达黎加": "哥斯大黎加", -"哥斯達黎加": "哥斯大黎加", -"图瓦卢": "吐瓦魯", -"圖瓦盧": "吐瓦魯", -"土库曼斯坦": "土庫曼", -"圣卢西亚": "聖露西亞", -"聖盧西亞": "聖露西亞", -"圣基茨和尼维斯": "聖克里斯多福及尼維斯", -"聖吉斯納域斯": "聖克里斯多福及尼維斯", -"圣文森特和格林纳丁斯": "聖文森及格瑞那丁", -"聖文森特和格林納丁斯": "聖文森及格瑞那丁", -"圣马力诺": "聖馬利諾", -"聖馬力諾": "聖馬利諾", -"圭亚那": "蓋亞那", -"圭亞那": "蓋亞那", -"坦桑尼亚": "坦尚尼亞", -"坦桑尼亞": "坦尚尼亞", -"埃塞俄比亚": "衣索比亞", -"埃塞俄比亞": "衣索比亞", -"基里巴斯": "吉里巴斯", -"基里巴斯": "吉里巴斯", -"塔吉克斯坦": "塔吉克", -"塞拉利昂": "獅子山", -"塞拉利昂": "獅子山", -"塞浦路斯": "塞普勒斯", -"塞浦路斯": "塞普勒斯", -"塞舌尔": "塞席爾", -"塞舌爾": "塞席爾", -"多米尼加": "多明尼加", -"多明尼加共和國": "多明尼加", -"多米尼加联邦": "多米尼克", -"多明尼加聯邦": "多米尼克", -"安提瓜和巴布达": "安地卡及巴布達", -"安提瓜和巴布達": "安地卡及巴布達", -"尼日利亚": "奈及利亞", -"尼日利亞": "奈及利亞", -"尼日尔": "尼日", -"尼日爾": "尼日", -"巴巴多斯": "巴貝多", -"巴巴多斯": "巴貝多", -"巴布亚新几内亚": "巴布亞紐幾內亞", -"巴布亞新畿內亞": "巴布亞紐幾內亞", -"布基纳法索": "布吉納法索", -"布基納法索": "布吉納法索", -"布隆迪": "蒲隆地", -"布隆迪": "蒲隆地", -"希腊": "希臘", -"帕劳": "帛琉", -"意大利": "義大利", -"意大利": "義大利", -"所罗门群岛": "索羅門群島", -"所羅門群島": "索羅門群島", -"文莱": "汶萊", -"斯威士兰": "史瓦濟蘭", -"斯威士蘭": "史瓦濟蘭", -"斯洛文尼亚": "斯洛維尼亞", -"斯洛文尼亞": "斯洛維尼亞", -"新西兰": "紐西蘭", -"新西蘭": "紐西蘭", -"格林纳达": "格瑞那達", -"格林納達": "格瑞那達", -"格鲁吉亚": "喬治亞", -"格魯吉亞": "喬治亞", -"佐治亚": "喬治亞", -"佐治亞": "喬治亞", -"毛里塔尼亚": "茅利塔尼亞", -"毛里塔尼亞": "茅利塔尼亞", -"毛里求斯": "模里西斯", -"毛里裘斯": "模里西斯", -"沙特阿拉伯": "沙烏地阿拉伯", -"沙地阿拉伯": "沙烏地阿拉伯", -"波斯尼亚和黑塞哥维那": "波士尼亞赫塞哥維納", -"波斯尼亞黑塞哥維那": "波士尼亞赫塞哥維納", -"津巴布韦": "辛巴威", -"津巴布韋": "辛巴威", -"洪都拉斯": "宏都拉斯", -"洪都拉斯": "宏都拉斯", -"特立尼达和托巴哥": "千里達托貝哥", -"特立尼達和多巴哥": "千里達托貝哥", -"瑙鲁": "諾魯", -"瑙魯": "諾魯", -"瓦努阿图": "萬那杜", -"瓦努阿圖": "萬那杜", -"溫納圖萬": "那杜", -"科摩罗": "葛摩", -"科摩羅": "葛摩", -"科特迪瓦": "象牙海岸", -"突尼斯": "突尼西亞", -"索马里": "索馬利亞", -"索馬里": "索馬利亞", -"老挝": "寮國", -"老撾": "寮國", -"肯尼亚": "肯亞", -"肯雅": "肯亞", -"苏里南": "蘇利南", -"莫桑比克": "莫三比克", -"莱索托": "賴索托", -"萊索托": "賴索托", -"贝宁": "貝南", -"貝寧": "貝南", -"赞比亚": "尚比亞", -"贊比亞": "尚比亞", -"阿塞拜疆": "亞塞拜然", -"阿塞拜疆": "亞塞拜然", -"阿拉伯联合酋长国": "阿拉伯聯合大公國", -"阿拉伯聯合酋長國": "阿拉伯聯合大公國", -"马尔代夫": "馬爾地夫", -"馬爾代夫": "馬爾地夫", -"马耳他": "馬爾他", -"马里共和国": "馬利共和國", -"馬里共和國": "馬利共和國", -"方便面": "速食麵", -"快速面": "速食麵", -"即食麵": "速食麵", -"薯仔": "土豆", -"蹦极跳": "笨豬跳", -"绑紧跳": "笨豬跳", -"冷菜": "冷盤", -"凉菜": "冷盤", -"出租车": "計程車", -"台球": "撞球", -"桌球": "撞球", -"雪糕": "冰淇淋", -"卫生": "衛生", -"衞生": "衛生", -"平治": "賓士", -"奔驰": "賓士", -"積架": "捷豹", -"福士": "福斯", -"雪铁龙": "雪鐵龍", -"马自达": "馬自達", -"萬事得": "馬自達", -"拿破仑": "拿破崙", -"拿破侖": "拿破崙", -"布什": "布希", -"布殊": "布希", -"克林顿": "柯林頓", -"克林頓": "柯林頓", -"侯赛因": "海珊", -"侯賽因": "海珊", -"凡高": "梵谷", -"狄安娜": "黛安娜", -"戴安娜": "黛安娜", -"赫拉": "希拉", -} - -zh2HK = { -"打印机": "打印機", -"印表機": "打印機", -"字节": "位元組", -"字節": "位元組", -"打印": "打印", -"列印": "打印", -"硬件": "硬件", -"硬體": "硬件", -"二极管": "二極管", -"二極體": "二極管", -"三极管": "三極管", -"三極體": "三極管", -"数码": "數碼", -"數位": "數碼", -"软件": "軟件", -"軟體": "軟件", -"网络": "網絡", -"網路": "網絡", -"人工智能": "人工智能", -"人工智慧": "人工智能", -"航天飞机": "穿梭機", -"太空梭": "穿梭機", -"因特网": "互聯網", -"網際網路": "互聯網", -"机器人": "機械人", -"機器人": "機械人", -"移动电话": "流動電話", -"行動電話": "流動電話", -"调制解调器": "調制解調器", -"數據機": "調制解調器", -"短信": "短訊", -"簡訊": "短訊", -"乍得": "乍得", -"查德": "乍得", -"也门": "也門", -"葉門": "也門", -"伯利兹": "伯利茲", -"貝里斯": "伯利茲", -"佛得角": "佛得角", -"維德角": "佛得角", -"克罗地亚": "克羅地亞", -"克羅埃西亞": "克羅地亞", -"冈比亚": "岡比亞", -"甘比亞": "岡比亞", -"几内亚比绍": "幾內亞比紹", -"幾內亞比索": "幾內亞比紹", -"列支敦士登": "列支敦士登", -"列支敦斯登": "列支敦士登", -"利比里亚": "利比里亞", -"賴比瑞亞": "利比里亞", -"加纳": "加納", -"迦納": "加納", -"加蓬": "加蓬", -"加彭": "加蓬", -"博茨瓦纳": "博茨瓦納", -"波札那": "博茨瓦納", -"卡塔尔": "卡塔爾", -"卡達": "卡塔爾", -"卢旺达": "盧旺達", -"盧安達": "盧旺達", -"危地马拉": "危地馬拉", -"瓜地馬拉": "危地馬拉", -"厄瓜多尔": "厄瓜多爾", -"厄瓜多": "厄瓜多爾", -"厄立特里亚": "厄立特里亞", -"厄利垂亞": "厄立特里亞", -"吉布提": "吉布堤", -"吉布地": "吉布堤", -"哥斯达黎加": "哥斯達黎加", -"哥斯大黎加": "哥斯達黎加", -"图瓦卢": "圖瓦盧", -"吐瓦魯": "圖瓦盧", -"圣卢西亚": "聖盧西亞", -"聖露西亞": "聖盧西亞", -"圣基茨和尼维斯": "聖吉斯納域斯", -"聖克里斯多福及尼維斯": "聖吉斯納域斯", -"圣文森特和格林纳丁斯": "聖文森特和格林納丁斯", -"聖文森及格瑞那丁": "聖文森特和格林納丁斯", -"圣马力诺": "聖馬力諾", -"聖馬利諾": "聖馬力諾", -"圭亚那": "圭亞那", -"蓋亞那": "圭亞那", -"坦桑尼亚": "坦桑尼亞", -"坦尚尼亞": "坦桑尼亞", -"埃塞俄比亚": "埃塞俄比亞", -"衣索匹亞": "埃塞俄比亞", -"衣索比亞": "埃塞俄比亞", -"基里巴斯": "基里巴斯", -"吉里巴斯": "基里巴斯", -"狮子山": "獅子山", -"塞普勒斯": "塞浦路斯", -"塞舌尔": "塞舌爾", -"塞席爾": "塞舌爾", -"多米尼加": "多明尼加共和國", -"多明尼加": "多明尼加共和國", -"多米尼加联邦": "多明尼加聯邦", -"多米尼克": "多明尼加聯邦", -"安提瓜和巴布达": "安提瓜和巴布達", -"安地卡及巴布達": "安提瓜和巴布達", -"尼日利亚": "尼日利亞", -"奈及利亞": "尼日利亞", -"尼日尔": "尼日爾", -"尼日": "尼日爾", -"巴巴多斯": "巴巴多斯", -"巴貝多": "巴巴多斯", -"巴布亚新几内亚": "巴布亞新畿內亞", -"巴布亞紐幾內亞": "巴布亞新畿內亞", -"布基纳法索": "布基納法索", -"布吉納法索": "布基納法索", -"布隆迪": "布隆迪", -"蒲隆地": "布隆迪", -"義大利": "意大利", -"所罗门群岛": "所羅門群島", -"索羅門群島": "所羅門群島", -"斯威士兰": "斯威士蘭", -"史瓦濟蘭": "斯威士蘭", -"斯洛文尼亚": "斯洛文尼亞", -"斯洛維尼亞": "斯洛文尼亞", -"新西兰": "新西蘭", -"紐西蘭": "新西蘭", -"格林纳达": "格林納達", -"格瑞那達": "格林納達", -"格鲁吉亚": "喬治亞", -"格魯吉亞": "喬治亞", -"梵蒂冈": "梵蒂岡", -"毛里塔尼亚": "毛里塔尼亞", -"茅利塔尼亞": "毛里塔尼亞", -"毛里求斯": "毛里裘斯", -"模里西斯": "毛里裘斯", -"沙烏地阿拉伯": "沙特阿拉伯", -"波斯尼亚和黑塞哥维那": "波斯尼亞黑塞哥維那", -"波士尼亞赫塞哥維納": "波斯尼亞黑塞哥維那", -"津巴布韦": "津巴布韋", -"辛巴威": "津巴布韋", -"洪都拉斯": "洪都拉斯", -"宏都拉斯": "洪都拉斯", -"特立尼达和托巴哥": "特立尼達和多巴哥", -"千里達托貝哥": "特立尼達和多巴哥", -"瑙鲁": "瑙魯", -"諾魯": "瑙魯", -"瓦努阿图": "瓦努阿圖", -"萬那杜": "瓦努阿圖", -"科摩罗": "科摩羅", -"葛摩": "科摩羅", -"索马里": "索馬里", -"索馬利亞": "索馬里", -"老挝": "老撾", -"寮國": "老撾", -"肯尼亚": "肯雅", -"肯亞": "肯雅", -"莫桑比克": "莫桑比克", -"莫三比克": "莫桑比克", -"莱索托": "萊索托", -"賴索托": "萊索托", -"贝宁": "貝寧", -"貝南": "貝寧", -"赞比亚": "贊比亞", -"尚比亞": "贊比亞", -"阿塞拜疆": "阿塞拜疆", -"亞塞拜然": "阿塞拜疆", -"阿拉伯联合酋长国": "阿拉伯聯合酋長國", -"阿拉伯聯合大公國": "阿拉伯聯合酋長國", -"马尔代夫": "馬爾代夫", -"馬爾地夫": "馬爾代夫", -"馬利共和國": "馬里共和國", -"方便面": "即食麵", -"快速面": "即食麵", -"速食麵": "即食麵", -"泡麵": "即食麵", -"土豆": "馬鈴薯", -"华乐": "中樂", -"民乐": "中樂", -"計程車": "的士", -"出租车": "的士", -"公車": "巴士", -"自行车": "單車", -"犬只": "狗隻", -"台球": "桌球", -"撞球": "桌球", -"冰淇淋": "雪糕", -"賓士": "平治", -"捷豹": "積架", -"福斯": "福士", -"雪铁龙": "先進", -"雪鐵龍": "先進", -"沃尓沃": "富豪", -"马自达": "萬事得", -"馬自達": "萬事得", -"寶獅": "標致", -"拿破崙": "拿破侖", -"布什": "布殊", -"布希": "布殊", -"克林顿": "克林頓", -"柯林頓": "克林頓", -"萨达姆": "薩達姆", -"海珊": "侯賽因", -"侯赛因": "侯賽因", -"大卫·贝克汉姆": "大衛碧咸", -"迈克尔·欧文": "米高奧雲", -"珍妮弗·卡普里亚蒂": "卡佩雅蒂", -"马拉特·萨芬": "沙芬", -"迈克尔·舒马赫": "舒麥加", -"希特勒": "希特拉", -"狄安娜": "戴安娜", -"黛安娜": "戴安娜", -} - -zh2CN = { -"記憶體": "内存", -"預設": "默认", -"串列": "串行", -"乙太網": "以太网", -"點陣圖": "位图", -"常式": "例程", -"游標": "光标", -"光碟": "光盘", -"光碟機": "光驱", -"全形": "全角", -"共用": "共享", -"載入": "加载", -"半形": "半角", -"變數": "变量", -"雜訊": "噪声", -"因數": "因子", -"功能變數名稱": "域名", -"音效卡": "声卡", -"字型大小": "字号", -"字型檔": "字库", -"欄位": "字段", -"字元": "字符", -"存檔": "存盘", -"定址": "寻址", -"章節附註": "尾注", -"非同步": "异步", -"匯流排": "总线", -"括弧": "括号", -"介面": "接口", -"控制項": "控件", -"許可權": "权限", -"碟片": "盘片", -"矽片": "硅片", -"矽谷": "硅谷", -"硬碟": "硬盘", -"磁碟": "磁盘", -"磁軌": "磁道", -"程式控制": "程控", -"運算元": "算子", -"演算法": "算法", -"晶片": "芯片", -"晶元": "芯片", -"片語": "词组", -"軟碟機": "软驱", -"快閃記憶體": "快闪存储器", -"滑鼠": "鼠标", -"進位": "进制", -"互動式": "交互式", -"優先順序": "优先级", -"感測": "传感", -"攜帶型": "便携式", -"資訊理論": "信息论", -"迴圈": "循环", -"防寫": "写保护", -"分散式": "分布式", -"解析度": "分辨率", -"伺服器": "服务器", -"等於": "等于", -"區域網": "局域网", -"巨集": "宏", -"掃瞄器": "扫瞄仪", -"寬頻": "宽带", -"資料庫": "数据库", -"乳酪": "奶酪", -"鉅賈": "巨商", -"手電筒": "手电", -"萬曆": "万历", -"永曆": "永历", -"辭彙": "词汇", -"母音": "元音", -"自由球": "任意球", -"頭槌": "头球", -"進球": "入球", -"顆進球": "粒入球", -"射門": "打门", -"蓋火鍋": "火锅盖帽", -"印表機": "打印机", -"打印機": "打印机", -"位元組": "字节", -"字節": "字节", -"列印": "打印", -"打印": "打印", -"硬體": "硬件", -"二極體": "二极管", -"二極管": "二极管", -"三極體": "三极管", -"三極管": "三极管", -"數位": "数码", -"數碼": "数码", -"軟體": "软件", -"軟件": "软件", -"網路": "网络", -"網絡": "网络", -"人工智慧": "人工智能", -"太空梭": "航天飞机", -"穿梭機": "航天飞机", -"網際網路": "因特网", -"互聯網": "因特网", -"機械人": "机器人", -"機器人": "机器人", -"行動電話": "移动电话", -"流動電話": "移动电话", -"調制解調器": "调制解调器", -"數據機": "调制解调器", -"短訊": "短信", -"簡訊": "短信", -"烏茲別克": "乌兹别克斯坦", -"查德": "乍得", -"乍得": "乍得", -"也門": "", -"葉門": "也门", -"伯利茲": "伯利兹", -"貝里斯": "伯利兹", -"維德角": "佛得角", -"佛得角": "佛得角", -"克羅地亞": "克罗地亚", -"克羅埃西亞": "克罗地亚", -"岡比亞": "冈比亚", -"甘比亞": "冈比亚", -"幾內亞比紹": "几内亚比绍", -"幾內亞比索": "几内亚比绍", -"列支敦斯登": "列支敦士登", -"列支敦士登": "列支敦士登", -"利比里亞": "利比里亚", -"賴比瑞亞": "利比里亚", -"加納": "加纳", -"迦納": "加纳", -"加彭": "加蓬", -"加蓬": "加蓬", -"博茨瓦納": "博茨瓦纳", -"波札那": "博茨瓦纳", -"卡塔爾": "卡塔尔", -"卡達": "卡塔尔", -"盧旺達": "卢旺达", -"盧安達": "卢旺达", -"危地馬拉": "危地马拉", -"瓜地馬拉": "危地马拉", -"厄瓜多爾": "厄瓜多尔", -"厄瓜多": "厄瓜多尔", -"厄立特里亞": "厄立特里亚", -"厄利垂亞": "厄立特里亚", -"吉布堤": "吉布提", -"吉布地": "吉布提", -"哈薩克": "哈萨克斯坦", -"哥斯達黎加": "哥斯达黎加", -"哥斯大黎加": "哥斯达黎加", -"圖瓦盧": "图瓦卢", -"吐瓦魯": "图瓦卢", -"土庫曼": "土库曼斯坦", -"聖盧西亞": "圣卢西亚", -"聖露西亞": "圣卢西亚", -"聖吉斯納域斯": "圣基茨和尼维斯", -"聖克里斯多福及尼維斯": "圣基茨和尼维斯", -"聖文森特和格林納丁斯": "圣文森特和格林纳丁斯", -"聖文森及格瑞那丁": "圣文森特和格林纳丁斯", -"聖馬力諾": "圣马力诺", -"聖馬利諾": "圣马力诺", -"圭亞那": "圭亚那", -"蓋亞那": "圭亚那", -"坦桑尼亞": "坦桑尼亚", -"坦尚尼亞": "坦桑尼亚", -"埃塞俄比亞": "埃塞俄比亚", -"衣索匹亞": "埃塞俄比亚", -"衣索比亞": "埃塞俄比亚", -"吉里巴斯": "基里巴斯", -"基里巴斯": "基里巴斯", -"塔吉克": "塔吉克斯坦", -"塞拉利昂": "塞拉利昂", -"塞普勒斯": "塞浦路斯", -"塞浦路斯": "塞浦路斯", -"塞舌爾": "塞舌尔", -"塞席爾": "塞舌尔", -"多明尼加共和國": "多米尼加", -"多明尼加": "多米尼加", -"多明尼加聯邦": "多米尼加联邦", -"多米尼克": "多米尼加联邦", -"安提瓜和巴布達": "安提瓜和巴布达", -"安地卡及巴布達": "安提瓜和巴布达", -"尼日利亞": "尼日利亚", -"奈及利亞": "尼日利亚", -"尼日爾": "尼日尔", -"尼日": "尼日尔", -"巴貝多": "巴巴多斯", -"巴巴多斯": "巴巴多斯", -"巴布亞新畿內亞": "巴布亚新几内亚", -"巴布亞紐幾內亞": "巴布亚新几内亚", -"布基納法索": "布基纳法索", -"布吉納法索": "布基纳法索", -"蒲隆地": "布隆迪", -"布隆迪": "布隆迪", -"希臘": "希腊", -"帛琉": "帕劳", -"義大利": "意大利", -"意大利": "意大利", -"所羅門群島": "所罗门群岛", -"索羅門群島": "所罗门群岛", -"汶萊": "文莱", -"斯威士蘭": "斯威士兰", -"史瓦濟蘭": "斯威士兰", -"斯洛文尼亞": "斯洛文尼亚", -"斯洛維尼亞": "斯洛文尼亚", -"新西蘭": "新西兰", -"紐西蘭": "新西兰", -"格林納達": "格林纳达", -"格瑞那達": "格林纳达", -"格魯吉亞": "乔治亚", -"喬治亞": "乔治亚", -"梵蒂岡": "梵蒂冈", -"毛里塔尼亞": "毛里塔尼亚", -"茅利塔尼亞": "毛里塔尼亚", -"毛里裘斯": "毛里求斯", -"模里西斯": "毛里求斯", -"沙地阿拉伯": "沙特阿拉伯", -"沙烏地阿拉伯": "沙特阿拉伯", -"波斯尼亞黑塞哥維那": "波斯尼亚和黑塞哥维那", -"波士尼亞赫塞哥維納": "波斯尼亚和黑塞哥维那", -"津巴布韋": "津巴布韦", -"辛巴威": "津巴布韦", -"宏都拉斯": "洪都拉斯", -"洪都拉斯": "洪都拉斯", -"特立尼達和多巴哥": "特立尼达和托巴哥", -"千里達托貝哥": "特立尼达和托巴哥", -"瑙魯": "瑙鲁", -"諾魯": "瑙鲁", -"瓦努阿圖": "瓦努阿图", -"萬那杜": "瓦努阿图", -"溫納圖": "瓦努阿图", -"科摩羅": "科摩罗", -"葛摩": "科摩罗", -"象牙海岸": "科特迪瓦", -"突尼西亞": "突尼斯", -"索馬里": "索马里", -"索馬利亞": "索马里", -"老撾": "老挝", -"寮國": "老挝", -"肯雅": "肯尼亚", -"肯亞": "肯尼亚", -"蘇利南": "苏里南", -"莫三比克": "莫桑比克", -"莫桑比克": "莫桑比克", -"萊索托": "莱索托", -"賴索托": "莱索托", -"貝寧": "贝宁", -"貝南": "贝宁", -"贊比亞": "赞比亚", -"尚比亞": "赞比亚", -"亞塞拜然": "阿塞拜疆", -"阿塞拜疆": "阿塞拜疆", -"阿拉伯聯合酋長國": "阿拉伯联合酋长国", -"阿拉伯聯合大公國": "阿拉伯联合酋长国", -"南韓": "韩国", -"馬爾代夫": "马尔代夫", -"馬爾地夫": "马尔代夫", -"馬爾他": "马耳他", -"馬利共和國": "马里共和国", -"即食麵": "方便面", -"快速面": "方便面", -"速食麵": "方便面", -"泡麵": "方便面", -"笨豬跳": "蹦极跳", -"绑紧跳": "蹦极跳", -"冷盤": "凉菜", -"冷菜": "凉菜", -"散钱": "零钱", -"谐星": "笑星", -"夜学": "夜校", -"华乐": "民乐", -"中樂": "民乐", -"屋价": "房价", -"的士": "出租车", -"計程車": "出租车", -"公車": "公共汽车", -"單車": "自行车", -"節慶": "节日", -"芝士": "乾酪", -"狗隻": "犬只", -"士多啤梨": "草莓", -"忌廉": "奶油", -"桌球": "台球", -"撞球": "台球", -"雪糕": "冰淇淋", -"衞生": "卫生", -"衛生": "卫生", -"賓士": "奔驰", -"平治": "奔驰", -"積架": "捷豹", -"福斯": "大众", -"福士": "大众", -"雪鐵龍": "雪铁龙", -"萬事得": "马自达", -"馬自達": "马自达", -"寶獅": "标志", -"拿破崙": "拿破仑", -"布殊": "布什", -"布希": "布什", -"柯林頓": "克林顿", -"克林頓": "克林顿", -"薩達姆": "萨达姆", -"海珊": "萨达姆", -"梵谷": "凡高", -"大衛碧咸": "大卫·贝克汉姆", -"米高奧雲": "迈克尔·欧文", -"卡佩雅蒂": "珍妮弗·卡普里亚蒂", -"沙芬": "马拉特·萨芬", -"舒麥加": "迈克尔·舒马赫", -"希特拉": "希特勒", -"黛安娜": "戴安娜", -"希拉": "赫拉", -} - -zh2SG = { -"方便面": "快速面", -"速食麵": "快速面", -"即食麵": "快速面", -"蹦极跳": "绑紧跳", -"笨豬跳": "绑紧跳", -"凉菜": "冷菜", -"冷盤": "冷菜", -"零钱": "散钱", -"散紙": "散钱", -"笑星": "谐星", -"夜校": "夜学", -"民乐": "华乐", -"住房": "住屋", -"房价": "屋价", -"泡麵": "快速面", -} diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py new file mode 100644 index 00000000..6448d28d --- /dev/null +++ b/zhenxun/builtin_plugins/__init__.py @@ -0,0 +1,10 @@ +from nonebot import require + +require("nonebot_plugin_apscheduler") +require("nonebot_plugin_alconna") +require("nonebot_plugin_session") +require("nonebot_plugin_saa") + +from nonebot_plugin_saa import enable_auto_select_bot + +enable_auto_select_bot() diff --git a/plugins/genshin/__init__.py b/zhenxun/builtin_plugins/admin/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from plugins/genshin/__init__.py rename to zhenxun/builtin_plugins/admin/__init__.py diff --git a/basic_plugins/admin_bot_manage/admin_config.py b/zhenxun/builtin_plugins/admin/admin_watch.py old mode 100755 new mode 100644 similarity index 55% rename from basic_plugins/admin_bot_manage/admin_config.py rename to zhenxun/builtin_plugins/admin/admin_watch.py index ee751ff9..4fc274ea --- a/basic_plugins/admin_bot_manage/admin_config.py +++ b/zhenxun/builtin_plugins/admin/admin_watch.py @@ -1,45 +1,59 @@ -from nonebot import on_notice -from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent - -from configs.config import Config -from models.group_member_info import GroupInfoUser -from models.level_user import LevelUser -from services.log import logger - -__zx_plugin_name__ = "群管理员变动监测 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - -admin_notice = on_notice(priority=5) - - -@admin_notice.handle() -async def _(event: GroupAdminNoticeEvent): - if user := await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ): - nickname = user.user_name - else: - nickname = event.user_id - if event.sub_type == "set": - admin_default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") - if admin_default_auth is not None: - await LevelUser.set_level( - event.user_id, - event.group_id, - admin_default_auth, - ) - logger.info( - f"成为管理员,添加权限: {admin_default_auth}", - "群管理员变动监测", - event.user_id, - event.group_id, - ) - else: - logger.warning( - f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空" - ) - elif event.sub_type == "unset": - await LevelUser.delete_level(event.user_id, event.group_id) - logger.info("撤销管理员,,取消权限等级", "群管理员变动监测", event.user_id, event.group_id) +from nonebot import on_notice +from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent +from nonebot.plugin import PluginMetadata + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__zx_plugin_name__ = "群管理员变动监测 [Hidden]" +__plugin_version__ = 0.1 +__plugin_author__ = "HibiKier" + + +__plugin_meta__ = PluginMetadata( + name="群管理员变动监测", + description="检测群管理员变动, 添加与删除管理员默认权限, 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限", + usage="", + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + ).dict(), +) + + +admin_notice = on_notice(priority=5) + +base_config = Config.get("admin_bot_manage") + + +@admin_notice.handle() +async def _(event: GroupAdminNoticeEvent): + nickname = event.user_id + if user := await GroupInfoUser.get_or_none( + user_id=str(event.user_id), group_id=str(event.group_id) + ): + nickname = user.user_name + if event.sub_type == "set": + admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH") + if admin_default_auth is not None: + await LevelUser.set_level( + event.user_id, + event.group_id, + admin_default_auth, + ) + logger.info( + f"成为管理员,添加权限: {admin_default_auth}", + "群管理员变动监测", + event.user_id, + event.group_id, + ) + else: + logger.warning( + f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空" + ) + elif event.sub_type == "unset": + await LevelUser.delete_level(event.user_id, event.group_id) + logger.info("撤销群管理员, 取消权限等级", "群管理员变动监测", event.user_id, event.group_id) diff --git a/zhenxun/builtin_plugins/admin/welcome_message.py b/zhenxun/builtin_plugins/admin/welcome_message.py new file mode 100644 index 00000000..96c47949 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/welcome_message.py @@ -0,0 +1,116 @@ +import shutil +from typing import Dict + +import ujson as json +from arclet.alconna import Args, Option +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + AlconnaMatch, + Arparma, + Match, + on_alconna, + store_true, +) +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.rules import admin_check, ensure_group + +base_config = Config.get("admin_bot_manage") + +__plugin_meta__ = PluginMetadata( + name="自定义群欢迎消息", + description="自定义群欢迎消息", + usage=""" + 设置欢迎消息 欢迎新人! + 设置欢迎消息 欢迎你 -at + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("SET_GROUP_WELCOME_MESSAGE_LEVEL", 2), + configs=[ + RegisterConfig( + key="SET_GROUP_WELCOME_MESSAGE_LEVEL", + value=2, + help="设置群欢迎消息所需要的管理员权限等级", + default_value=2, + ) + ], + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "设置欢迎消息", + Args["message", str], + Option("-at", action=store_true, help_text="是否at新入群用户"), + ), + rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL") + & ensure_group, + priority=5, + block=True, +) + +BASE_PATH = DATA_PATH / "welcome_message" +BASE_PATH.mkdir(parents=True, exist_ok=True) + +# 旧数据迁移 +old_file = DATA_PATH / "custom_welcome_msg" / "custom_welcome_msg.json" +if old_file.exists(): + try: + old_data: Dict[str, str] = json.load(old_file.open(encoding="utf8")) + for group_id, message in old_data.items(): + file = BASE_PATH / "qq" / f"{group_id}" / "text.json" + file.parent.mkdir(parents=True, exist_ok=True) + json.dump( + {"at": "[at]" in message, "message": message.replace("[at]", "")}, + file.open("w", encoding="utf8"), + ensure_ascii=False, + indent=4, + ) + logger.debug("群欢迎消息数据迁移", group_id=group_id) + shutil.rmtree(old_file.parent.absolute()) + except Exception as e: + pass + + +@_matcher.handle() +async def _( + session: EventSession, + matcher: AlconnaMatcher, + arparma: Arparma, + message: str, +): + file = ( + BASE_PATH + / f"{session.platform or session.bot_type}" + / f"{session.id2}" + / "text.json" + ) + if session.id3: + file = ( + BASE_PATH + / f"{session.platform or session.bot_type}" + / f"{session.id3}" + / f"{session.id2}" + / "text.json" + ) + if not file.exists(): + file.parent.mkdir(exist_ok=True, parents=True) + json.dump( + {"at": arparma.find("at"), "message": message}, + file.open("w"), + ensure_ascii=False, + indent=4, + ) + logger.info(f"设置群欢迎消息成功: {message}", arparma.header_result, session=session) + await Text(f"设置欢迎消息成功: \n{message}").send() diff --git a/basic_plugins/chat_history/__init__.py b/zhenxun/builtin_plugins/init/__init__.py similarity index 99% rename from basic_plugins/chat_history/__init__.py rename to zhenxun/builtin_plugins/init/__init__.py index 838488cf..eb35e275 100644 --- a/basic_plugins/chat_history/__init__.py +++ b/zhenxun/builtin_plugins/init/__init__.py @@ -1,4 +1,5 @@ -import nonebot from pathlib import Path +import nonebot + nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/init/init_config.py b/zhenxun/builtin_plugins/init/init_config.py new file mode 100644 index 00000000..a837d4f9 --- /dev/null +++ b/zhenxun/builtin_plugins/init/init_config.py @@ -0,0 +1,123 @@ +from pathlib import Path + +import nonebot +from nonebot import get_loaded_plugins +from nonebot.drivers import Driver +from nonebot.plugin import Plugin +from ruamel import yaml +from ruamel.yaml import YAML, round_trip_dump, round_trip_load +from ruamel.yaml.comments import CommentedMap + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import RegisterConfig +from zhenxun.services.log import logger + +_yaml = YAML(pure=True) +_yaml.allow_unicode = True +_yaml.indent = 2 + +driver: Driver = nonebot.get_driver() + +SIMPLE_CONFIG_FILE = DATA_PATH / "config.yaml" + +old_config_file = Path() / "zhenxun" / "configs" / "config.yaml" +if old_config_file.exists(): + old_config_file.rename(SIMPLE_CONFIG_FILE) + + +def _handle_config(plugin: Plugin): + """处理配置项 + + 参数: + plugin: Plugin + """ + if plugin.metadata and plugin.metadata.extra: + extra = plugin.metadata.extra + if configs := extra.get("configs"): + for config in configs: + reg_config = RegisterConfig(**config) + module = reg_config.module or plugin.name + g_config = Config.get(module) + g_config.name = plugin.metadata.name + Config.add_plugin_config( + module, + reg_config.key, + reg_config.value, + help=reg_config.help, + default_value=reg_config.default_value, + type=reg_config.type, + arg_parser=reg_config.arg_parser, + _override=False, + ) + + +def _generate_simple_config(): + """ + 生成简易配置 + + 异常: + AttributeError: _description_ + """ + # 读取用户配置 + _data = {} + _tmp_data = {} + if SIMPLE_CONFIG_FILE.exists(): + _data = _yaml.load(SIMPLE_CONFIG_FILE.open(encoding="utf8")) + # 将简易配置文件的数据填充到配置文件 + for module in Config.keys(): + _tmp_data[module] = {} + for k in Config[module].configs.keys(): + try: + if _data.get(module) and k in _data[module].keys(): + Config.set_config(module, k, _data[module][k]) + _tmp_data[module][k] = Config.get_config(module, k) + except AttributeError as e: + raise AttributeError(f"{e}\n" + "可能为config.yaml配置文件填写不规范") + Config.save() + temp_file = DATA_PATH / "temp_config.yaml" + # 重新生成简易配置文件 + try: + with open(temp_file, "w", encoding="utf8") as wf: + # yaml.dump(_tmp_data, wf, Dumper=yaml.RoundTripDumper, allow_unicode=True) + _yaml.dump(_tmp_data, wf) + with open(temp_file, "r", encoding="utf8") as rf: + _data = _yaml.load(rf) + # 添加注释 + for module in _data.keys(): + help_text = "" + plugin_name = Config.get(module).name or module + help_text += plugin_name + "\n" + for k in _data[module].keys(): + help_text += f"{k}: {Config[module].configs[k].help}" + "\n" + _data.yaml_set_comment_before_after_key(after=help_text[:-1], key=module) + with SIMPLE_CONFIG_FILE.open("w", encoding="utf8") as wf: + _yaml.dump(_data, wf) + except Exception as e: + logger.error(f"生成简易配置注释错误...", e=e) + if temp_file.exists(): + temp_file.unlink() + + +@driver.on_startup +def _(): + """ + 初始化插件数据配置 + """ + plugins2config_file = DATA_PATH / "configs" / "plugins2config.yaml" + for plugin in get_loaded_plugins(): + if plugin.metadata: + _handle_config(plugin) + if not Config.is_empty(): + Config.save() + _data: CommentedMap = _yaml.load(plugins2config_file.open(encoding="utf8")) + for module in _data.keys(): + plugin_name = Config.get(module).name + _data.yaml_set_comment_before_after_key( + after=f"{plugin_name}", + key=module, + ) + # 存完插件基本设置 + with plugins2config_file.open("w", encoding="utf8") as wf: + _yaml.dump(_data, wf) + _generate_simple_config() diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py new file mode 100644 index 00000000..2a19cebc --- /dev/null +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -0,0 +1,126 @@ +from pathlib import Path +from typing import List + +import nonebot +from nonebot import get_loaded_plugins +from nonebot.drivers import Driver +from nonebot.plugin import Plugin +from ruamel import yaml +from ruamel.yaml import YAML, round_trip_dump, round_trip_load +from ruamel.yaml.comments import CommentedMap + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import ( + BaseBlock, + PluginExtraData, + PluginSetting, + RegisterConfig, +) +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.plugin_limit import PluginLimit +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginLimitType + +_yaml = YAML(pure=True) +_yaml.allow_unicode = True +_yaml.indent = 2 + +driver: Driver = nonebot.get_driver() + + +def _handle_setting( + plugin: Plugin, plugin_list: List[PluginInfo], limit_list: List[PluginLimit] +): + """处理插件设置 + + 参数: + plugin: Plugin + plugin_list: 插件列表 + limit_list: 插件限制列表 + """ + metadata = plugin.metadata + if metadata: + extra = metadata.extra + extra_data = PluginExtraData(**extra) + logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据") + setting = extra_data.setting or PluginSetting() + plugin_list.append( + PluginInfo( + module=plugin.name, + module_path=plugin.module_name, + name=metadata.name, + author=extra_data.author, + version=extra_data.version, + level=setting.level, + default_status=setting.default_status, + limit_superuser=setting.limit_superuser, + menu_type=extra_data.menu_type, + cost_gold=setting.cost_gold, + plugin_type=extra_data.plugin_type, + admin_level=extra_data.admin_level, + ) + ) + if extra_data.limits: + for limit in extra_data.limits: + limit_list.append( + PluginLimit( + module=plugin.name, + module_path=plugin.module_name, + limit_type=limit._type, + watch_type=limit.watch_type, + status=limit.status, + check_type=limit.check_type, + result=limit.result, + cd=getattr(limit, "cd", None), + max_count=getattr(limit, "max_count", None), + ) + ) + + +@driver.on_startup +async def _(): + """ + 初始化插件数据配置 + """ + plugin_list: List[PluginInfo] = [] + limit_list: List[PluginLimit] = [] + module2id = {} + if module_list := await PluginInfo.all().values("id", "module_path"): + module2id = {m["module_path"]: m["id"] for m in module_list} + for plugin in get_loaded_plugins(): + if plugin.metadata: + _handle_setting(plugin, plugin_list, limit_list) + create_list = [] + update_list = [] + for plugin in plugin_list: + if plugin.module_path not in module2id: + create_list.append(plugin) + else: + plugin.id = module2id[plugin.module_path] + update_list.append(plugin) + if create_list: + await PluginInfo.bulk_create(create_list, 10) + if update_list: + await PluginInfo.bulk_update( + update_list, + ["name", "author", "version", "admin_level"], + 10, + ) + if limit_list: + limit_create = [] + plugins = [] + if module_path_list := [limit.module_path for limit in limit_list]: + plugins = await PluginInfo.filter(module_path__in=module_path_list).all() + if plugins: + for limit in limit_list: + if l := [p for p in plugins if p.module_path == limit.module_path]: + plugin = l[0] + limit_type_list = [ + _limit.limit_type for _limit in await plugin.plugin_limit.all() # type: ignore + ] + if limit.limit_type not in limit_type_list: + limit.plugin = plugin + limit_create.append(limit) + if limit_create: + await PluginLimit.bulk_create(limit_create, 10) diff --git a/basic_plugins/invite_manager/__init__.py b/zhenxun/builtin_plugins/record_request.py old mode 100755 new mode 100644 similarity index 50% rename from basic_plugins/invite_manager/__init__.py rename to zhenxun/builtin_plugins/record_request.py index 0317713b..2743e662 --- a/basic_plugins/invite_manager/__init__.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -1,170 +1,182 @@ -import asyncio -import re -import time -from datetime import datetime - -from nonebot import on_message, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - FriendRequestEvent, - GroupRequestEvent, - MessageEvent, -) - -from configs.config import NICKNAME, Config -from models.friend_user import FriendUser -from models.group_info import GroupInfo -from services.log import logger -from utils.manager import requests_manager -from utils.utils import scheduler - -from .utils import time_manager - -__zx_plugin_name__ = "好友群聊处理请求 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_configs__ = { - "AUTO_ADD_FRIEND": { - "value": False, - "help": "是否自动同意好友添加", - "default_value": False, - "type": bool, - } -} - -friend_req = on_request(priority=5, block=True) -group_req = on_request(priority=5, block=True) -x = on_message(priority=999, block=False, rule=lambda: False) - - -@friend_req.handle() -async def _(bot: Bot, event: FriendRequestEvent): - if time_manager.add_user_request(event.user_id): - logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) - user = await bot.get_stranger_info(user_id=event.user_id) - nickname = user["nickname"] - sex = user["sex"] - age = str(user["age"]) - comment = event.comment - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"*****一份好友申请*****\n" - f"昵称:{nickname}({event.user_id})\n" - f"自动同意:{'√' if Config.get_config('invite_manager', 'AUTO_ADD_FRIEND') else '×'}\n" - f"日期:{str(datetime.now()).split('.')[0]}\n" - f"备注:{event.comment}", - ) - if Config.get_config("invite_manager", "AUTO_ADD_FRIEND"): - logger.debug(f"已开启好友请求自动同意,成功通过该请求", "好友请求", target=event.user_id) - await bot.set_friend_add_request(flag=event.flag, approve=True) - await FriendUser.create( - user_id=str(user["user_id"]), user_name=user["nickname"] - ) - else: - requests_manager.add_request( - str(bot.self_id), - event.user_id, - "private", - event.flag, - nickname=nickname, - sex=sex, - age=age, - comment=comment, - ) - else: - logger.debug(f"好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) - - -@group_req.handle() -async def _(bot: Bot, event: GroupRequestEvent): - # 邀请 - if event.sub_type == "invite": - if str(event.user_id) in bot.config.superusers: - try: - logger.debug( - f"超级用户自动同意加入群聊", "群聊请求", event.user_id, target=event.group_id - ) - await bot.set_group_add_request( - flag=event.flag, sub_type="invite", approve=True - ) - group_info = await bot.get_group_info(group_id=event.group_id) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - except ActionFailed as e: - logger.error( - "超级用户自动同意加入群聊发生错误", - "群聊请求", - event.user_id, - target=event.group_id, - e=e, - ) - else: - if time_manager.add_group_request(event.user_id, event.group_id): - logger.debug( - f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", "群聊请求" - ) - user = await bot.get_stranger_info(user_id=event.user_id) - sex = user["sex"] - age = str(user["age"]) - nickname = await FriendUser.get_user_name(event.user_id) - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"*****一份入群申请*****\n" - f"申请人:{nickname}({event.user_id})\n" - f"群聊:{event.group_id}\n" - f"邀请日期:{datetime.now().replace(microsecond=0)}", - ) - await bot.send_private_msg( - user_id=event.user_id, - message=f"想要邀请我偷偷入群嘛~已经提醒{NICKNAME}的管理员大人了\n" - "请确保已经群主或群管理沟通过!\n" - "等待管理员处理吧!", - ) - requests_manager.add_request( - str(bot.self_id), - event.user_id, - "group", - event.flag, - invite_group=event.group_id, - nickname=nickname, - sex=sex, - age=age, - ) - else: - logger.debug( - f"群聊请求五分钟内重复, 已忽略", - "群聊请求", - target=f"{event.user_id}:{event.group_id}", - ) - - -@x.handle() -async def _(event: MessageEvent): - await asyncio.sleep(0.1) - r = re.search(r'groupcode="(.*?)"', str(event.get_message())) - if r: - group_id = int(r.group(1)) - else: - return - r = re.search(r'groupname="(.*?)"', str(event.get_message())) - if r: - group_name = r.group(1) - else: - group_name = "None" - requests_manager.set_group_name(group_name, group_id) - - -@scheduler.scheduled_job( - "interval", - minutes=5, -) -async def _(): - time_manager.clear() +import time +from datetime import datetime +from typing import Dict + +from nonebot import on_message, on_request +from nonebot.adapters.onebot.v11 import ( + ActionFailed, + Bot, + FriendRequestEvent, + GroupRequestEvent, +) +from nonebot.plugin import PluginMetadata +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import TargetQQPrivate, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_info import GroupInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestType + +base_config = Config.get("invite_manager") + +__plugin_meta__ = PluginMetadata( + name="记录请求", + description="自定义群欢迎消息", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="invite_manager", + key="AUTO_ADD_FRIEND", + value=False, + help="是否自动同意好友添加", + type=bool, + default_value=False, + ) + ], + ).dict(), +) + + +class Timer: + data: Dict[str, float] = {} + + @classmethod + def check(cls, uid: int | str): + if uid not in cls.data: + return True + return time.time() - cls.data[uid] > 5 * 60 + + @classmethod + def clear(cls): + now = time.time() + cls.data = {k: v for k, v in cls.data.items() if v - now < 5 * 60} + + +friend_req = on_request(priority=5, block=True) +group_req = on_request(priority=5, block=True) +_t = on_message(priority=999, block=False, rule=lambda: False) + + +@friend_req.handle() +async def _(bot: Bot, event: FriendRequestEvent, session: EventSession): + if event.user_id and Timer.check(event.user_id): + logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) + user = await bot.get_stranger_info(user_id=event.user_id) + nickname = user["nickname"] + # sex = user["sex"] + # age = str(user["age"]) + comment = event.comment + superuser = int(list(bot.config.superusers)[0]) + await Text( + f"*****一份好友申请*****\n" + f"昵称:{nickname}({event.user_id})\n" + f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n" + f"日期:{str(datetime.now()).split('.')[0]}\n" + f"备注:{event.comment}" + ).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot) + if base_config.get("AUTO_ADD_FRIEND"): + logger.debug(f"已开启好友请求自动同意,成功通过该请求", "好友请求", target=event.user_id) + await bot.set_friend_add_request(flag=event.flag, approve=True) + await FriendUser.create( + user_id=str(user["user_id"]), user_name=user["nickname"] + ) + else: + await FgRequest.create( + request_type=RequestType.FRIEND, + platform=session.platform, + bot_id=bot.self_id, + flag=event.flag, + user_id=event.user_id, + nickname=nickname, + comment=comment, + ) + else: + logger.debug(f"好友请求五分钟内重复, 已忽略", "好友请求", target=event.user_id) + + +@group_req.handle() +async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): + # 邀请 + if event.sub_type == "invite": + if str(event.user_id) in bot.config.superusers: + try: + logger.debug( + f"超级用户自动同意加入群聊", + "群聊请求", + session=event.user_id, + target=event.group_id, + ) + await bot.set_group_add_request( + flag=event.flag, sub_type="invite", approve=True + ) + group_info = await bot.get_group_info(group_id=event.group_id) + await GroupInfo.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + except ActionFailed as e: + logger.error( + "超级用户自动同意加入群聊发生错误", + "群聊请求", + session=event.user_id, + target=event.group_id, + e=e, + ) + else: + if Timer.check(f"{event.user_id}:{event.group_id}"): + logger.debug( + f"收录 用户[{event.user_id}] 群聊[{event.group_id}] 群聊请求", + "群聊请求", + target=event.group_id, + ) + user = await bot.get_stranger_info(user_id=event.user_id) + nickname = await FriendUser.get_user_name(event.user_id) + superuser = int(list(bot.config.superusers)[0]) + await Text( + f"*****一份入群申请*****\n" + f"申请人:{nickname}({event.user_id})\n" + f"群聊:{event.group_id}\n" + f"邀请日期:{datetime.now().replace(microsecond=0)}" + ).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot) + await bot.send_private_msg( + user_id=event.user_id, + message=f"想要邀请我偷偷入群嘛~已经提醒{NICKNAME}的管理员大人了\n" + "请确保已经群主或群管理沟通过!\n" + "等待管理员处理吧!", + ) + await FgRequest.create( + request_type=RequestType.FRIEND, + platform=session.platform, + bot_id=bot.self_id, + flag=event.flag, + user_id=event.user_id, + nickname=nickname, + ) + else: + logger.debug( + f"群聊请求五分钟内重复, 已忽略", + "群聊请求", + target=f"{event.user_id}:{event.group_id}", + ) + + +@scheduler.scheduled_job( + "interval", + minutes=5, +) +async def _(): + Timer.clear() diff --git a/basic_plugins/super_cmd/__init__.py b/zhenxun/builtin_plugins/superuser/__init__.py old mode 100755 new mode 100644 similarity index 95% rename from basic_plugins/super_cmd/__init__.py rename to zhenxun/builtin_plugins/superuser/__init__.py index 87ae4077..eb35e275 --- a/basic_plugins/super_cmd/__init__.py +++ b/zhenxun/builtin_plugins/superuser/__init__.py @@ -1,5 +1,5 @@ -from pathlib import Path - -import nonebot - -nonebot.load_plugins(str(Path(__file__).parent.resolve())) +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/superuser/clear_data.py b/zhenxun/builtin_plugins/superuser/clear_data.py new file mode 100644 index 00000000..5455cfcb --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/clear_data.py @@ -0,0 +1,86 @@ +import os +import time + +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.utils import run_sync +from nonebot_plugin_alconna import Alconna, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.utils import ResourceDirManager + +__plugin_meta__ = PluginMetadata( + name="清理数据", + description="清理已添加的临时文件夹中的数据", + usage=""" + 清理临时数据 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna("清理临时数据"), + rule=to_me(), + permission=SUPERUSER, + priority=5, + block=True, +) + + +ResourceDirManager.add_temp_dir(TEMP_PATH, True) + + +@_matcher.handle() +async def _(session: EventSession): + await Text("开始清理临时数据...").send() + size = await _clear_data() + await Text("共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)).send() + logger.info( + "清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), session=session + ) + + +@run_sync +def _clear_data() -> float: + logger.debug("开始清理临时文件...") + size = 0 + dir_list = [dir_ for dir_ in ResourceDirManager.temp_path if dir_.exists()] + for dir_ in dir_list: + logger.debug(f"尝试清理文件夹: {dir_.absolute()}", "清理临时数据") + dir_size = 0 + for file in os.listdir(dir_): + file = dir_ / file + if file.is_file(): + try: + if time.time() - os.path.getatime(file) > 10: + file_size = os.path.getsize(file) + file.unlink() + size += file_size + dir_size += file_size + logger.debug(f"移除临时文件: {file.absolute()}", "清理临时数据") + except Exception as e: + logger.error(f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", "清理临时数据", e=e) + logger.debug("清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据") + return float(size) + + +@scheduler.scheduled_job( + "cron", + hour=1, + minute=1, +) +async def _(): + size = await _clear_data() + logger.info("自动清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), "定时任务") diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py new file mode 100644 index 00000000..e4f81915 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -0,0 +1,78 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + AlconnaQuery, + Args, + Arparma, + Match, + Option, + Query, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession +from tortoise import Tortoise + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.db_context import TestSQL +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="数据库操作", + description="执行sql语句与查看表", + usage=""" + 查看所有表 + exec [sql语句] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "exec", + Args["sql?", str], + Option("-l|--list", action=store_true, help_text="查看数据表"), + ), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + + +@_matcher.handle() +async def _( + sql: Match[str], + session: EventSession, + arparma: Arparma, + query_list: Query[bool] = AlconnaQuery("list.value", False), +): + db = Tortoise.get_connection("default") + if query_list.result: + query = await db.execute_query_dict( + "select tablename from pg_tables where schemaname = 'public'" + ) + msg = "数据库中的所有表名:\n" + for tablename in query: + msg += str(tablename["tablename"]) + "\n" + logger.info("查看数据库所有表", arparma.header_result, session=session) + await Text(msg[:-1]).finish() + else: + if not sql.available: + await Text("必须带有需要执行的 SQL 语句...").finish() + sql_text = sql.result + if not sql_text.lower().startswith("select"): + await TestSQL.raw(sql_text) + await Text("执行 SQL 语句成功!").finish() + else: + res = await db.execute_query_dict(sql_text) + # TODO: Alconna空格sql无法接收 diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py new file mode 100644 index 00000000..9e6e79d1 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/fg_manage.py @@ -0,0 +1,114 @@ + + +from nonebot.adapters import Bot +from nonebot.adapters.kaiheila.exception import ApiNotAvailable +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + AlconnaMatch, + Arparma, + Match, + Query, + Subcommand, + UniMessage, + on_alconna, + store_true, +) +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import ConfigModel, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.rules import admin_check, ensure_group + +__plugin_meta__ = PluginMetadata( + name="好友群组列表", + description="查看好友群组列表以", + usage=""" + 查看所有好友 + 查看所有群组 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_friend_matcher = on_alconna( + Alconna("好友列表"), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +_group_matcher = on_alconna( + Alconna("群组列表"), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +# _friend_handle_matcher = on_alconna( +# Alconna( +# "好友操作", +# Subcommand("delete", Args["uid", str], help_text="删除好友"), +# Subcommand("send", Args["uid", str]["message", str], help_text="发送消息"), +# ) +# ) + +# _group_handle_matcher = on_alconna( +# Alconna( +# "群组操作", +# Subcommand("delete", Args["gid", str], help_text="删除好友"), +# Subcommand("send", Args["gid", str]["message", str], help_text="发送消息"), +# ) +# ) + + +@_friend_matcher.handle() +async def _( + bot: Bot, + session: EventSession, +): + try: + # TODO: 其他adapter的好友api + fl = await bot.get_friend_list() + msg = ["{user_id} {nickname}".format_map(g) for g in fl] + msg = "\n".join(msg) + msg = f"| UID | 昵称 | 共{len(fl)}个好友\n" + msg + await Text(msg).send() + logger.info("查看好友列表", "好友列表", session=session) + except (ApiNotAvailable, AttributeError) as e: + await Text("Api未实现...").send() + except Exception as e: + logger.error("好友列表发生错误", "好友列表", session=session, e=e) + await Text("其他未知错误...").send() + + +@_group_matcher.handle() +async def _( + bot: Bot, + session: EventSession, +): + try: + # TODO: 其他adapter的群组api + gl = await bot.get_group_list() + msg = ["{group_id} {group_name}".format_map(g) for g in gl] + msg = "\n".join(msg) + msg = f"| GID | 名称 | 共{len(gl)}个群组\n" + msg + await Text(msg).send() + logger.info("查看群组列表", "群组列表", session=session) + except (ApiNotAvailable, AttributeError) as e: + await Text("Api未实现...").send() + except Exception as e: + logger.error("查看群组列表发生错误", "群组列表", session=session, e=e) + await Text("其他未知错误...").send() diff --git a/zhenxun/builtin_plugins/superuser/reload_setting.py b/zhenxun/builtin_plugins/superuser/reload_setting.py new file mode 100644 index 00000000..e32c7b0c --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/reload_setting.py @@ -0,0 +1,68 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="重载配置", + description="重新加载config.yaml", + usage=""" + 重载配置 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + configs=[ + RegisterConfig( + key="AUTO_RELOAD", + value=False, + help="自动重载配置文件", + default_value=False, + type=bool, + ), + RegisterConfig( + key="AUTO_RELOAD_TIME", + value=180, + help="自动重载配置文件时长", + default_value=180, + type=int, + ), + ], + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "重载配置", + ), + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + Config.reload() + logger.debug("自动重载配置文件", arparma.header_result, session=session) + await Text("重载完成!").send(reply=True) + + +@scheduler.scheduled_job( + "interval", + seconds=Config.get_config("reload_setting", "AUTO_RELOAD_TIME", 180), +) +async def _(): + if Config.get_config("reload_setting", "AUTO_RELOAD"): + Config.reload() + logger.debug("已自动重载配置文件...") diff --git a/zhenxun/builtin_plugins/superuser/request_manage.py b/zhenxun/builtin_plugins/superuser/request_manage.py new file mode 100644 index 00000000..24188197 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/request_manage.py @@ -0,0 +1,266 @@ +from io import BytesIO + +from arclet.alconna import Args, Option +from arclet.alconna.typing import CommandMeta +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + AlconnaQuery, + Arparma, + Query, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.fg_request import FgRequest +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import get_user_avatar + +usage = """ +查看请求 +清空请求 +请求处理 -fa [id] / 同意好友请求 [id] # 同意好友请求 +请求处理 -fr [id] / 拒绝好友请求 [id] # 拒绝好友请求 +请求处理 -fi [id] / 忽略好友请求 [id] # 忽略好友请求 +请求处理 -ga [id] / 同意群组请求 [id] # 同意群聊请求 +请求处理 -gr [id] / 拒绝群组请求 [id] # 拒绝群聊请求 +请求处理 -gi [id] / 忽略群组请求 [id] # 忽略群聊请求 +""".strip() + + +__plugin_meta__ = PluginMetadata( + name="请求处理", + description="好友与邀请群组请求处理", + usage=usage, + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_req_matcher = on_alconna( + Alconna( + "请求处理", + Args["handle", ["-fa", "-fr", "-fi", "-ga", "-gr", "-gi"]]["id", int], + meta=CommandMeta( + description="好友/群组请求处理", + usage=usage, + example="同意好友请求 20", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), +) + +_read_matcher = on_alconna( + Alconna( + "查看请求", + Option("-f|--friend", action=store_true, help_text="查看好友请求"), + Option("-g|--group", action=store_true, help_text="查看群组请求"), + meta=CommandMeta( + description="查看所有请求或好友群组请求", + usage="查看请求\n查看请求 -f\n查看请求-g", + example="查看请求 -f", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), +) + +_clear_matcher = on_alconna( + Alconna( + "清空请求", + Option("-f|--friend", action=store_true, help_text="清空好友请求"), + Option("-g|--group", action=store_true, help_text="清空群组请求"), + meta=CommandMeta( + description="清空请求", + usage="清空请求\n清空请求 -f\n清空请求-g", + example="清空请求 -f", + compact=True, + ), + ), + permission=SUPERUSER, + priority=1, + rule=to_me(), +) + +reg_arg_list = [ + (r"同意好友请求", ["-fa", "{%0}"]), + (r"拒绝好友请求", ["-fr", "{%0}"]), + (r"忽略好友请求", ["-fi", "{%0}"]), + (r"同意群组请求", ["-ga", "{%0}"]), + (r"拒绝群组请求", ["-gr", "{%0}"]), + (r"忽略群组请求", ["-gi", "{%0}"]), +] + +for r in reg_arg_list: + _req_matcher.shortcut( + r[0], + command="请求处理", + arguments=r[1], + prefix=True, + ) + + +@_req_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + handle: str, + id: int, + arparma: Arparma, +): + request_type = RequestType.FRIEND if handle.startswith("-f") else RequestType.GROUP + type_dict = { + "a": RequestHandleType.APPROVE, + "r": RequestHandleType.REFUSED, + "i": RequestHandleType.IGNORE, + } + handle_type = type_dict[handle[-1]] + try: + if handle_type == RequestHandleType.APPROVE: + await FgRequest.approve(bot, id, request_type) + if handle_type == RequestHandleType.REFUSED: + await FgRequest.refused(bot, id, request_type) + if handle_type == RequestHandleType.IGNORE: + await FgRequest.ignore(bot, id, request_type) + except NotFoundError: + await Text("未发现此id的请求...").finish(reply=True) + except Exception: + await Text("其他错误, 可能flag已失效...").finish(reply=True) + logger.info("处理请求", arparma.header_result, session=session) + await Text("成功处理请求!").finish(reply=True) + + +@_read_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + is_friend: Query[bool] = AlconnaQuery("friend.value", False), + is_group: Query[bool] = AlconnaQuery("group.value", False), +): + if all_request := await FgRequest.filter(handle_type__isnull=True).all(): + req_list = list(all_request) + req_list.reverse() + friend_req = [] + group_req = [] + for req in req_list: + if req.request_type == RequestType.FRIEND: + friend_req.append(req) + else: + group_req.append(req) + if is_friend.result: + group_req = [] + elif is_group.result: + friend_req = [] + req_image_list: list[BuildImage] = [] + for i, req_list in enumerate([friend_req, group_req]): + img_list = [] + for req in req_list: + content = await get_user_avatar(req.user_id) + ava_img = BuildImage( + 80, 80, background=BytesIO(content) if content else None + ) + await ava_img.circle() + handle_img = BuildImage( + 130, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" + ) + await handle_img.text((0, 0), "同意/拒绝/忽略", center_type="center") + await handle_img.circle_corner(10) + background = BuildImage(500, 100, font_size=22, color=(255, 255, 255)) + await background.paste(ava_img, (55, 0), center_type="height") + if session.platform and session.platform != "unknown": + platform_icon = BuildImage( + 30, + 30, + background=IMAGE_PATH / "_icon" / f"{session.platform}.png", + ) + await background.paste(platform_icon, (46, 10)) + await background.text((150, 12), req.nickname) + comment_img = await BuildImage.build_text_image( + f"对方留言:{req.comment}", size=15, font_color=(140, 140, 143) + ) + await background.paste(comment_img, (150, 65)) + tag = await BuildImage.build_text_image( + f"{req.platform}", + size=13, + color=(0, 167, 250), + font="HYWenHei-85W.ttf", + font_color=(255, 255, 255), + padding=(1, 6, 1, 6), + ) + await tag.circle_corner(5) + await background.paste(tag, (150, 42)) + await background.paste(handle_img, (360, 35)) + _id_img = BuildImage( + 32, 32, font_size=15, color="#EEEFF4", font="HYWenHei-85W.ttf" + ) + await _id_img.text((0, 0), f"{req.id}", center_type="center") + await _id_img.circle_corner(10) + await background.paste(_id_img, (10, 0), center_type="height") + img_list.append(background) + A = await BuildImage.auto_paste(img_list, 1) + if A: + result_image = BuildImage( + A.width, A.height + 30, color=(255, 255, 255), font_size=20 + ) + await result_image.paste(A, (0, 30)) + _type_text = "好友请求" if i == 0 else "群组请求" + await result_image.text((15, 13), _type_text, fill=(140, 140, 143)) + req_image_list.append(result_image) + if not req_image_list: + await Text("没有任何请求喔...").finish(reply=True) + if len(req_image_list) == 1: + await Image(req_image_list[0].pic2bs4()).finish() + width = sum([img.width for img in req_image_list]) + height = max([img.height for img in req_image_list]) + background = BuildImage(width, height) + await background.paste(req_image_list[0]) + await req_image_list[1].line((0, 10, 1, req_image_list[1].height - 10), width=1) + await background.paste(req_image_list[1], (req_image_list[1].width, 0)) + logger.info("查看请求", arparma.header_result, session=session) + await Image(background.pic2bs4()).finish() + await Text("没有任何请求喔...").finish(reply=True) + + +@_clear_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + is_friend: Query[bool] = AlconnaQuery("friend.value", False), + is_group: Query[bool] = AlconnaQuery("group.value", False), +): + _type = "" + if is_friend.result: + _type = "好友" + await FgRequest.filter( + handle_type__isnull=True, request_type=RequestType.FRIEND + ).update(handle_type=RequestHandleType.IGNORE) + elif is_group.result: + _type = "群组" + await FgRequest.filter( + handle_type__isnull=True, request_type=RequestType.GROUP + ).update(handle_type=RequestHandleType.IGNORE) + else: + _type = "所有" + await FgRequest.filter(handle_type__isnull=True).update( + handle_type=RequestHandleType.IGNORE + ) + logger.info(f"清空{_type}请求", arparma.header_result, session=session) + await Text(f"已清空{_type}请求!").finish() diff --git a/zhenxun/builtin_plugins/superuser/set_admin.py b/zhenxun/builtin_plugins/superuser/set_admin.py new file mode 100644 index 00000000..3696fc2a --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/set_admin.py @@ -0,0 +1,105 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + At, + Match, + Subcommand, + on_alconna, +) +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession, SessionLevel + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="用户权限管理", + description="设置用户权限", + usage=""" + 权限设置 add [level: 权限等级] [at: at对象或用户id] [gid: 群组] + 权限设置 delete [at: at对象或用户id] + + 权限设置 add 5 @user + 权限设置 add 5 422 352352 + + 权限设置 delete @user + 权限设置 delete 123456 + + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "权限设置", + Subcommand( + "add", Args["level", int]["uid", [str, At]]["gid?", str], help_text="添加权限" + ), + Subcommand("delete", Args["uid", [str, At]]["gid?", str], help_text="删除权限"), + ), + permission=SUPERUSER, + priority=5, + block=True, +) + + +@_matcher.assign("add") +async def _( + session: EventSession, + arparma: Arparma, + level: int, + gid: Match[str], + uid: str | At, +): + group_id = gid.result if gid.available else session.id3 or session.id2 + if group_id: + if isinstance(uid, At): + uid = uid.target + user = await LevelUser.get_or_none(user_id=uid, group_id=group_id) + old_level = user.user_level if user else 0 + await LevelUser.set_level(uid, group_id, level, 1) + logger.info( + f"修改权限: {old_level} -> {level}", arparma.header_result, session=session + ) + if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: + await MessageFactory( + [Text("成功为 "), Mention(uid), Text(f" 设置权限:{old_level} -> {level}")] + ).finish(reply=True) + await Text( + f"成功为 \n群组:{group_id}\n用户:{uid} \n设置权限!\n权限:{old_level} -> {level}" + ).finish() + await Text(f"设置权限时群组不能为空...").finish() + + +@_matcher.assign("delete") +async def _( + session: EventSession, + arparma: Arparma, + gid: Match[str], + uid: str | At, +): + group_id = gid.result if gid.available else session.id3 or session.id2 + if group_id: + if isinstance(uid, At): + uid = uid.target + if user := await LevelUser.get_or_none(user_id=uid, group_id=group_id): + await user.delete() + if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: + await MessageFactory( + [Text("成功删除 "), Mention(uid), Text(f" 的权限等级!")] + ).finish(reply=True) + await Text( + f"成功删除 \n群组:{group_id}\n用户:{uid} \n的权限等级!\n权限:{user.user_level} -> 0" + ).finish() + await Text(f"对方目前暂无权限喔...").finish() + await Text(f"设置权限时群组不能为空...").finish() diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py new file mode 100644 index 00000000..18cf31cb --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/update_fg_info.py @@ -0,0 +1,119 @@ +from nonebot.adapters import Bot +from nonebot.adapters.kaiheila.exception import ApiNotAvailable +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, At, Match, on_alconna +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession, SessionLevel + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_info import GroupInfo +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="更新群组/好友信息", + description="更新群组/好友信息", + usage=""" + 更新群组信息 + 更新好友信息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_group_matcher = on_alconna( + Alconna( + "更新群组信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + +_friend_matcher = on_alconna( + Alconna( + "更新好友信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + +# TODO: 其他adapter的更新操作 + +@_group_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + try: + gl = await bot.get_group_list() + gl = [g["group_id"] for g in gl] + num = 0 + for g in gl: + try: + group_info = await bot.get_group_info(group_id=g) + await GroupInfo.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + }, + ) + num += 1 + logger.debug( + "群聊信息更新成功", "更新群信息", session=session, target=group_info["group_id"] + ) + except Exception as e: + logger.error( + f"更新群聊信息失败", + arparma.header_result, + session=session, + target=g, + ) + await Text(f"成功更新了 {len(gl)} 个群的信息").send() + logger.info( + f"更新群聊信息完成,共更新了 {len(gl)} 个群的信息", arparma.header_result, session=session + ) + except (ApiNotAvailable, AttributeError) as e: + await Text("Api未实现...").send() + except Exception as e: + logger.error("更新好友信息发生错误", arparma.header_result, session=session, e=e) + await Text("其他未知错误...").send() + + +@_friend_matcher.assign("delete") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + num = 0 + error_list = [] + fl = await bot.get_friend_list() + for f in fl: + try: + await FriendUser.update_or_create( + user_id=str(f["user_id"]), defaults={"nickname": f["nickname"]} + ) + logger.debug(f"更新好友信息成功", "更新好友信息", session=session, target=f["user_id"]) + num += 1 + except Exception as e: + logger.error(f"更新好友信息失败", "更新好友信息", session=session, target=f["user_id"], e=e) + await Text(f"成功更新了 {num} 个好友的信息!").send() + if error_list: + await Text(f"以下好友更新失败:\n" + "\n".join(error_list)).send() + logger.info(f"更新好友信息完成,共更新了 {num} 个群的信息", arparma.header_result, session=session) diff --git a/configs/config.py b/zhenxun/configs/config.py similarity index 92% rename from configs/config.py rename to zhenxun/configs/config.py index 8a8ac935..e1140a88 100644 --- a/configs/config.py +++ b/zhenxun/configs/config.py @@ -1,6 +1,5 @@ import platform from pathlib import Path -from typing import Optional from .utils import ConfigsManager @@ -30,7 +29,7 @@ database: str = "" # 数据库名称 # 代理,例如 "http://127.0.0.1:7890" # 如果是WLS 可以 f"http://{hostip}:7890" 使用寄主机的代理 -SYSTEM_PROXY: Optional[str] = None # 全局代理 +SYSTEM_PROXY: str | None = None # 全局代理 Config = ConfigsManager(Path() / "data" / "configs" / "plugins2config.yaml") diff --git a/zhenxun/configs/path_config.py b/zhenxun/configs/path_config.py new file mode 100644 index 00000000..1a22cb02 --- /dev/null +++ b/zhenxun/configs/path_config.py @@ -0,0 +1,33 @@ +from pathlib import Path + +# 图片路径 +IMAGE_PATH = Path() / "resources" / "image" +# 语音路径 +RECORD_PATH = Path() / "resources" / "record" +# 文本路径 +TEXT_PATH = Path() / "resources" / "text" +# 日志路径 +LOG_PATH = Path() / "log" +# 字体路径 +FONT_PATH = Path() / "resources" / "font" +# 数据路径 +DATA_PATH = Path() / "data" +# 临时数据路径 +TEMP_PATH = Path() / "resources" / "temp" +# 网页模板路径 +TEMPLATE_PATH = Path() / "resources" / "template" + + + +IMAGE_PATH.mkdir(parents=True, exist_ok=True) +RECORD_PATH.mkdir(parents=True, exist_ok=True) +TEXT_PATH.mkdir(parents=True, exist_ok=True) +LOG_PATH.mkdir(parents=True, exist_ok=True) +FONT_PATH.mkdir(parents=True, exist_ok=True) +DATA_PATH.mkdir(parents=True, exist_ok=True) +TEMP_PATH.mkdir(parents=True, exist_ok=True) + + + + + diff --git a/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py similarity index 56% rename from configs/utils/__init__.py rename to zhenxun/configs/utils/__init__.py index a4fd27af..8ed7cd1e 100644 --- a/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -1,17 +1,44 @@ import copy from pathlib import Path -from typing import Any, Callable, Dict, Optional, Type, Union +from typing import Any, Callable, Dict, List, Type, Union import cattrs from pydantic import BaseModel -from ruamel import yaml from ruamel.yaml import YAML from ruamel.yaml.scanner import ScannerError -from services.log import logger +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, LimitWatchType, PluginLimitType, PluginType + +_yaml = YAML(pure=True) +_yaml.indent = 2 +_yaml.allow_unicode = True -class Config(BaseModel): +class RegisterConfig(BaseModel): + + """ + 注册配置项 + """ + + key: str + """配置项键""" + value: Any + """配置项值""" + module: str | None = None + """模块名""" + help: str | None + """配置注解""" + default_value: Any | None = None + """默认值""" + type: Any = None + """参数类型""" + arg_parser: Callable | None = None + """参数解析""" + + +class ConfigModel(BaseModel): """ 配置项 @@ -19,17 +46,13 @@ class Config(BaseModel): value: Any """配置项值""" - name: Optional[str] - """插件名称""" - help: Optional[str] + help: str | None """配置注解""" - default_value: Optional[Any] = None + default_value: Any | None = None """默认值""" - level_module: Optional[str] - """受权限模块""" type: Any = None """参数类型""" - arg_parser: Optional[Callable] = None + arg_parser: Callable | None = None """参数解析""" @@ -41,9 +64,94 @@ class ConfigGroup(BaseModel): module: str """模块名""" - configs: Dict[str, Config] = {} + name: str | None = None + """插件名""" + configs: Dict[str, ConfigModel] = {} """配置项列表""" + def get(self, c: str, default: Any = None) -> Any: + cfg = self.configs.get(c) + if cfg is not None: + return cfg + return default + + +class BaseBlock(BaseModel): + """ + 插件阻断基本类(插件阻断限制) + """ + + status: bool = True + """限制状态""" + check_type: BlockType = BlockType.ALL + """检查类型""" + watch_type: LimitWatchType = LimitWatchType.USER + """监听对象""" + result: str | None = None + """阻断时回复内容""" + _type: PluginLimitType = PluginLimitType.BLOCK + """类型""" + + +class PluginCdBlock(BaseBlock): + """ + 插件cd限制 + """ + + cd: int = 5 + """cd""" + _type: PluginLimitType = PluginLimitType.CD + """类型""" + + +class PluginCountBlock(BaseBlock): + """ + 插件次数限制 + """ + + max_count: int + """最大调用次数""" + _type: PluginLimitType = PluginLimitType.COUNT + """类型""" + + +class PluginSetting(BaseModel): + """ + 插件基本配置 + """ + + level: int = 5 + """群权限等级""" + default_status: bool = True + """进群默认开关状态""" + limit_superuser: bool = False + """是否限制超级用户""" + cost_gold: int = 0 + """调用插件花费金币""" + + +class PluginExtraData(BaseModel): + """ + 插件扩展信息 + """ + + author: str | None = None + """作者""" + version: str | None = None + """版本""" + plugin_type: PluginType | None = None + """插件类型""" + menu_type: str = "功能" + """菜单类型""" + admin_level: int | None = None + """管理员插件所需权限等级""" + configs: List[RegisterConfig] | None = None + """插件配置""" + setting: PluginSetting | None = None + """插件基本配置""" + limits: List[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None + """插件限制""" + class NoSuchConfig(Exception): pass @@ -57,8 +165,7 @@ class ConfigsManager: def __init__(self, file: Path): self._data: Dict[str, ConfigGroup] = {} self._simple_data: dict = {} - self._admin_level_data = [] - self._simple_file = Path() / "configs" / "config.yaml" + self._simple_file = DATA_PATH / "config.yaml" _yaml = YAML() if file: file.parent.mkdir(exist_ok=True, parents=True) @@ -79,34 +186,39 @@ class ConfigsManager: self, module: str, key: str, - value: Optional[Any], + value: Any, *, - name: Optional[str] = None, - help_: Optional[str] = None, - default_value: Optional[Any] = None, - type: Optional[Type] = None, - arg_parser: Optional[Callable] = None, + help: str | None = None, + default_value: Any = None, + type: Type | None = None, + arg_parser: Callable | None = None, _override: bool = False, ): + """为插件添加一个配置,不会被覆盖,只有第一个生效 + + 参数: + module: 模块 + key: 键 + value: 值 + help: 配置注解. + default_value: 默认值. + type: 值类型. + arg_parser: 值解析器,一般与webui配合使用. + _override: 强制覆盖值. + + 异常: + ValueError: _description_ + ValueError: _description_ """ - 为插件添加一个配置,不会被覆盖,只有第一个生效 - :param module: 模块 - :param key: 键 - :param value: 值 - :param name: 插件名称 - :param help_: 配置注解 - :param default_value: 默认值 - :param _override: 强制覆盖值 - """ + if not module or not key: raise ValueError("add_plugin_config: module和key不能为为空") if module in self._data and (config := self._data[module].configs.get(key)): - config.help = help_ + config.help = help config.arg_parser = arg_parser config.type = type if _override: config.value = value - config.name = name config.default_value = default_value else: _module = None @@ -116,18 +228,13 @@ class ConfigsManager: raise ValueError(f"module: {module} 填写错误") _module = module_split[-1] module = module_split[0] - if "[LEVEL]" in key and _module: - key = key.replace("[LEVEL]", "").strip() - self._admin_level_data.append((_module, value)) key = key.upper() if not self._data.get(module): self._data[module] = ConfigGroup(module=module) - self._data[module].configs[key] = Config( + self._data[module].configs[key] = ConfigModel( value=value, - name=name, - help=help_, + help=help, default_value=default_value, - level_module=_module, type=type, ) @@ -139,32 +246,36 @@ class ConfigsManager: auto_save: bool = False, save_simple_data: bool = True, ): - """ - 设置配置值 - :param module: 模块名 - :param key: 配置名称 - :param value: 值 - :param auto_save: 自动保存 - :param save_simple_data: 保存至config.yaml + """设置配置值 + + 参数: + module: 模块名 + key: 配置名称 + value: 值 + auto_save: 自动保存. + save_simple_data: 保存至config.yaml. """ if module in self._data: - if ( - self._data[module].configs.get(key) - and self._data[module].configs[key] != value - ): + data = self._data[module].configs.get(key) + if data and data != value: self._data[module].configs[key].value = value self._simple_data[module][key] = value if auto_save: self.save(save_simple_data=save_simple_data) - def get_config( - self, module: str, key: str, default: Optional[Any] = None - ) -> Optional[Any]: - """ - 获取指定配置值 - :param module: 模块名 - :param key: 配置名称 - :param default: 没有key值内容的默认返回值 + def get_config(self, module: str, key: str, default: Any = None) -> Any: + """获取指定配置值 + + 参数: + module: 模块名 + key: 配置键 + default: 没有key值内容的默认返回值. + + 异常: + NoSuchConfig: 未查询到配置 + + 返回: + Any: 配置值 """ logger.debug( f"尝试获取配置 MODULE: [{module}] | KEY: [{key}]" @@ -207,41 +318,34 @@ class ConfigsManager: ) return value - def get_level2module(self, module: str, key: str) -> Optional[str]: - """ - 获取指定key所绑定的module,一般为权限等级 - :param module: 模块名 - :param key: 配置名称 - :return: - """ - if self._data.get(module) is not None: - if config := self._data[module].configs.get(key): - return config.level_module + def get(self, key: str) -> ConfigGroup: + """获取插件配置数据 - def get(self, key: str) -> Optional[ConfigGroup]: - """ - 获取插件配置数据 - :param key: 名称 - """ - return self._data.get(key) + 参数: + key: 键,一般为模块名 - def save( - self, path: Optional[Union[str, Path]] = None, save_simple_data: bool = False - ): + 返回: + ConfigGroup: ConfigGroup """ - 保存数据 - :param path: 路径 - :param save_simple_data: 同时保存至config.yaml + return self._data.get(key) or ConfigGroup(module="") + + def save(self, path: str | Path | None = None, save_simple_data: bool = False): + """保存数据 + + 参数: + path: 路径. + save_simple_data: 同时保存至config.yaml. """ if save_simple_data: with open(self._simple_file, "w", encoding="utf8") as f: - yaml.dump( - self._simple_data, - f, - indent=2, - Dumper=yaml.RoundTripDumper, - allow_unicode=True, - ) + # yaml.dump( + # self._simple_data, + # f, + # indent=2, + # Dumper=yaml.RoundTripDumper, + # allow_unicode=True, + # ) + _yaml.dump(self._simple_data, f) path = path or self.file data = {} for module in self._data: @@ -249,16 +353,16 @@ class ConfigsManager: for config in self._data[module].configs: value = self._data[module].configs[config].dict() del value["type"] + del value["arg_parser"] data[module][config] = value with open(path, "w", encoding="utf8") as f: - yaml.dump( - data, f, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True - ) + # yaml.dump( + # data, f, indent=2, Dumper=yaml.RoundTripDumper, allow_unicode=True + # ) + _yaml.dump(data, f) def reload(self): - """ - 重新加载配置文件 - """ + """重新加载配置文件""" _yaml = YAML() if self._simple_file.exists(): with open(self._simple_file, "r", encoding="utf8") as f: @@ -269,14 +373,12 @@ class ConfigsManager: self.save() def load_data(self): - """ - 加载数据 + """加载数据 - Raises: - ValueError: _description_ + 异常: + ValueError: 配置文件为空! """ if self.file.exists(): - _yaml = YAML() with open(self.file, "r", encoding="utf8") as f: temp_data = _yaml.load(f) if not temp_data: @@ -291,19 +393,15 @@ class ConfigsManager: for module in temp_data: config_group = ConfigGroup(module=module) for config in temp_data[module]: - config_group.configs[config] = Config(**temp_data[module][config]) + config_group.configs[config] = ConfigModel( + **temp_data[module][config] + ) count += 1 self._data[module] = config_group logger.info( f"加载配置完成,共加载 {len(temp_data)} 个配置组及对应 {count} 个配置项" ) - def get_admin_level_data(self): - """ - 获取管理插件等级 - """ - return self._admin_level_data - def get_data(self) -> Dict[str, ConfigGroup]: return copy.deepcopy(self._data) diff --git a/zhenxun/models/fg_request.py b/zhenxun/models/fg_request.py new file mode 100644 index 00000000..ad740eff --- /dev/null +++ b/zhenxun/models/fg_request.py @@ -0,0 +1,111 @@ +from nonebot.adapters import Bot +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError + + +class FgRequest(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + request_type = fields.CharEnumField(RequestType, default=None, description="请求类型") + """请求类型""" + platform = fields.CharField(255, description="平台") + """平台""" + bot_id = fields.CharField(255, description="Bot Id") + """botId""" + flag = fields.CharField(max_length=255, default="", description="flag") + """flag""" + user_id = fields.CharField(max_length=255, description="请求用户id") + """请求用户id""" + group_id = fields.CharField(max_length=255, null=True, description="邀请入群id") + """邀请入群id""" + nickname = fields.CharField(max_length=255, description="请求人名称") + """对象名称""" + comment = fields.CharField(max_length=255, null=True, description="验证信息") + """验证信息""" + handle_type = fields.CharEnumField(RequestHandleType, null=True, description="处理类型") + """处理类型""" + + class Meta: + table = "fg_request" + table_description = "好友群组请求" + + @classmethod + async def approve(cls, bot: Bot, id: int, request_type: RequestType): + """同意请求 + + 参数: + bot: Bot + id: 请求id + request_type: 请求类型 + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(bot, id, request_type, RequestHandleType.APPROVE) + + @classmethod + async def refused(cls, bot: Bot, id: int, request_type: RequestType): + """拒绝请求 + + 参数: + bot: Bot + id: 请求id + request_type: 请求类型 + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(bot, id, request_type, RequestHandleType.REFUSED) + + @classmethod + async def ignore(cls, bot: Bot, id: int, request_type: RequestType): + """忽略请求 + + 参数: + bot: Bot + id: 请求id + request_type: 请求类型 + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(bot, id, request_type, RequestHandleType.IGNORE) + + @classmethod + async def _handle_request( + cls, + bot: Bot, + id: int, + request_type: RequestType, + handle_type: RequestHandleType, + ): + """处理请求 + + 参数: + bot: Bot + id: 请求id + request_type: 请求类型 + handle_type: 处理类型 + + 异常: + NotFoundError: 未发现请求 + """ + req = await cls.get_or_none(id=id, request_type=request_type) + if not req: + raise NotFoundError + req.handle_type = RequestHandleType + await req.save(update_fields=["handle_type"]) + if handle_type != RequestHandleType.IGNORE: + if request_type == RequestType.FRIEND: + await bot.set_friend_add_request( + flag=req.flag, approve=handle_type == RequestHandleType.APPROVE + ) + else: + await bot.set_group_add_request( + flag=req.flag, + sub_type="invite", + approve=handle_type == RequestHandleType.APPROVE, + ) diff --git a/models/friend_user.py b/zhenxun/models/friend_user.py old mode 100755 new mode 100644 similarity index 73% rename from models/friend_user.py rename to zhenxun/models/friend_user.py index 9de696c9..f871e389 --- a/models/friend_user.py +++ b/zhenxun/models/friend_user.py @@ -1,18 +1,19 @@ -from tortoise import fields from typing import Union -from configs.config import Config -from services.db_context import Model + +from tortoise import fields + +from zhenxun.configs.config import Config +from zhenxun.services.db_context import Model class FriendUser(Model): - id = fields.IntField(pk=True, generated=True, auto_increment=True) """自增id""" - user_id = fields.CharField(255, unique=True) + user_id = fields.CharField(255, unique=True, description="用户id") """用户id""" - user_name = fields.CharField(max_length=255, default="") + user_name = fields.CharField(max_length=255, default="", description="用户名称") """用户名称""" - nickname = fields.CharField(max_length=255, null=True) + nickname = fields.CharField(max_length=255, null=True, description="用户自定义昵称") """私聊下自定义昵称""" class Meta: @@ -57,9 +58,13 @@ class FriendUser(Model): :param user_id: 用户id :param nickname: 昵称 """ - await cls.update_or_create(user_id=str(user_id), defaults={"nickname": nickname}) + await cls.update_or_create( + user_id=str(user_id), defaults={"nickname": nickname} + ) @classmethod async def _run_script(cls): - await cls.raw("ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);") + await cls.raw( + "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);" + ) # 将user_id字段类型改为character varying(255)) diff --git a/models/group_info.py b/zhenxun/models/group_info copy.py old mode 100755 new mode 100644 similarity index 55% rename from models/group_info.py rename to zhenxun/models/group_info copy.py index 85b2d468..64df8c5a --- a/models/group_info.py +++ b/zhenxun/models/group_info copy.py @@ -2,12 +2,11 @@ from typing import List, Optional from tortoise import fields -from services.db_context import Model -from services.log import logger +from zhenxun.services.db_context import Model +from zhenxun.services.log import logger class GroupInfo(Model): - group_id = fields.CharField(255, pk=True) """群聊id""" group_name = fields.TextField(default="") @@ -16,7 +15,7 @@ class GroupInfo(Model): """最大人数""" member_count = fields.IntField(default=0) """当前人数""" - group_flag: int = fields.IntField(default=0) + group_flag = fields.IntField(default=0) """群认证标记""" class Meta: @@ -25,7 +24,8 @@ class GroupInfo(Model): @classmethod def _run_script(cls): - return ["ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag - "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" - # 将group_id字段类型改为character varying(255) - ] + return [ + "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag + "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" + # 将group_id字段类型改为character varying(255) + ] diff --git a/zhenxun/models/group_info.py b/zhenxun/models/group_info.py new file mode 100644 index 00000000..e8249c44 --- /dev/null +++ b/zhenxun/models/group_info.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class GroupInfo(Model): + group_id = fields.CharField(255, pk=True, description="群组id") + """群聊id""" + group_name = fields.TextField(default="", description="群组名称") + """群聊名称""" + max_member_count = fields.IntField(default=0, description="最大人数") + """最大人数""" + member_count = fields.IntField(default=0, description="当前人数") + """当前人数""" + group_flag = fields.IntField(default=0, description="群认证标记") + """群认证标记""" + + class Meta: + table = "group_info" + table_description = "群聊信息表" + + @classmethod + def _run_script(cls): + return [ + "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag + "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" + # 将group_id字段类型改为character varying(255) + ] diff --git a/models/group_member_info.py b/zhenxun/models/group_member_info.py old mode 100755 new mode 100644 similarity index 87% rename from models/group_member_info.py rename to zhenxun/models/group_member_info.py index 687af219..e7bbe586 --- a/models/group_member_info.py +++ b/zhenxun/models/group_member_info.py @@ -3,13 +3,12 @@ from typing import List, Optional, Set, Union from tortoise import fields -from configs.config import Config -from services.db_context import Model -from services.log import logger +from zhenxun.configs.config import Config +from zhenxun.services.db_context import Model +from zhenxun.services.log import logger class GroupInfoUser(Model): - id = fields.IntField(pk=True, generated=True, auto_increment=True) """自增id""" user_id = fields.CharField(255) @@ -18,7 +17,7 @@ class GroupInfoUser(Model): """用户昵称""" group_id = fields.CharField(255) """群聊id""" - user_join_time: datetime = fields.DatetimeField(null=True) + user_join_time = fields.DatetimeField(null=True) """用户入群时间""" nickname = fields.CharField(255, null=True) """群聊昵称""" @@ -43,7 +42,9 @@ class GroupInfoUser(Model): ) # type: ignore @classmethod - async def set_user_nickname(cls, user_id: Union[int, str], group_id: Union[int, str], nickname: str): + async def set_user_nickname( + cls, user_id: Union[int, str], group_id: Union[int, str], nickname: str + ): """ 说明: 设置群员在该群内的昵称 @@ -71,7 +72,9 @@ class GroupInfoUser(Model): ) # type: ignore @classmethod - async def get_user_nickname(cls, user_id: Union[int, str], group_id: Union[int, str]) -> str: + async def get_user_nickname( + cls, user_id: Union[int, str], group_id: Union[int, str] + ) -> str: """ 说明: 获取用户在该群的昵称 @@ -90,7 +93,9 @@ class GroupInfoUser(Model): return "" @classmethod - async def get_group_member_uid(cls, user_id: Union[int, str], group_id: Union[int, str]) -> Optional[int]: + async def get_group_member_uid( + cls, user_id: Union[int, str], group_id: Union[int, str] + ) -> Optional[int]: logger.debug( f"GroupInfoUser 尝试获取 用户[{user_id}] 群聊[{group_id}] UID" ) @@ -118,5 +123,5 @@ class GroupInfoUser(Model): "ALTER TABLE group_info_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id "ALTER TABLE group_info_users ALTER COLUMN user_id TYPE character varying(255);", # 将user_id字段类型改为character varying(255) - "ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);" + "ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);", ] diff --git a/zhenxun/models/level_user.py b/zhenxun/models/level_user.py new file mode 100644 index 00000000..1495a315 --- /dev/null +++ b/zhenxun/models/level_user.py @@ -0,0 +1,134 @@ +from datetime import datetime +from typing import Union + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class LevelUser(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + user_level = fields.BigIntField() + """用户权限等级""" + group_flag = fields.IntField(default=0) + """特殊标记,是否随群管理员变更而设置权限""" + + class Meta: + table = "level_users" + table_description = "用户权限数据库" + unique_together = ("user_id", "group_id") + + @classmethod + async def get_user_level(cls, user_id: int | str, group_id: int | str) -> int: + """ + 获取用户在群内的等级 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + int: 权限等级 + """ + if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + return user.user_level + return 0 + + @classmethod + async def set_level( + cls, + user_id: int | str, + group_id: int | str, + level: int, + group_flag: int = 0, + ): + """ + 设置用户在群内的权限 + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + group_flag: 是否被自动更新刷新权限 0:是, 1:否. + """ + await cls.update_or_create( + user_id=str(user_id), + group_id=str(group_id), + defaults={ + "user_level": level, + "group_flag": group_flag, + }, + ) + + @classmethod + async def delete_level(cls, user_id: int | str, group_id: int | str) -> bool: + """ + 删除用户权限 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否含有用户权限 + """ + if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + await user.delete() + return True + return False + + @classmethod + async def check_level( + cls, user_id: int | str, group_id: int | str, level: int + ) -> bool: + """ + 检查用户权限等级是否大于 level + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + + 返回: + bool: 是否大于level + """ + if group_id: + if user := await cls.get_or_none( + user_id=str(user_id), group_id=str(group_id) + ): + return user.user_level >= level + else: + user_list = await cls.filter(user_id=str(user_id)).all() + user = max(user_list, key=lambda x: x.user_level) + return user.user_level >= level + return False + + @classmethod + async def is_group_flag(cls, user_id: int | str, group_id: int | str) -> bool: + """ + 检测是否会被自动更新刷新权限 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否会被自动更新权限刷新 + """ + if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + return user.group_flag == 1 + return False + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE level_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE level_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE level_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/plugin_info.py b/zhenxun/models/plugin_info.py new file mode 100644 index 00000000..15eb2a0a --- /dev/null +++ b/zhenxun/models/plugin_info.py @@ -0,0 +1,51 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import BlockType, PluginType + +from .plugin_limit import PluginLimit + + +class PluginInfo(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="模块名") + """模块名""" + module_path = fields.CharField(255, description="模块路径", unique=True) + """模块路径""" + name = fields.CharField(255, description="插件名称") + """插件名称""" + status = fields.BooleanField(default=True, description="全局开关状态") + """全局开关状态""" + block_type = fields.CharEnumField( + BlockType, default=None, null=True, description="禁用类型" + ) + """禁用类型""" + load_status = fields.BooleanField(default=True, description="加载状态") + """加载状态""" + author = fields.CharField(255, null=True, description="作者") + """作者""" + version = fields.CharField(max_length=255, null=True, description="版本") + """版本""" + level = fields.IntField(default=5, description="所需群权限") + """所需群权限""" + default_status = fields.BooleanField(default=True, description="进群默认开关状态") + """进群默认开关状态""" + limit_superuser = fields.BooleanField(default=False, description="是否限制超级用户") + """是否限制超级用户""" + menu_type = fields.CharField(max_length=255, default="功能", description="菜单类型") + """菜单类型""" + plugin_type = fields.CharEnumField(PluginType, null=True, description="插件类型") + """插件类型""" + cost_gold = fields.IntField(default=0, description="调用插件所需金币") + """调用插件所需金币""" + plugin_limit = fields.ReverseRelation["PluginLimit"] + """插件限制""" + admin_level = fields.IntField(default=0, null=True, description="调用所需权限等级") + """调用所需权限等级""" + is_delete = fields.BooleanField(default=False, description="是否删除") + """是否删除""" + + class Meta: + table = "plugin_info" + table_description = "插件基本信息" diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py new file mode 100644 index 00000000..89247886 --- /dev/null +++ b/zhenxun/models/plugin_limit.py @@ -0,0 +1,44 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import ( + BlockType, + LimitCheckType, + LimitWatchType, + PluginLimitType, +) + + +class PluginLimit(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="模块名") + """模块名""" + module_path = fields.CharField(255, description="模块路径") + """模块路径""" + plugin = fields.ForeignKeyField( + "models.PluginInfo", + related_name="plugin_limit", + on_delete=fields.CASCADE, + description="所属插件", + ) + limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") + """限制类型""" + watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") + """限制类型""" + status = fields.BooleanField(default=True, description="限制的开关状态") + """限制的开关状态""" + check_type = fields.CharEnumField( + LimitCheckType, default=BlockType.ALL, description="检查类型" + ) + """检查类型""" + result = fields.CharField(max_length=255, null=True, description="返回信息") + """返回信息""" + cd = fields.IntField(null=True, description="cd") + """cd""" + max_count = fields.IntField(null=True, description="最大调用次数") + """最大调用次数""" + + class Meta: + table = "plugin_limit" + table_description = "插件限制" diff --git a/services/__init__.py b/zhenxun/services/__init__.py old mode 100755 new mode 100644 similarity index 95% rename from services/__init__.py rename to zhenxun/services/__init__.py index 8bbd77c0..aae2c093 --- a/services/__init__.py +++ b/zhenxun/services/__init__.py @@ -1,2 +1,2 @@ -from .db_context import * -from .log import * +from .db_context import * +from .log import * diff --git a/services/db_context.py b/zhenxun/services/db_context.py old mode 100755 new mode 100644 similarity index 74% rename from services/db_context.py rename to zhenxun/services/db_context.py index 20afb41d..a0f9c7dc --- a/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -1,83 +1,90 @@ -from typing import List - -from nonebot.utils import is_coroutine_callable -from tortoise import Tortoise, fields -from tortoise.connection import connections -from tortoise.models import Model as Model_ -from tortoise.queryset import RawSQLQuery - -from configs.config import address, bind, database, password, port, sql_name, user -from utils.text_utils import prompt2cn - -from .log import logger - -MODELS: List[str] = [] - -SCRIPT_METHOD = [] - - -class Model(Model_): - """ - 自动添加模块 - - Args: - Model_ (_type_): _description_ - """ - - def __init_subclass__(cls, **kwargs): - MODELS.append(cls.__module__) - - if func := getattr(cls, "_run_script", None): - SCRIPT_METHOD.append((cls.__module__, func)) - - -class TestSQL(Model): - - id = fields.IntField(pk=True, generated=True, auto_increment=True) - """自增id""" - - class Meta: - table = "test_sql" - table_description = "执行SQL命令,不记录任何数据" - - -async def init(): - if not bind and not any([user, password, address, port, database]): - raise ValueError("\n" + prompt2cn("数据库配置未填写", 28)) - i_bind = bind - if not i_bind: - i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" - try: - await Tortoise.init( - db_url=i_bind, - modules={"models": MODELS}, - # timezone="Asia/Shanghai" - ) - await Tortoise.generate_schemas() - logger.info(f"Database loaded successfully!") - except Exception as e: - raise Exception(f"数据库连接错误.... {type(e)}: {e}") - if SCRIPT_METHOD: - logger.debug(f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个...") - sql_list = [] - for module, func in SCRIPT_METHOD: - try: - if is_coroutine_callable(func): - sql = await func() - else: - sql = func() - if sql: - sql_list += sql - - except Exception as e: - logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) - for sql in sql_list: - logger.debug(f"执行SQL: {sql}") - try: - await TestSQL.raw(sql) - except Exception as e: - logger.debug(f"执行SQL: {sql} 错误...", e=e) - - -async def disconnect(): - await connections.close_all() +from typing import List + +from nonebot.utils import is_coroutine_callable +from tortoise import Tortoise, fields +from tortoise.connection import connections +from tortoise.models import Model as Model_ +from tortoise.queryset import RawSQLQuery + +from zhenxun.configs.config import ( + address, + bind, + database, + password, + port, + sql_name, + user, +) + +from .log import logger + +MODELS: List[str] = [] + +SCRIPT_METHOD = [] + + +class Model(Model_): + """ + 自动添加模块 + + Args: + Model_ (_type_): _description_ + """ + + def __init_subclass__(cls, **kwargs): + MODELS.append(cls.__module__) + + if func := getattr(cls, "_run_script", None): + SCRIPT_METHOD.append((cls.__module__, func)) + + +class TestSQL(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + + class Meta: + abstract = True + table = "test_sql" + table_description = "执行SQL命令,不记录任何数据" + + +async def init(): + if not bind and not any([user, password, address, port, database]): + raise ValueError("\n数据库配置未填写.......") + i_bind = bind + if not i_bind: + i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" + # try: + await Tortoise.init( + db_url=i_bind, + modules={"models": MODELS}, + # timezone="Asia/Shanghai" + ) + await Tortoise.generate_schemas() + logger.info(f"Database loaded successfully!") + # except Exception as e: + # raise Exception(f"数据库连接错误... {type(e)}: {e}") + if SCRIPT_METHOD: + logger.debug(f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个...") + sql_list = [] + for module, func in SCRIPT_METHOD: + try: + if is_coroutine_callable(func): + sql = await func() + else: + sql = func() + if sql: + sql_list += sql + + except Exception as e: + logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) + for sql in sql_list: + logger.debug(f"执行SQL: {sql}") + try: + await TestSQL.raw(sql) + except Exception as e: + logger.debug(f"执行SQL: {sql} 错误...", e=e) + + +async def disconnect(): + await connections.close_all() diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py new file mode 100644 index 00000000..8220ca01 --- /dev/null +++ b/zhenxun/services/log.py @@ -0,0 +1,307 @@ +from datetime import datetime, timedelta +from typing import Any, Dict, overload + +from nonebot import require + +require("nonebot_plugin_session") +from loguru import logger as logger_ +from nonebot.log import default_filter, default_format +from nonebot_plugin_session import Session + +from zhenxun.configs.path_config import LOG_PATH + +logger_.add( + LOG_PATH / f"{datetime.now().date()}.log", + level="INFO", + rotation="00:00", + format=default_format, + filter=default_filter, + retention=timedelta(days=30), +) + +logger_.add( + LOG_PATH / f"error_{datetime.now().date()}.log", + level="ERROR", + rotation="00:00", + format=default_format, + filter=default_filter, + retention=timedelta(days=30), +) + + +class logger: + TEMPLATE_A = "Adapter[{}] {}" + TEMPLATE_B = "Adapter[{}] [{}]: {}" + TEMPLATE_C = "Adapter[{}] 用户[{}] 触发 [{}]: {}" + TEMPLATE_D = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}]: {}" + TEMPLATE_E = "Adapter[{}] 群聊[{}] 用户[{}] 触发 [{}] [Target]({}): {}" + + TEMPLATE_ADAPTER = "Adapter[{}] " + TEMPLATE_USER = "用户[{}] " + TEMPLATE_GROUP = "群聊[{}] " + TEMPLATE_COMMAND = "CMD[{}] " + TEMPLATE_TARGET = "[Target]([{}]) " + + SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" + + WARNING_TEMPLATE = "[{}]: {}" + + ERROR_TEMPLATE = "[{}]: {}" + + @overload + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + ): + ... + + @overload + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + ): + ... + + @classmethod + def info( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3 or session.id2: + group_id = f"{session.id3}:{session.id2}" + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target + ) + logger_.opt(colors=True).info(template) + + @classmethod + def success( + cls, + info: str, + command: str, + param: Dict[str, Any] | None = None, + result: str = "", + ): + param_str = "" + if param: + param_str = ",".join([f"{k}:{v}" for k, v in param.items()]) + logger_.opt(colors=True).success( + cls.SUCCESS_TEMPLATE.format(command, info, param_str, result) + ) + + @overload + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @overload + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @classmethod + def warning( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3 or session.id2: + group_id = f"{session.id3}:{session.id2}" + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target + ) + if e: + template += f" || 错误{type(e)}: {e}" + logger_.opt(colors=True).warning(template) + + @overload + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @overload + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @classmethod + def error( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3 or session.id2: + group_id = f"{session.id3}:{session.id2}" + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target + ) + if e: + template += f" || 错误 {type(e)}: {e}" + logger_.opt(colors=True).error(template) + + @overload + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @overload + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: Session | None = None, + target: Any = None, + e: Exception | None = None, + ): + ... + + @classmethod + def debug( + cls, + info: str, + command: str | None = None, + *, + session: int | str | Session | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + e: Exception | None = None, + ): + user_id: str | None = session # type: ignore + group_id = None + if type(session) == Session: + user_id = session.id1 + adapter = session.bot_type + if session.id3 or session.id2: + group_id = f"{session.id3}:{session.id2}" + template = cls.__parser_template( + info, command, user_id, group_id, adapter, target + ) + if e: + template += f" || 错误 {type(e)}: {e}" + logger_.opt(colors=True).debug(template) + + @classmethod + def __parser_template( + cls, + info: str, + command: str | None = None, + user_id: int | str | None = None, + group_id: int | str | None = None, + adapter: str | None = None, + target: Any = None, + ) -> str: + arg_list = [] + template = "" + if adapter is not None: + template += cls.TEMPLATE_ADAPTER + arg_list.append(adapter) + if group_id is not None: + template += cls.TEMPLATE_GROUP + arg_list.append(group_id) + if user_id is not None: + template += cls.TEMPLATE_USER + arg_list.append(user_id) + if command is not None: + template += cls.TEMPLATE_COMMAND + arg_list.append(command) + if target is not None: + template += cls.TEMPLATE_TARGET + arg_list.append(target) + arg_list.append(info) + template += "{}" + return template.format(*arg_list) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py new file mode 100644 index 00000000..6649e542 --- /dev/null +++ b/zhenxun/utils/_build_image.py @@ -0,0 +1,656 @@ +import base64 +import math +from io import BytesIO +from pathlib import Path +from typing import List, Literal, Tuple, TypeAlias, overload + +from nonebot.utils import run_sync +from PIL import Image, ImageDraw, ImageFilter, ImageFont +from PIL.Image import Image as tImage +from PIL.ImageFont import FreeTypeFont +from typing_extensions import Self + +from zhenxun.configs.path_config import FONT_PATH + +ModeType = Literal[ + "1", "CMYK", "F", "HSV", "I", "L", "LAB", "P", "RGB", "RGBA", "RGBX", "YCbCr" +] +"""图片类型""" + +ColorAlias: TypeAlias = str | Tuple[int, int, int] | Tuple[int, int, int, int] | None + +CenterType = Literal["center", "height", "width"] +""" +粘贴居中 + +center: 水平垂直居中 + +height: 垂直居中 + +width: 水平居中 +""" + + +class BuildImage: + """ + 快捷生成图片与操作图片的工具类 + """ + + def __init__( + self, + width: int = 0, + height: int = 0, + color: ColorAlias = None, + mode: ModeType = "RGBA", + font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf", + font_size: int = 20, + background: str | BytesIO | Path | None = None, + ) -> None: + self.width = width + self.height = height + self.color = color + self.font = ( + self.load_font(font, font_size) + if not isinstance(font, FreeTypeFont) + else font + ) + if background: + self.markImg = Image.open(background) + if width and height: + self.markImg = self.markImg.resize((width, height), Image.LANCZOS) + else: + if not width or not height: + raise ValueError("长度和宽度不能为空...") + self.markImg = Image.new(mode, (width, height), color) # type: ignore + self.draw = ImageDraw.Draw(self.markImg) + + @property + def size(self) -> Tuple[int, int]: + return self.markImg.size + + @classmethod + async def build_text_image( + cls, + text: str, + font: str | Path = "HYWenHei-85W.ttf", + size: int = 10, + font_color: str | Tuple[int, int, int] = (0, 0, 0), + color: ColorAlias = None, + padding: int | Tuple[int, int, int, int] | None = None, + ) -> Self: + """构建文本图片 + + 参数: + text: 文本 + font: 字体路径 + size: 字体大小 + font_color: 字体颜色. + color: 背景颜色 + padding: 外边距 + + 返回: + Self: Self + """ + _font = cls.load_font(font, size) + width, height = cls.get_text_size(text, _font) + if type(padding) == int: + width += padding * 2 + height += padding * 2 + elif type(padding) == tuple: + width += padding[1] + padding[3] + height += padding[0] + padding[2] + markImg = cls(width, height, color) + await markImg.text( + (0, 0), text, fill=font_color, font=_font, center_type="center" + ) + return markImg + + @classmethod + async def auto_paste( + cls, + img_list: list[Self | tImage], + row: int, + space: int = 10, + padding: int = 50, + color: ColorAlias = (255, 255, 255, 0), + background: str | BytesIO | Path | None = None, + ) -> Self | None: + """自动贴图 + + 参数: + img_list: 图片列表 + row: 一行图片的数量 + space: 图片之间的间距. + padding: 外边距. + color: 图片背景颜色. + background: 图片背景图片. + + 返回: + Self: Self + """ + if not img_list: + return None + width, height = img_list[0].size + background_width = width * row + space * (row - 1) + padding * 2 + column = math.ceil(len(img_list) / row) + background_height = height * column + space * (column - 1) + padding * 2 + background_image = cls( + background_width, background_height, color=color, background=background + ) + _cur_width, _cur_height = padding, padding + for img in img_list: + await background_image.paste(img, (_cur_width, _cur_height)) + _cur_width += space + img.width + _cur_height += space + img.height + if _cur_width + padding >= background_image.width: + _cur_width = padding + return background_image + + @classmethod + def load_font(cls, font: str | Path, font_size: int) -> FreeTypeFont: + """ + 加载字体 + + 参数: + font: 字体名称 + font_size: 字体大小 + + 返回: + FreeTypeFont: 字体 + """ + path = FONT_PATH / font if type(font) == str else font + return ImageFont.truetype(str(path), font_size) + + @overload + @classmethod + def get_text_size( + cls, text: str, font: FreeTypeFont | None = None + ) -> Tuple[int, int]: + ... + + @overload + @classmethod + def get_text_size( + cls, text: str, font: str | None = None, font_size: int = 10 + ) -> Tuple[int, int]: + ... + + @classmethod + def get_text_size( + cls, text: str, font: str | FreeTypeFont | None = None, font_size: int = 10 + ) -> Tuple[int, int]: + """获取该字体下文本需要的长宽 + + 参数: + text: 文本内容 + font: 字体名称或FreeTypeFont + font_size: 字体大小 + + 返回: + Tuple[int, int]: 长宽 + """ + _font = font + if font and type(font) == str: + _font = cls.load_font(font, font_size) + return _font.getsize(text) # type: ignore + + def getsize(self, msg: str) -> Tuple[int, int]: + """ + 获取文字在该图片 font_size 下所需要的空间 + + 参数: + msg: 文本 + + 返回: + Tuple[int, int]: 长宽 + """ + return self.font.getsize(msg) # type: ignore + + def __center_xy( + self, + pos: Tuple[int, int], + width: int, + height: int, + center_type: CenterType | None, + ) -> Tuple[int, int]: + """ + 根据居中类型定位xy + + 参数: + pos: 定位 + image: image + center_type: 居中类型 + + 返回: + Tuple[int, int]: 定位 + """ + # _width, _height = pos + if self.width and self.height: + if center_type == "center": + width = int((self.width - width) / 2) + height = int((self.height - height) / 2) + elif center_type == "width": + width = int((self.width - width) / 2) + height = pos[1] + elif center_type == "height": + width = pos[0] + height = int((self.height - height) / 2) + return width, height + + @run_sync + def paste( + self, + image: Self | tImage, + pos: Tuple[int, int] = (0, 0), + center_type: CenterType | None = None, + ) -> Self: + """贴图 + + 参数: + image: BuildImage 或 Image + pos: 定位. + center_type: 居中. + + 返回: + BuildImage: Self + + 异常: + ValueError: 居中类型错误 + """ + if center_type and center_type not in ["center", "height", "width"]: + raise ValueError("center_type must be 'center', 'width' or 'height'") + width, height = 0, 0 + _image = image + if isinstance(image, BuildImage): + _image = image.markImg + if _image.width and _image.height and center_type: + pos = self.__center_xy(pos, _image.width, _image.height, center_type) + self.markImg.paste(_image, pos, _image) # type: ignore + return self + + @run_sync + def point( + self, pos: Tuple[int, int], fill: Tuple[int, int, int] | None = None + ) -> Self: + """ + 绘制多个或单独的像素 + + 参数: + pos: 坐标 + fill: 填充颜色. + + 返回: + BuildImage: Self + """ + self.draw.point(pos, fill=fill) + return self + + @run_sync + def ellipse( + self, + pos: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | None = None, + outline: Tuple[int, int, int] | None = None, + width: int = 1, + ) -> Self: + """ + 绘制圆 + + 参数: + pos: 坐标范围 + fill: 填充颜色. + outline: 描线颜色. + width: 描线宽度. + + 返回: + BuildImage: Self + """ + self.draw.ellipse(pos, fill, outline, width) + return self + + @run_sync + def text( + self, + pos: Tuple[int, int], + text: str, + fill: str | Tuple[int, int, int] = (0, 0, 0), + center_type: CenterType | None = None, + font: FreeTypeFont | str | Path | None = None, + font_size: int = 10, + ) -> Self: + """ + 在图片上添加文字 + + 参数: + pos: 文字位置 + text: 文字内容 + fill: 文字颜色. + center_type: 居中类型. + font: 字体. + font_size: 字体大小. + + 返回: + BuildImage: Self + + 异常: + ValueError: 居中类型错误 + """ + if center_type and center_type not in ["center", "height", "width"]: + raise ValueError("center_type must be 'center', 'width' or 'height'") + width, height = 0, 0 + max_length_text = "" + sentence = text.split("\n") + for x in sentence: + max_length_text = x if len(x) > len(max_length_text) else max_length_text + if font: + if not isinstance(font, FreeTypeFont): + font = self.load_font(font, font_size) + else: + font = self.font + if center_type: + ttf_w, ttf_h = font.getsize(max_length_text) # type: ignore + ttf_h = ttf_h * len(sentence) + pos = self.__center_xy(pos, ttf_w, ttf_h, center_type) + self.draw.text(pos, text, fill=fill, font=font) + return self + + @run_sync + def save(self, path: str | Path): + """ + 保存图片 + + 参数: + path: 图片路径 + """ + self.markImg.save(path) # type: ignore + + def show(self): + """ + 说明: + 显示图片 + """ + self.markImg.show() + + @run_sync + def resize(self, ratio: float = 0, width: int = 0, height: int = 0) -> Self: + """ + 压缩图片 + + 参数: + ratio: 压缩倍率. + width: 压缩图片宽度至 width. + height: 压缩图片高度至 height. + + 返回: + BuildImage: Self + + 异常: + ValueError: 缺少参数 + """ + if not width and not height and not ratio: + raise ValueError("缺少参数...") + if self.width and self.height: + if not width and not height and ratio: + width = int(self.width * ratio) + height = int(self.height * ratio) + self.markImg = self.markImg.resize((width, height), Image.LANCZOS) # type: ignore + self.width, self.height = self.markImg.size + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def crop(self, box: Tuple[int, int, int, int]) -> Self: + """ + 裁剪图片 + + 参数: + box: 左上角坐标,右下角坐标 (left, upper, right, lower) + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.crop(box) + self.width, self.height = self.markImg.size + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def transparent(self, alpha_ratio: float = 1, n: int = 0) -> Self: + """ + 图片透明化 + + 参数: + alpha_ratio: 透明化程度. + n: 透明化大小内边距. + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.convert("RGBA") + x, y = self.markImg.size + for i in range(n, x - n): + for k in range(n, y - n): + color = self.markImg.getpixel((i, k)) + color = color[:-1] + (int(100 * alpha_ratio),) + self.markImg.putpixel((i, k), color) + self.draw = ImageDraw.Draw(self.markImg) + return self + + def pic2bs4(self) -> str: + """ + BuildImage 转 base64 + """ + buf = BytesIO() + self.markImg.save(buf, format="PNG") + base64_str = base64.b64encode(buf.getvalue()).decode() + return "base64://" + base64_str + + def convert(self, type_: ModeType) -> Self: + """ + 修改图片类型 + + 参数: + type_: ModeType + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.convert(type_) + return self + + @run_sync + def rectangle( + self, + xy: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | None = None, + outline: str | None = None, + width: int = 1, + ) -> Self: + """ + 画框 + + 参数: + xy: 坐标 + fill: 填充颜色. + outline: 轮廓颜色. + width: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.rectangle(xy, fill, outline, width) + return self + + @run_sync + def polygon( + self, + xy: List[Tuple[int, int]], + fill: Tuple[int, int, int] = (0, 0, 0), + outline: int = 1, + ) -> Self: + """ + 画多边形 + + 参数: + xy: 坐标 + fill: 颜色. + outline: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.polygon(xy, fill, outline) + return self + + @run_sync + def line( + self, + xy: Tuple[int, int, int, int], + fill: Tuple[int, int, int] | str = "#D8DEE4", + width: int = 1, + ) -> Self: + """ + 画线 + + 参数: + xy: 坐标 + fill: 填充. + width: 线宽. + + 返回: + BuildImage: Self + """ + self.draw.line(xy, fill, width) + return self + + @run_sync + def circle(self) -> Self: + """ + 图像变圆 + + 返回: + BuildImage: Self + """ + self.markImg.convert("RGBA") + size = self.markImg.size + r2 = min(size[0], size[1]) + if size[0] != size[1]: + self.markImg = self.markImg.resize((r2, r2), Image.LANCZOS) # type: ignore + width = 1 + antialias = 4 + ellipse_box = [0, 0, r2 - 2, r2 - 2] + mask = Image.new( + size=[int(dim * antialias) for dim in self.markImg.size], # type: ignore + mode="L", + color="black", + ) + draw = ImageDraw.Draw(mask) + for offset, fill in (width / -2.0, "black"), (width / 2.0, "white"): + left, top = [(value + offset) * antialias for value in ellipse_box[:2]] + right, bottom = [(value - offset) * antialias for value in ellipse_box[2:]] + draw.ellipse([left, top, right, bottom], fill=fill) + mask = mask.resize(self.markImg.size, Image.LANCZOS) + try: + self.markImg.putalpha(mask) + except ValueError: + pass + return self + + @run_sync + def circle_corner( + self, + radii: int = 30, + point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], + ) -> Self: + """ + 矩形四角变圆 + + 参数: + radii: 半径. + point_list: 需要变化的角. + + 返回: + BuildImage: Self + """ + # 画圆(用于分离4个角) + img = self.markImg.convert("RGBA") + alpha = img.split()[-1] + circle = Image.new("L", (radii * 2, radii * 2), 0) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形 + w, h = img.size + if "lt" in point_list: + alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) + if "rt" in point_list: + alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0)) + if "lb" in point_list: + alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii)) + if "rb" in point_list: + alpha.paste( + circle.crop((radii, radii, radii * 2, radii * 2)), + (w - radii, h - radii), + ) + img.putalpha(alpha) + self.markImg = img + self.draw = ImageDraw.Draw(self.markImg) + return self + + @run_sync + def rotate(self, angle: int, expand: bool = False) -> Self: + """ + 旋转图片 + + 参数: + angle: 角度 + expand: 放大图片适应角度. + + 返回: + BuildImage: Self + """ + self.markImg = self.markImg.rotate(angle, expand=expand) + return self + + @run_sync + def transpose(self, angle: Literal[0, 1, 2, 3, 4, 5, 6]) -> Self: + """ + 旋转图片(包括边框) + + 参数: + angle: 角度 + + 返回: + BuildImage: Self + """ + self.markImg.transpose(angle) + return self + + @run_sync + def filter(self, filter_: str, aud: int | None = None) -> Self: + """ + 图片变化 + + 参数: + filter_: 变化效果 + aud: 利率. + + 返回: + BuildImage: Self + """ + _type = None + if filter_ == "GaussianBlur": # 高斯模糊 + _type = ImageFilter.GaussianBlur + elif filter_ == "EDGE_ENHANCE": # 锐化效果 + _type = ImageFilter.EDGE_ENHANCE + elif filter_ == "BLUR": # 模糊效果 + _type = ImageFilter.BLUR + elif filter_ == "CONTOUR": # 铅笔滤镜 + _type = ImageFilter.CONTOUR + elif filter_ == "FIND_EDGES": # 边缘检测 + _type = ImageFilter.FIND_EDGES + if _type: + if aud: + self.markImg = self.markImg.filter(_type(aud)) # type: ignore + else: + self.markImg = self.markImg.filter(_type) + self.draw = ImageDraw.Draw(self.markImg) + return self diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py new file mode 100644 index 00000000..65b6d371 --- /dev/null +++ b/zhenxun/utils/_build_mat.py @@ -0,0 +1,3 @@ +class BuildMat: + def pic2bs4(self): + return "" diff --git a/utils/browser.py b/zhenxun/utils/browser.py old mode 100755 new mode 100644 similarity index 94% rename from utils/browser.py rename to zhenxun/utils/browser.py index 66530008..88e252b0 --- a/utils/browser.py +++ b/zhenxun/utils/browser.py @@ -1,48 +1,47 @@ -import asyncio -from typing import Optional - -from nonebot import get_driver -from playwright.async_api import Browser, Playwright, async_playwright - -from services.log import logger - -driver = get_driver() - -_playwright: Optional[Playwright] = None -_browser: Optional[Browser] = None - - -@driver.on_startup -async def start_browser(): - global _playwright - global _browser - _playwright = await async_playwright().start() - _browser = await _playwright.chromium.launch() - - -@driver.on_shutdown -async def shutdown_browser(): - if _browser: - await _browser.close() - if _playwright: - await _playwright.stop() # type: ignore - - -def get_browser() -> Browser: - if not _browser: - raise RuntimeError("playwright is not initalized") - return _browser - - -def install(): - """自动安装、更新 Chromium""" - logger.info("正在检查 Chromium 更新") - import sys - - from playwright.__main__ import main - - sys.argv = ["", "install", "chromium"] - try: - main() - except SystemExit: - pass +from typing import Optional + +from nonebot import get_driver +from playwright.async_api import Browser, Playwright, async_playwright + +from services.log import logger + +driver = get_driver() + +_playwright: Optional[Playwright] = None +_browser: Optional[Browser] = None + + +@driver.on_startup +async def start_browser(): + global _playwright + global _browser + _playwright = await async_playwright().start() + _browser = await _playwright.chromium.launch() + + +@driver.on_shutdown +async def shutdown_browser(): + if _browser: + await _browser.close() + if _playwright: + await _playwright.stop() # type: ignore + + +def get_browser() -> Browser: + if not _browser: + raise RuntimeError("playwright is not initalized") + return _browser + + +def install(): + """自动安装、更新 Chromium""" + logger.info("正在检查 Chromium 更新") + import sys + + from playwright.__main__ import main + + sys.argv = ["", "install", "chromium"] + try: + main() + except SystemExit: + pass diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py new file mode 100644 index 00000000..5de75bf4 --- /dev/null +++ b/zhenxun/utils/enum.py @@ -0,0 +1,73 @@ +from strenum import StrEnum + + +class PluginType(StrEnum): + """ + 插件类型 + """ + + SUPERUSER = "超级管理员插件" + ADMIN = "管理员插件" + NORMAL = "普通插件" + HIDDEN = "被动插件" + + +class BlockType(StrEnum): + """ + 禁用状态 + """ + + FRIEND = "PRIVATE" + GROUP = "GROUP" + ALL = "ALL" + + +class PluginLimitType(StrEnum): + """ + 插件限制类型 + """ + + CD = "CD" + COUNT = "COUNT" + BLOCK = "BLOCK" + + +class LimitCheckType(StrEnum): + """ + 插件限制类型 + """ + + PRIVATE = "PRIVATE" + GROUP = "GROUP" + ALL = "ALL" + + +class LimitWatchType(StrEnum): + """ + 插件限制监听对象 + """ + + USER = "USER" + GROUP = "GROUP" + + +class RequestType(StrEnum): + """ + 请求类型 + """ + + FRIEND = "FRIEND" + GROUP = "GROUP" + + +class RequestHandleType(StrEnum): + """ + 请求类型 + """ + + APPROVE = "APPROVE" + """同意""" + REFUSED = "REFUSED" + """拒绝""" + IGNORE = "IGNORE" + """忽略""" diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py new file mode 100644 index 00000000..c901a5c9 --- /dev/null +++ b/zhenxun/utils/exception.py @@ -0,0 +1,2 @@ +class NotFoundError(Exception): + pass diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py new file mode 100644 index 00000000..fe09e627 --- /dev/null +++ b/zhenxun/utils/image_utils.py @@ -0,0 +1,2 @@ +from ._build_image import BuildImage +from ._build_mat import BuildMat diff --git a/zhenxun/utils/rules.py b/zhenxun/utils/rules.py new file mode 100644 index 00000000..f48c1106 --- /dev/null +++ b/zhenxun/utils/rules.py @@ -0,0 +1,46 @@ +from nonebot.adapters import Bot, Event +from nonebot.internal.rule import Rule +from nonebot.permission import SUPERUSER +from nonebot_plugin_session import EventSession, SessionLevel + +from zhenxun.configs.config import Config +from zhenxun.models.level_user import LevelUser + + +def admin_check(a: int | str, key: str | None = None) -> Rule: + """ + 管理员权限等级检查 + + 参数: + a: 权限等级或 配置项 module + key: 配置项 key. + + 返回: + Rule: Rule + """ + + async def _rule(bot: Bot, event: Event, session: EventSession) -> bool: + if await SUPERUSER(bot, event): + return True + if session.id1 and session.id2: + level = a + if type(a) == str and key: + level = Config.get_config(a, key) + if level is not None: + return bool(LevelUser.check_level(session.id1, session.id2, int(level))) + return False + + return Rule(_rule) + + +def ensure_group(session: EventSession) -> bool: + """ + 是否在群聊中 + + 参数: + session: session + + 返回: + bool: bool + """ + return session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3] diff --git a/plugins/my_info/data_source.py b/zhenxun/utils/typing.py similarity index 50% rename from plugins/my_info/data_source.py rename to zhenxun/utils/typing.py index 6fb66a5e..b28b04f6 100644 --- a/plugins/my_info/data_source.py +++ b/zhenxun/utils/typing.py @@ -1,6 +1,3 @@ - - - diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py new file mode 100644 index 00000000..660b0327 --- /dev/null +++ b/zhenxun/utils/utils.py @@ -0,0 +1,77 @@ +import os +from pathlib import Path + +import httpx + +from zhenxun.services.log import logger + + +class ResourceDirManager: + """ + 临时文件管理器 + """ + + temp_path = [] + + @classmethod + def __tree_append(cls, path: Path): + """递归添加文件夹 + + 参数: + path: 文件夹路径 + """ + for f in os.listdir(path): + file = path / f + if file.is_dir(): + if file not in cls.temp_path: + cls.temp_path.append(file) + logger.debug(f"添加临时文件夹: {path}") + cls.__tree_append(file) + + @classmethod + def add_temp_dir(cls, path: str | Path, tree: bool = False): + """添加临时清理文件夹,这些文件夹会被自动清理 + + 参数: + path: 文件夹路径 + tree: 是否递归添加文件夹 + """ + if isinstance(path, str): + path = Path(path) + if path not in cls.temp_path: + cls.temp_path.append(path) + logger.debug(f"添加临时文件夹: {path}") + if tree: + cls.__tree_append(path) + + +async def get_user_avatar(uid: int | str) -> bytes | None: + """快捷获取用户头像 + + 参数: + uid: 用户id + """ + url = f"http://q1.qlogo.cn/g?b=qq&nk={uid}&s=160" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error("获取用户头像错误", "Util", target=uid) + return None + + +async def get_group_avatar(gid: int | str) -> bytes | None: + """快捷获取用群头像 + + 参数: + :param gid: 群号 + """ + url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error("获取群头像错误", "Util", target=gid) + return None From eb0572ea770fe66bcc6a28060c1d579aef7c811b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 25 Feb 2024 03:18:34 +0800 Subject: [PATCH 002/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 11 +- .vscode/settings.json | 10 +- poetry.lock | 486 +++++++++++++++++- pyproject.toml | 4 + resources/image/sign/sign_res/bar.png | Bin 2535 -> 2435 bytes resources/image/sign/sign_res/bar_white.png | Bin 1431 -> 1587 bytes zhenxun/builtin_plugins/__init__.py | 9 + zhenxun/builtin_plugins/admin/admin_help.py | 165 ++++++ zhenxun/builtin_plugins/admin/admin_watch.py | 22 +- zhenxun/builtin_plugins/admin/ban/__init__.py | 196 +++++++ .../builtin_plugins/admin/ban/_data_source.py | 120 +++++ .../admin/group_member_update/__init__.py | 63 +++ .../admin/group_member_update/_data_source.py | 177 +++++++ .../admin/plugin_switch/__init__.py | 162 ++++++ .../admin/plugin_switch/_data_source.py | 244 +++++++++ .../builtin_plugins/admin/welcome_message.py | 71 +-- .../builtin_plugins/chat_history/__init__.py | 4 + .../chat_history/chat_message.py | 83 +++ .../chat_history/chat_message_handle.py | 116 +++++ zhenxun/builtin_plugins/help/__init__.py | 80 +++ zhenxun/builtin_plugins/help/_config.py | 13 + zhenxun/builtin_plugins/help/_data_source.py | 35 ++ zhenxun/builtin_plugins/help/_utils.py | 242 +++++++++ zhenxun/builtin_plugins/hooks/__init__.py | 43 ++ zhenxun/builtin_plugins/hooks/ban_hook.py | 61 +++ zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 104 ++++ .../builtin_plugins/hooks/withdraw_hook.py | 46 ++ zhenxun/builtin_plugins/init/init_plugin.py | 65 ++- zhenxun/builtin_plugins/nickname.py | 240 +++++++++ .../platform/qq/group_handle.py | 297 +++++++++++ zhenxun/builtin_plugins/record_request.py | 20 +- zhenxun/builtin_plugins/scheduler/__init__.py | 5 + .../builtin_plugins/scheduler/auto_backup.py | 62 +++ .../scheduler/auto_update_group.py | 68 +++ zhenxun/builtin_plugins/scheduler/morning.py | 28 + zhenxun/builtin_plugins/scripts.py | 59 +++ zhenxun/builtin_plugins/shop/__init__.py | 102 ++++ zhenxun/builtin_plugins/shop/_data_source.py | 319 ++++++++++++ zhenxun/builtin_plugins/sign_in/__init__.py | 148 ++++++ .../builtin_plugins/sign_in/_data_source.py | 175 +++++++ .../builtin_plugins/sign_in/_random_event.py | 33 ++ zhenxun/builtin_plugins/sign_in/config.py | 49 ++ .../builtin_plugins/sign_in/goods_register.py | 75 +++ zhenxun/builtin_plugins/sign_in/utils.py | 326 ++++++++++++ .../superuser/broadcast/__init__.py | 61 +++ .../superuser/broadcast/_data_source.py | 117 +++++ .../builtin_plugins/superuser/fg_manage.py | 14 +- .../builtin_plugins/superuser/super_help.py | 161 ++++++ .../superuser/update_fg_info.py | 119 ----- .../superuser/update_fg_info/__init__.py | 92 ++++ .../superuser/update_fg_info/_data_source.py | 156 ++++++ zhenxun/configs/utils/__init__.py | 30 +- zhenxun/models/bag_user.py | 160 ++++++ zhenxun/models/ban_console.py | 172 +++++++ zhenxun/models/chat_history.py | 125 +++++ zhenxun/models/friend_user.py | 61 ++- zhenxun/models/goods_info.py | 162 ++++++ zhenxun/models/group_console.py | 54 ++ zhenxun/models/group_info copy.py | 31 -- zhenxun/models/group_info.py | 29 +- zhenxun/models/group_member_info.py | 96 ++-- zhenxun/models/level_user.py | 47 +- zhenxun/models/plugin_info.py | 2 +- zhenxun/models/plugin_limit.py | 1 + zhenxun/models/sign_log.py | 26 + zhenxun/models/sign_user.py | 72 +++ zhenxun/models/task_info.py | 22 + zhenxun/models/user_console.py | 79 +++ zhenxun/models/user_gold_log.py | 24 + zhenxun/models/user_props.py | 25 + zhenxun/models/user_props_log.py | 30 ++ zhenxun/services/db_context.py | 16 +- zhenxun/services/log.py | 69 ++- zhenxun/utils/_build_image.py | 74 ++- zhenxun/utils/_image_template.py | 156 ++++++ zhenxun/utils/browser.py | 8 +- zhenxun/utils/decorator/shop.py | 204 ++++++++ zhenxun/utils/enum.py | 31 +- zhenxun/utils/exception.py | 12 + zhenxun/utils/http_utils.py | 379 ++++++++++++++ zhenxun/utils/image_utils.py | 339 +++++++++++- zhenxun/utils/user_agent.py | 50 ++ zhenxun/utils/utils.py | 94 ++++ 83 files changed, 7588 insertions(+), 450 deletions(-) create mode 100644 zhenxun/builtin_plugins/admin/admin_help.py create mode 100644 zhenxun/builtin_plugins/admin/ban/__init__.py create mode 100644 zhenxun/builtin_plugins/admin/ban/_data_source.py create mode 100644 zhenxun/builtin_plugins/admin/group_member_update/__init__.py create mode 100644 zhenxun/builtin_plugins/admin/group_member_update/_data_source.py create mode 100644 zhenxun/builtin_plugins/admin/plugin_switch/__init__.py create mode 100644 zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py create mode 100644 zhenxun/builtin_plugins/chat_history/__init__.py create mode 100644 zhenxun/builtin_plugins/chat_history/chat_message.py create mode 100644 zhenxun/builtin_plugins/chat_history/chat_message_handle.py create mode 100644 zhenxun/builtin_plugins/help/__init__.py create mode 100644 zhenxun/builtin_plugins/help/_config.py create mode 100644 zhenxun/builtin_plugins/help/_data_source.py create mode 100644 zhenxun/builtin_plugins/help/_utils.py create mode 100644 zhenxun/builtin_plugins/hooks/__init__.py create mode 100644 zhenxun/builtin_plugins/hooks/ban_hook.py create mode 100644 zhenxun/builtin_plugins/hooks/chkdsk_hook.py create mode 100644 zhenxun/builtin_plugins/hooks/withdraw_hook.py create mode 100644 zhenxun/builtin_plugins/nickname.py create mode 100644 zhenxun/builtin_plugins/platform/qq/group_handle.py create mode 100644 zhenxun/builtin_plugins/scheduler/__init__.py create mode 100644 zhenxun/builtin_plugins/scheduler/auto_backup.py create mode 100644 zhenxun/builtin_plugins/scheduler/auto_update_group.py create mode 100644 zhenxun/builtin_plugins/scheduler/morning.py create mode 100644 zhenxun/builtin_plugins/scripts.py create mode 100644 zhenxun/builtin_plugins/shop/__init__.py create mode 100644 zhenxun/builtin_plugins/shop/_data_source.py create mode 100644 zhenxun/builtin_plugins/sign_in/__init__.py create mode 100644 zhenxun/builtin_plugins/sign_in/_data_source.py create mode 100644 zhenxun/builtin_plugins/sign_in/_random_event.py create mode 100644 zhenxun/builtin_plugins/sign_in/config.py create mode 100644 zhenxun/builtin_plugins/sign_in/goods_register.py create mode 100644 zhenxun/builtin_plugins/sign_in/utils.py create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/__init__.py create mode 100644 zhenxun/builtin_plugins/superuser/broadcast/_data_source.py create mode 100644 zhenxun/builtin_plugins/superuser/super_help.py delete mode 100644 zhenxun/builtin_plugins/superuser/update_fg_info.py create mode 100644 zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py create mode 100644 zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py create mode 100644 zhenxun/models/bag_user.py create mode 100644 zhenxun/models/ban_console.py create mode 100644 zhenxun/models/chat_history.py create mode 100644 zhenxun/models/goods_info.py create mode 100644 zhenxun/models/group_console.py delete mode 100644 zhenxun/models/group_info copy.py create mode 100644 zhenxun/models/sign_log.py create mode 100644 zhenxun/models/sign_user.py create mode 100644 zhenxun/models/task_info.py create mode 100644 zhenxun/models/user_console.py create mode 100644 zhenxun/models/user_gold_log.py create mode 100644 zhenxun/models/user_props.py create mode 100644 zhenxun/models/user_props_log.py create mode 100644 zhenxun/utils/_image_template.py create mode 100644 zhenxun/utils/decorator/shop.py create mode 100644 zhenxun/utils/http_utils.py create mode 100644 zhenxun/utils/user_agent.py diff --git a/.env.dev b/.env.dev index e225f8b8..e7257740 100644 --- a/.env.dev +++ b/.env.dev @@ -10,11 +10,18 @@ NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"] SESSION_EXPIRE_TIMEOUT=30 +PLATFORM_SUPERUSERS = ' + { + "qq": [""], + "dodo": [""] + } +' + # DRIVER=~fastapi DRIVER=~fastapi+~httpx+~websockets # kook adapter toekn -kaiheila_bots =[{""}] +kaiheila_bots =[{"token": ""}] # discode adapter DISCORD_BOTS=' @@ -41,6 +48,8 @@ DODO_BOTS=' ] ' +# application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令 +# {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册 LOG_LEVEL=DEBUG # 服务器和端口 diff --git a/.vscode/settings.json b/.vscode/settings.json index 784fa6a0..b23be7bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,9 +7,15 @@ "Alconna", "arclet", "Arparma", + "displayname", "getbbox", "httpx", + "kaiheila", "nonebot", + "onebot", + "tobytes", + "userinfo", "zhenxun" - ] -} \ No newline at end of file + ], + "python.analysis.autoImportCompletions": true +} diff --git a/poetry.lock b/poetry.lock index f30884aa..58022200 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,21 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiofiles" +version = "23.2.1" +description = "File support for asyncio." +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, + {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "aiosqlite" version = "0.17.0" @@ -267,6 +283,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cachetools" +version = "5.3.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "cashews" version = "6.4.0" @@ -534,6 +566,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "emoji" +version = "2.10.1" +description = "Emoji for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "emoji-2.10.1-py2.py3-none-any.whl", hash = "sha256:11fb369ea79d20c14efa4362c732d67126df294a7959a2c98bfd7447c12a218e"}, + {file = "emoji-2.10.1.tar.gz", hash = "sha256:16287283518fb7141bde00198f9ffff4e1c1cb570efb68b2f1ec50975c3a581d"}, +] + +[package.extras] +dev = ["coverage", "coveralls", "pytest"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -553,6 +604,30 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "fastapi" +version = "0.109.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.36.3,<0.37.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "filelock" version = "3.13.1" @@ -574,21 +649,6 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "fleep" -version = "1.0.1" -description = "File format determination library" -optional = false -python-versions = ">=3.1" -files = [ - {file = "fleep-1.0.1.tar.gz", hash = "sha256:c8f62b258ee5364d7f6c1ed1f3f278e99020fc3f0a60a24ad1e10846e31d104c"}, -] - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "greenlet" version = "3.0.3" @@ -707,6 +767,59 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "httpx" version = "0.26.0" @@ -813,6 +926,26 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "markdown" +version = "3.5.2" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, + {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1223,19 +1356,18 @@ reference = "ali" [[package]] name = "nonebot-plugin-alconna" -version = "0.36.0" +version = "0.36.3" description = "Alconna Adapter for Nonebot" optional = false python-versions = ">=3.8" files = [ - {file = "nonebot_plugin_alconna-0.36.0-py3-none-any.whl", hash = "sha256:37f8afc272924802fe75146df5f68b44e8e5537420cbb983d2d9d65195e625e7"}, - {file = "nonebot_plugin_alconna-0.36.0.tar.gz", hash = "sha256:e524fac76ee0f1a08817007e649c2b491b44094e0262a3d36fcef3e1259edfa2"}, + {file = "nonebot_plugin_alconna-0.36.3-py3-none-any.whl", hash = "sha256:8f26f96c711d3adadc538ebf40d51ba2249c18fe1689bf36baed0e4d1e05246a"}, + {file = "nonebot_plugin_alconna-0.36.3.tar.gz", hash = "sha256:ed8e4f2fd845d0c3d8becdd68678c203ee76109b9104a3b1c18f63525e85c6d4"}, ] [package.dependencies] -arclet-alconna = ">=1.7.38,<2.0.0" -arclet-alconna-tools = ">=0.6.7,<0.7.0" -fleep = ">=1.0.1" +arclet-alconna = ">=1.7.42,<2.0.0" +arclet-alconna-tools = ">=0.6.11,<0.7.0" nepattern = ">=0.5.14,<0.6.0" nonebot2 = ">=2.1.0" @@ -1264,6 +1396,32 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "nonebot-plugin-htmlrender" +version = "0.3.0" +description = "通过浏览器渲染图片" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "nonebot_plugin_htmlrender-0.3.0-py3-none-any.whl", hash = "sha256:c05588bad4738421a49a47a7db974359adeb624c1ed6af49d6237023fa014bcf"}, + {file = "nonebot_plugin_htmlrender-0.3.0.tar.gz", hash = "sha256:34b4ff5b898ea47480d3488a2a0b01c46e0ca3d938ab4b891d1db91a70d83d2d"}, +] + +[package.dependencies] +aiofiles = ">=0.8.0" +jinja2 = ">=3.0.3" +markdown = ">=3.3.6" +nonebot2 = {version = ">=2.2.0", extras = ["fastapi"]} +playwright = ">=1.17.2" +Pygments = ">=2.10.0" +pymdown-extensions = ">=9.1" +python-markdown-math = ">=0.8" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nonebot-plugin-send-anything-anywhere" version = "0.5.0" @@ -1306,23 +1464,49 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "nonebot-plugin-userinfo" +version = "0.1.3" +description = "Nonebot2 用户信息获取插件" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "nonebot_plugin_userinfo-0.1.3-py3-none-any.whl", hash = "sha256:e20b22c81e86e81f7953560bd8ce0a54559a87ad615358c613b78cb5a4918191"}, + {file = "nonebot_plugin_userinfo-0.1.3.tar.gz", hash = "sha256:d0a4d64c612486df63cd16950446072f8dfd2063ea28f15d56305a585a6b0b6e"}, +] + +[package.dependencies] +cachetools = ">=5.0.0,<6.0.0" +emoji = ">=2.0.0,<3.0.0" +httpx = ">=0.20.0,<1.0.0" +nonebot2 = {version = ">=2.0.0,<3.0.0", extras = ["fastapi"]} +strenum = ">=0.4.8,<0.5.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nonebot2" -version = "2.1.3" +version = "2.2.0" description = "An asynchronous python bot framework." optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot2-2.1.3-py3-none-any.whl", hash = "sha256:c36c1a60ce4355d9777fee431c08619f22ffd60f7060993fbbbd1fe67b6368f7"}, - {file = "nonebot2-2.1.3.tar.gz", hash = "sha256:e750e615f1ad2503721ce055fbe55ec3b061277135d995be112fecd27f7232e5"}, + {file = "nonebot2-2.2.0-py3-none-any.whl", hash = "sha256:447fa63d384414c0e610f4ce6d2b3999db81ac2becd8d86716c4117013dc032f"}, + {file = "nonebot2-2.2.0.tar.gz", hash = "sha256:138800846fa3dc635bda9f2ddc589519ee8d9d3b401013fbb95e47676fc830fb"}, ] [package.dependencies] +fastapi = {version = ">=0.93.0,<1.0.0", optional = true, markers = "extra == \"fastapi\" or extra == \"all\""} loguru = ">=0.6.0,<1.0.0" -pydantic = {version = ">=1.10.0,<2.0.0", extras = ["dotenv"]} +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" pygtrie = ">=2.4.1,<3.0.0" +python-dotenv = ">=0.21.0,<2.0.0" tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.4.0,<5.0.0" +uvicorn = {version = ">=0.20.0,<1.0.0", extras = ["standard"], optional = true, markers = "extra == \"quart\" or extra == \"fastapi\" or extra == \"all\""} yarl = ">=1.7.2,<2.0.0" [package.extras] @@ -1551,7 +1735,6 @@ files = [ ] [package.dependencies] -python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""} typing-extensions = ">=4.2.0" [package.extras] @@ -1637,6 +1820,29 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pymdown-extensions" +version = "10.7" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, + {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, +] + +[package.dependencies] +markdown = ">=3.5" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pypika-tortoise" version = "0.1.6" @@ -1691,6 +1897,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "python-markdown-math" +version = "0.8" +description = "Math extension for Python-Markdown" +optional = false +python-versions = ">=3.6" +files = [ + {file = "python-markdown-math-0.8.tar.gz", hash = "sha256:8564212af679fc18d53f38681f16080fcd3d186073f23825c7ce86fadd3e3635"}, + {file = "python_markdown_math-0.8-py3-none-any.whl", hash = "sha256:c685249d84b5b697e9114d7beb352bd8ca2e07fd268fd4057ffca888c14641e5"}, +] + +[package.dependencies] +Markdown = ">=3.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-slugify" version = "8.0.2" @@ -1820,6 +2045,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "retrying" +version = "1.3.4" +description = "Retrying" +optional = false +python-versions = "*" +files = [ + {file = "retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35"}, + {file = "retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e"}, +] + +[package.dependencies] +six = ">=1.7.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "rich" version = "13.7.0" @@ -1962,6 +2206,28 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "starlette" +version = "0.36.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "strenum" version = "0.4.15" @@ -2287,6 +2553,86 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "uvicorn" +version = "0.27.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "virtualenv" version = "20.25.0" @@ -2420,6 +2766,92 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "win32-setctime" version = "1.1.0" @@ -2550,4 +2982,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "bc2932cc9955e05badaaf34f0bda8031edd80d3a832ccd05f9c079fadc4c5cdf" +content-hash = "2e5c4963196533949601dff69762b6f5586056a8775419c2ee1aef0df91b016a" diff --git a/pyproject.toml b/pyproject.toml index b79c47c6..41f7f799 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,10 @@ nonebot2 = "^2.1.3" nonebot-adapter-discord = "^0.1.3" nonebot-adapter-dodo = "^0.1.4" pillow = "9.5" +retrying = "^1.3.4" +aiofiles = "^23.2.1" +nonebot-plugin-htmlrender = "^0.3.0" +nonebot-plugin-userinfo = "^0.1.3" [tool.poetry.dev-dependencies] diff --git a/resources/image/sign/sign_res/bar.png b/resources/image/sign/sign_res/bar.png index 4fc99ec35a3c52cb1afac18e8baefa5efd3adcc6..18b898d19a538c1941d9c0aa1645cd1e790fa05d 100644 GIT binary patch literal 2435 zcmXYzdpy(oAIIks<<2U~kxQ;I_shY|+}g%!k}ir!O0(>Y$z?8c$t_Oq33JP(lt|54 zF0)FtSmu6tL^-4%V%u--k~wVs^L>0jkMH~ae7`=g_xJtze$(Ag!=xpSN&o-=X$0Ka z0{{@2+r2m4FSdK_Wpj-IfCH`wX9v$n66?$5OrLWqbS~#obgz^!NKyXzcMl1dyWV&9 zYj|X6D=Ad<&xuSqD74E1bCjweo6{L5U4Vc0SE%F9@y6mTS6d z(AV7zpIvO`K}h)8!FegOQYO+U&R%Db)V$uZh+nI=rENC4-A(%!u+KSRz|r>hmYKY* z>XW8zujyg&pi%%_&UgOysBkr!6Q;&8kNlQQ9?Tc<&7Wlsl(nOZN6I>gJ8O{gZNIYU zc$PIS<2zXAB{^_=QG9tKiX6yWME`G_5q0`XRqV&!E^n5Wwazkzw#Es!A=4I8mq^C+ z^+?^FouaQ-`tXBsTZJ>xK1{qRY#hT{3~p?GNuiD~Y5dIH)fLR{>M6B?vnqMZ72|OO zL~pux);iB>pbrUoTy=m6&ZY`KbPTRQSAyKxPgnbW3tnLGb&W&=|KWx-#$keS zt|lT^Q{{d~_Xj0jjh~iLh$RZAWjnAnsHE)G>v|sGt{Y&Ao}|ftqPL3>l8e=?`oJpO z#jMt)k6CVB&CMejwGcxGDPqdF#97`GwOtlsAg8ucPBZacM!lcD{#bYG#?{wB;W&}? zM`1 zv6tMK6DWj8NEaZp;i5&ZZm`;=gO8S_Ek9iTR=rSJYffJk!x*`)x`0 zSEl`7pVaiPcE_LfLUwZ~(NXAfQp0!*tIRI&SIvkOrB5sEs;+N4N|(P)d0luIInN=M zG&r3S869J3*Pima*p zKgjY!+co80F1QGM0X}I|fqF(F`ah&>ZuwBIUbKh+xA2AH9|uTCA79x3D$-X?Am!N| zzEIVMzv39qe=V!*28t5S_h{{5ZJ>51Z_aP%D}GotVPeC9N#jy$2j)6)ae)`un>;nr zx?e!lJ@38x)m2CsW4|4*J2?acHnwitlF}CWiTkvf)C^ghPfus0eCsLXJQhGDr1j?` zLZu@@WkD>Yx}6hoB5Eb@1_xU__a6Hv_W2hqYmca@Qa{jGN`Dq|1ob?&mOFQ3?R+n7 z^!7ToZuJx>?DT#ezFivW4ni?j^!JM2>WW5Sy+S$a_Q0lPH!3?NEKZ_XuR)`b>zHdqf3VocJIQmJ@1M|<+r*+ntw!IWYY64jPw3NV5brT1( z_LAOSy(G7KzDV-o2v?)e_XevyKZ`SI^&4s6L4>>_Yu|?2SCr=s>d`yd@iRy^UjZm- z3b$-!A_mp?ZsuXbswai7jEjZwS@*rQ%(_JjLv40>!3R$^GJ7Iv)CfX^ppd9(fm(vZ_U6=J0=#yjSSz^^GaiN8LKt}!U@nJZt>ivdrqM(*`n1G#|+`Ktn*6(;h z{jv)`2AwXd7kh%Gk(W;0v<>{YuRGbgbgSzhDrJpg?L`t$^v>#;dowMuGp;`>UT)2> zdG;-i+e`51E@h9;*Wm(8jCJIUN*8Ta1;h>XgM*PN-!R9q{P5Ldp-`FsXj8)jhfzE$s^|?wWYCxi@?6KG)y!z zn`ma)`tvSHwJBHNnlL#mZ|WBXO=>0~V)Yf z3|~Y~=k7drfeYhGKC~oC1}=Ptm|qU+J@f;iR~k7wHs+;@8SmLw96tp0YDEc-Y~&4)LO)yk8Vh?BWgk4gZk_mOp>?t# zbpDu6%|+8~bYpybM#Aufge=@45NjrK zc~l7x`9czk{&q zR7}|n^%hDMu7kHwL`5yOw~|p-oOqdSL(ca4Lru8i40$baqG5Xi&R9$1!Z2Wh?=vG- zCS3NUosgQtugW2Y%*t#2yJ&N(Jp)AQVazWMg_zKvmz}jPe~G>*36=9aQPiG|lMz?y zOD%RNT$N~E)5*jzk?7%~Qh z$X@^0`Gk;sRPInvVt#F=1KouGxvW0!RVkNrStF-QQ-Ss7`;R)FII}|t2~Ai&xBGJi NK)9TCZgBKV{0~5Am*W5c literal 2535 zcmX|DcU)6v7Y?Fjw6bIs%F-~j76_suG8M#tQ2|qt5mXRGHYh7JVOf?XkgyU$pvaO6 z1P6hDs0>*OX;1@PfwBw;1Vh3|2=I~6et*38cgK03^W5j0_xYW)%NHH?ORGtPK%o85 z^L8#EkofG*y@}*^JJ$gg+ZY6rs)gFwUWq0yjv^m|?KNJrL*Q%uwUs|5-igyG)s$6A zGq`J9S)~I}lPoz`3Q_5lxNDK7wjz7n*{76^ms4t+Qgl|(jfPlVR`@#=5t+fERQho1J!x;CZNM!K(-}D-{6th!CkFJfFVRP z#_-cq*zX-@F~4~?XdFTahT11*0*3XFY5HS&k0#;%IQHaAG^O45)jTfnz&TCmKVNcW0C$Eu`f}wnG>$X3Hr;ZA_c0p`#e)5=e zU|aDC(=c;FkMxGoc5T9()HnJ>OYxH_G**=lJ^M#dN#z?W>FW;t?A~e=U*#*`3ve}$ z+KLr(*|?kkk#;TiqYw%@a!HZfZD*3b{mq~u!Voxh(wj8l{JA%##X3-$!SSWq;Hp%R z+uaY|evJ#+7NbcRR#)RLu~_`!(;J(G4VCwTRM@$3j$07#FL$H~9;00s z=qY+1@J-W#r`6_LT(4$U%i(fH4sCzdNLk!I8JLh#nfO&b7s#=~s_op$FP!D#Wng)j z>yPD{em(m;c$TA|)LgyguDy$VuW`vx#8fg3OOSD0m`?G`@-Cn}#FZ^&mrw>yz7nFB z!|%^1E<9&=iU{d`eU^zeq>vB4G7gzQ5fE%0A9|D%6_}25f`>Hjpb`>`esiOY+|Ls# zI#+T9(yn;A=>aSz0`-1jft67JqX7^mrQDR0hgfT-turXGWv5q zIaC+~uF0~P1-&XxnTcY)&#=_8^dlVIWD%z}_#Zz>=JqrRL!dtg(lYvw$30vM?`UJs z)|bZk@pIvu&(X}dz~o*JKvx*!uA7i!ojpzs4LcgINiMG)Q`au{I~XPWgndq@YcpFD zD4`OritjS*G4exsba6C(8Lv0H%c1l_SxMKn8MekkslbUQR$DXA{2}q~8F-FJ7MgK^ zv7vY2xXsPTte7Y9>R-7gAmqSq)?14~EZ|f|v%!c%Z6`83Gio}{eEoBTDDAIadJ2z+ zdYlEsLlS+d6lTlT7!f0rb%D0KRXohNLvY(d^G+;nJCDQPd#gYrPgG#+r!!kfl<$Vs z7=i8z`Y-%YQrhIu+c}V3sxDXGO$33J@zQ;{TI16%dK!`KK={SdbF)OGvp!6TEdtS= zKSU*B+;GBI{_lhS`LzVvTP!y@4y+xh5&axAC7pKRgQliB3q&Xr45_k^atqyZMLoL} zJWlJML|LFg+BR|bBsaS{LOI$eZ1Y86FLWKv1^KvPF;mR7c~E8NB*E4b_7o-3Q-ga@ z3Ey4+xx1d*Z7OB{`jovu*(Nc(GnajOQ@5A>&222*hzWnxuQS`vHog!0w-!(B zpk>lVQsd&|Xl1i^`SYZze{dA~<`2zdm-7l0axN;FK6Xg;LKttyznd&*m> zHMl$jY8Go6jjIwc_bVE#etz4>C)Xyk{(r(?witqp!?%rZouOjcUrwx&nbpm@vp2}v zd&Ub`En6Q{twSzRg{6S@U8Wbo`s=>eXPfbTl@~2<*LN;Pa}S;E9w)I{_&-8|=h~@z zVy#fSCIxx!(~p^^Oi)7=D#N$Q9)L%N@{4CwE%`I8s93&$kvCJJX5JJN92){Zvg?cq zhA#4tcX$tb{)AGzNQCugI2W?YwZ-PedG!7dx%rV57tJh^tr@hm%fpX$Ey|{>w8Wtl z>>RGcOjd7GAGSdgr(1Zb@(=KSlZ`tm>Ox3(R9lRF{VChw)vA2j@oS>4(s0?6cHSPE zZ*$PuWl*S7t*oq6v82agf0G#qHbOPKu<1XVWDhoiJ(V+MttFY9JzMtsk%e-> zkeT)S`1ym@k5bo&&iAbDA$zqpW4($6AE{R89jh9aawyl0s}LzIkxb|h4ztBfHIh^! z!pu&`6%RIsj8T2o2m_QdUn)!xKp?FgxH(%S*XJz=uz|B)$*&m<=E`ttb@D0WrSX6#Nir~4m71q4IM8`y-h$Yc0a{N#AihyA~zKx zD1%1J4_BIs76jhGkqx2DsV*^sIO3dz2gM?Gw7x+@VW239tc`GMz1;%TFZDGzJ#-*t z>+$wE=boq?A=zg(7rIZILn#O zy3>EH^whV`fZZMttI}{K`74Z>t-nl6YT1_UqCZ-(a?W+%8Q?%r`-^t9XRasy4{6#K Ah5!Hn diff --git a/resources/image/sign/sign_res/bar_white.png b/resources/image/sign/sign_res/bar_white.png index c09c4635dc82360d7a0b27691d7c4ade713fe46f..2f3bcae440fd9eceb0ba1cceaba8eeeaf54ed884 100644 GIT binary patch literal 1587 zcmW-gdstIt9LFa}WC>(>DK4Y30=tkXZphdWGRS10Z46qAs`PqDj32p9f3!NL$<~^;x-t)^Pls)&-e2^-}iSp=iH%`B)=B| zUqB!beg{6;{}}?|JqOOao_`KJM=Z9T2!xOG!2WpRsoDjjRh*R;GH6>)Cdco)e&q0u zuQmoYe|I)4aZ}S8w=_O0~2@PfJ3d`{1Q75IC6c_d0TH6U#y4f=7gyC%UoeFKe6-UsEa zruM3(ifS)M>Jy4Wcgio!3xN=o+%F=`A=~hmCB@QCtr#tjBW_h`MYt2ZE+=7B?G0TS z95}gb3;6x#wYNdKsN`<=r{aVs;m4`1?yNeg=Hy+PQ?`C-lLopHx5l%mXB;rFF+(TR zsroqSvJ|49hoY`R;SCHqTG-5O=U(6OV|`CmjW+_(>wQdkD2}Qm*3qmTHgC#@4b}jY zy2cfNEk}Y32q$xH)tp@_eJ*ht+GrvNp?v|BGbXvA3X`-#dyT!OsZ{+6 zg%66E0?vt=fBXSfScrt*U0H-r)*ACuvHGsK1~Jn;I6k#9#=`CZ++a#UeL2t8=Hsj2 zh0+K^Rw-`wk2Tq&$KB<(@AYZdfljgAncMDAEh6l{i9Y-vCI&O@NhZNmFnHNw_@!2T zgfb_B=e9S9L-bUcK<5nK1IPnSrPtFYZ4$d=By*{s))jbewUBUg)#gahvH_0H%%QpR z@!BP0t7W)_=4j-0!Z+!E>;))T+%KI=q%w-KVcYRAATsF#9gDkV;#i1MDmacLV!@O|GkZFg#gc>U zlH3gbXLWVH1^%Ad4d(w%+$+3#D|%UWi9KU$flXd9Kn_;7-&wK$olT-fRgI?m3VeVT z9dWyC@kQBbevB$b+$92Q>jh<-grB`P9IyhS>hdkHc zo>$vlvc0C83usLA`8ORO0Zm1wRAZjmq6Nd|Phq8} z5NU)a1mnwp5g4O)Veng)#F9>Y|Kh`m*DX^HV|%_Q;I~B9iWUE{eF1#g60Y2tKK`?( zR2CUkwzQjF5*eeYE-Dn3SPbr6KY40JBJ}u4m$BswM~XdTZg{-BG0wPoJSojH*=TNs zxzM$^Tqr0=A0f4MM|6QGL{cts7-|*lQ4}wqA)oloN>tP9rpr7XMaOKP;-+oHKw?ejOGh*l5bA;iiRiE#9H-0CPF`!-2AO8#2&iY{oJ;YWY7)74^HI@fM-v@3_o z9`~(Z$}G2DbVF?s#8l!4NgO<#4Xn&Iog7E6J68{(i{=vGqu$P0ab?~@U%xq2n`@sy zo7`#h|KNbpP?6n+V^rhaO!yewHy+PRE>2n;wtVGFdwjdw57?ONs*$o;Klo5fo#aHnsv z@UB~VH>l?c1oLEE)vAK2qt$nK!YrOC75~$ro1gCdw#l~F>2Di{j6Z}K1s`!(=M4wQ z?`^kA<@esn6e(8(f96JXo%hheZ{)ItA>B}ta*gU*XLv|N#H`H@T6_shG(xJWpf-B8 z9{_ni-QtwMHLw_d7Dk%J;H@2mDjMk|r`+pIuFs?y1_&(X7c>e)jWz$5OS80A7_gIf z5kr}TX0N&&A2;q1s#Hv)KFyWA4|v6@@?w#M9z?>C4p75vL zh54n|LuD@N=po}7n`MzC*U0RApu9oqJ zzb?VJFF|zQBM1?Y^P0l+vCr!X{YA4Inyy++u5o8tc#v!WGmtZuUbg&BBlS)mauMc> z^8!6BAt*KN>2p0#)1&o@!*vJIx@g79#mlxpUdavKV$9R@((G!iR4U1ga?}S!J&s`s zEu>J{MUi!m*&+uiRt~H~LwUa3##K>C^;G^x`p|k&8#1hZrP&JzqBtx4#$rwxw`(LD zJ33o2GV-6yeH4v49#yg2JKp@!YFUl&TQVIpSqy+CFv~MqCPcLqdNCn*KbMnS_zZRt zm_bm}SF(3qV-4dFq%ZS=fdoLN-a5~kEM-{ z76VD@QYe$1X6#HK9x^G|*W|@W|JrV}cqj6)c9rWMGR7D{@NNYNbXXnN*ZOO+itQpq zx9rItiS+dzOh|4M1G1^Ug6!D;EekfXs!pGk_dP&~!IIn)cu||%1S;uCfxSY@S6q|O zDDA51Q-;FQb5(E^%-UD`(EA%k*=%ZK$`MWJT-biJfhF%#;W19xbi!?;qdHMv5{3ehyrRKHzbh7mvgpk}0MI6V;%yX?WJqU4umkV7^R zk_LB%1odc_AN8MKNpu^ncXyto*3!B>>c<tjUTk^$HKqlerS!Mu>g-Y`Wv{ zAPmI6^uj@4Dmgw{FRlufSK;7-Zqpk zazhZU*GzS=!5?`?=>=AizK;i2P0Ysd6Byp{-_YO|1 z$y8bUaQb(|uO^O)krD0dq$e3{aLFIsOf2@Jy;0^$a)g+>hL`G?8@qo4k^ zrZa8dY9L~*-HvT*vM;?(m9e`mH|`d8)fIXnmC#BDVisix(~rq*(Z(1URrz&#xY&hi zDr5MxW31hTv;Q2i0CP#*8=AcUU5P|L7(x>lPx# diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 6448d28d..38776d55 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -1,3 +1,5 @@ +import os + from nonebot import require require("nonebot_plugin_apscheduler") @@ -8,3 +10,10 @@ require("nonebot_plugin_saa") from nonebot_plugin_saa import enable_auto_select_bot enable_auto_select_bot() +from pathlib import Path + +import nonebot + +path = Path(__file__).parent / "platform" +for d in os.listdir(path): + nonebot.load_plugins(str((path / d).resolve())) diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py new file mode 100644 index 00000000..6cad3cc0 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -0,0 +1,165 @@ +import nonebot +from arclet.alconna import Args, Option +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.exception import EmptyError +from zhenxun.utils.image_utils import ( + BuildImage, + build_sort_image, + group_image, + text2image, +) +from zhenxun.utils.rules import admin_check, ensure_group + +base_config = Config.get("admin_bot_manage") + +__plugin_meta__ = PluginMetadata( + name="群组管理员帮助", + description="管理员帮助列表", + usage=""" + 管理员帮助 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=1, + ).dict(), +) + +_matcher = on_alconna( + Alconna("管理员帮助"), + rule=admin_check(1) & ensure_group, + priority=5, + block=True, +) + + +ADMIN_HELP_IMAGE = IMAGE_PATH / "ADMIN_HELP.png" +if ADMIN_HELP_IMAGE.exists(): + ADMIN_HELP_IMAGE.unlink() + + +async def build_help() -> BuildImage: + """构造管理员帮助图片 + + 异常: + EmptyError: 管理员帮助为空 + + 返回: + BuildImage: 管理员帮助图片 + """ + plugin_list = await PluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN] + ).all() + data_list = [] + for plugin in plugin_list: + if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path): + if _plugin.metadata: + data_list.append({"plugin": plugin, "metadata": _plugin.metadata}) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + image_list = [] + for data in data_list: + plugin = data["plugin"] + metadata = data["metadata"] + try: + usage = None + description = None + if metadata.usage: + usage = await text2image( + metadata.usage, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + if metadata.description: + description = await text2image( + metadata.description, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + width = 0 + height = 100 + if usage: + width = usage.width + height += usage.height + if description and description.width > width: + width = description.width + height += description.height + font_width, font_height = BuildImage.get_text_size( + plugin.name + f"[{plugin.level}]", font + ) + if font_width > width: + width = font_width + A = BuildImage(width + 30, height + 120, "#EAEDF2") + await A.text((15, 10), plugin.name + f"[{plugin.level}]") + await A.text((15, 70), "简介:") + if not description: + description = BuildImage(A.width - 30, 30, (255, 255, 255)) + await description.circle_corner(10) + await A.paste(description, (15, 100)) + if not usage: + usage = BuildImage(A.width - 30, 30, (255, 255, 255)) + await usage.circle_corner(10) + await A.text((15, description.height + 115), "用法:") + await A.paste(usage, (15, description.height + 145)) + await A.circle_corner(10) + image_list.append(A) + except Exception as e: + logger.warning( + f"获取群管理员插件 {plugin.module}: {plugin.name} 设置失败...", + "管理员帮助", + e=e, + ) + if task_list := await TaskInfo.all(): + task_str = "\n".join([task.name for task in task_list]) + task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str + task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) + await task_image.circle_corner(10) + A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") + await A.text((25, 10), "被动技能") + await A.paste(task_image, (25, 50)) + await A.circle_corner(10) + image_list.append(A) + if not image_list: + raise EmptyError() + image_group, _ = group_image(image_list) + A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160) + text = await BuildImage.build_text_image( + "群管理员帮助", + size=40, + ) + tip = await BuildImage.build_text_image( + "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red" + ) + await A.paste(text, (50, 30)) + await A.paste(tip, (50, 90)) + await A.save(ADMIN_HELP_IMAGE) + return BuildImage(1, 1) + + +@_matcher.handle() +async def _( + session: EventSession, + matcher: AlconnaMatcher, + arparma: Arparma, +): + if not ADMIN_HELP_IMAGE.exists(): + try: + await build_help() + except EmptyError: + await Text("管理员帮助为空").finish(reply=True) + await Image(ADMIN_HELP_IMAGE).send() + logger.info("查看管理员帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/admin/admin_watch.py b/zhenxun/builtin_plugins/admin/admin_watch.py index 4fc274ea..02fe9417 100644 --- a/zhenxun/builtin_plugins/admin/admin_watch.py +++ b/zhenxun/builtin_plugins/admin/admin_watch.py @@ -9,11 +9,6 @@ from zhenxun.models.level_user import LevelUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType -__zx_plugin_name__ = "群管理员变动监测 [Hidden]" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" - - __plugin_meta__ = PluginMetadata( name="群管理员变动监测", description="检测群管理员变动, 添加与删除管理员默认权限, 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限", @@ -40,20 +35,25 @@ async def _(event: GroupAdminNoticeEvent): admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH") if admin_default_auth is not None: await LevelUser.set_level( - event.user_id, - event.group_id, + str(event.user_id), + str(event.group_id), admin_default_auth, ) logger.info( f"成为管理员,添加权限: {admin_default_auth}", "群管理员变动监测", - event.user_id, - event.group_id, + session=event.user_id, + group_id=event.group_id, ) else: logger.warning( f"配置项 MODULE: [admin_bot_manage] | KEY: [ADMIN_DEFAULT_AUTH] 为空" ) elif event.sub_type == "unset": - await LevelUser.delete_level(event.user_id, event.group_id) - logger.info("撤销群管理员, 取消权限等级", "群管理员变动监测", event.user_id, event.group_id) + await LevelUser.delete_level(str(event.user_id), str(event.group_id)) + logger.info( + "撤销群管理员, 取消权限等级", + "群管理员变动监测", + session=event.user_id, + group_id=event.group_id, + ) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py new file mode 100644 index 00000000..289ef6b6 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -0,0 +1,196 @@ +from arclet.alconna import Args +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Arparma, + At, + Match, + Option, + Subcommand, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.rules import admin_check + +from ._data_source import BanManage + +base_config = Config.get("ban") + +__plugin_meta__ = PluginMetadata( + name="封禁用户/群组", + description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息", + usage=""" + .ban [at] ?[小时] ?[分钟] + .unban + 示例:.ban @user + 示例:.ban @user 6 + 示例:.ban @user 3 10 + 示例:.unban @user + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + admin_level=base_config.get("BAN_LEVEL", 5), + configs=[ + RegisterConfig( + key="BAN_LEVEL", + value=5, + help="ban/unban所需要的管理员权限等级", + default_value=5, + type=int, + ) + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "ban-console", + Subcommand( + "ban", + Args["user?", [str, At]]["duration?", int], + Option("-g|--group", Args["group_id", str]), + ), + Subcommand( + "unban", + Args["user?", [str, At]], + Option("-g|--group", Args["group_id", str]), + ), + ), + rule=admin_check("ban", "BAN_LEVEL"), + priority=5, + block=True, +) + +_status_matcher = on_alconna( + Alconna( + "ban-status", + Option("-u|--user", Args["user_id", str]), + Option("-g|--group", Args["group_id", str]), + ), + permission=SUPERUSER, + priority=1, + block=True, +) +# TODO: shortcut + + +@_status_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + user_id: Match[str], + group_id: Match[str], +): + _user_id = user_id.result if user_id.available else None + _group_id = group_id.result if group_id.available else None + if image := await BanManage.build_ban_image(_user_id, _group_id): + await Image(image.pic2bs4()).finish(reply=True) + else: + await Text("数据为空捏...").finish(reply=True) + + +@_matcher.assign("ban") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + user: Match[str | At], + duration: Match[int], + group_id: Match[str], +): + user_id = None + if user.available: + if isinstance(user.result, At): + user_id = user.result.target + else: + user_id = user.result + _duration = duration.result * 60 if duration.available else -1 + if gid := session.id3 or session.id2: + if group_id.available: + gid = group_id.result + await BanManage.ban( + user_id, gid, _duration, session, session.id1 in bot.config.superusers + ) + logger.info( + f"管理员Ban", + arparma.header_result, + session=session, + target=f"{gid}:{user_id}", + ) + await MessageFactory( + [ + Text("对 "), + Mention(user_id), # type: ignore + Text(f" 狠狠惩戒了一番,一脚踢进了小黑屋!"), + ] + ).finish(reply=True) + elif session.id1 in bot.config.superusers: + _group_id = group_id.result if group_id.available else None + await BanManage.ban(user_id, _group_id, _duration, session, True) + logger.info( + f"超级用户Ban", + arparma.header_result, + session=session, + target=f"{_group_id}:{user_id}", + ) + at_msg = user_id if user_id else f"群组:{_group_id}" + await Text(f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!").finish(reply=True) + + +@_matcher.assign("unban") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + user: Match[str | At], + group_id: Match[str], +): + user_id = None + if user.available: + if isinstance(user.result, At): + user_id = user.result.target + else: + user_id = user.result + if gid := session.id3 or session.id2: + if group_id.available: + gid = group_id.result + await BanManage.unban( + user_id, gid, session, session.id1 in bot.config.superusers + ) + logger.info( + f"管理员UnBan", + arparma.header_result, + session=session, + target=f"{gid}:{user_id}", + ) + await MessageFactory( + [ + Text("将 "), + Mention(user_id), # type: ignore + Text(f" 从黑屋中拉了出来并急救了一下!"), + ] + ).finish(reply=True) + elif session.id1 in bot.config.superusers: + _group_id = group_id.result if group_id.available else None + await BanManage.unban(user_id, _group_id, session, True) + logger.info( + f"超级用户UnBan", + arparma.header_result, + session=session, + target=f"{_group_id}:{user_id}", + ) + at_msg = user_id if user_id else f"群组:{_group_id}" + await Text(f"对 {at_msg} 从黑屋中拉了出来并急救了一下!").finish(reply=True) diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py new file mode 100644 index 00000000..7418eb47 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -0,0 +1,120 @@ +import time + +from nonebot_plugin_session import EventSession + +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.level_user import LevelUser +from zhenxun.utils.image_utils import ImageTemplate + + +class BanManage: + + @classmethod + async def build_ban_image(cls, user_id: str | None, group_id: str | None): + data_list = None + if not user_id and not group_id: + data_list = await BanConsole.all() + elif user_id: + if group_id: + data_list = await BanConsole.filter( + user_id=user_id, group_id=group_id + ).all() + else: + data_list = await BanConsole.filter( + user_id=user_id, group_id__isnull=True + ).all() + else: + if group_id: + data_list = await BanConsole.filter( + user_id__isnull=True, group_id=group_id + ).all() + if not data_list: + return None + column_name = [ + "ID", + "用户ID", + "群组ID", + "BAN LEVEL", + "剩余时长(分钟)", + "操作员ID", + ] + row_data = [] + for data in data_list: + duration = int((data.ban_time + data.duration - time.time()) / 60) + if duration < 0: + duration = 0 + row_data.append( + [ + data.id, + data.user_id, + data.group_id, + data.ban_level, + duration, + data.operator, + ] + ) + return await ImageTemplate.table_page( + "Ban / UnBan 列表", "在黑屋中狠狠调教!", column_name, row_data + ) + + @classmethod + async def is_ban(cls, user_id: str, group_id: str | None): + """判断用户是否被ban + + 参数: + user_id: 用户id + + 返回: + bool: 是否被ban + """ + return await BanConsole.is_ban(user_id, group_id) + + @classmethod + async def unban( + cls, + user_id: str | None, + group_id: str | None, + session: EventSession, + is_superuser: bool = False, + ) -> bool: + """ban掉目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + session: Session + is_superuser: 是否为超级用户操作 + + 返回: + bool: 是否unban成功 + """ + user_level = 9999 + if not is_superuser and user_id and session.id1: + user_level = await LevelUser.get_user_level(session.id1, group_id) + if await BanConsole.check_ban_level(user_id, group_id, user_level): + await BanConsole.unban(user_id, group_id) + return True + return False + + @classmethod + async def ban( + cls, + user_id: str | None, + group_id: str | None, + duration: int, + session: EventSession, + is_superuser: bool, + ): + """ban掉目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + duration: 时长,秒 + session: Session + is_superuser: 是否为超级用户操作 + """ + level = 9999 + if not is_superuser and user_id and session.id1: + level = await LevelUser.get_user_level(session.id1, group_id) + await BanConsole.ban(user_id, group_id, level, duration, session.id1) diff --git a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py new file mode 100644 index 00000000..1a7bfe4a --- /dev/null +++ b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py @@ -0,0 +1,63 @@ +from nonebot import on_notice +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.rules import admin_check, ensure_group + +from ._data_source import MemberUpdateManage + +__plugin_meta__ = PluginMetadata( + name="更新群组成员列表", + description="更新群组成员列表", + usage=""" + 更新群组成员的基本信息 + 指令: + 更新群组成员信息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + admin_level=1, + ).dict(), +) + + +_matcher = on_alconna( + Alconna("更新群组成员信息"), + rule=admin_check(1) & ensure_group, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma): + if gid := session.id3 or session.id2: + logger.info("更新群组成员信息", arparma.header_result, session=session) + await MemberUpdateManage.update(bot, gid) + await Text("已经成功更新了群组成员信息!").finish(reply=True) + await Text("群组id为空...").send() + + +_notice = on_notice(priority=1, block=False) + + +@_notice.handle() +async def _(bot: Bot, event: GroupIncreaseNoticeEvent): + # TODO: 其他适配器的加群自动更新群组成员信息 + if str(event.user_id) == bot.self_id: + await MemberUpdateManage.update(bot, str(event.group_id)) + logger.info( + "{NICKNAME}加入群聊更新群组信息", + "更新群组成员列表", + session=event.user_id, + group_id=event.group_id, + ) diff --git a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py new file mode 100644 index 00000000..442d337e --- /dev/null +++ b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py @@ -0,0 +1,177 @@ +import time +from datetime import datetime, timedelta, timezone + +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.dodo.models import MemberInfo +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot + +from zhenxun.configs.config import Config +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.level_user import LevelUser +from zhenxun.services.log import logger + + +class MemberUpdateManage: + + @classmethod + async def update(cls, bot: Bot, group_id: str): + if isinstance(bot, v11Bot): + await cls.v11(bot, group_id) + elif isinstance(bot, v12Bot): + await cls.v12(bot, group_id) + elif isinstance(bot, KaiheilaBot): + await cls.kaiheila(bot, group_id) + elif isinstance(bot, DodoBot): + await cls.dodo(bot, group_id) + elif isinstance(bot, DiscordBot): + await cls.discord(bot, group_id) + + @classmethod + async def discord(cls, bot: DiscordBot, group_id: str): + # TODO: discord更新群组成员信息 + pass + + @classmethod + async def dodo(cls, bot: DodoBot, group_id: str): + page_size = 100 + result_size = 100 + max_id = 0 + exist_member_list = [] + group_member_list: list[MemberInfo] = [] + while result_size == page_size: + group_member_data = await bot.get_member_list( + island_source_id=group_id, page_size=page_size + ) + result_size = len(group_member_data.list) + group_member_list += group_member_data.list + max_id = group_member_data.max_id + if group_member_list: + for user in group_member_list: + exist_member_list.append(user.dodo_source_id) + await GroupInfoUser.update_or_create( + user_id=user.dodo_source_id, + group_id=group_id, + defaults={ + "user_name": user.nick_name or user.personal_nick_name, + "user_join_time": user.join_time, + "platform": "dodo", + }, + ) + if delete_member_list := list( + set(exist_member_list).difference( + set(await GroupInfoUser.get_group_member_id_list(group_id)) + ) + ): + await GroupInfoUser.filter( + user_id__in=delete_member_list, group_id=group_id + ).delete() + logger.info( + f"删除已退群用户", + "更新群组成员信息", + group_id=group_id, + platform="dodo", + ) + + @classmethod + async def kaiheila(cls, bot: KaiheilaBot, group_id: str): + # TODO: kaiheila 更新群组成员信息 + pass + + @classmethod + async def v11(cls, bot: v11Bot, group_id: str): + exist_member_list = [] + default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") + group_member_list = await bot.get_group_member_list(group_id=int(group_id)) + for user_info in group_member_list: + user_id = user_info["user_id"] + nickname = user_info["card"] or user_info["nickname"] + role = user_info["role"] + if default_auth: + if role in ["owner", "admin"] and not LevelUser.is_group_flag( + str(user_id), group_id + ): + await LevelUser.set_level(user_id, group_id, default_auth) + if str(user_id) in bot.config.superusers: + await LevelUser.set_level(str(user_id), group_id, 9) + join_time = datetime.strptime( + time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"]) + ), + "%Y-%m-%d %H:%M:%S", + ) + await GroupInfoUser.update_or_create( + user_id=str(user_id), + group_id=group_id, + defaults={ + "user_name": nickname, + "user_join_time": join_time.replace( + tzinfo=timezone(timedelta(hours=8)) + ), + "platform": "qq", + }, + ) + exist_member_list.append(str(user_id)) + logger.debug( + "更新成功", "更新群组成员信息", session=user_id, group_id=group_id + ) + if delete_member_list := list( + set(exist_member_list).difference( + set(await GroupInfoUser.get_group_member_id_list(group_id)) + ) + ): + await GroupInfoUser.filter( + user_id__in=delete_member_list, group_id=group_id + ).delete() + logger.info( + f"删除已退群用户", "更新群组成员信息", group_id=group_id, platform="qq" + ) + + @classmethod + async def v12(cls, bot: v12Bot, group_id: str): + # TODO: v12更新群组成员信息 + pass + # exist_member_list = [] + # default_auth = Config.get_config("admin_bot_manage", "ADMIN_DEFAULT_AUTH") + # group_member_list: list[GetGroupMemberInfoResp] = await bot.get_group_member_list( + # group_id=group_id + # ) + # for user_info in group_member_list: + # user_id = user_info.user_id + # nickname = user_info.user_displayname or user_info.user_name + # role = user_info["role"] + # if default_auth: + # if role in ["owner", "admin"] and not LevelUser.is_group_flag( + # str(user_id), group_id + # ): + # await LevelUser.set_level(user_id, group_id, default_auth) + # if str(user_id) in bot.config.superusers: + # await LevelUser.set_level(str(user_id), group_id, 9) + # join_time = datetime.strptime( + # time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user_info["join_time"])), + # "%Y-%m-%d %H:%M:%S", + # ) + # await GroupInfoUser.update_or_create( + # user_id=str(user_id), + # group_id=group_id, + # defaults={ + # "user_name": nickname, + # "user_join_time": join_time.replace( + # tzinfo=timezone(timedelta(hours=8)) + # ), + # }, + # ) + # exist_member_list.append(str(user_id)) + # logger.debug("更新成功", "更新群组成员信息", session=user_id, group_id=group_id) + # if delete_member_list := list( + # set(exist_member_list).difference( + # set(await GroupInfoUser.get_group_member_id_list(group_id)) + # ) + # ): + # await GroupInfoUser.filter( + # user_id__in=delete_member_list, group_id=group_id + # ).delete() + # logger.info(f"删除已退群用户", "更新群组成员信息", group_id=group_id) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py new file mode 100644 index 00000000..42da314f --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -0,0 +1,162 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + Subcommand, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession +from requests import session + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.rules import admin_check, ensure_group + +from ._data_source import PluginManage, build_plugin, build_task + +base_config = Config.get("admin_bot_manage") + + +__plugin_meta__ = PluginMetadata( + name="功能开关", + description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制", + usage=""" + 开启/关闭[功能] + 群被动状态 + 开启全部被动 + 关闭全部被动 + 醒来/休息吧 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2), + configs=[ + RegisterConfig( + key="CHANGE_GROUP_SWITCH_LEVEL", + value=2, + help="开关群功能权限", + default_value=2, + type=int, + ) + ], + ).dict(), +) + + +_status_matcher = on_alconna( + Alconna( + "switch", + Option("-t|--task", action=store_true, help_text="被动技能"), + Subcommand( + "open", + Args["name", str], + Option( + "-g|--group", + Args["group_id", str], + ), + ), + Subcommand( + "close", + Args["name", str], + Option( + "-t|--type", + Args["block_type", ["all", "a", "private", "p", "group", "g"]], + ), + Option( + "-g|--group", + Args["group_id", str], + ), + ), + ), + rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), + priority=5, + block=True, +) + +# TODO: shortcut + +_group_status_matcher = on_alconna( + Alconna("group-status", Args["status", ["sleep", "wake"]]), + rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group, + priority=5, + block=True, +) + + +@_status_matcher.assign("$main") +async def _(bot: Bot, session: EventSession, arparma: Arparma): + image = None + if arparma.find("task"): + image = await build_task(session.id3 or session.id2) + elif session.id1 in bot.config.superusers: + image = await build_plugin() + if image: + await Image(image.pic2bs4()).send(reply=True) + logger.info( + f"查看{'被动' if arparma.find('task') else '功能'}列表", + arparma.header_result, + session=session, + ) + + +@_status_matcher.assign("open") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + name: str, + group: Match[str], +): + if gid := session.id3 or session.id2: + result = await PluginManage.block_group_plugin(name, gid) + await Text(result).send(reply=True) + logger.info(f"开启功能 {name}", arparma.header_result, session=session) + elif session.id1 in bot.config.superusers: + result = await PluginManage.superuser_block(name, None, group.result) + await Text(result).send(reply=True) + logger.info( + f"超级用户开启功能 {name}", + arparma.header_result, + session=session, + target=group.result, + ) + + +@_status_matcher.assign("close") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + name: str, + block_type: Match[str], + group: Match[str], +): + if gid := session.id3 or session.id2: + result = await PluginManage.unblock_group_plugin(name, gid) + await Text(result).send(reply=True) + logger.info(f"关闭功能 {name}", arparma.header_result, session=session) + elif session.id1 in bot.config.superusers: + _type = BlockType.ALL + if block_type.available: + if block_type.result in ["p", "private"]: + _type = BlockType.FRIEND + elif block_type.result in ["g", "group"]: + _type = BlockType.GROUP + result = await PluginManage.superuser_block(name, _type, group.result) + await Text(result).send(reply=True) + logger.info( + f"超级用户关闭功能 {name}, 禁用类型: {_type}", + arparma.header_result, + session=session, + target=group.result, + ) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py new file mode 100644 index 00000000..bd7fd937 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -0,0 +1,244 @@ +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.exception import GroupInfoNotFound +from zhenxun.utils.image_utils import BuildImage, ImageTemplate, RowStyle + + +def plugin_row_style(column: str, text: str) -> RowStyle: + """被动技能文本风格 + + 参数: + column: 表头 + text: 文本内容 + + 返回: + RowStyle: RowStyle + """ + style = RowStyle() + if column == "全局状态": + if text == "开启": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + if column == "加载状态": + if text == "SUCCESS": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + return style + + +async def build_plugin() -> BuildImage: + column_name = [ + "ID", + "模块", + "名称", + "全局状态", + "禁用类型", + "加载状态", + "菜单分类", + "作者", + "版本", + "金币花费", + ] + plugin_list = await PluginInfo.filter(plugin_type__not=PluginType.HIDDEN).all() + column_data = [] + for plugin in plugin_list: + column_data.append( + [ + plugin.id, + plugin.module, + plugin.name, + "开启" if plugin.status else "关闭", + plugin.block_type, + "SUCCESS" if plugin.load_status else "ERROR", + plugin.menu_type, + plugin.author, + plugin.version, + plugin.cost_gold, + ] + ) + return await ImageTemplate.table_page( + "Plugin", + "插件状态", + column_name, + column_data, + text_style=plugin_row_style, + ) + + +def task_row_style(column: str, text: str) -> RowStyle: + """被动技能文本风格 + + 参数: + column: 表头 + text: 文本内容 + + 返回: + RowStyle: RowStyle + """ + style = RowStyle() + if column in ["群组状态", "全局状态"]: + if text == "开启": + style.font_color = "#67C23A" + else: + style.font_color = "#F56C6C" + return style + + +async def build_task(group_id: str | None) -> BuildImage: + """构造被动技能状态图片 + + 参数: + group_id: 群组id + + 异常: + GroupInfoNotFound: 未找到群组 + + 返回: + BuildImage: 被动技能状态图片 + """ + task_list = await TaskInfo.all() + column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"] + group = None + if group_id: + group = await GroupConsole.get_or_none(group_id=group_id) + if not group: + raise GroupInfoNotFound() + else: + column_name.remove("群组状态") + column_data = [] + for task in task_list: + if group: + column_data.append( + [ + task.id, + task.module, + task.name, + "开启" if task.module not in group.block_task else "关闭", + "开启" if task.status else "关闭", + task.run_time, + ] + ) + else: + column_data.append( + [ + task.id, + task.module, + task.name, + "开启" if task.status else "关闭", + task.run_time, + ] + ) + return await ImageTemplate.table_page( + "Task", + "被动技能状态", + column_name, + column_data, + text_style=task_row_style, + ) + + +class PluginManage: + + @classmethod + async def block(cls, module: str): + await PluginInfo.filter(module=module).update(status=False) + + @classmethod + async def unblock(cls, module: str): + await PluginInfo.filter(module=module).update(status=True) + + @classmethod + async def block_group_plugin(cls, plugin_name: str, group_id: str) -> str: + """禁用群组插件 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_plugin(plugin_name, group_id, True) + + @classmethod + async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: + """启用群组插件 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_plugin(plugin_name, group_id, False) + + @classmethod + async def _change_group_plugin( + cls, plugin_name: str, group_id: str, status: bool + ) -> str: + """修改群组插件状态 + + 参数: + plugin_name: 插件名称 + group_id: 群组id + status: 插件状态 + + 返回: + str: 返回信息 + """ + status_str = "开启" if status else "关闭" + if plugin := await PluginInfo.get_or_none(name=plugin_name): + group, _ = await GroupConsole.get_or_create(group_id=group_id) + if status: + if plugin.module in group.block_plugin: + group.block_plugin = group.block_plugin.replace( + f"{plugin.module},", "" + ) + await group.save(update_fields=["block_plugin"]) + return f"已成功{status_str} {plugin_name} 功能!" + else: + if plugin.module not in group.block_plugin: + group.block_plugin += f"{plugin.module}," + await group.save(update_fields=["block_plugin"]) + return f"已成功{status_str} {plugin_name} 功能!" + return f"该功能已经{status_str}了喔,不要重复{status_str}..." + return "没有找到这个功能喔..." + + @classmethod + async def superuser_block( + cls, plugin_name: str, block_type: BlockType | None, group_id: str | None + ) -> str: + """超级用户禁用 + + 参数: + plugin_name: 插件名称 + block_type: 禁用类型 + group_id: 群组id + + 返回: + str: 返回信息 + """ + if plugin := await PluginInfo.get_or_none(name=plugin_name): + if group_id: + if group := await GroupConsole.get_or_none(group_id=group_id): + if f"super:{plugin_name}," not in group.block_plugin: + group.block_plugin += f"super:{plugin_name}," + await group.save(update_fields=["block_plugin"]) + return ( + f"已成功关闭群组 {group.group_name} 的 {plugin_name} 功能!" + ) + return "此群组该功能已被超级用户关闭,不要重复关闭..." + return "群组信息未更新,请先更新群组信息..." + plugin.block_type = block_type + plugin.status = not bool(block_type) + await plugin.save(update_fields=["status", "block_type"]) + if not block_type: + return f"已成功将 {plugin_name} 全局启用!" + else: + return f"已成功将 {plugin_name} 全局关闭!" + return "没有找到这个功能喔..." diff --git a/zhenxun/builtin_plugins/admin/welcome_message.py b/zhenxun/builtin_plugins/admin/welcome_message.py index 96c47949..7909a036 100644 --- a/zhenxun/builtin_plugins/admin/welcome_message.py +++ b/zhenxun/builtin_plugins/admin/welcome_message.py @@ -1,26 +1,22 @@ +import os import shutil -from typing import Dict +from typing import Annotated, Dict import ujson as json -from arclet.alconna import Args, Option +from nonebot import on_command +from nonebot.params import Command from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import ( - Alconna, - AlconnaMatch, - Arparma, - Match, - on_alconna, - store_true, -) -from nonebot_plugin_alconna.matcher import AlconnaMatcher -from nonebot_plugin_saa import Text +from nonebot_plugin_alconna import Image +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.path_config import DATA_PATH -from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.rules import admin_check, ensure_group base_config = Config.get("admin_bot_manage") @@ -48,18 +44,15 @@ __plugin_meta__ = PluginMetadata( ).dict(), ) -_matcher = on_alconna( - Alconna( - "设置欢迎消息", - Args["message", str], - Option("-at", action=store_true, help_text="是否at新入群用户"), - ), +_matcher = on_command( + "设置欢迎消息", rule=admin_check("admin_bot_manage", "SET_GROUP_WELCOME_MESSAGE_LEVEL") & ensure_group, priority=5, block=True, ) + BASE_PATH = DATA_PATH / "welcome_message" BASE_PATH.mkdir(parents=True, exist_ok=True) @@ -86,31 +79,43 @@ if old_file.exists(): @_matcher.handle() async def _( session: EventSession, - matcher: AlconnaMatcher, - arparma: Arparma, - message: str, + message: UniMsg, + command: Annotated[tuple[str, ...], Command()], ): - file = ( - BASE_PATH - / f"{session.platform or session.bot_type}" - / f"{session.id2}" - / "text.json" - ) + path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}" if session.id3: - file = ( + path = ( BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id3}" / f"{session.id2}" - / "text.json" ) + file = path / "text.json" + idx = 0 + text = "" + for f in os.listdir(path): + (path / f).unlink() + message[0].text = message[0].text.replace(command[0], "").strip() + for msg in message: + if isinstance(msg, alcText): + text += msg.text + elif isinstance(msg, Image): + if msg.url: + text += f"[image:{idx}]" + await AsyncHttpx.download_file(msg.url, path / f"{idx}.png") + idx += 1 + else: + logger.debug("图片 URL 为空...", command[0]) if not file.exists(): file.parent.mkdir(exist_ok=True, parents=True) + is_at = "-at" in message + text = text.replace("-at", "") json.dump( - {"at": arparma.find("at"), "message": message}, + {"at": is_at, "message": text}, file.open("w"), ensure_ascii=False, indent=4, ) - logger.info(f"设置群欢迎消息成功: {message}", arparma.header_result, session=session) - await Text(f"设置欢迎消息成功: \n{message}").send() + uni_msg = alcText("设置欢迎消息成功: \n") + message + await uni_msg.send() + logger.info(f"设置群欢迎消息成功: {text}", command[0], session=session) diff --git a/zhenxun/builtin_plugins/chat_history/__init__.py b/zhenxun/builtin_plugins/chat_history/__init__.py new file mode 100644 index 00000000..838488cf --- /dev/null +++ b/zhenxun/builtin_plugins/chat_history/__init__.py @@ -0,0 +1,4 @@ +import nonebot +from pathlib import Path + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py new file mode 100644 index 00000000..602a0199 --- /dev/null +++ b/zhenxun/builtin_plugins/chat_history/chat_message.py @@ -0,0 +1,83 @@ +from nonebot import on_message +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.chat_history import ChatHistory +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="消息存储", + description="消息存储,被动存储群消息", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="chat_history", + key="FLAG", + value=True, + help="是否开启消息自从存储", + default_value=True, + type=bool, + ) + ], + ).dict(), +) + + +def rule(message: UniMsg) -> bool: + return bool(Config.get_config("chat_history", "FLAG") and message) + + +chat_history = on_message(rule=rule, priority=1, block=False) + + +TEMP_LIST = [] + + +@chat_history.handle() +async def _(message: UniMsg, session: EventSession): + group_id = session.id3 or session.id2 + TEMP_LIST.append( + ChatHistory( + user_id=session.id1, + group_id=group_id, + text=str(message), + plain_text=message.extract_plain_text(), + bot_id=session.bot_id, + platform=session.platform, + ) + ) + + +@scheduler.scheduled_job( + "interval", + minutes=1, +) +async def _(): + try: + message_list = TEMP_LIST.copy() + TEMP_LIST.clear() + if message_list: + await ChatHistory.bulk_create(message_list) + logger.debug(f"批量添加聊天记录 {len(message_list)} 条", "定时任务") + except Exception as e: + logger.error(f"定时批量添加聊天记录", "定时任务", e=e) + + +# @test.handle() +# async def _(event: MessageEvent): +# print(await ChatHistory.get_user_msg(event.user_id, "private")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# print(await ChatHistory.get_user_msg(event.user_id, "group")) +# print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# print(await ChatHistory.get_group_msg(event.group_id)) +# print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py new file mode 100644 index 00000000..8b8cb697 --- /dev/null +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -0,0 +1,116 @@ +from datetime import datetime, timedelta + +import pytz +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import ImageTemplate + +__plugin_meta__ = PluginMetadata( + name="消息统计查询", + description="消息统计查询", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="数据统计", + ).dict(), +) + +# TODO: shortcut + +_matcher = on_alconna( + Alconna( + "消息排行", + Option("--des", default=False, action=store_true), + Args["type?", ["日", "周", "月", "年"]]["count?", int, 10], + ), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + type: Match[str], + count: Match[int], +): + group_id = session.id3 or session.id2 + if not group_id: + await Text("群组id为空...").finish() + time_now = datetime.now() + date_scope = None + zero_today = time_now - timedelta( + hours=time_now.hour, minutes=time_now.minute, seconds=time_now.second + ) + date = type.result if type.available else None + if date: + if date in ["日"]: + date_scope = (zero_today, time_now) + elif date in ["周"]: + date_scope = (time_now - timedelta(days=7), time_now) + elif date in ["月"]: + date_scope = (time_now - timedelta(days=30), time_now) + column_name = ["名次", "昵称", "发言次数"] + if rank_data := await ChatHistory.get_group_msg_rank( + group_id, count.result, "DES" if arparma.find("des") else "DESC", date_scope + ): + idx = 1 + data_list = [] + for uid, num in rank_data: + if user := await GroupInfoUser.filter( + user_id=uid, group_id=group_id + ).first(): + user_name = user.user_name + else: + user_name = uid + data_list.append([idx, user_name, num]) + idx += 1 + if not date_scope: + if date_scope := await ChatHistory.get_group_first_msg_datetime(group_id): + date_scope = date_scope.astimezone( + pytz.timezone("Asia/Shanghai") + ).replace(microsecond=0) + else: + date_scope = time_now.replace(microsecond=0) + date_str = f"{date_scope} - 至今" + else: + date_str = f"{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" + A = await ImageTemplate.table_page( + f"消息排行({count.result})", date_str, column_name, data_list + ) + logger.info( + f"查看消息排行 数量={count.result}", arparma.header_result, session=session + ) + await Image(A.pic2bs4()).finish(reply=True) + await Text("群组消息记录为空...").finish() + + +# # @test.handle() +# # async def _(event: MessageEvent): +# # print(await ChatHistory.get_user_msg(event.user_id, "private")) +# # print(await ChatHistory.get_user_msg_count(event.user_id, "private")) +# # print(await ChatHistory.get_user_msg(event.user_id, "group")) +# # print(await ChatHistory.get_user_msg_count(event.user_id, "group")) +# # print(await ChatHistory.get_group_msg(event.group_id)) +# # print(await ChatHistory.get_group_msg_count(event.group_id)) diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py new file mode 100644 index 00000000..41c190b2 --- /dev/null +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -0,0 +1,80 @@ +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import create_help_img, get_plugin_help +from ._utils import GROUP_HELP_PATH + +__plugin_meta__ = PluginMetadata( + name="帮助", + description="帮助", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + key="type", + value="normal", + help="帮助图片样式 ['normal', 'HTML']", + default_value="normal", + ) + ], + ).dict(), +) + + +SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png" +if SIMPLE_HELP_IMAGE.exists(): + SIMPLE_HELP_IMAGE.unlink() + +_matcher = on_alconna( + Alconna( + "功能", + Args["name?", str], + ), + aliases={"help", "帮助"}, + rule=to_me(), + priority=1, + block=True, +) + +# TODO: 插件使用详情 图片形式的帮助回复 + + +@_matcher.handle() +async def _( + name: Match[str], + session: EventSession, +): + + if name.available: + if text := await get_plugin_help(name.result): + await Text(text).send(reply=True) + else: + await Text("没有此功能的帮助信息...").send() + logger.info( + f"查看帮助详情: {name.result}", + "帮助", + session=session, + ) + else: + if gid := session.id3 or session.id2: + _image_path = GROUP_HELP_PATH / f"{gid}.png" + if not _image_path.exists(): + await create_help_img(gid) + await Image(_image_path).finish() + else: + if not SIMPLE_HELP_IMAGE.exists(): + if SIMPLE_HELP_IMAGE.exists(): + SIMPLE_HELP_IMAGE.unlink() + await create_help_img(None) + await Image(SIMPLE_HELP_IMAGE).finish() diff --git a/zhenxun/builtin_plugins/help/_config.py b/zhenxun/builtin_plugins/help/_config.py new file mode 100644 index 00000000..b38bf066 --- /dev/null +++ b/zhenxun/builtin_plugins/help/_config.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class Item(BaseModel): + plugin_name: str + sta: int + + +class PluginList(BaseModel): + plugin_type: str + icon: str + logo: str + items: list[Item] diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py new file mode 100644 index 00000000..75d8b66e --- /dev/null +++ b/zhenxun/builtin_plugins/help/_data_source.py @@ -0,0 +1,35 @@ +import nonebot + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.utils.image_utils import BuildImage + +from ._utils import HelpImageBuild + +random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help" + +background = IMAGE_PATH / "background" / "0.png" + + +async def create_help_img(group_id: int | None): + """ + 说明: + 生成帮助图片 + 参数: + :param group_id: 群号 + """ + await HelpImageBuild().build_image(group_id) + + +async def get_plugin_help(name: str) -> str: + """获取功能的帮助信息 + + 参数: + name: 插件名称 + """ + if plugin := await PluginInfo.get_or_none(name=name): + _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) + if _plugin and _plugin.metadata: + return _plugin.metadata.usage + return "糟糕! 该功能没有帮助喔..." + return "没有查找到这个功能噢..." diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py new file mode 100644 index 00000000..b3f8d3a4 --- /dev/null +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -0,0 +1,242 @@ +import os +import random +from typing import Dict + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH, TEMPLATE_PATH +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.image_utils import BuildImage, build_sort_image, group_image + +from ._config import Item + +GROUP_HELP_PATH = DATA_PATH / "group_help" +GROUP_HELP_PATH.mkdir(exist_ok=True, parents=True) +for f in os.listdir(GROUP_HELP_PATH): + group_help_image = GROUP_HELP_PATH / f + group_help_image.unlink() + +BACKGROUND_PATH = IMAGE_PATH / "background" / "help" / "simple_help" + +LOGO_PATH = TEMPLATE_PATH / "menu" / "res" / "logo" + + +class HelpImageBuild: + def __init__(self): + self._data: list[PluginInfo] = [] + self._sort_data: Dict[str, list[PluginInfo]] = {} + self._image_list = [] + self.icon2str = { + "normal": "fa fa-cog", + "原神相关": "fa fa-circle-o", + "常规插件": "fa fa-cubes", + "联系管理员": "fa fa-envelope-o", + "抽卡相关": "fa fa-credit-card-alt", + "来点好康的": "fa fa-picture-o", + "数据统计": "fa fa-bar-chart", + "一些工具": "fa fa-shopping-cart", + "商店": "fa fa-shopping-cart", + "其它": "fa fa-tags", + "群内小游戏": "fa fa-gamepad", + } + + async def sort_type(self): + """ + 对插件按照菜单类型分类 + """ + if not self._data: + self._data = await PluginInfo.filter(plugin_type=PluginType.NORMAL) + if not self._sort_data: + for plugin in self._data: + menu_type = plugin.menu_type or "normal" + if not self._sort_data.get(menu_type): + self._sort_data[menu_type] = [] + self._sort_data[menu_type].append(plugin) + + async def build_image(self, group_id: int | None): + if group_id: + help_image = GROUP_HELP_PATH / f"{group_id}.png" + else: + help_image = IMAGE_PATH / f"SIMPLE_HELP.png" + build_type = Config.get_config("help", "TYPE") + if build_type == "HTML": + byt = await self.build_html_image(group_id) + with open(help_image, "wb") as f: + f.write(byt) + else: + img = await self.build_pil_image(group_id) + await img.save(help_image) + + async def build_html_image(self, group_id: int | None) -> bytes: + from nonebot_plugin_htmlrender import template_to_pic + + await self.sort_type() + classify = {} + for menu in self._sort_data: + for plugin in self._sort_data[menu]: + sta = 0 + if not plugin.status: + if group_id and plugin.block_type in [ + BlockType.ALL, + BlockType.GROUP, + ]: + sta = 2 + if not group_id and plugin.block_type in [ + BlockType.ALL, + BlockType.FRIEND, + ]: + sta = 2 + if group_id and ( + group := await GroupConsole.get_or_none(group_id=group_id) + ): + if f"{plugin.module}:super," in group.block_plugin: + sta = 2 + if f"{plugin.module}," in group.block_plugin: + sta = 1 + if classify.get(menu): + classify[menu].append(Item(plugin_name=plugin.name, sta=sta)) + else: + classify[menu] = [Item(plugin_name=plugin.name, sta=sta)] + max_len = 0 + flag_index = -1 + max_data = None + plugin_list = [] + for index, plu in enumerate(classify.keys()): + if plu in self.icon2str.keys(): + icon = self.icon2str[plu] + else: + icon = "fa fa-pencil-square-o" + logo = LOGO_PATH / random.choice(os.listdir(LOGO_PATH)) + data = { + "name": plu if plu != "normal" else "功能", + "items": classify[plu], + "icon": icon, + "logo": str(logo.absolute()), + } + if len(classify[plu]) > max_len: + max_len = len(classify[plu]) + flag_index = index + max_data = data + plugin_list.append(data) + del plugin_list[flag_index] + plugin_list.insert(0, max_data) + pic = await template_to_pic( + template_path=str((TEMPLATE_PATH / "menu").absolute()), + template_name="zhenxun_menu.html", + templates={"plugin_list": plugin_list}, + pages={ + "viewport": {"width": 1903, "height": 975}, + "base_url": f"file://{TEMPLATE_PATH}", + }, + wait=2, + ) + return pic + + async def build_pil_image(self, group_id: int | None) -> BuildImage: + """构造帮助图片 + + 参数: + group_id: 群号 + """ + self._image_list = [] + await self.sort_type() + font_size = 24 + build_type = Config.get_config("help", "TYPE") + _image = BuildImage.build_text_image("1", size=font_size) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + for idx, menu_type in enumerate(self._sort_data.keys()): + plugin_list = self._sort_data[menu_type] + wh_list = [BuildImage.get_text_size(x.name, font) for x in plugin_list] + wh_list.append(BuildImage.get_text_size(menu_type, font)) + # sum_height = sum([x[1] for x in wh_list]) + if build_type == "VV": + sum_height = 50 * len(plugin_list) + 10 + else: + sum_height = (font_size + 6) * len(plugin_list) + 10 + max_width = max([x[0] for x in wh_list]) + 20 + bk = BuildImage( + max_width + 40, + sum_height + 50, + font_size=30, + color="#a7d1fc", + font="CJGaoDeGuo.otf", + ) + title_size = bk.getsize(menu_type) + max_width = max_width if max_width > title_size[0] else title_size[0] + B = BuildImage( + max_width + 40, + sum_height, + font_size=font_size, + color="white" if not idx % 2 else "black", + ) + curr_h = 10 + if group := await GroupConsole.get_or_none(group_id=group_id): + for i, plugin in enumerate(plugin_list): + text_color = (255, 255, 255) if idx % 2 else (0, 0, 0) + if f"{plugin.module}," in group.block_plugin: + text_color = (252, 75, 13) + pos = None + # 禁用状态划线 + if ( + plugin.block_type in [BlockType.ALL, BlockType.GROUP] + or f"{plugin.module}:super," in group.block_plugin + ): + w = curr_h + int(B.getsize(plugin.name)[1] / 2) + 2 + pos = ( + 7, + w, + B.getsize(plugin.name)[0] + 35, + w, + ) + if build_type == "VV": + name_image = await self.build_name_image( # type: ignore + max_width, + plugin.name, + "black" if not idx % 2 else "white", + text_color, + pos, + ) + await B.paste(name_image, (0, curr_h), center_type="width") + curr_h += name_image.h + 5 + else: + await B.text((10, curr_h), f"{i + 1}.{plugin.name}", text_color) + if pos: + await B.line(pos, (236, 66, 7), 3) + curr_h += font_size + 5 + if menu_type == "normal": + menu_type = "功能" + await bk.text((0, 14), menu_type, center_type="width") + await bk.paste(B, (0, 50)) + await bk.transparent(2) + # await bk.acircle_corner(point_list=['lt', 'rt']) + self._image_list.append(bk) + image_group, h = group_image(self._image_list) + B = await build_sort_image( + image_group, + h, + background_path=BACKGROUND_PATH, + background_handle=lambda image: image.filter("GaussianBlur", 5), + ) + w = 10 + h = 10 + for msg in [ + "目前支持的功能列表:", + "可以通过 ‘帮助[功能名称]’ 来获取对应功能的使用方法", + ]: + text = await BuildImage.build_text_image(msg, "HYWenHei-85W.ttf", 24) + await B.paste(text, (w, h)) + h += 50 + if msg == "目前支持的功能列表:": + w += 50 + text = await BuildImage.build_text_image( + "注: 红字代表功能被群管理员禁用,红线代表功能正在维护", + "HYWenHei-85W.ttf", + 24, + (231, 74, 57), + ) + await B.paste( + text, + (300, 10), + ) + return B diff --git a/zhenxun/builtin_plugins/hooks/__init__.py b/zhenxun/builtin_plugins/hooks/__init__.py new file mode 100644 index 00000000..80aa7181 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/__init__.py @@ -0,0 +1,43 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "hook", + "CHECK_NOTICE_INFO_CD", + 300, + help="群检测,个人权限检测等各种检测提示信息cd", + default_value=300, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_BAN_TIME", + 30, + help="恶意命令触发检测触发后ban的时长(分钟)", + default_value=30, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_CHECK_TIME", + 5, + help="恶意命令触发检测规定时间内(秒)", + default_value=5, + type=int, +) + +Config.add_plugin_config( + "hook", + "MALICIOUS_BAN_COUNT", + 6, + help="恶意命令触发检测最大触发次数", + default_value=6, + type=int, +) + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py new file mode 100644 index 00000000..3f10e078 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/ban_hook.py @@ -0,0 +1,61 @@ +from nonebot.adapters import Bot, Event +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.models.ban_console import BanConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.utils import FreqLimiter + +Config.add_plugin_config( + "hook", + "BAN_RESULT", + "才不会给你发消息.", + help="对被ban用户发送的消息", +) + +_flmt = FreqLimiter(300) + + +# 检查是否被ban +@run_preprocessor +async def _( + matcher: Matcher, bot: Bot, event: Event, state: T_State, session: EventSession +): + if plugin := matcher.plugin: + if metadata := plugin.metadata: + extra = metadata.extra + if extra.get("plugin_type") == PluginType.HIDDEN: + return + user_id = session.id1 + group_id = session.id3 or session.id2 + if user_id: + ban_result = Config.get_config("hook", "BAN_RESULT") + if user_id in bot.config.superusers: + return + if await BanConsole.is_ban(user_id) or await BanConsole.is_ban( + user_id, group_id + ): + time = await BanConsole.check_ban_time(user_id) + if time == -1: + time_str = "∞" + else: + time = abs(int(time)) + if time < 60: + time_str = str(time) + " 秒" + else: + time_str = str(int(time / 60)) + " 分钟" + if ban_result and _flmt.check(user_id): + _flmt.start_cd(user_id) + await MessageFactory( + [ + Mention(user_id), + Text(f"{ban_result}\n在..在 {time_str} 后才会理你喔"), + ] + ).send() + raise IgnoredException("用户处于黑名单中") diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py new file mode 100644 index 00000000..fffc1b80 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -0,0 +1,104 @@ +import time +from collections import defaultdict + +from click import command +from nonebot.adapters.onebot.v11 import ActionFailed, Bot, GroupMessageEvent +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State +from nonebot_plugin_alconna import Arparma +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.models.ban_console import BanConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME") +malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT") + +if not malicious_check_time: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_CHECK_TIME] 为空或小于0") +if not malicious_ban_count: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_COUNT] 为空或小于0") + + +class BanCheckLimiter: + """ + 恶意命令触发检测 + """ + + def __init__(self, default_check_time: float = 5, default_count: int = 4): + self.mint = defaultdict(int) + self.mtime = defaultdict(float) + self.default_check_time = default_check_time + self.default_count = default_count + + def add(self, key: str | int | float): + if self.mint[key] == 1: + self.mtime[key] = time.time() + self.mint[key] += 1 + + def check(self, key: str | int | float) -> bool: + if time.time() - self.mtime[key] > self.default_check_time: + self.mtime[key] = time.time() + self.mint[key] = 0 + return False + if ( + self.mint[key] >= self.default_count + and time.time() - self.mtime[key] < self.default_check_time + ): + self.mtime[key] = time.time() + self.mint[key] = 0 + return True + return False + + +_blmt = BanCheckLimiter( + malicious_check_time, + malicious_ban_count, +) + + +# 恶意触发命令检测 +@run_preprocessor +async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): + if plugin := matcher.plugin: + if metadata := plugin.metadata: + extra = metadata.extra + if extra.get("plugin_type") == PluginType.HIDDEN: + return + user_id = session.id1 + group_id = session.id3 or session.id2 + malicious_ban_time = Config.get_config("hook", "MALICIOUS_BAN_TIME") + if not malicious_ban_time: + raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") + if user_id: + command = state["_prefix"]["raw_command"] + if state["_alc_result"]: + command = state["_alc_result"].source.command + if command: + if _blmt.check(f"{user_id}__{command}"): + await BanConsole.ban( + user_id, group_id, 9, malicious_ban_time * 60, bot.self_id + ) + logger.info( + f"触发了恶意触发检测: {matcher.plugin_name}", + "HOOK", + session=session, + ) + await MessageFactory( + [ + Mention(user_id), + Text(f"检测到恶意触发命令,您将被封禁 30 分钟"), + ] + ).send() + logger.debug( + f"触发了恶意触发检测: {matcher.plugin_name}", + "HOOK", + session=session, + ) + raise IgnoredException("检测到恶意触发命令") + _blmt.add(f"{user_id}__{command}") diff --git a/zhenxun/builtin_plugins/hooks/withdraw_hook.py b/zhenxun/builtin_plugins/hooks/withdraw_hook.py new file mode 100644 index 00000000..eab5267a --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/withdraw_hook.py @@ -0,0 +1,46 @@ +import asyncio +from typing import Optional + +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor + +from zhenxun.services.log import logger +from zhenxun.utils.utils import WithdrawManager + +# TODO: 其他平台撤回消息 + + +# 消息撤回 +@run_postprocessor +async def _( + matcher: Matcher, + exception: Optional[Exception], + bot: Bot, +): + tasks = [] + for message_id in WithdrawManager._data: + second = WithdrawManager._data[message_id] + tasks.append(asyncio.ensure_future(_withdraw_message(bot, message_id, second))) + WithdrawManager.remove(message_id) + await asyncio.gather(*tasks) + + +async def _withdraw_message(bot: Bot, message_id: str, time: int): + await asyncio.sleep(time) + logger.debug(f"撤回消息ID: {message_id}", "HOOK") + if isinstance(bot, v11Bot): + await bot.delete_msg(message_id=int(message_id)) + elif isinstance(bot, v12Bot): + await bot.delete_message(message_id=message_id) + elif isinstance(bot, DodoBot): + pass + elif isinstance(bot, KaiheilaBot): + pass + elif isinstance(bot, DiscordBot): + pass diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 2a19cebc..e80fc8f6 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -1,26 +1,15 @@ -from pathlib import Path -from typing import List - import nonebot from nonebot import get_loaded_plugins from nonebot.drivers import Driver from nonebot.plugin import Plugin -from ruamel import yaml -from ruamel.yaml import YAML, round_trip_dump, round_trip_load -from ruamel.yaml.comments import CommentedMap +from ruamel.yaml import YAML -from zhenxun.configs.config import Config -from zhenxun.configs.path_config import DATA_PATH -from zhenxun.configs.utils import ( - BaseBlock, - PluginExtraData, - PluginSetting, - RegisterConfig, -) +from zhenxun.configs.utils import PluginExtraData, PluginSetting from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_limit import PluginLimit +from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginLimitType +from zhenxun.utils.enum import PluginType _yaml = YAML(pure=True) _yaml.allow_unicode = True @@ -29,8 +18,11 @@ _yaml.indent = 2 driver: Driver = nonebot.get_driver() -def _handle_setting( - plugin: Plugin, plugin_list: List[PluginInfo], limit_list: List[PluginLimit] +async def _handle_setting( + plugin: Plugin, + plugin_list: list[PluginInfo], + limit_list: list[PluginLimit], + task_list: list[TaskInfo], ): """处理插件设置 @@ -45,6 +37,8 @@ def _handle_setting( extra_data = PluginExtraData(**extra) logger.debug(f"{metadata.name}:{plugin.name} -> {extra}", "初始化插件数据") setting = extra_data.setting or PluginSetting() + if metadata.type == "library": + extra_data.plugin_type = PluginType.HIDDEN plugin_list.append( PluginInfo( module=plugin.name, @@ -76,6 +70,16 @@ def _handle_setting( max_count=getattr(limit, "max_count", None), ) ) + if extra_data.tasks: + for task in extra_data.tasks: + task_list.append( + TaskInfo( + module=task.module, + name=task.name, + status=task.status, + run_time=task.run_time, + ) + ) @driver.on_startup @@ -83,14 +87,15 @@ async def _(): """ 初始化插件数据配置 """ - plugin_list: List[PluginInfo] = [] - limit_list: List[PluginLimit] = [] + plugin_list: list[PluginInfo] = [] + limit_list: list[PluginLimit] = [] + task_list: list[TaskInfo] = [] module2id = {} if module_list := await PluginInfo.all().values("id", "module_path"): module2id = {m["module_path"]: m["id"] for m in module_list} for plugin in get_loaded_plugins(): if plugin.metadata: - _handle_setting(plugin, plugin_list, limit_list) + await _handle_setting(plugin, plugin_list, limit_list, task_list) create_list = [] update_list = [] for plugin in plugin_list: @@ -124,3 +129,23 @@ async def _(): limit_create.append(limit) if limit_create: await PluginLimit.bulk_create(limit_create, 10) + if task_list: + module_dict = { + t[1]: t[0] for t in await TaskInfo.all().values_list("id", "module") + } + create_list = [] + update_list = [] + for task in task_list: + if task.module not in module_list: + create_list.append(task) + else: + task.id = module_dict[task.module] + update_list.append(task) + if create_list: + await TaskInfo.bulk_create(create_list, 10) + if update_list: + await TaskInfo.bulk_update( + update_list, + ["run_time", "status", "name"], + 10, + ) diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py new file mode 100644 index 00000000..d672db69 --- /dev/null +++ b/zhenxun/builtin_plugins/nickname.py @@ -0,0 +1,240 @@ +import random +from typing import Any, List + +from nonebot import on_regex +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.params import Depends, RegexGroup +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Option, UniMsg, on_alconna, store_true +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="昵称系统", + description="区区昵称,才不想叫呢!", + usage=f""" + 个人昵称,将替换{NICKNAME}称呼你的名称,群聊 与 私聊 昵称相互独立,全局昵称设置将更改您目前所有群聊中及私聊的昵称 + 指令: + 以后叫我 [昵称]: 设置当前群聊/私聊的昵称 + 全局昵称设置 [昵称]: 设置当前所有群聊和私聊的昵称 + {NICKNAME}我是谁 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="商店", + configs=[ + RegisterConfig( + key="BLACK_WORD", + value=["爸", "爹", "爷", "父"], + help="昵称所屏蔽的关键词,已设置的昵称会被替换为 *,未设置的昵称会在设置时提示", + default_value=None, + type=List[str], + ) + ], + ).dict(), +) + +_nickname_matcher = on_regex( + "(?:以后)?(?:叫我|请叫我|称呼我)(.*)", + rule=to_me(), + priority=5, + block=True, +) + +_global_nickname_matcher = on_regex( + "设置全局昵称(.*)", rule=to_me(), priority=5, block=True +) + +_matcher = on_alconna( + Alconna( + "nickname", + Option("--name", action=store_true, help_text="用户昵称"), + Option("--cancel", action=store_true, help_text="取消昵称"), + ), + rule=to_me(), + priority=5, + block=True, +) + + +CALL_NAME = [ + "好啦好啦,我知道啦,{},以后就这么叫你吧", + f"嗯嗯,{NICKNAME}" + "记住你的昵称了哦,{}", + "好突然,突然要叫你昵称什么的...{}..", + f"{NICKNAME}" + "会好好记住{}的,放心吧", + "好..好.,那窝以后就叫你{}了.", +] + +REMIND = [ + "我肯定记得你啊,你是{}啊", + "我不会忘记你的,你也不要忘记我!{}", + f"哼哼,{NICKNAME}" + "记忆力可是很好的,{}", + "嗯?你是失忆了嘛...{}..", + f"不要小看{NICKNAME}" + "的记忆力啊!笨蛋{}!QAQ", + "哎?{}..怎么了吗..突然这样问..", +] + +CANCEL = [ + f"呜..{NICKNAME}" + "睡一觉就会忘记的..和梦一样..{}", + "窝知道了..{}..", + f"是{NICKNAME}" + "哪里做的不好嘛..好吧..晚安{}", + "呃,{},下次我绝对绝对绝对不会再忘记你!", + "可..可恶!{}!太可恶了!呜", +] + + +def CheckNickname(): + """ + 检查名称是否合法 + """ + + async def dependency( + bot: Bot, + matcher: Matcher, + session: EventSession, + message: UniMsg, + reg_group: tuple[Any, ...] = RegexGroup(), + ): + black_word = Config.get_config("nickname", "BLACK_WORD") + (name,) = reg_group + logger.debug(f"昵称检查: {name}", "昵称设置", session=session) + if not name: + await Text("叫你空白?叫你虚空?叫你无名??").finish(at_sender=True) + if session.id1 in bot.config.superusers: + logger.debug( + f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session + ) + return + if len(name) > 20: + await Text("昵称可不能超过20个字!").finish(at_sender=True) + if name in bot.config.nickname: + await Text("笨蛋!休想占用我的名字! #").finish(at_sender=True) + if black_word: + for x in name: + if x in black_word: + logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", session=session) + await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True) + for word in black_word: + if word in name: + logger.debug( + "昵称设置禁止字符: [{word}]", "昵称设置", session=session + ) + await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True) + + return Depends(dependency) + + +@_nickname_matcher.handle(parameterless=[CheckNickname()]) +async def _( + session: EventSession, + user_info: UserInfo = EventUserInfo(), + reg_group: tuple[Any, ...] = RegexGroup(), +): + if session.id1: + (name,) = reg_group + if len(name) < 5: + if random.random() < 0.3: + name = "~".join(name) + if gid := session.id3 or session.id2: + await GroupInfoUser.set_user_nickname( + session.id1, + gid, + name, + user_info.user_displayname + or user_info.user_remark + or user_info.user_name, + session.platform, + ) + logger.info(f"设置群昵称成功: {name}", "昵称设置", session=session) + await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) + else: + await FriendUser.set_user_nickname( + session.id1, + name, + user_info.user_displayname + or user_info.user_remark + or user_info.user_name, + session.platform, + ) + logger.info(f"设置私聊昵称成功: {name}", "昵称设置", session=session) + await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) + await Text("用户id为空...").send() + + +@_global_nickname_matcher.handle(parameterless=[CheckNickname()]) +async def _( + session: EventSession, + user_info: UserInfo = EventUserInfo(), + reg_group: tuple[Any, ...] = RegexGroup(), +): + if session.id1: + (name,) = reg_group + await FriendUser.set_user_nickname( + session.id1, + name, + user_info.user_displayname or user_info.user_remark or user_info.user_name, + session.platform, + ) + await GroupInfoUser.filter(user_id=session.id1).update(nickname=name) + logger.info(f"设置全局昵称成功: {name}", "设置全局昵称", session=session) + await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) + await Text("用户id为空...").send() + + +@_matcher.assign("name") +async def _(session: EventSession, user_info: UserInfo = EventUserInfo()): + if session.id1: + if gid := session.id3 or session.id2: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + card = user_info.user_displayname or user_info.user_name + else: + nickname = await FriendUser.get_user_nickname(session.id1) + card = user_info.user_name + if nickname: + await Text(random.choice(REMIND).format(nickname)).finish(reply=True) + else: + await Text( + random.choice( + [ + "没..没有昵称嘛,{}", + "啊,你是{}啊,我想叫你的昵称!", + "是{}啊,有什么事吗?", + "你是{}?", + ] + ).format(card) + ).finish(reply=True) + await Text("用户id为空...").send() + + +@_matcher.assign("cancel") +async def _(bot: Bot, session: EventSession, user_info: UserInfo = EventUserInfo()): + if session.id1: + gid = session.id3 or session.id2 + if gid: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + else: + nickname = await FriendUser.get_user_nickname(session.id1) + if nickname: + await Text(random.choice(CANCEL).format(nickname)).send(reply=True) + if gid: + await GroupInfoUser.set_user_nickname(session.id1, gid, "") + else: + await FriendUser.set_user_nickname(session.id1, "") + await BanConsole.ban(session.id1, gid, 9, 60, bot.self_id) + return + else: + await Text("你在做梦吗?你没有昵称啊").finish(reply=True) + await Text("用户id为空...").send() diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py new file mode 100644 index 00000000..141b58bc --- /dev/null +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -0,0 +1,297 @@ +import os +import random +import re +from datetime import datetime + +import nonebot +import ujson as json +from nonebot import on_notice, on_request +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import ( + GroupDecreaseNoticeEvent, + GroupIncreaseNoticeEvent, +) +from nonebot.adapters.onebot.v12 import ( + GroupMemberDecreaseEvent, + GroupMemberIncreaseEvent, +) +from nonebot.plugin import PluginMetadata +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.level_user import LevelUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.utils import FreqLimiter + +__plugin_meta__ = PluginMetadata( + name="QQ群事件处理", + description="群事件处理", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="invite_manager", + key="message", + value=f"请不要未经同意就拉{NICKNAME}入群!告辞!", + help="强制拉群后进群回复的内容", + ), + RegisterConfig( + module="invite_manager", + key="flag", + value=True, + help="强制拉群后进群回复的内容", + default_value=True, + type=bool, + ), + RegisterConfig( + module="invite_manager", + key="welcome_msg_cd", + value=5, + help="群欢迎消息cd", + default_value=5, + type=int, + ), + RegisterConfig( + module="_task", + key="DEFAULT_GROUP_WELCOME", + value=True, + help="被动 进群欢迎 进群默认开关状态", + default_value=True, + type=bool, + ), + RegisterConfig( + module="_task", + key="DEFAULT_REFUND_GROUP_REMIND", + value=True, + help="被动 退群提醒 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + tasks=[ + Task(module="group_welcome", name="进群欢迎"), + Task(module="refund_group_remind", name="退群提醒"), + ], + ).dict(), +) + + +superuser = nonebot.get_driver().config.platform_superusers["qq"][0] + +base_config = Config.get("invite_manager") + + +limit_cd = base_config.get("welcome_msg_cd") + +_flmt = FreqLimiter(limit_cd) + + +group_increase_handle = on_notice(priority=1, block=False) +"""群员增加处理""" +group_decrease_handle = on_notice(priority=1, block=False) +"""群员减少处理""" +add_group = on_request(priority=1, block=False) +"""加群同意请求""" + + +@group_increase_handle.handle() +async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent): + user_id = str(event.user_id) + group_id = str(event.group_id) + if user_id == bot.self_id: + """新成员为bot本身""" + group = await GroupConsole.get_or_none(group_id=group_id) + if (not group or group.group_flag == 0) and base_config.get("flag"): + """群聊不存在或被强制拉群,退出该群""" + try: + if result_msg := base_config.get("message"): + await bot.send_group_msg( + group_id=event.group_id, message=result_msg + ) + await bot.set_group_leave(group_id=event.group_id) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,已成功退出群聊 {group_id}...", + ) + logger.info( + f"强制拉群或未有群信息,退出群聊成功", + "入群检测", + group_id=event.group_id, + ) + if req := await FgRequest.get_or_none(group_id=group_id): + req.handle_type = RequestHandleType.IGNORE + await req.save(update_fields=["handle_type"]) + except Exception as e: + logger.error( + f"强制拉群或未有群信息,退出群聊失败", + "入群检测", + group_id=event.group_id, + e=e, + ) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", + ) + elif group_id not in await GroupConsole.all().values_list( + "group_id", flat=True + ): + """默认群功能开关""" + block_plugin = "" + if plugin_list := await PluginInfo.filter(default_status=False).all(): + for plugin in plugin_list: + block_plugin += f"{plugin.module}," + group_info = await bot.get_group_info(group_id=event.group_id) + await GroupConsole.create( + group_id=group_info["group_id"], + group_name=group_info["group_name"], + max_member_count=group_info["max_member_count"], + member_count=group_info["member_count"], + group_flag=1, + block_plugin=block_plugin, + platform="qq", + ) + admin_default_auth = Config.get_config( + "admin_bot_manage", "ADMIN_DEFAULT_AUTH" + ) + # 即刻刷新权限 + for user_info in await bot.get_group_member_list(group_id=event.group_id): + """即刻刷新权限""" + if ( + user_info["role"] + in [ + "owner", + "admin", + ] + and not await LevelUser.is_group_flag( + user_info["user_id"], group_id + ) + and admin_default_auth is not None + ): + await LevelUser.set_level( + user_info["user_id"], + user_info["group_id"], + admin_default_auth, + ) + logger.debug( + f"添加默认群管理员权限: {admin_default_auth}", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) + if str(user_info["user_id"]) in bot.config.superusers: + await LevelUser.set_level( + user_info["user_id"], user_info["group_id"], 9 + ) + logger.debug( + f"添加超级用户权限: 9", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) + else: + join_time = datetime.now() + user_info = await bot.get_group_member_info( + group_id=event.group_id, user_id=event.user_id + ) + await GroupInfoUser.update_or_create( + user_id=str(user_info["user_id"]), + group_id=str(user_info["group_id"]), + defaults={"user_name": user_info["nickname"], "user_join_time": join_time}, + ) + logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") + + if _flmt.check(group_id): + """群欢迎消息""" + _flmt.start_cd(group_id) + path = DATA_PATH / "welcome_message" / "qq" / f"{group_id}" + data = json.load((path / "text.json").open()) + message = data["message"] + msg_split = re.split(r"\[image:\d+\]", message) + msg_list = [] + if data["at"]: + msg_list.append(Mention(user_id)) + for i, text in enumerate(msg_split): + msg_list.append(Text(text)) + img_file = path / f"{i}.png" + if img_file.exists(): + msg_list.append(Image(img_file)) + if GroupConsole.is_block_task(group_id, "group_welcome"): + logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id) + if msg_list: + await MessageFactory(msg_list).send() + else: + await MessageFactory( + [ + Text("新人快跑啊!!本群现状↓(快使用自定义!)"), + Image( + IMAGE_PATH + / "qxz" + / random.choice(os.listdir(IMAGE_PATH / "qxz")) + ), + ] + ).send() + + +@group_decrease_handle.handle() +async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent): + if event.sub_type == "kick_me": + """踢出Bot""" + group_id = event.group_id + operator_id = event.operator_id + if user := await GroupInfoUser.get_or_none( + user_id=str(event.operator_id), group_id=str(event.group_id) + ): + operator_name = user.user_name + else: + operator_name = "None" + group = await GroupConsole.filter(group_id=str(group_id)).first() + group_name = group.group_name if group else "" + coffee = int(list(bot.config.superusers)[0]) + await bot.send_private_msg( + user_id=coffee, + message=f"****呜..一份踢出报告****\n" + f"我被 {operator_name}({operator_id})\n" + f"踢出了 {group_name}({group_id})\n" + f"日期:{str(datetime.now()).split('.')[0]}", + ) + return + if str(event.user_id) == bot.self_id: + """踢出Bot""" + await GroupConsole.filter(group_id=str(event.group_id)).delete() + return + if user := await GroupInfoUser.get_or_none( + user_id=str(event.user_id), group_id=str(event.group_id) + ): + user_name = user.user_name + else: + user_name = f"{event.user_id}" + await GroupInfoUser.filter( + user_id=str(event.user_id), group_id=str(event.group_id) + ).delete() + logger.info( + f"名称: {user_name} 退出群聊", + "group_decrease_handle", + session=event.user_id, + group_id=event.group_id, + ) + result = "" + if event.sub_type == "leave": + result = f"{user_name}离开了我们..." + if event.sub_type == "kick": + operator = await bot.get_group_member_info( + user_id=event.operator_id, group_id=event.group_id + ) + operator_name = operator["card"] if operator["card"] else operator["nickname"] + result = f"{user_name} 被 {operator_name} 送走了." + if GroupConsole.is_block_task(str(event.group_id), "refund_group_remind"): + await group_decrease_handle.send(f"{result}") diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py index 2743e662..367031fc 100644 --- a/zhenxun/builtin_plugins/record_request.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -2,7 +2,8 @@ import time from datetime import datetime from typing import Dict -from nonebot import on_message, on_request +import nonebot +from nonebot import drivers, on_message, on_request from nonebot.adapters.onebot.v11 import ( ActionFailed, Bot, @@ -18,7 +19,7 @@ from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.models.fg_request import FgRequest from zhenxun.models.friend_user import FriendUser -from zhenxun.models.group_info import GroupInfo +from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType, RequestType @@ -26,7 +27,7 @@ base_config = Config.get("invite_manager") __plugin_meta__ = PluginMetadata( name="记录请求", - description="自定义群欢迎消息", + description="记录 好友/群组 请求", usage="", extra=PluginExtraData( author="HibiKier", @@ -61,6 +62,8 @@ class Timer: cls.data = {k: v for k, v in cls.data.items() if v - now < 5 * 60} +# TODO: 其他平台请求 + friend_req = on_request(priority=5, block=True) group_req = on_request(priority=5, block=True) _t = on_message(priority=999, block=False, rule=lambda: False) @@ -69,13 +72,14 @@ _t = on_message(priority=999, block=False, rule=lambda: False) @friend_req.handle() async def _(bot: Bot, event: FriendRequestEvent, session: EventSession): if event.user_id and Timer.check(event.user_id): + superuser = nonebot.get_driver().config.platform_superusers["qq"][0] logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) user = await bot.get_stranger_info(user_id=event.user_id) nickname = user["nickname"] # sex = user["sex"] # age = str(user["age"]) comment = event.comment - superuser = int(list(bot.config.superusers)[0]) + superuser = int(superuser) await Text( f"*****一份好友申请*****\n" f"昵称:{nickname}({event.user_id})\n" @@ -84,7 +88,11 @@ async def _(bot: Bot, event: FriendRequestEvent, session: EventSession): f"备注:{event.comment}" ).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot) if base_config.get("AUTO_ADD_FRIEND"): - logger.debug(f"已开启好友请求自动同意,成功通过该请求", "好友请求", target=event.user_id) + logger.debug( + f"已开启好友请求自动同意,成功通过该请求", + "好友请求", + target=event.user_id, + ) await bot.set_friend_add_request(flag=event.flag, approve=True) await FriendUser.create( user_id=str(user["user_id"]), user_name=user["nickname"] @@ -119,7 +127,7 @@ async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): flag=event.flag, sub_type="invite", approve=True ) group_info = await bot.get_group_info(group_id=event.group_id) - await GroupInfo.update_or_create( + await GroupConsole.update_or_create( group_id=str(group_info["group_id"]), defaults={ "group_name": group_info["group_name"], diff --git a/zhenxun/builtin_plugins/scheduler/__init__.py b/zhenxun/builtin_plugins/scheduler/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/scheduler/auto_backup.py b/zhenxun/builtin_plugins/scheduler/auto_backup.py new file mode 100644 index 00000000..92f1119b --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/auto_backup.py @@ -0,0 +1,62 @@ +import shutil +from pathlib import Path + +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.config import Config +from zhenxun.services.log import logger + +Config.add_plugin_config( + "_backup", + "BACKUP_FLAG", + True, + help="是否开启文件备份", + default_value=True, + type=bool, +) + +Config.add_plugin_config( + "_backup", + "BACKUP_DIR_OR_FILE", + [ + "data/black_word", + "data/configs", + "data/statistics", + "data/word_bank", + "data/manager", + "configs", + ], + help="备份的文件夹或文件", + default_value=[], + type=list[str], +) + + +# 自动备份 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=25, +) +async def _(): + if Config.get_config("_backup", "BACKUP_FLAG"): + _backup_path = Path() / "backup" + _backup_path.mkdir(exist_ok=True, parents=True) + if backup_dir_or_file := Config.get_config("_backup", "BACKUP_DIR_OR_FILE"): + for path_file in backup_dir_or_file: + try: + path = Path(path_file) + _p = _backup_path / path_file + if path.exists(): + if path.is_dir(): + if _p.exists(): + shutil.rmtree(_p, ignore_errors=True) + shutil.copytree(path_file, _p) + else: + if _p.exists(): + _p.unlink() + shutil.copy(path_file, _p) + logger.debug(f"已完成自动备份:{path_file}", "自动备份") + except Exception as e: + logger.error(f"自动备份文件 {path_file} 发生错误", "自动备份", e=e) + logger.info("自动备份成功...", "自动备份") diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py new file mode 100644 index 00000000..1cfdacb3 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py @@ -0,0 +1,68 @@ +import nonebot +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger + +# TODO: 其他平台更新 + + +# 自动更新群组信息 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=1, +) +async def _(): + bots = nonebot.get_bots() + _used_group = [] + for bot in bots.values(): + try: + group_list = await bot.get_group_list() + gl = [g["group_id"] for g in group_list if g["group_id"] not in _used_group] + for g in gl: + _used_group.append(g) + group_info = await bot.get_group_info(group_id=g) + await GroupConsole.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + logger.debug("自动更新群组信息成功", "自动更新群组", group_id=g) + except Exception as e: + logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) + logger.info("自动更新群组成员信息成功...") + + +# 自动更新好友信息 +@scheduler.scheduled_job( + "cron", + hour=3, + minute=1, +) +async def _(): + bots = nonebot.get_bots() + for key in bots: + try: + bot = bots[key] + fl = await bot.get_friend_list() + for f in fl: + if FriendUser.exists(user_id=str(f["user_id"])): + await FriendUser.create( + user_id=str(f["user_id"]), user_name=f["nickname"] + ) + logger.debug( + f"更新好友信息成功", "自动更新好友", session=f["user_id"] + ) + else: + logger.debug( + f"好友信息已存在", "自动更新好友", session=f["user_id"] + ) + except Exception as e: + logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) + logger.info("自动更新好友信息成功...") diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py new file mode 100644 index 00000000..62964462 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -0,0 +1,28 @@ +from nonebot_plugin_apscheduler import scheduler + +# TODO: 消息发送 + +# # 早上好 +# @scheduler.scheduled_job( +# "cron", +# hour=6, +# minute=1, +# ) +# async def _(): +# img = image(IMAGE_PATH / "zhenxun" / "zao.jpg") +# await broadcast_group("[[_task|zwa]]早上好" + img, log_cmd="被动早晚安") +# logger.info("每日早安发送...") + + +# # 睡觉了 +# @scheduler.scheduled_job( +# "cron", +# hour=23, +# minute=59, +# ) +# async def _(): +# img = image(IMAGE_PATH / "zhenxun" / "sleep.jpg") +# await broadcast_group( +# f"[[_task|zwa]]{NICKNAME}要睡觉了,你们也要早点睡呀" + img, log_cmd="被动早晚安" +# ) +# logger.info("每日晚安发送...") diff --git a/zhenxun/builtin_plugins/scripts.py b/zhenxun/builtin_plugins/scripts.py new file mode 100644 index 00000000..13589454 --- /dev/null +++ b/zhenxun/builtin_plugins/scripts.py @@ -0,0 +1,59 @@ +from asyncio.exceptions import TimeoutError + +import nonebot +import ujson as json +from nonebot.drivers import Driver +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.configs.path_config import TEXT_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def update_city(): + """ + 部分插件需要中国省份城市 + 这里直接更新,避免插件内代码重复 + """ + china_city = TEXT_PATH / "china_city.json" + data = {} + if not china_city.exists(): + try: + logger.debug("开始更新城市列表...") + res = await AsyncHttpx.get( + "http://www.weather.com.cn/data/city3jdata/china.html", timeout=5 + ) + res.encoding = "utf8" + provinces_data = json.loads(res.text) + for province in provinces_data.keys(): + data[provinces_data[province]] = [] + res = await AsyncHttpx.get( + f"http://www.weather.com.cn/data/city3jdata/provshi/{province}.html", + timeout=5, + ) + res.encoding = "utf8" + city_data = json.loads(res.text) + for city in city_data.keys(): + data[provinces_data[province]].append(city_data[city]) + with open(china_city, "w", encoding="utf8") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + logger.info("自动更新城市列表完成...") + except TimeoutError as e: + logger.warning("自动更新城市列表超时...", e=e) + except ValueError as e: + logger.warning("自动城市列表失败...", e=e) + except Exception as e: + logger.error(f"自动城市列表未知错误...", e=e) + + +# 自动更新城市列表 +@scheduler.scheduled_job( + "cron", + hour=6, + minute=1, +) +async def _(): + await update_city() diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py new file mode 100644 index 00000000..9ea6a6cb --- /dev/null +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -0,0 +1,102 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType + +from ._data_source import ShopManage + +__plugin_meta__ = PluginMetadata( + name="商店", + description="商店系统[金币回收计划]", + usage=""" + 商品操作 + 指令: + 添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时) + 删除商品 [名称或序号] + 修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] + 示例:添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 + 示例:添加商品 name:可疑的药 price:5 des:效果未知 + 示例:删除商品 2 + 示例:修改商品 name:1 price:900 修改序号为1的商品的价格为900 + * 修改商品只需添加需要值即可 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.NORMAL, + menu_type="商店", + limits=[BaseBlock(check_type=BlockType.GROUP)], + ).dict(), +) + +# TODO: 修改操作,shortcut + +_matcher = on_alconna( + Alconna( + "shop", + Subcommand("my-cost", help_text="我的金币"), + Subcommand("my-props", help_text="我的道具"), + Subcommand("buy", Args["name", str]["num", int, 1], help_text="购买道具"), + Subcommand("use", Args["name", str]["num?", int, 1], help_text="使用道具"), + ), + priority=5, + block=True, +) + + +@_matcher.assign("$main") +async def _(session: EventSession, arparma: Arparma): + image = await ShopManage.build_shop_image() + logger.info("查看商店", arparma.header_result, session=session) + await Image(image.pic2bs4()).send() + + +@_matcher.assign("my-cost") +async def _(session: EventSession, arparma: Arparma): + if session.id1: + logger.info("查看金币", arparma.header_result, session=session) + gold = await ShopManage.my_cost(session.id1, session.platform) + await Text(f"你的当前余额: {gold}").send(reply=True) + else: + await Text(f"用户id为空...").send(reply=True) + + +@_matcher.assign("my-props") +async def _( + session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() +): + if session.id1: + logger.info("查看道具", arparma.header_result, session=session) + if image := await ShopManage.my_props( + session.id1, + user_info.user_displayname or user_info.user_name, + session.platform, + ): + await Image(image.pic2bs4()).finish(reply=True) + return await Text(f"你的道具为空捏...").send(reply=True) + else: + await Text(f"用户id为空...").send(reply=True) + + +@_matcher.assign("buy") +async def _(session: EventSession, arparma: Arparma, name: str, num: int): + if session.id1: + logger.info( + f"购买道具 {name}, 数量: {num}", + arparma.header_result, + session=session, + ) + result = await ShopManage.buy_prop(session.id1, name, num, session.platform) + await Text(result).send(reply=True) + else: + await Text(f"用户id为空...").send(reply=True) + + +@_matcher.assign("use") +async def _(session: EventSession, arparma: Arparma, name: str, num: int): + pass diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py new file mode 100644 index 00000000..96bed3f0 --- /dev/null +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -0,0 +1,319 @@ +import time +from typing import Dict + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.user_console import UserConsole +from zhenxun.models.user_gold_log import UserGoldLog +from zhenxun.models.user_props_log import UserPropsLog +from zhenxun.services.log import logger +from zhenxun.utils.enum import GoldHandle, PropHandle +from zhenxun.utils.image_utils import BuildImage, ImageTemplate, text2image + +ICON_PATH = IMAGE_PATH / "shop_icon" + + +class ShopManage: + + @classmethod + async def buy_prop( + cls, user_id: str, name: str, num: int = 1, platform: str | None = None + ) -> str: + if name == "神秘药水": + return "你们看看就好啦,这是不可能卖给你们的~" + if num < 0: + return "购买的数量要大于0!" + goods_list = await GoodsInfo.annotate().order_by("-id").all() + goods_list = [ + goods + for goods in goods_list + if goods.goods_limit_time > time.time() or goods.goods_limit_time == 0 + ] + if name.isdigit(): + goods = goods_list[int(name) - 1] + else: + if filter_goods := [g for g in goods_list if g.goods_name == name]: + goods = filter_goods[0] + else: + return "道具名称不存在..." + user, _ = await UserConsole.get_or_create( + user_id=user_id, defaults={"platform": platform} + ) + price = goods.goods_price * num * goods.goods_discount + if user.gold < price: + return "糟糕! 您的金币好像不太够哦..." + count = await UserPropsLog.filter( + user_id=user_id, handle=PropHandle.BUY + ).count() + if goods.daily_limit and count >= goods.daily_limit: + return "今天的购买已达限制了喔!" + await UserGoldLog.create(user_id=user_id, gold=price, handle=GoldHandle.BUY) + await UserPropsLog.create( + user_id=user_id, uuid=goods.uuid, gold=price, num=num, handle=PropHandle.BUY + ) + logger.info( + f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!", + "购买道具", + session=user_id, + ) + user.gold -= int(price) + if goods.uuid not in user.props: + user.props[goods.uuid] = 0 + user.props[goods.uuid] += num + await user.save(update_fields=["gold", "props"]) + return f"花费 {price} 金币购买 {goods.goods_name} ×{num} 成功!" + + @classmethod + async def my_props( + cls, user_id: str, name: str, platform: str | None = None + ) -> BuildImage | None: + """获取道具背包 + + 参数: + user_id: 用户id + name: 用户昵称 + platform: 平台. + + 返回: + BuildImage | None: 道具背包图片 + """ + user, _ = await UserConsole.get_or_create( + user_id=user_id, defaults={"platform": platform} + ) + if not user.props: + return None + result = await GoodsInfo.filter(uuid__in=user.props.keys()).all() + data_list = [] + uuid2goods = {item.uuid: item for item in result} + column_name = ["-", "使用ID", "名称", "数量", "简介"] + for i, p in enumerate(user.props): + prop = uuid2goods[p] + data_list.append( + [ + (ICON_PATH / prop.icon, 33, 33) if prop.icon else "", + i, + prop.goods_name, + user.props[p], + prop.goods_description, + ] + ) + + return await ImageTemplate.table_page( + f"{name}的道具仓库", "", column_name, data_list + ) + + @classmethod + async def my_cost(cls, user_id: str, platform: str | None = None) -> int: + """用户金币 + + 参数: + user_id: 用户id + platform: 平台. + + 返回: + int: 金币数量 + """ + user, _ = await UserConsole.get_or_create( + user_id=user_id, defaults={"platform": platform} + ) + return user.gold + + @classmethod + async def build_shop_image(cls) -> BuildImage: + """制作商店图片 + + 返回: + BuildImage: 商店图片 + """ + goods_lst = await GoodsInfo.get_all_goods() + _dc = {} + font_h = BuildImage.get_text_size("正")[1] + h = 10 + _list: list[GoodsInfo] = [] + for goods in goods_lst: + if goods.goods_limit_time == 0 or time.time() < goods.goods_limit_time: + _list.append(goods) + # A = BuildImage(1100, h, color="#f9f6f2") + total_n = 0 + image_list = [] + for idx, goods in enumerate(_list): + name_image = BuildImage( + 580, 40, font_size=25, color="#e67b6b", font="CJGaoDeGuo.otf" + ) + await name_image.text( + (15, 0), f"{idx + 1}.{goods.goods_name}", center_type="height" + ) + await name_image.line((380, -5, 280, 45), "#a29ad6", 5) + await name_image.text((390, 0), "售价:", center_type="height") + if goods.goods_discount != 1: + discount_price = int(goods.goods_discount * goods.goods_price) + old_price_image = await BuildImage.build_text_image( + str(goods.goods_price), font_color=(194, 194, 194), size=15 + ) + await old_price_image.line( + ( + 0, + int(old_price_image.height / 2), + old_price_image.width + 1, + int(old_price_image.height / 2), + ), + (0, 0, 0), + ) + await name_image.paste(old_price_image, (440, 0)) + await name_image.text((440, 15), str(discount_price), (255, 255, 255)) + else: + await name_image.text( + (440, 0), + str(goods.goods_price), + (255, 255, 255), + center_type="height", + ) + _tmp = await BuildImage.build_text_image(str(goods.goods_price), size=25) + await name_image.text( + ( + 440 + _tmp.width, + 0, + ), + f" 金币", + center_type="height", + ) + des_image = None + font_img = BuildImage(600, 80, font_size=20, color="#a29ad6") + p = font_img.getsize("简介:")[0] + 20 + if goods.goods_description: + des_list = goods.goods_description.split("\n") + desc = "" + for des in des_list: + if font_img.getsize(des)[0] > font_img.width - p - 20: + msg = "" + tmp = "" + for i in range(len(des)): + if font_img.getsize(tmp)[0] < font_img.width - p - 20: + tmp += des[i] + else: + msg += tmp + "\n" + tmp = des[i] + desc += msg + if tmp: + desc += tmp + else: + desc += des + "\n" + if desc[-1] == "\n": + desc = desc[:-1] + des_image = await text2image(desc, color="#a29ad6") + goods_image = BuildImage( + 600, + (50 + des_image.height) if des_image else 50, + font_size=20, + color="#a29ad6", + font="CJGaoDeGuo.otf", + ) + if des_image: + await goods_image.text((15, 50), "简介:") + await goods_image.paste(des_image, (p, 50)) + await name_image.circle_corner(5) + await goods_image.paste(name_image, (0, 5), center_type="width") + await goods_image.circle_corner(20) + bk = BuildImage( + 1180, + (50 + des_image.height) if des_image else 50, + font_size=15, + color="#f9f6f2", + font="CJGaoDeGuo.otf", + ) + if goods.icon and (ICON_PATH / goods.icon).exists(): + icon = BuildImage(70, 70, background=ICON_PATH / goods.icon) + await bk.paste(icon) + await bk.paste(goods_image, (70, 0)) + n = 0 + _w = 650 + # 添加限时图标和时间 + if goods.goods_limit_time > 0: + n += 140 + _limit_time_logo = BuildImage( + 40, 40, background=f"{IMAGE_PATH}/other/time.png" + ) + await bk.paste(_limit_time_logo, (_w + 50, 0)) + _time_img = await BuildImage.build_text_image("限时!", size=23) + await bk.paste( + _time_img, + (_w + 90, 10), + ) + limit_time = time.strftime( + "%Y-%m-%d %H:%M", time.localtime(goods.goods_limit_time) + ).split() + y_m_d = limit_time[0] + _h_m = limit_time[1].split(":") + h_m = _h_m[0] + "时 " + _h_m[1] + "分" + await bk.text((_w + 55, 38), str(y_m_d)) + await bk.text((_w + 65, 57), str(h_m)) + _w += 140 + if goods.goods_discount != 1: + n += 140 + _discount_logo = BuildImage( + 30, 30, background=f"{IMAGE_PATH}/other/discount.png" + ) + await bk.paste(_discount_logo, (_w + 50, 10)) + _tmp = await BuildImage.build_text_image("折扣!", size=23) + await bk.paste(_tmp, (_w + 90, 15)) + _tmp = await BuildImage.build_text_image( + f"{10 * goods.goods_discount:.1f} 折", + size=30, + font_color=(85, 156, 75), + ) + await bk.paste(_tmp, (_w + 50, 44)) + _w += 140 + if goods.daily_limit != 0: + n += 140 + _daily_limit_logo = BuildImage( + 35, 35, background=f"{IMAGE_PATH}/other/daily_limit.png" + ) + await bk.paste(_daily_limit_logo, (_w + 50, 10)) + _tmp = await BuildImage.build_text_image( + "限购!", + size=23, + ) + await bk.paste(_tmp, (_w + 90, 20)) + _tmp = await BuildImage.build_text_image( + f"{goods.daily_limit}", size=30 + ) + await bk.paste(_tmp, (_w + 72, 45)) + if total_n < n: + total_n = n + if n: + await bk.line((650, -1, 650 + n, -1), "#a29ad6", 5) + # await bk.aline((650, 80, 650 + n, 80), "#a29ad6", 5) + + # 添加限时图标和时间 + image_list.append(bk) + # await A.apaste(bk, (0, current_h), True) + # current_h += 90 + h = 0 + current_h = 0 + for img in image_list: + h += img.height + 10 + A = BuildImage(1100, h, color="#f9f6f2") + for img in image_list: + await A.paste(img, (0, current_h)) + current_h += img.height + 10 + w = 950 + if total_n: + w += total_n + h = A.height + 230 + 100 + h = 1000 if h < 1000 else h + shop_logo = BuildImage(100, 100, background=f"{IMAGE_PATH}/other/shop_text.png") + shop = BuildImage(w, h, font_size=20, color="#f9f6f2") + await shop.paste(A, (20, 230)) + await shop.paste(shop_logo, (450, 30)) + await shop.text( + ( + int((1000 - shop.getsize("注【通过 序号 或者 商品名称 购买】")[0]) / 2), + 170, + ), + "注【通过 序号 或者 商品名称 购买】", + ) + await shop.text( + (20, h - 100), + "神秘药水\t\t售价:9999999金币\n\t\t鬼知道会有什么效果~", + ) + return shop diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py new file mode 100644 index 00000000..cb1155f2 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -0,0 +1,148 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger + +from ._data_source import SignManage +from .goods_register import driver +from .utils import clear_sign_data_pic + +__plugin_meta__ = PluginMetadata( + name="签到", + description="每日签到,证明你在这里", + usage=""" + 每日签到 + 会影响色图概率和开箱次数,以及签到的随机道具获取 + 指令: + 我的签到 + 好感度排行 + * 签到时有 3% 概率 * 2 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + module="send_setu", + key="INITIAL_SETU_PROBABILITY", + value=0.7, + help="初始色图概率,总概率 = 初始色图概率 + 好感度", + default_value=0.7, + type=float, + ), + RegisterConfig( + key="MAX_SIGN_GOLD", + value=200, + help="签到好感度加成额外获得的最大金币数", + default_value=200, + type=int, + ), + RegisterConfig( + key="SIGN_CARD1_PROB", + value=0.2, + help="签到好感度双倍加持卡Ⅰ掉落概率", + default_value=0.2, + type=float, + ), + RegisterConfig( + key="SIGN_CARD2_PROB", + value=0.09, + help="签到好感度双倍加持卡Ⅲ掉落概率", + default_value=0.09, + type=float, + ), + RegisterConfig( + key="SIGN_CARD3_PROB", + value=0.05, + help="签到好感度双倍加持卡Ⅲ掉落概率", + default_value=0.05, + type=float, + ), + ], + limits=[PluginCdBlock()], + ).dict(), +) + + +_sign_matcher = on_alconna( + Alconna( + "签到", + Option("--my", action=store_true, help_text="我的签到"), + Option( + "-l|--list", Args["num", int, 10], action=store_true, help_text="好感度排行" + ), + ), + priority=5, + block=True, +) + +# TODO: shortcut + + +@_sign_matcher.assign("$main") +async def _( + session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() +): + nickname = ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) + if session.id1: + if path := await SignManage.sign(session, nickname): + logger.info("签到成功", arparma.header_result, session=session) + await Image(path).finish(reply=True) + return Text("用户id为空...").send() + + +@_sign_matcher.assign("my") +async def _( + session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() +): + nickname = ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) + if session.id1: + if image := await SignManage.sign(session, nickname, True): + logger.info("查看我的签到", arparma.header_result, session=session) + await Image(image).finish(reply=True) + return Text("用户id为空...").send() + + +@_sign_matcher.assign("list") +async def _( + session: EventSession, + arparma: Arparma, + num: int, + user_info: UserInfo = EventUserInfo(), +): + nickname = ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) + if session.id1: + if image := await SignManage.rank(session.id1, num): + logger.info("查看签到排行", arparma.header_result, session=session) + await Image(image.pic2bs4()).finish() + return Text("用户id为空...").send() + + +@scheduler.scheduled_job( + "interval", + hours=1, +) +async def _(): + try: + clear_sign_data_pic() + logger.info("清理日常签到图片数据数据完成...", "签到") + except Exception as e: + logger.error(f"清理日常签到图片数据数据失败...", e=e) diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py new file mode 100644 index 00000000..07f3fcbe --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -0,0 +1,175 @@ +import os +import random +import secrets +from datetime import datetime +from pathlib import Path + +import pytz +from nonebot_plugin_session import EventSession +from tortoise.functions import Count + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.sign_log import SignLog +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage, ImageTemplate +from zhenxun.utils.utils import get_user_avatar + +from ._random_event import random_event +from .utils import SIGN_TODAY_CARD_PATH, get_card + +ICON_PATH = IMAGE_PATH / "_icon" + +PLATFORM_PATH = { + "dodo": ICON_PATH / "dodo.png", + "discord": ICON_PATH / "discord.png", + "kaiheila": ICON_PATH / "kook.png", + "qq": ICON_PATH / "qq.png", +} + + +class SignManage: + + @classmethod + async def rank(cls, user_id: str, num: int) -> BuildImage: + all_list = ( + await SignUser.annotate() + .order_by("impression") + .values_list("user_id", flat=True) + ) + index = all_list.index(user_id) + 1 # type: ignore + user_list = await SignUser.annotate().order_by("impression").limit(num).all() + user_id_list = [u.user_id for u in user_list] + log_list = ( + await SignLog.filter(user_id__in=user_id_list) + .annotate(count=Count("id")) + .group_by("user_id") + .values_list("user_id", "count") + ) + uid2cnt = {l[0]: l[1] for l in log_list} + column_name = ["排名", "-", "名称", "好感度", "签到次数", "平台"] + friend_list = await FriendUser.filter(user_id__in=user_id_list).values_list( + "user_id", "user_name" + ) + uid2name = {f[0]: f[1] for f in friend_list} + group_member_list = await GroupInfoUser.filter( + user_id__in=user_id_list + ).values_list("user_id", "user_name") + for gm in group_member_list: + uid2name[gm[0]] = gm[1] + data_list = [] + for i, user in enumerate(user_list): + bytes = await get_user_avatar(user.user_id) + data_list.append( + [ + f"{i+1}", + (bytes, 30, 30) if user.platform == "qq" else "", + uid2name.get(user.user_id), + user.impression, + uid2cnt.get(user.user_id) or 0, + (PLATFORM_PATH.get(user.platform), 30, 30), + ] + ) + return await ImageTemplate.table_page( + "好感度排行", f"你的排名在第 {index} 位哦!", column_name, data_list + ) + + @classmethod + async def sign( + cls, session: EventSession, nickname: str, is_view_card: bool = False + ) -> Path | None: + """签到 + + 参数: + session: Session + nickname: 用户昵称 + is_view_card: 是否展示卡片 + + 返回: + Path: 卡片路径 + """ + if not session.id1: + return None + now = datetime.now(pytz.timezone("Asia/Shanghai")) + user_console, _ = await UserConsole.get_or_create( + user_id=session.id1, + defaults={ + "uid": await UserConsole.get_new_uid(), + "platform": session.platform, + }, + ) + user, _ = await SignUser.get_or_create( + user_id=session.id1, + defaults={"user_console": user_console, "platform": session.platform}, + ) + new_log = await SignLog.filter(user_id=session.id1).first() + file_name = f"{user}_sign_{datetime.now().date()}.png" + if ( + user.sign_count != 0 + or (new_log and now > new_log.create_time) + or file_name in os.listdir(SIGN_TODAY_CARD_PATH) + ): + user_console, _ = await UserConsole.get_or_create(user_id=session.id1) + path = await get_card(user, nickname, -1, user_console.gold, "") + else: + path = await cls._handle_sign_in(user, nickname, session, is_view_card) + return path + + @classmethod + async def _handle_sign_in( + cls, + user: SignUser, + nickname: str, + session: EventSession, + is_view_card: bool, + ) -> Path: + """签到处理 + + 参数: + user: SignUser + nickname: 用户昵称 + session: Session + is_view_card: 是否展示卡片 + + 返回: + Path: 卡片路径 + """ + impression_added = (secrets.randbelow(99) + 1) / 100 + rand = random.random() + add_probability = float(user.add_probability) + specify_probability = user.specify_probability + if rand + add_probability > 0.97: + impression_added *= 2 + elif rand < specify_probability: + impression_added *= 2 + await SignUser.sign(user, impression_added, session.bot_id, session.platform) + gold = random.randint(1, 100) + gift = random_event(float(user.impression)) + if isinstance(gift, int): + gold += gift + await UserConsole.add_gold( + user.user_id, gold + gift, "sign_in", session.platform + ) + gift = f"额外金币 +{gift}" + else: + await UserConsole.add_gold(user.user_id, gold, "sign_in", session.platform) + await UserConsole.add_props(user.user_id, gift, 1, session.platform) + gift += " + 1" + logger.info( + f"签到成功. score: {user.impression:.2f} " + f"(+{impression_added:.2f}).获取金币/道具: {gold}", + "签到", + session=session, + ) + return await get_card( + user, + nickname, + impression_added, + gold, + gift, + rand + add_probability > 0.97 or rand < specify_probability, + is_view_card, + ) diff --git a/zhenxun/builtin_plugins/sign_in/_random_event.py b/zhenxun/builtin_plugins/sign_in/_random_event.py new file mode 100644 index 00000000..32d133c1 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/_random_event.py @@ -0,0 +1,33 @@ +import random + +from zhenxun.configs.config import Config + +PROB_DATA = None + + +def random_event(impression: float) -> str | int: + """签到随机事件 + + 参数: + impression: 好感度 + + 返回: + 额外奖励 和 类型 + """ + global PROB_DATA + if not PROB_DATA: + PROB_DATA = { + Config.get_config("sign_in", "SIGN_CARD3_PROB"): "好感度双倍加持卡Ⅲ", + Config.get_config("sign_in", "SIGN_CARD2_PROB"): "好感度双倍加持卡Ⅱ", + Config.get_config("sign_in", "SIGN_CARD1_PROB"): "好感度双倍加持卡Ⅰ", + } + rand = random.random() - impression / 1000 + for prob in PROB_DATA.keys(): + if rand <= prob: + return PROB_DATA[prob] + gold = random.randint( + 1, random.randint(1, int(1 if impression < 1 else impression)) + ) + max_sign_gold = Config.get_config("sign_in", "MAX_SIGN_GOLD") + gold = max_sign_gold if gold > max_sign_gold else gold + return gold diff --git a/zhenxun/builtin_plugins/sign_in/config.py b/zhenxun/builtin_plugins/sign_in/config.py new file mode 100644 index 00000000..e2bfdbc6 --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/config.py @@ -0,0 +1,49 @@ +from zhenxun.configs.path_config import IMAGE_PATH + +SIGN_RESOURCE_PATH = IMAGE_PATH / "sign" / "sign_res" +SIGN_TODAY_CARD_PATH = IMAGE_PATH / "sign" / "today_card" +SIGN_BORDER_PATH = SIGN_RESOURCE_PATH / "border" +SIGN_BACKGROUND_PATH = SIGN_RESOURCE_PATH / "background" + +SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True) +SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True) + + +lik2relation = { + "0": "路人", + "1": "陌生", + "2": "初识", + "3": "普通", + "4": "熟悉", + "5": "信赖", + "6": "相知", + "7": "厚谊", + "8": "亲密", +} + +level2attitude = { + "0": "排斥", + "1": "警惕", + "2": "可以交流", + "3": "一般", + "4": "是个好人", + "5": "好朋友", + "6": "可以分享小秘密", + "7": "喜欢", + "8": "恋人", +} + +weekdays = {1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu", 5: "Fri", 6: "Sat", 7: "Sun"} + +lik2level = { + 9999: "9", + 400: "8", + 270: "7", + 200: "6", + 140: "5", + 90: "4", + 50: "3", + 25: "2", + 10: "1", + 0: "0", +} diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py new file mode 100644 index 00000000..3af6514f --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -0,0 +1,75 @@ +from decimal import Decimal + +import nonebot +from nonebot.drivers import Driver +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def _(): + """ + 导入内置的三个商品 + """ + + @shop_register( + name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), + price=(30, 150, 250), + des=( + "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", + ), + load_status=bool(Config.get_config("shop", "IMPORT_DEFAULT_SHOP_GOODS")), + icon=( + "favorability_card_1.png", + "favorability_card_2.png", + "favorability_card_3.png", + ), + **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore + ) + async def _(session: EventSession, user_id: int, group_id: int, prob: float): + user_console, _ = await UserConsole.get_or_create( + user_id=session.id1, + defaults={ + "uid": await UserConsole.get_new_uid(), + "platform": session.platform, + }, + ) + user, _ = await SignUser.get_or_create( + user_id=user_id, + defaults={"platform": session.platform, "user_console": user_console}, + ) + user.add_probability = Decimal(prob) + await user.save(update_fields=["add_probability"]) + + @shop_register( + name="测试道具A", + price=99, + des="随便侧而出", + load_status=False, + icon="sword.png", + ) + async def _(user_id: int, group_id: int): + print(user_id, group_id, "使用测试道具") + + @shop_register.before_handle(name="测试道具A", load_status=False) + async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用前函数(before handle)") + + @shop_register.before_handle(name="测试道具A", load_status=False) + async def _(user_id: int, group_id: int): + print(user_id, group_id, "第二个使用前函数(before handle)222") + raise NotMeetUseConditionsException( + "太笨了!" + ) # 抛出异常,阻断使用,并返回信息 + + @shop_register.after_handle(name="测试道具A", load_status=False) + async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用后函数(after handle)") diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py new file mode 100644 index 00000000..950a7aab --- /dev/null +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -0,0 +1,326 @@ +import os +import random +from datetime import datetime +from io import BytesIO +from pathlib import Path + +import nonebot +import pytz +from nonebot.drivers import Driver + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.sign_log import SignLog +from zhenxun.models.sign_user import SignUser +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import get_user_avatar + +from .config import ( + SIGN_BACKGROUND_PATH, + SIGN_BORDER_PATH, + SIGN_RESOURCE_PATH, + SIGN_TODAY_CARD_PATH, + level2attitude, + lik2level, + lik2relation, +) + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def init_image(): + SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) + SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) + await generate_progress_bar_pic() + clear_sign_data_pic() + + +async def get_card( + user: SignUser, + nickname: str, + add_impression: float, + gold: int | None, + gift: str, + is_double: bool = False, + is_card_view: bool = False, +) -> Path: + """获取好感度卡片 + + 参数: + user: SignUser + nickname: 用户昵称 + impression: 新增的好感度 + gold: 金币 + gift: 礼物 + is_double: 是否触发双倍. + is_card_view: 是否展示好感度卡片. + + 返回: + Path: 卡片路径 + """ + user_id = user.user_id + date = datetime.now().date() + _type = "view" if is_card_view else "sign" + file_name = f"{user_id}_{_type}_{date}.png" + view_name = f"{user_id}_view_{date}.png" + card_file = Path(SIGN_TODAY_CARD_PATH) / file_name + if card_file.exists(): + return IMAGE_PATH / "sign" / "today_card" / file_name + else: + if add_impression == -1: + card_file = Path(SIGN_TODAY_CARD_PATH) / view_name + if card_file.exists(): + return card_file + is_card_view = True + return await _generate_card( + user, nickname, add_impression, gold, gift, is_double, is_card_view + ) + + +async def _generate_card( + user: SignUser, + nickname: str, + impression: float, + gold: int | None, + gift: str, + is_double: bool = False, + is_card_view: bool = False, +) -> Path: + """生成签到卡片 + + 参数: + user: SignUser + nickname: 用户昵称 + impression: 新增的好感度 + gold: 金币 + gift: 礼物 + is_double: 是否触发双倍. + is_card_view: 是否展示好感度卡片. + + 返回: + Path: 卡片路径 + """ + ava_bk = BuildImage(140, 140, (255, 255, 255, 0)) + ava_border = BuildImage( + 140, + 140, + background=SIGN_BORDER_PATH / "ava_border_01.png", + ) + if user.platform == "qq" and (byt := await get_user_avatar(user.user_id)): + ava = BuildImage(107, 107, background=BytesIO(byt)) + else: + ava = BuildImage(107, 107, (0, 0, 0)) + await ava.circle() + await ava_bk.paste(ava, (19, 18)) + await ava_bk.paste(ava_border, center_type="center") + add_impression = impression + impression = float(user.impression) + info_img = BuildImage(250, 150, color=(255, 255, 255, 0), font_size=15) + level, next_impression, previous_impression = get_level_and_next_impression( + impression + ) + interpolation = next_impression - impression + if level == "9": + level = "8" + interpolation = 0 + await info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]") + await info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}") + await info_img.text((0, 40), f"· 距离升级还差 {interpolation:.2f} 好感度") + + bar_bk = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png") + bar = BuildImage(220, 20, background=SIGN_RESOURCE_PATH / "bar.png") + ratio = 1 - (next_impression - user.impression) / ( + next_impression - previous_impression + ) + if next_impression == 0: + ratio = 0 + await bar.resize(width=int(bar.width * ratio) or bar.width, height=bar.height) + await bar_bk.paste(bar) + font_size = 30 + if "好感度双倍加持卡" in gift: + font_size = 20 + gift_border = BuildImage( + 270, + 100, + background=SIGN_BORDER_PATH / "gift_border_02.png", + font_size=font_size, + ) + await gift_border.text((0, 0), gift, center_type="center") + + bk = BuildImage( + 876, + 424, + background=SIGN_BACKGROUND_PATH + / random.choice(os.listdir(SIGN_BACKGROUND_PATH)), + font_size=25, + ) + A = BuildImage(876, 274, background=SIGN_RESOURCE_PATH / "white.png") + line = BuildImage(2, 180, color="black") + await A.transparent(2) + await A.paste(ava_bk, (25, 80)) + await A.paste(line, (200, 70)) + nickname_img = await BuildImage.build_text_image( + nickname, size=50, font_color=(255, 255, 255) + ) + user_console = await user.user_console.first() + if user_console and user_console.uid: + uid = f"{user_console.uid}".rjust(12, "0") + uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] + else: + uid = "XXXX XXXX XXXX" + uid_img = await BuildImage.build_text_image( + f"UID: {uid}", size=30, font_color=(255, 255, 255) + ) + sign_count = await SignLog.filter(user_id=user.user_id).count() + sign_day_img = await BuildImage.build_text_image( + f"{sign_count}", size=40, font_color=(211, 64, 33) + ) + lik_text1_img = await BuildImage.build_text_image("当前", size=20) + lik_text2_img = await BuildImage.build_text_image( + f"好感度:{user.impression:.2f}", size=30 + ) + watermark = await BuildImage.build_text_image( + f"{NICKNAME}@{datetime.now().year}", size=15, font_color=(155, 155, 155) + ) + today_data = BuildImage(300, 300, color=(255, 255, 255, 0), font_size=20) + if is_card_view: + today_sign_text_img = await BuildImage.build_text_image("", size=30) + value_list = ( + await SignUser.annotate() + .order_by("impression") + .values_list("user_id", flat=True) + ) + index = value_list.index(user.user_id) + 1 # type: ignore + rank_img = await BuildImage.build_text_image( + f"* 好感度排名第 {index} 位", size=30 + ) + await A.paste(rank_img, ((A.width - rank_img.width - 32), 20)) + last_log = ( + await SignLog.filter(user_id=user.user_id).order_by("create_time").first() + ) + last_date = "从未" + if last_log: + last_date = last_log.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ).date() + await today_data.text( + (0, 0), + f"上次签到日期:{last_date}", + ) + await today_data.text((0, 25), f"总金币:{gold}") + default_setu_prob = ( + Config.get_config("send_setu", "INITIAL_SETU_PROBABILITY") * 100 # type: ignore + ) + await today_data.text( + (0, 50), + f"色图概率:{(default_setu_prob + float(user.impression) if user.impression < 100 else 100):.2f}%", + ) + await today_data.text((0, 75), f"开箱次数:{(20 + int(user.impression / 3))}") + _type = "view" + else: + await A.paste(gift_border, (570, 140)) + today_sign_text_img = await BuildImage.build_text_image("今日签到", size=30) + if is_double: + await today_data.text((0, 0), f"好感度 + {add_impression / 2:.2f} × 2") + else: + await today_data.text((0, 0), f"好感度 + {add_impression:.2f}") + await today_data.text((0, 25), f"金币 + {gold}") + _type = "sign" + current_date = datetime.now() + current_datetime_str = current_date.strftime("%Y-%m-%d %a %H:%M:%S") + data = current_date.date() + data_img = await BuildImage.build_text_image( + f"时间:{current_datetime_str}", size=20 + ) + await bk.paste(nickname_img, (30, 15)) + await bk.paste(uid_img, (30, 85)) + await bk.paste(A, (0, 150)) + await bk.text((30, 167), "Accumulative check-in for") + _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45 + await bk.paste(sign_day_img, (380, 158)) + await bk.text((_x, 167), "days") + await bk.paste(data_img, (220, 370)) + await bk.paste(lik_text1_img, (220, 240)) + await bk.paste(lik_text2_img, (262, 234)) + await bk.paste(bar_bk, (225, 275)) + await bk.paste(info_img, (220, 305)) + await bk.paste(today_sign_text_img, (550, 180)) + await bk.paste(today_data, (580, 220)) + await bk.paste(watermark, (15, 400)) + await bk.save(SIGN_TODAY_CARD_PATH / f"{user.user_id}_{_type}_{data}.png") + return IMAGE_PATH / "sign" / "today_card" / f"{user.user_id}_{_type}_{data}.png" + + +async def generate_progress_bar_pic(): + """ + 初始化进度条图片 + """ + bg_2 = (254, 1, 254) + bg_1 = (0, 245, 246) + + bk = BuildImage(1000, 50) + img_x = BuildImage(50, 50, color=bg_2) + await img_x.circle() + await img_x.crop((25, 0, 50, 50)) + img_y = BuildImage(50, 50, color=bg_1) + await img_y.circle() + await img_y.crop((0, 0, 25, 50)) + A = BuildImage(950, 50) + width, height = A.size + + step_r = (bg_2[0] - bg_1[0]) / width + step_g = (bg_2[1] - bg_1[1]) / width + step_b = (bg_2[2] - bg_1[2]) / width + + for y in range(0, width): + bg_r = round(bg_1[0] + step_r * y) + bg_g = round(bg_1[1] + step_g * y) + bg_b = round(bg_1[2] + step_b * y) + for x in range(0, height): + await A.point((y, x), fill=(bg_r, bg_g, bg_b)) + await bk.paste(img_y, (0, 0)) + await bk.paste(A, (25, 0)) + await bk.paste(img_x, (975, 0)) + await bk.save(SIGN_RESOURCE_PATH / "bar.png") + + A = BuildImage(950, 50) + bk = BuildImage(1000, 50) + img_x = BuildImage(50, 50) + await img_x.circle() + await img_x.crop((25, 0, 50, 50)) + img_y = BuildImage(50, 50) + await img_y.circle() + await img_y.crop((0, 0, 25, 50)) + await bk.paste(img_y, (0, 0)) + await bk.paste(A, (25, 0)) + await bk.paste(img_x, (975, 0)) + await bk.save(SIGN_RESOURCE_PATH / "bar_white.png") + + +def get_level_and_next_impression(impression: float) -> tuple[str, int, int]: + """获取当前好感等级与下一等级的差距 + + 参数: + impression: 好感度 + + 返回: + tuple[str, int, int]: 好感度等级中文,好感度等级,下一等级好感差距 + """ + if impression == 0: + return lik2level[10], 10, 0 + keys = list(lik2level.keys()) + for i in range(len(keys)): + if impression > keys[i]: + return lik2level[keys[i]], keys[i - 1], keys[i] + return lik2level[10], 10, 0 + + +def clear_sign_data_pic(): + """ + 清空当前签到图片数据 + """ + date = datetime.now().date() + for file in os.listdir(SIGN_TODAY_CARD_PATH): + if str(date) not in file: + os.remove(SIGN_TODAY_CARD_PATH / file) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py new file mode 100644 index 00000000..271652a7 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py @@ -0,0 +1,61 @@ +from typing import Annotated + +from nonebot import on_command +from nonebot.adapters import Bot +from nonebot.params import Command +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import BroadcastManage + +__plugin_meta__ = PluginMetadata( + name="广播", + description="昭告天下!", + usage=""" + 广播 [消息] [图片] + 示例:广播 你们好! + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + configs=[ + RegisterConfig( + module="_task", + key="DEFAULT_BROADCAST", + value=True, + help="被动 广播 进群默认开关状态", + default_value=True, + type=bool, + ) + ], + tasks=[Task(module="broadcast", name="广播")], + ).dict(), +) + +_matcher = on_command("广播", priority=1, permission=SUPERUSER, block=True) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + message: UniMsg, + command: Annotated[tuple[str, ...], Command()], +): + message[0].text = message[0].text.replace(command[0], "").strip() + # await Text("正在发送..请等一下哦!").send() + count, error_count = await BroadcastManage.send(bot, message, session) + result = f"成功广播 {count} 个群组" + if error_count: + result += f"\n广播失败 {error_count} 个群组" + await Text(f"发送广播完成!\n{result}").send(reply=True) + logger.info(f"发送广播信息: {message}", "广播", session=session) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py new file mode 100644 index 00000000..967fee82 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -0,0 +1,117 @@ +import nonebot_plugin_alconna as alc +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import ( + Image, + MessageFactory, + TargetDoDoChannel, + TargetQQGroup, + Text, +) +from nonebot_plugin_session import EventSession +from pydantic import BaseModel + +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger + + +class GroupChannel(BaseModel): + + group_id: str + """群组id""" + channel_id: str | None = None + """频道id""" + + +class BroadcastManage: + + @classmethod + async def send( + cls, bot: Bot, message: UniMsg, session: EventSession + ) -> tuple[int, int]: + """发送广播消息 + + 参数: + bot: Bot + message: 消息内容 + session: Session + + 返回: + tuple[int, int]: 发送成功的群组数量, 发送失败的群组数量 + """ + message_list = [] + for msg in message: + if isinstance(msg, alc.Image) and msg.url: + message_list.append(Image(msg.url)) + elif isinstance(msg, alc.Text): + message_list.append(Text(msg.text)) + if group_list := await cls.__get_group_list(bot): + error_count = 0 + for group in group_list: + try: + if not await GroupConsole.is_block_task( + group.group_id, "broadcast", group.channel_id + ): + if isinstance(bot, (v11Bot, v12Bot)): + target = TargetQQGroup(group_id=int(group.group_id)) + elif isinstance(bot, DodoBot): + target = TargetDoDoChannel(channel_id=group.channel_id) # type: ignore + await MessageFactory(message_list).send_to(target, bot) + logger.debug( + "发送成功", + "广播", + session=session, + target=f"{group.group_id}:{group.channel_id}", + ) + except Exception as e: + error_count += 1 + logger.error( + "发送失败", + "广播", + session=session, + target=f"{group.group_id}:{group.channel_id}", + e=e, + ) + return len(group_list) - error_count, error_count + return 0, 0 + + @classmethod + async def __get_group_list(cls, bot: Bot) -> list[GroupChannel]: + """获取群组id列表 + + 参数: + bot: Bot + + 返回: + list[str]: 群组id列表 + """ + if isinstance(bot, (v11Bot, v12Bot)): + group_list = await bot.get_group_list() + return [GroupChannel(group_id=str(g["group_id"])) for g in group_list] + if isinstance(bot, DodoBot): + island_list = await bot.get_island_list() + source_id_list = [ + g.island_source_id for g in island_list if g.island_source_id + ] + channel_id_list = [] + for id in source_id_list: + channel_list = await bot.get_channel_list(island_source_id=id) + channel_id_list += [ + GroupChannel(group_id=id, channel_id=c.channel_id) + for c in channel_list + ] + return channel_id_list + if isinstance(bot, KaiheilaBot): + pass + # group_list = await bot.guild_list() + # if group_list.guilds: + # return [g.open_id for g in group_list.guilds if g.open_id] + if isinstance(bot, DiscordBot): + # TODO: discord获取群组列表 + pass + return [] diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py index 9e6e79d1..fe17f8f3 100644 --- a/zhenxun/builtin_plugins/superuser/fg_manage.py +++ b/zhenxun/builtin_plugins/superuser/fg_manage.py @@ -1,21 +1,9 @@ - - from nonebot.adapters import Bot from nonebot.adapters.kaiheila.exception import ApiNotAvailable from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import ( - Alconna, - AlconnaMatch, - Arparma, - Match, - Query, - Subcommand, - UniMessage, - on_alconna, - store_true, -) +from nonebot_plugin_alconna import Alconna, on_alconna from nonebot_plugin_alconna.matcher import AlconnaMatcher from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession diff --git a/zhenxun/builtin_plugins/superuser/super_help.py b/zhenxun/builtin_plugins/superuser/super_help.py new file mode 100644 index 00000000..a47943d7 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/super_help.py @@ -0,0 +1,161 @@ +import nonebot +from arclet.alconna import Args, Option +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_alconna.matcher import AlconnaMatcher +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.exception import EmptyError +from zhenxun.utils.image_utils import ( + BuildImage, + build_sort_image, + group_image, + text2image, +) +from zhenxun.utils.rules import admin_check, ensure_group + +__plugin_meta__ = PluginMetadata( + name="超级用户帮助", + description="超级用户帮助", + usage=""" + 超级用户帮助 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + +_matcher = on_alconna( + Alconna("超级用户帮助"), + permission=SUPERUSER, + priority=5, + block=True, +) + + +SUPERUSER_HELP_IMAGE = IMAGE_PATH / "SUPERUSER_HELP.png" +if SUPERUSER_HELP_IMAGE.exists(): + SUPERUSER_HELP_IMAGE.unlink() + + +async def build_help() -> BuildImage: + """构造超级用户帮助图片 + + 异常: + EmptyError: 超级用户帮助为空 + + 返回: + BuildImage: 超级用户帮助图片 + """ + plugin_list = await PluginInfo.filter(plugin_type=PluginType.SUPERUSER).all() + data_list = [] + for plugin in plugin_list: + if _plugin := nonebot.get_plugin_by_module_name(plugin.module_path): + if _plugin.metadata: + data_list.append({"plugin": plugin, "metadata": _plugin.metadata}) + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + image_list = [] + for data in data_list: + plugin = data["plugin"] + metadata = data["metadata"] + try: + usage = None + description = None + if metadata.usage: + usage = await text2image( + metadata.usage, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + if metadata.description: + description = await text2image( + metadata.description, + padding=5, + color=(255, 255, 255), + font_color=(0, 0, 0), + ) + width = 0 + height = 100 + if usage: + width = usage.width + height += usage.height + if description and description.width > width: + width = description.width + height += description.height + font_width, font_height = BuildImage.get_text_size( + plugin.name + f"[{plugin.level}]", font + ) + if font_width > width: + width = font_width + A = BuildImage(width + 30, height + 120, "#EAEDF2") + await A.text((15, 10), plugin.name + f"[{plugin.level}]") + await A.text((15, 70), "简介:") + if not description: + description = BuildImage(A.width - 30, 30, (255, 255, 255)) + await description.circle_corner(10) + await A.paste(description, (15, 100)) + if not usage: + usage = BuildImage(A.width - 30, 30, (255, 255, 255)) + await usage.circle_corner(10) + await A.text((15, description.height + 115), "用法:") + await A.paste(usage, (15, description.height + 145)) + await A.circle_corner(10) + image_list.append(A) + except Exception as e: + logger.warning( + f"获取超级用户管理员插件 {plugin.module}: {plugin.name} 设置失败...", + "超级用户帮助", + e=e, + ) + if task_list := await TaskInfo.all(): + task_str = "\n".join([task.name for task in task_list]) + task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str + task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) + await task_image.circle_corner(10) + A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") + await A.text((25, 10), "被动技能") + await A.paste(task_image, (25, 50)) + await A.circle_corner(10) + image_list.append(A) + if not image_list: + raise EmptyError() + image_group, _ = group_image(image_list) + A = await build_sort_image(image_group, color=(255, 255, 255), padding_top=160) + text = await BuildImage.build_text_image( + "超级用户帮助", + size=40, + ) + tip = await BuildImage.build_text_image( + "注: ‘*’ 代表可有多个相同参数 ‘?’ 代表可省略该参数", size=25, font_color="red" + ) + await A.paste(text, (50, 30)) + await A.paste(tip, (50, 90)) + await A.save(SUPERUSER_HELP_IMAGE) + return BuildImage(1, 1) + + +@_matcher.handle() +async def _( + session: EventSession, + matcher: AlconnaMatcher, + arparma: Arparma, +): + if not SUPERUSER_HELP_IMAGE.exists(): + try: + await build_help() + except EmptyError: + await Text("超级用户帮助为空").finish(reply=True) + await Image(SUPERUSER_HELP_IMAGE).send() + logger.info("查看超级用户帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py deleted file mode 100644 index 18cf31cb..00000000 --- a/zhenxun/builtin_plugins/superuser/update_fg_info.py +++ /dev/null @@ -1,119 +0,0 @@ -from nonebot.adapters import Bot -from nonebot.adapters.kaiheila.exception import ApiNotAvailable -from nonebot.permission import SUPERUSER -from nonebot.plugin import PluginMetadata -from nonebot.rule import to_me -from nonebot_plugin_alconna import Alconna, Arparma, At, Match, on_alconna -from nonebot_plugin_saa import Mention, MessageFactory, Text -from nonebot_plugin_session import EventSession, SessionLevel - -from zhenxun.configs.config import Config -from zhenxun.configs.utils import PluginExtraData -from zhenxun.models.friend_user import FriendUser -from zhenxun.models.group_info import GroupInfo -from zhenxun.models.level_user import LevelUser -from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType - -__plugin_meta__ = PluginMetadata( - name="更新群组/好友信息", - description="更新群组/好友信息", - usage=""" - 更新群组信息 - 更新好友信息 - """.strip(), - extra=PluginExtraData( - author="HibiKier", - version="0.1", - plugin_type=PluginType.SUPERUSER, - ).dict(), -) - - -_group_matcher = on_alconna( - Alconna( - "更新群组信息", - ), - permission=SUPERUSER, - rule=to_me(), - priority=1, - block=True, -) - -_friend_matcher = on_alconna( - Alconna( - "更新好友信息", - ), - permission=SUPERUSER, - rule=to_me(), - priority=1, - block=True, -) - -# TODO: 其他adapter的更新操作 - -@_group_matcher.handle() -async def _( - bot: Bot, - session: EventSession, - arparma: Arparma, -): - try: - gl = await bot.get_group_list() - gl = [g["group_id"] for g in gl] - num = 0 - for g in gl: - try: - group_info = await bot.get_group_info(group_id=g) - await GroupInfo.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - }, - ) - num += 1 - logger.debug( - "群聊信息更新成功", "更新群信息", session=session, target=group_info["group_id"] - ) - except Exception as e: - logger.error( - f"更新群聊信息失败", - arparma.header_result, - session=session, - target=g, - ) - await Text(f"成功更新了 {len(gl)} 个群的信息").send() - logger.info( - f"更新群聊信息完成,共更新了 {len(gl)} 个群的信息", arparma.header_result, session=session - ) - except (ApiNotAvailable, AttributeError) as e: - await Text("Api未实现...").send() - except Exception as e: - logger.error("更新好友信息发生错误", arparma.header_result, session=session, e=e) - await Text("其他未知错误...").send() - - -@_friend_matcher.assign("delete") -async def _( - bot: Bot, - session: EventSession, - arparma: Arparma, -): - num = 0 - error_list = [] - fl = await bot.get_friend_list() - for f in fl: - try: - await FriendUser.update_or_create( - user_id=str(f["user_id"]), defaults={"nickname": f["nickname"]} - ) - logger.debug(f"更新好友信息成功", "更新好友信息", session=session, target=f["user_id"]) - num += 1 - except Exception as e: - logger.error(f"更新好友信息失败", "更新好友信息", session=session, target=f["user_id"], e=e) - await Text(f"成功更新了 {num} 个好友的信息!").send() - if error_list: - await Text(f"以下好友更新失败:\n" + "\n".join(error_list)).send() - logger.info(f"更新好友信息完成,共更新了 {num} 个群的信息", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py b/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py new file mode 100644 index 00000000..2c8bcca0 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py @@ -0,0 +1,92 @@ +from nonebot.adapters import Bot +from nonebot.adapters.kaiheila.exception import ApiNotAvailable +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.friend_user import FriendUser +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import FgUpdateManage + +__plugin_meta__ = PluginMetadata( + name="更新群组/好友信息", + description="更新群组/好友信息", + usage=""" + 更新群组信息 + 更新好友信息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_group_matcher = on_alconna( + Alconna( + "更新群组信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + +_friend_matcher = on_alconna( + Alconna( + "更新好友信息", + ), + permission=SUPERUSER, + rule=to_me(), + priority=1, + block=True, +) + + +@_group_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + try: + num = await FgUpdateManage.update_group(bot, session.platform) + logger.info( + f"更新群聊信息完成,共更新了 {num} 个群组的信息!", + arparma.header_result, + session=session, + ) + await Text(f"成功更新了 {num} 个群组的信息").send() + except Exception as e: + logger.error( + "更新群组信息发生错误", arparma.header_result, session=session, e=e + ) + await Text("其他未知错误...").send() + + +@_friend_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, +): + try: + num = await FgUpdateManage.update_friend(bot, session.platform) + logger.info( + f"更新好友信息完成,共更新了 {num} 个好友的信息!", + arparma.header_result, + session=session, + ) + await Text(f"成功更新了 {num} 个好友的信息").send() + except Exception as e: + logger.error( + "更新好友信息发生错误", arparma.header_result, session=session, e=e + ) + await Text("其他未知错误...").send() diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py b/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py new file mode 100644 index 00000000..646a1136 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py @@ -0,0 +1,156 @@ +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot + +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger + + +class FgUpdateManage: + + @classmethod + async def update_group(cls, bot: Bot, platform: str) -> int: + """更新群组信息 + + 参数: + bot: Bot + platform: 平台 + + 返回: + int: 更新个数 + """ + create_list = [] + if group_list := await cls.__get_group_list(bot): + exists_group_list = await GroupConsole.all().values_list( + "group_id", "channel_id" + ) + for group in group_list: + group.platform = platform + if (group.group_id, group.channel_id) not in exists_group_list: + create_list.append(group) + logger.debug( + "群聊信息更新成功", + "更新群信息", + target=f"{group.group_id}:{group.channel_id}", + ) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + return len(create_list) + + @classmethod + async def __get_group_list(cls, bot: Bot) -> list[GroupConsole]: + """获取群组列表 + + 参数: + bot: Bot + + 返回: + list[GroupConsole]: 群组列表 + """ + if isinstance(bot, v11Bot): + group_list = await bot.get_group_list() + return [ + GroupConsole( + group_id=str(g["group_id"]), + group_name=g["group_name"], + max_member_count=g["max_member_count"], + member_count=g["member_count"], + ) + for g in group_list + ] + if isinstance(bot, v12Bot): + group_list = await bot.get_group_list() + return [ + GroupConsole( + group_id=g.group_id, # type: ignore + user_name=g.group_name, # type: ignore + ) + for g in group_list + ] + if isinstance(bot, DodoBot): + island_list = await bot.get_island_list() + source_id_list = [ + (g.island_source_id, g.island_name) + for g in island_list + if g.island_source_id + ] + group_list = [] + for id, name in source_id_list: + channel_list = await bot.get_channel_list(island_source_id=id) + group_list.append(GroupConsole(group_id=id, group_name=name)) + group_list += [ + GroupConsole( + group_id=id, group_name=c.channel_name, channel_id=c.channel_id + ) + for c in channel_list + ] + return group_list + if isinstance(bot, KaiheilaBot): + # TODO: kaiheila群组列表 + pass + if isinstance(bot, DiscordBot): + # TODO: discord群组列表 + pass + return [] + + @classmethod + async def update_friend(cls, bot: Bot, platform: str) -> int: + """更新好友信息 + + 参数: + bot: Bot + platform: 平台 + + 返回: + int: 更新个数 + """ + create_list = [] + if friend_list := await cls.__get_friend_list(bot): + user_id_list = await FriendUser.all().values_list("user_id", flat=True) + for friend in friend_list: + friend.platform = platform + if friend.user_id not in user_id_list: + create_list.append(friend) + if create_list: + await FriendUser.bulk_create(create_list, 10) + return len(create_list) + + @classmethod + async def __get_friend_list(cls, bot: Bot) -> list[FriendUser]: + """获取好友列表 + + 参数: + bot: Bot + + 返回: + list[FriendUser]: 好友列表 + """ + if isinstance(bot, v11Bot): + friend_list = await bot.get_friend_list() + return [ + FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"]) + for f in friend_list + ] + if isinstance(bot, v12Bot): + friend_list = await bot.get_friend_list() + return [ + FriendUser( + user_id=f.user_id, # type: ignore + user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore + ) + for f in friend_list + ] + if isinstance(bot, DodoBot): + # TODO: dodo好友列表 + pass + if isinstance(bot, KaiheilaBot): + # TODO: kaiheila好友列表 + pass + if isinstance(bot, DiscordBot): + # TODO: discord好友列表 + pass + return [] diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 8ed7cd1e..7ef9e0f4 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -1,6 +1,6 @@ import copy from pathlib import Path -from typing import Any, Callable, Dict, List, Type, Union +from typing import Any, Callable, Dict, Type import cattrs from pydantic import BaseModel @@ -17,7 +17,6 @@ _yaml.allow_unicode = True class RegisterConfig(BaseModel): - """ 注册配置项 """ @@ -39,7 +38,6 @@ class RegisterConfig(BaseModel): class ConfigModel(BaseModel): - """ 配置项 """ @@ -57,7 +55,6 @@ class ConfigModel(BaseModel): class ConfigGroup(BaseModel): - """ 配置组 """ @@ -72,7 +69,7 @@ class ConfigGroup(BaseModel): def get(self, c: str, default: Any = None) -> Any: cfg = self.configs.get(c) if cfg is not None: - return cfg + return cfg.value return default @@ -130,6 +127,17 @@ class PluginSetting(BaseModel): """调用插件花费金币""" +class Task(BaseBlock): + module: str + """被动技能模块名""" + name: str + """被动技能名称""" + status: bool = True + """全局开关状态""" + run_time: str | None = None + """运行时间""" + + class PluginExtraData(BaseModel): """ 插件扩展信息 @@ -139,18 +147,20 @@ class PluginExtraData(BaseModel): """作者""" version: str | None = None """版本""" - plugin_type: PluginType | None = None + plugin_type: PluginType = PluginType.NORMAL """插件类型""" menu_type: str = "功能" """菜单类型""" admin_level: int | None = None """管理员插件所需权限等级""" - configs: List[RegisterConfig] | None = None + configs: list[RegisterConfig] | None = None """插件配置""" setting: PluginSetting | None = None """插件基本配置""" - limits: List[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None + limits: list[BaseBlock | PluginCdBlock | PluginCountBlock] | None = None """插件限制""" + tasks: list[Task] | None = None + """技能被动""" class NoSuchConfig(Exception): @@ -287,7 +297,9 @@ class ConfigsManager: if not config: config = self._data[module].configs.get(f"{key} [LEVEL]") if not config: - raise NoSuchConfig(f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]") + raise NoSuchConfig( + f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]" + ) if config.arg_parser: value = config.arg_parser(value or config.default_value) else: diff --git a/zhenxun/models/bag_user.py b/zhenxun/models/bag_user.py new file mode 100644 index 00000000..7f3b0322 --- /dev/null +++ b/zhenxun/models/bag_user.py @@ -0,0 +1,160 @@ +# from typing import Dict + +# from services.db_context import Model +# from tortoise import fields + +# from .goods_info import GoodsInfo + + +# class BagUser(Model): + +# id = fields.IntField(pk=True, generated=True, auto_increment=True) +# """自增id""" +# user_id = fields.CharField(255) +# """用户id""" +# group_id = fields.CharField(255) +# """群聊id""" +# gold = fields.IntField(default=100) +# """金币数量""" +# spend_total_gold = fields.IntField(default=0) +# """花费金币总数""" +# get_total_gold = fields.IntField(default=0) +# """获取金币总数""" +# get_today_gold = fields.IntField(default=0) +# """今日获取金币""" +# spend_today_gold = fields.IntField(default=0) +# """今日获取金币""" +# property: Dict[str, int] = fields.JSONField(default={}) # type: ignore +# """道具""" + +# class Meta: +# table = "bag_users" +# table_description = "用户道具数据表" +# unique_together = ("user_id", "group_id") + +# @classmethod +# async def get_gold(cls, user_id: str, group_id: str) -> int: +# """获取当前金币 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id + +# 返回: +# int: 金币数量 +# """ +# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) +# return user.gold + +# @classmethod +# async def get_property( +# cls, user_id: str, group_id: str, only_active: bool = False +# ) -> Dict[str, int]: +# """获取当前道具 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id +# only_active: 仅仅获取主动使用的道具 + +# 返回: +# Dict[str, int]: 道具名称与数量 +# """ +# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) +# if only_active and user.property: +# data = {} +# name_list = [ +# x.goods_name +# for x in await GoodsInfo.get_all_goods() +# if not x.is_passive +# ] +# for key in [x for x in user.property if x in name_list]: +# data[key] = user.property[key] +# return data +# return user.property + +# @classmethod +# async def add_gold(cls, user_id: str, group_id: str, num: int): +# """增加金币 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id +# num: 金币数量 +# """ +# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) +# user.gold = user.gold + num +# user.get_total_gold = user.get_total_gold + num +# user.get_today_gold = user.get_today_gold + num +# await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) + +# @classmethod +# async def spend_gold(cls, user_id: str, group_id: str, num: int): +# """花费金币 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id +# num: 金币数量 +# """ +# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) +# user.gold = user.gold - num +# user.spend_total_gold = user.spend_total_gold + num +# user.spend_today_gold = user.spend_today_gold + num +# await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) + +# @classmethod +# async def add_property(cls, user_id: str, group_id: str, name: str, num: int = 1): +# """增加道具 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id +# name: 道具名称 +# num: 道具数量 +# """ +# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) +# property_ = user.property +# if property_.get(name) is None: +# property_[name] = 0 +# property_[name] += num +# user.property = property_ +# await user.save(update_fields=["property"]) + +# @classmethod +# async def delete_property( +# cls, user_id: str, group_id: str, name: str, num: int = 1 +# ) -> bool: +# """使用/删除 道具 + +# 参数: +# user_id: 用户id +# group_id: 所在群组id +# name: 道具名称 +# num: 使用个数 + +# 返回: +# bool: 是否使用/删除成功 +# """ +# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) +# property_ = user.property +# if name in property_: +# if (n := property_.get(name, 0)) < num: +# return False +# if n == num: +# del property_[name] +# else: +# property_[name] -= num +# await user.save(update_fields=["property"]) +# return True +# return False + +# @classmethod +# async def _run_script(cls): +# return [ +# "ALTER TABLE bag_users DROP props;", # 删除 props 字段 +# "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id +# "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", +# # 将user_id字段类型改为character varying(255) +# "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);", +# ] diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py new file mode 100644 index 00000000..5996e55f --- /dev/null +++ b/zhenxun/models/ban_console.py @@ -0,0 +1,172 @@ +import time + +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model +from zhenxun.services.log import logger +from zhenxun.utils.exception import UserAndGroupIsNone + + +class BanConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, null=True) + """用户id""" + group_id = fields.CharField(255, null=True) + """群组id""" + ban_level = fields.IntField() + """使用ban命令的用户等级""" + ban_time = fields.BigIntField() + """ban开始的时间""" + duration = fields.BigIntField() + """ban时长""" + operator = fields.CharField(255) + """使用Ban命令的用户""" + + class Meta: + table = "ban_console" + table_description = ".ban/b了 封禁人员/群组数据表" + + @classmethod + async def _get_data(cls, user_id: str | None, group_id: str | None) -> Self | None: + """获取数据 + + 参数: + user_id: 用户id + group_id: 群组id + + 异常: + UserAndGroupIsNone: 用户id和群组id都为空 + + 返回: + Self | None: Self + """ + if not user_id and not group_id: + raise UserAndGroupIsNone() + user = None + if user_id: + if group_id: + user = await cls.get_or_none(user_id=user_id, group_id=group_id) + else: + user = await cls.get_or_none(user_id=user_id, group_id__isnull=True) + else: + if group_id: + user = await cls.get_or_none(user_id__isnull=True, group_id=group_id) + return user + + @classmethod + async def check_ban_level( + cls, user_id: str | None, group_id: str | None, level: int + ) -> bool: + """检测ban掉目标的用户与unban用户的权限等级大小 + + 参数: + user_id: 用户id + group_id: 群组id + level: 权限等级 + + 返回: + bool: 权限判断 + """ + user = await cls._get_data(user_id, group_id) + if user: + logger.debug( + f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", + target=f"{group_id}:{user_id}", + ) + return bool(user and user.ban_level >= level) + return False + + @classmethod + async def check_ban_time( + cls, user_id: str | None, group_id: str | None = None + ) -> int: + """检测用户被ban时长 + + 参数: + user_id: 用户id + + 返回: + int: ban剩余时长,-1时为永久ban,0表示未被ban + """ + logger.debug(f"获取用户ban时长", target=f"{group_id}:{user_id}") + user = await cls._get_data(user_id, group_id) + if user: + if user.duration == -1: + return -1 + _time = time.time() - (user.ban_time + user.duration) + if _time > 0: + return 0 + return int(time.time() - user.ban_time - user.duration) + return 0 + + @classmethod + async def is_ban(cls, user_id: str | None, group_id: str | None = None) -> bool: + """判断用户是否被ban + + 参数: + user_id: 用户id + + 返回: + bool: 是否被ban + """ + logger.debug(f"检测是否被ban", target=f"{group_id}:{user_id}") + if await cls.check_ban_time(user_id, group_id): + return True + else: + await cls.unban(user_id, group_id) + return False + + @classmethod + async def ban( + cls, + user_id: str | None, + group_id: str | None, + ban_level: int, + duration: int, + operator: str | None, + ): + """ban掉目标用户 + + 参数: + user_id: 用户id + group_id: 群组id + ban_level: 使用命令者的权限等级 + duration: 时长,秒 + operator: 操作者id + """ + logger.debug( + f"封禁用户,等级:{ban_level},时长: {duration}", + target=f"{group_id}:{user_id}", + ) + user = await cls._get_data(user_id, group_id) + if user: + await cls.unban(user_id, group_id) + await cls.create( + user_id=user_id, + group_id=group_id, + ban_level=ban_level, + ban_time=int(time.time()), + duration=duration, + operator=operator or 0, + ) + + @classmethod + async def unban(cls, user_id: str | None, group_id: str | None = None) -> bool: + """unban用户 + + 参数: + user_id: 用户id + group_id: 群组id + + 返回: + bool: 是否被ban + """ + user = await cls._get_data(user_id, group_id) + if user: + logger.debug("解除封禁", target=f"{group_id}:{user_id}") + await user.delete() + return True + return False diff --git a/zhenxun/models/chat_history.py b/zhenxun/models/chat_history.py new file mode 100644 index 00000000..117397ce --- /dev/null +++ b/zhenxun/models/chat_history.py @@ -0,0 +1,125 @@ +from datetime import datetime, timedelta +from typing import Literal, Tuple + +from tortoise import fields +from tortoise.functions import Count +from typing_extensions import Self + +from zhenxun.services.db_context import Model + + +class ChatHistory(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255, null=True) + """群聊id""" + text = fields.TextField(null=True) + """文本内容""" + plain_text = fields.TextField(null=True) + """纯文本""" + create_time = fields.DatetimeField(auto_now_add=True) + """创建时间""" + bot_id = fields.CharField(255, null=True) + """bot记录id""" + platform = fields.CharField(255, null=True) + """平台""" + + class Meta: + table = "chat_history" + table_description = "聊天记录数据表" + + @classmethod + async def get_group_msg_rank( + cls, + gid: str, + limit: int = 10, + order: str = "DESC", + date_scope: tuple[datetime, datetime] | None = None, + ) -> list[Self]: + """获取排行数据 + + 参数: + gid: 群号 + limit: 获取数量 + order: 排序类型,desc,des + date_scope: 日期范围 + """ + o = "-" if order == "DESC" else "" + query = cls.filter(group_id=gid) + if date_scope: + query = query.filter(create_time__range=date_scope) + return list( + await query.annotate(count=Count("user_id")) + .order_by(o + "count") + .group_by("user_id") + .limit(limit) + .values_list("user_id", "count") + ) # type: ignore + + @classmethod + async def get_group_first_msg_datetime(cls, group_id: str) -> datetime | None: + """获取群第一条记录消息时间 + + 参数: + group_id: 群组id + """ + if ( + message := await cls.filter(group_id=group_id) + .order_by("create_time") + .first() + ): + return message.create_time + + @classmethod + async def get_message( + cls, + uid: str, + gid: str, + type_: Literal["user", "group"], + msg_type: Literal["private", "group"] | None = None, + days: int | Tuple[datetime, datetime] | None = None, + ) -> list[Self]: + """获取消息查询query + + 参数: + uid: 用户id + gid: 群聊id + type_: 类型,私聊或群聊 + msg_type: 消息类型,用户或群聊 + days: 限制日期 + """ + if type_ == "user": + query = cls.filter(user_id=uid) + if msg_type == "private": + query = query.filter(group_id__isnull=True) + elif msg_type == "group": + query = query.filter(group_id__not_isnull=True) + else: + query = cls.filter(group_id=gid) + if uid: + query = query.filter(user_id=uid) + if days: + if isinstance(days, int): + query = query.filter( + create_time__gte=datetime.now() - timedelta(days=days) + ) + elif isinstance(days, tuple): + query = query.filter(create_time__range=days) + return await query.all() # type: ignore + + @classmethod + async def _run_script(cls): + return [ + "alter table chat_history alter group_id drop not null;", # 允许 group_id 为空 + "alter table chat_history alter text drop not null;", # 允许 text 为空 + "alter table chat_history alter plain_text drop not null;", # 允许 plain_text 为空 + "ALTER TABLE chat_history RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE chat_history ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE chat_history ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE chat_history ADD bot_id VARCHAR(255);", # 添加bot_id字段 + "ALTER TABLE chat_history ALTER COLUMN bot_id TYPE character varying(255);", + "ALTER TABLE chat_history ADD COLUMN platform character varying(255);", + ] diff --git a/zhenxun/models/friend_user.py b/zhenxun/models/friend_user.py index f871e389..597ce4f1 100644 --- a/zhenxun/models/friend_user.py +++ b/zhenxun/models/friend_user.py @@ -15,32 +15,32 @@ class FriendUser(Model): """用户名称""" nickname = fields.CharField(max_length=255, null=True, description="用户自定义昵称") """私聊下自定义昵称""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" class Meta: table = "friend_users" table_description = "好友信息数据表" @classmethod - async def get_user_name(cls, user_id: Union[int, str]) -> str: - """ - 说明: - 获取好友用户名称 + async def get_user_name(cls, user_id: str) -> str: + """获取好友用户名称 + 参数: - :param user_id: 用户id + user_id: 用户id """ - if user := await cls.get_or_none(user_id=str(user_id)): + if user := await cls.get_or_none(user_id=user_id): return user.user_name return "" @classmethod - async def get_user_nickname(cls, user_id: Union[int, str]) -> str: - """ - 说明: - 获取用户昵称 + async def get_user_nickname(cls, user_id: str) -> str: + """获取用户昵称 + 参数: - :param user_id: 用户id + user_id: 用户id """ - if user := await cls.get_or_none(user_id=str(user_id)): + if user := await cls.get_or_none(user_id=user_id): if user.nickname: _tmp = "" if black_word := Config.get_config("nickname", "BLACK_WORD"): @@ -50,21 +50,34 @@ class FriendUser(Model): return "" @classmethod - async def set_user_nickname(cls, user_id: Union[int, str], nickname: str): - """ - 说明: - 设置用户昵称 + async def set_user_nickname( + cls, + user_id: str, + nickname: str, + uname: str | None = None, + platform: str | None = None, + ): + """设置用户昵称 + 参数: - :param user_id: 用户id - :param nickname: 昵称 + user_id: 用户id + nickname: 昵称 + uname: 用户昵称 + platform: 平台 """ + defaults = {"nickname": nickname} + if uname is not None: + defaults["user_name"] = uname + if platform is not None: + defaults["platform"] = platform await cls.update_or_create( - user_id=str(user_id), defaults={"nickname": nickname} + user_id=user_id, + defaults=defaults, ) @classmethod - async def _run_script(cls): - await cls.raw( - "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);" - ) - # 将user_id字段类型改为character varying(255)) + def _run_script(cls): + return [ + "ALTER TABLE friend_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE friend_users ADD COLUMN platform character varying(255) default 'qq';", + ] diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py new file mode 100644 index 00000000..60f64c4b --- /dev/null +++ b/zhenxun/models/goods_info.py @@ -0,0 +1,162 @@ +import uuid +from typing import Dict + +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model + + +class GoodsInfo(Model): + __tablename__ = "goods_info" + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + uuid = fields.CharField(255, null=True) + """uuid""" + goods_name = fields.CharField(255, unique=True) + """商品名称""" + goods_price = fields.IntField() + """价格""" + goods_description = fields.TextField() + """描述""" + goods_discount = fields.FloatField(default=1) + """折扣""" + goods_limit_time = fields.BigIntField(default=0) + """限时""" + daily_limit = fields.IntField(default=0) + """每日限购""" + is_passive = fields.BooleanField(default=False) + """是否为被动道具""" + icon = fields.TextField(null=True) + """图标路径""" + + class Meta: + table = "goods_info" + table_description = "商品数据表" + + @classmethod + async def add_goods( + cls, + goods_name: str, + goods_price: int, + goods_description: str, + goods_discount: float = 1, + goods_limit_time: int = 0, + daily_limit: int = 0, + is_passive: bool = False, + icon: str | None = None, + ): + """添加商品 + + 参数: + goods_name: 商品名称 + goods_price: 商品价格 + goods_description: 商品简介 + goods_discount: 商品折扣 + goods_limit_time: 商品限时 + daily_limit: 每日购买限制 + is_passive: 是否为被动道具 + icon: 图标 + """ + if not await cls.exists(goods_name=goods_name): + await cls.create( + uuid=uuid.uuid1(), + goods_name=goods_name, + goods_price=goods_price, + goods_description=goods_description, + goods_discount=goods_discount, + goods_limit_time=goods_limit_time, + daily_limit=daily_limit, + is_passive=is_passive, + icon=icon, + ) + + @classmethod + async def delete_goods(cls, goods_name: str) -> bool: + """删除商品 + + 参数: + goods_name: 商品名称 + + 返回: + bool: 是否删除成功 + """ + if goods := await cls.get_or_none(goods_name=goods_name): + await goods.delete() + return True + return False + + @classmethod + async def update_goods( + cls, + goods_name: str, + goods_price: int | None = None, + goods_description: str | None = None, + goods_discount: float | None = None, + goods_limit_time: int | None = None, + daily_limit: int | None = None, + is_passive: bool | None = None, + icon: str | None = None, + ): + """更新商品信息 + + 参数: + goods_name: 商品名称 + goods_price: 商品价格 + goods_description: 商品简介 + goods_discount: 商品折扣 + goods_limit_time: 商品限时时间 + daily_limit: 每日次数限制 + is_passive: 是否为被动 + icon: 图标 + """ + if goods := await cls.get_or_none(goods_name=goods_name): + await cls.update_or_create( + goods_name=goods_name, + defaults={ + "goods_price": goods_price or goods.goods_price, + "goods_description": goods_description or goods.goods_description, + "goods_discount": goods_discount or goods.goods_discount, + "goods_limit_time": ( + goods_limit_time + if goods_limit_time is not None + else goods.goods_limit_time + ), + "daily_limit": ( + daily_limit if daily_limit is not None else goods.daily_limit + ), + "is_passive": ( + is_passive if is_passive is not None else goods.is_passive + ), + "icon": icon or goods.icon, + }, + ) + + @classmethod + async def get_all_goods(cls) -> list[Self]: + """ + 获得全部有序商品对象 + """ + query = await cls.all() + id_lst = [x.id for x in query] + goods_lst = [] + for _ in range(len(query)): + min_id = min(id_lst) + goods_lst.append([x for x in query if x.id == min_id][0]) + id_lst.remove(min_id) + return goods_lst + + @classmethod + async def _run_script(cls): + if goods_list := await cls.filter(uuid__isnull=True).all(): + for goods in goods_list: + goods.uuid = uuid.uuid1() + await cls.bulk_update(goods_list, ["uuid"], 10) + return [ + "ALTER TABLE goods_info ADD uuid VARCHAR(255);", + "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;", + "ALTER TABLE goods_info ADD is_passive boolean DEFAULT False;", + "ALTER TABLE goods_info ADD icon VARCHAR(255);", + "ALTER TABLE goods_info DROP daily_purchase_limit;", # 删除 daily_purchase_limit 字段 + ] diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py new file mode 100644 index 00000000..a0ff26fe --- /dev/null +++ b/zhenxun/models/group_console.py @@ -0,0 +1,54 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class GroupConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + group_id = fields.CharField(255, description="群组id") + """群聊id""" + channel_id = fields.CharField(255, null=True, description="频道id") + """频道id""" + group_name = fields.TextField(default="", description="群组名称") + """群聊名称""" + max_member_count = fields.IntField(default=0, description="最大人数") + """最大人数""" + member_count = fields.IntField(default=0, description="当前人数") + """当前人数""" + group_flag = fields.IntField(default=0, description="群认证标记") + """群认证标记""" + block_plugin = fields.TextField(default="", description="禁用插件") + """禁用插件""" + block_task = fields.TextField(default="", description="禁用插件") + """禁用插件""" + platform = fields.CharField(255, default="qq", description="所属平台") + """所属平台""" + + class Meta: + table = "group_console" + table_description = "群组信息表" + unique_together = ("group_id", "channel_id") + + @classmethod + async def is_block_task( + cls, group_id: str, task: str, channel_id: str | None = None + ) -> bool: + """查看群组是否禁用被动 + + 参数: + group_id: 群组id + task: 任务模块 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists( + group_id=group_id, channel_id=channel_id, block_task__contains=f"{task}," + ) + + @classmethod + def _run_script(cls): + return [] diff --git a/zhenxun/models/group_info copy.py b/zhenxun/models/group_info copy.py deleted file mode 100644 index 64df8c5a..00000000 --- a/zhenxun/models/group_info copy.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List, Optional - -from tortoise import fields - -from zhenxun.services.db_context import Model -from zhenxun.services.log import logger - - -class GroupInfo(Model): - group_id = fields.CharField(255, pk=True) - """群聊id""" - group_name = fields.TextField(default="") - """群聊名称""" - max_member_count = fields.IntField(default=0) - """最大人数""" - member_count = fields.IntField(default=0) - """当前人数""" - group_flag = fields.IntField(default=0) - """群认证标记""" - - class Meta: - table = "group_info" - table_description = "群聊信息表" - - @classmethod - def _run_script(cls): - return [ - "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag - "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" - # 将group_id字段类型改为character varying(255) - ] diff --git a/zhenxun/models/group_info.py b/zhenxun/models/group_info.py index e8249c44..8183bf16 100644 --- a/zhenxun/models/group_info.py +++ b/zhenxun/models/group_info.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from tortoise import fields from zhenxun.services.db_context import Model @@ -8,6 +6,8 @@ from zhenxun.services.db_context import Model class GroupInfo(Model): group_id = fields.CharField(255, pk=True, description="群组id") """群聊id""" + # channel_id = fields.CharField(255, description="群组id") + # """频道id""" group_name = fields.TextField(default="", description="群组名称") """群聊名称""" max_member_count = fields.IntField(default=0, description="最大人数") @@ -16,15 +16,36 @@ class GroupInfo(Model): """当前人数""" group_flag = fields.IntField(default=0, description="群认证标记") """群认证标记""" + block_plugin = fields.TextField(default="", description="禁用插件") + """禁用插件""" + block_task = fields.TextField(default="", description="禁用插件") + """禁用插件""" + platform = fields.CharField(255, default="qq", description="所属平台") + """所属平台""" class Meta: table = "group_info" table_description = "群聊信息表" + @classmethod + async def is_block_task(cls, group_id: str, task: str) -> bool: + """查看群组是否禁用被动 + + 参数: + group_id: 群组id + task: 任务模块 + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists(group_id=group_id, block_task__contains=f"{task},") + @classmethod def _run_script(cls): return [ "ALTER TABLE group_info ADD group_flag Integer NOT NULL DEFAULT 0;", # group_info表添加一个group_flag - "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);" - # 将group_id字段类型改为character varying(255) + "ALTER TABLE group_info ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE group_info ADD block_plugin Text NOT NULL DEFAULT '';", + "ALTER TABLE group_info ADD block_task Text NOT NULL DEFAULT '';", + "ALTER TABLE group_info ADD platform character varying(255) NOT NULL DEFAULT 'qq';", ] diff --git a/zhenxun/models/group_member_info.py b/zhenxun/models/group_member_info.py index e7bbe586..cf73f14d 100644 --- a/zhenxun/models/group_member_info.py +++ b/zhenxun/models/group_member_info.py @@ -1,9 +1,8 @@ -from datetime import datetime -from typing import List, Optional, Set, Union +from typing import Set from tortoise import fields - from zhenxun.configs.config import Config + from zhenxun.services.db_context import Model from zhenxun.services.log import logger @@ -23,6 +22,8 @@ class GroupInfoUser(Model): """群聊昵称""" uid = fields.BigIntField(null=True) """用户uid""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" class Meta: table = "group_info_users" @@ -30,59 +31,65 @@ class GroupInfoUser(Model): unique_together = ("user_id", "group_id") @classmethod - async def get_group_member_id_list(cls, group_id: Union[int, str]) -> Set[int]: - """ - 说明: - 获取该群所有用户id + async def get_group_member_id_list(cls, group_id: str) -> Set[int]: + """获取该群所有用户id + 参数: - :param group_id: 群号 + group_id: 群号 """ return set( - await cls.filter(group_id=str(group_id)).values_list("user_id", flat=True) + await cls.filter(group_id=group_id).values_list("user_id", flat=True) ) # type: ignore @classmethod async def set_user_nickname( - cls, user_id: Union[int, str], group_id: Union[int, str], nickname: str + cls, + user_id: str, + group_id: str, + nickname: str, + uname: str | None = None, + platform: str | None = None, ): - """ - 说明: - 设置群员在该群内的昵称 + """设置群员在该群内的昵称 + 参数: - :param user_id: 用户id - :param group_id: 群号 - :param nickname: 昵称 + user_id: 用户id + group_id: 群号 + nickname: 昵称 + uname: 用户昵称 + platform: 平台 """ + defaults = {"nickname": nickname} + if uname is not None: + defaults["user_name"] = uname + if platform is not None: + defaults["platform"] = platform await cls.update_or_create( - user_id=str(user_id), - group_id=str(group_id), - defaults={"nickname": nickname}, + user_id=user_id, + group_id=group_id, + defaults=defaults, ) @classmethod - async def get_user_all_group(cls, user_id: Union[int, str]) -> List[int]: - """ - 说明: - 获取该用户所在的所有群聊 + async def get_user_all_group(cls, user_id: str) -> list[int]: + """获取该用户所在的所有群聊 + 参数: - :param user_id: 用户id + user_id: 用户id """ return list( await cls.filter(user_id=str(user_id)).values_list("group_id", flat=True) ) # type: ignore @classmethod - async def get_user_nickname( - cls, user_id: Union[int, str], group_id: Union[int, str] - ) -> str: - """ - 说明: - 获取用户在该群的昵称 + async def get_user_nickname(cls, user_id: str, group_id: str) -> str: + """获取用户在该群的昵称 + 参数: - :param user_id: 用户id - :param group_id: 群号 + user_id: 用户id + group_id: 群号 """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): if user.nickname: nickname = "" if black_word := Config.get_config("nickname", "BLACK_WORD"): @@ -92,28 +99,6 @@ class GroupInfoUser(Model): return user.nickname return "" - @classmethod - async def get_group_member_uid( - cls, user_id: Union[int, str], group_id: Union[int, str] - ) -> Optional[int]: - logger.debug( - f"GroupInfoUser 尝试获取 用户[{user_id}] 群聊[{group_id}] UID" - ) - user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) - _max_uid_user, _ = await cls.get_or_create(user_id="114514", group_id="114514") - _max_uid = _max_uid_user.uid - if not user.uid: - all_user = await cls.filter(user_id=str(user_id)).all() - for x in all_user: - if x.uid: - return x.uid - user.uid = _max_uid + 1 - _max_uid_user.uid = _max_uid + 1 - await cls.bulk_update([user, _max_uid_user], ["uid"]) - logger.debug( - f"GroupInfoUser 获取 用户[{user_id}] 群聊[{group_id}] UID: {user.uid}" - ) - return user.uid @classmethod async def _run_script(cls): @@ -124,4 +109,5 @@ class GroupInfoUser(Model): "ALTER TABLE group_info_users ALTER COLUMN user_id TYPE character varying(255);", # 将user_id字段类型改为character varying(255) "ALTER TABLE group_info_users ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE group_info_users ADD COLUMN platform character varying(255) default 'qq';", ] diff --git a/zhenxun/models/level_user.py b/zhenxun/models/level_user.py index 1495a315..da229216 100644 --- a/zhenxun/models/level_user.py +++ b/zhenxun/models/level_user.py @@ -24,9 +24,8 @@ class LevelUser(Model): unique_together = ("user_id", "group_id") @classmethod - async def get_user_level(cls, user_id: int | str, group_id: int | str) -> int: - """ - 获取用户在群内的等级 + async def get_user_level(cls, user_id: str, group_id: str | None) -> int: + """获取用户在群内的等级 参数: user_id: 用户id @@ -35,20 +34,21 @@ class LevelUser(Model): 返回: int: 权限等级 """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + if not group_id: + return 0 + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): return user.user_level return 0 @classmethod async def set_level( cls, - user_id: int | str, - group_id: int | str, + user_id: str, + group_id: str, level: int, group_flag: int = 0, ): - """ - 设置用户在群内的权限 + """设置用户在群内的权限 参数: user_id: 用户id @@ -57,8 +57,8 @@ class LevelUser(Model): group_flag: 是否被自动更新刷新权限 0:是, 1:否. """ await cls.update_or_create( - user_id=str(user_id), - group_id=str(group_id), + user_id=user_id, + group_id=group_id, defaults={ "user_level": level, "group_flag": group_flag, @@ -66,9 +66,8 @@ class LevelUser(Model): ) @classmethod - async def delete_level(cls, user_id: int | str, group_id: int | str) -> bool: - """ - 删除用户权限 + async def delete_level(cls, user_id: str, group_id: str) -> bool: + """删除用户权限 参数: user_id: 用户id @@ -77,17 +76,14 @@ class LevelUser(Model): 返回: bool: 是否含有用户权限 """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): await user.delete() return True return False @classmethod - async def check_level( - cls, user_id: int | str, group_id: int | str, level: int - ) -> bool: - """ - 检查用户权限等级是否大于 level + async def check_level(cls, user_id: str, group_id: str, level: int) -> bool: + """检查用户权限等级是否大于 level 参数: user_id: 用户id @@ -98,20 +94,17 @@ class LevelUser(Model): bool: 是否大于level """ if group_id: - if user := await cls.get_or_none( - user_id=str(user_id), group_id=str(group_id) - ): + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): return user.user_level >= level else: - user_list = await cls.filter(user_id=str(user_id)).all() + user_list = await cls.filter(user_id=user_id).all() user = max(user_list, key=lambda x: x.user_level) return user.user_level >= level return False @classmethod - async def is_group_flag(cls, user_id: int | str, group_id: int | str) -> bool: - """ - 检测是否会被自动更新刷新权限 + async def is_group_flag(cls, user_id: str, group_id: str) -> bool: + """检测是否会被自动更新刷新权限 参数: user_id: 用户id @@ -120,7 +113,7 @@ class LevelUser(Model): 返回: bool: 是否会被自动更新权限刷新 """ - if user := await cls.get_or_none(user_id=str(user_id), group_id=str(group_id)): + if user := await cls.get_or_none(user_id=user_id, group_id=group_id): return user.group_flag == 1 return False diff --git a/zhenxun/models/plugin_info.py b/zhenxun/models/plugin_info.py index 15eb2a0a..0eaea950 100644 --- a/zhenxun/models/plugin_info.py +++ b/zhenxun/models/plugin_info.py @@ -17,7 +17,7 @@ class PluginInfo(Model): """插件名称""" status = fields.BooleanField(default=True, description="全局开关状态") """全局开关状态""" - block_type = fields.CharEnumField( + block_type: BlockType | None = fields.CharEnumField( BlockType, default=None, null=True, description="禁用类型" ) """禁用类型""" diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py index 89247886..9bf4259f 100644 --- a/zhenxun/models/plugin_limit.py +++ b/zhenxun/models/plugin_limit.py @@ -22,6 +22,7 @@ class PluginLimit(Model): on_delete=fields.CASCADE, description="所属插件", ) + """所属插件""" limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") """限制类型""" watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") diff --git a/zhenxun/models/sign_log.py b/zhenxun/models/sign_log.py new file mode 100644 index 00000000..fffdee2c --- /dev/null +++ b/zhenxun/models/sign_log.py @@ -0,0 +1,26 @@ +from datetime import datetime +from typing import List, Literal, Optional, Tuple, Union + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class SignLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + impression = fields.DecimalField(10, 3, default=0, description="好感度") + """好感度""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + bot_id = fields.CharField(255, null=True, description="botId") + """bot记录id""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "sign_log" + table_description = "用户签到记录表" diff --git a/zhenxun/models/sign_user.py b/zhenxun/models/sign_user.py new file mode 100644 index 00000000..eb25b6cb --- /dev/null +++ b/zhenxun/models/sign_user.py @@ -0,0 +1,72 @@ +from tortoise import fields +from typing_extensions import Self + +from zhenxun.services.db_context import Model + +from .sign_log import SignLog +from .user_console import UserConsole + + +class SignUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + sign_count = fields.IntField(default=0, description="签到次数") + """签到次数""" + impression = fields.DecimalField(10, 3, default=0, description="好感度") + """好感度""" + user_console: fields.OneToOneRelation[UserConsole] = fields.OneToOneField( + "models.UserConsole", related_name="user_console", description="用户数据" + ) + """用户数据""" + add_probability = fields.DecimalField( + 10, 3, default=0, description="双倍签到增加概率" + ) + """双倍签到增加概率""" + specify_probability = fields.DecimalField( + 10, 3, default=0, description="指定双倍概率" + ) + """使用指定双倍概率""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "sign_users" + table_description = "用户签到数据表" + + @classmethod + async def sign( + cls, + user_id: str | Self, + impression: float, + bot_id: str | None = None, + platform: str | None = None, + ) -> Self: + """签到 + + 参数: + user_id: 用户id + impression: 好感度 + bot_id: bot Id + platform: 平台 + """ + if isinstance(user_id, SignUser): + user = user_id + else: + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform} + ) + user.impression = float(user.impression) + impression + user.add_probability = 0 + user.specify_probability = 0 + user.sign_count += 1 + await user.save() + await SignLog.create( + user_id=user.user_id, + impression=impression, + bot_id=bot_id, + platform=platform, + ) + return user diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py new file mode 100644 index 00000000..7e02a3d0 --- /dev/null +++ b/zhenxun/models/task_info.py @@ -0,0 +1,22 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class TaskInfo(Model): + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + module = fields.CharField(255, description="被动技能模块名") + """被动技能模块名""" + name = fields.CharField(255, description="被动技能名称") + """被动技能名称""" + status = fields.BooleanField(default=True, description="全局开关状态") + """全局开关状态""" + run_time = fields.CharField(255, null=True, description="运行时间") + """运行时间""" + run_count = fields.IntField(default=0, description="运行次数") + """运行次数""" + + class Meta: + table = "task_info" + table_description = "被动技能基本信息" diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py new file mode 100644 index 00000000..59b643f4 --- /dev/null +++ b/zhenxun/models/user_console.py @@ -0,0 +1,79 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import GoldHandle + +from .user_gold_log import UserGoldLog + + +class UserConsole(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + uid = fields.IntField(description="UID") + """UID""" + gold = fields.IntField(default=100, description="金币数量") + """金币数量""" + sign = fields.ReverseRelation["SignUser"] # type: ignore + """好感度""" + props: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_console" + table_description = "用户数据表" + + @classmethod + async def get_new_uid(cls): + if user := await cls.annotate().order_by("uid").first(): + return user.uid + 1 + return 1 + + @classmethod + async def add_gold( + cls, user_id: str, gold: int, source: str, platform: str | None = None + ): + """添加金币 + + 参数: + user_id: 用户id + gold: 金币 + source: 来源 + platform: 平台. + """ + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + ) + user.gold += gold + await user.save(update_fields=["gold"]) + await UserGoldLog.create( + user_id=user_id, gold=gold, handle=GoldHandle.GET, source=source + ) + + @classmethod + async def add_props( + cls, user_id: str, goods_uuid: str, num: int = 1, platform: str | None = None + ): + """添加道具 + + 参数: + user_id: 用户id + goods_uuid: 道具uuid + num: 道具数量. + platform: 平台. + """ + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + ) + if goods_uuid not in user.props: + user.props[goods_uuid] = 0 + user.props[goods_uuid] += num + await user.save(update_fields=["props"]) diff --git a/zhenxun/models/user_gold_log.py b/zhenxun/models/user_gold_log.py new file mode 100644 index 00000000..30e83242 --- /dev/null +++ b/zhenxun/models/user_gold_log.py @@ -0,0 +1,24 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import GoldHandle + + +class UserGoldLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + gold = fields.IntField(description="金币") + """金币""" + handle = fields.CharEnumField(GoldHandle, default=None, description="道具处理类型") + """金币处理类型""" + source = fields.CharField(255, null=True, description="来源插件") + """来源插件""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_gold_log" + table_description = "用户金币记录表" diff --git a/zhenxun/models/user_props.py b/zhenxun/models/user_props.py new file mode 100644 index 00000000..3e3a5e2b --- /dev/null +++ b/zhenxun/models/user_props.py @@ -0,0 +1,25 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model + +from .sign_user import SignUser + + +class UserProps(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, unique=True, description="用户id") + """用户id""" + name = fields.CharField(255, description="道具名称") + """道具名称""" + property: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" + platform = fields.CharField(255, null=True) + """平台""" + + class Meta: + table = "user_props" + table_description = "用户道具表" diff --git a/zhenxun/models/user_props_log.py b/zhenxun/models/user_props_log.py new file mode 100644 index 00000000..16ae405c --- /dev/null +++ b/zhenxun/models/user_props_log.py @@ -0,0 +1,30 @@ +from typing import Dict + +from tortoise import fields + +from zhenxun.services.db_context import Model +from zhenxun.utils.enum import PropHandle + +from .sign_user import SignUser + + +class UserPropsLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + uuid = fields.CharField(255, description="道具uuid") + """道具uuid""" + num = fields.IntField(null=True, description="道具金币") + """数量""" + gold = fields.IntField(null=True, description="道具金币") + """道具金币""" + handle = fields.CharEnumField(PropHandle, default=None, description="道具处理类型") + """道具处理类型""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + + class Meta: + table = "user_props_log" + table_description = "用户道具记录表" diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index a0f9c7dc..3e2e4649 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -1,10 +1,7 @@ -from typing import List - from nonebot.utils import is_coroutine_callable from tortoise import Tortoise, fields from tortoise.connection import connections from tortoise.models import Model as Model_ -from tortoise.queryset import RawSQLQuery from zhenxun.configs.config import ( address, @@ -18,7 +15,7 @@ from zhenxun.configs.config import ( from .log import logger -MODELS: List[str] = [] +MODELS: list[str] = [] SCRIPT_METHOD = [] @@ -60,12 +57,14 @@ async def init(): modules={"models": MODELS}, # timezone="Asia/Shanghai" ) - await Tortoise.generate_schemas() logger.info(f"Database loaded successfully!") # except Exception as e: # raise Exception(f"数据库连接错误... {type(e)}: {e}") if SCRIPT_METHOD: - logger.debug(f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个...") + db = Tortoise.get_connection("default") + logger.debug( + f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..." + ) sql_list = [] for module, func in SCRIPT_METHOD: try: @@ -75,15 +74,16 @@ async def init(): sql = func() if sql: sql_list += sql - except Exception as e: logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) for sql in sql_list: logger.debug(f"执行SQL: {sql}") try: - await TestSQL.raw(sql) + await db.execute_query_dict(sql) + # await TestSQL.raw(sql) except Exception as e: logger.debug(f"执行SQL: {sql} 错误...", e=e) + await Tortoise.generate_schemas() async def disconnect(): diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py index 8220ca01..e7bfed4a 100644 --- a/zhenxun/services/log.py +++ b/zhenxun/services/log.py @@ -40,6 +40,7 @@ class logger: TEMPLATE_USER = "用户[{}] " TEMPLATE_GROUP = "群聊[{}] " TEMPLATE_COMMAND = "CMD[{}] " + TEMPLATE_PLATFORM = "平台[{}] " TEMPLATE_TARGET = "[Target]([{}]) " SUCCESS_TEMPLATE = "[{}]: {} | 参数[{}] 返回: [{}]" @@ -59,8 +60,8 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, - ): - ... + platform: str | None = None, + ): ... @overload @classmethod @@ -71,8 +72,8 @@ class logger: *, session: Session | None = None, target: Any = None, - ): - ... + platform: str | None = None, + ): ... @classmethod def info( @@ -84,16 +85,20 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, ): user_id: str | None = session # type: ignore group_id = None if type(session) == Session: user_id = session.id1 adapter = session.bot_type - if session.id3 or session.id2: + if session.id3: group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform template = cls.__parser_template( - info, command, user_id, group_id, adapter, target + info, command, user_id, group_id, adapter, target, platform ) logger_.opt(colors=True).info(template) @@ -123,9 +128,9 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @overload @classmethod @@ -137,9 +142,9 @@ class logger: session: Session | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @classmethod def warning( @@ -151,6 +156,7 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, ): user_id: str | None = session # type: ignore @@ -158,10 +164,13 @@ class logger: if type(session) == Session: user_id = session.id1 adapter = session.bot_type - if session.id3 or session.id2: + if session.id3: group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform template = cls.__parser_template( - info, command, user_id, group_id, adapter, target + info, command, user_id, group_id, adapter, target, platform ) if e: template += f" || 错误{type(e)}: {e}" @@ -178,9 +187,9 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @overload @classmethod @@ -191,9 +200,9 @@ class logger: *, session: Session | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @classmethod def error( @@ -205,6 +214,7 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, ): user_id: str | None = session # type: ignore @@ -212,10 +222,13 @@ class logger: if type(session) == Session: user_id = session.id1 adapter = session.bot_type - if session.id3 or session.id2: + if session.id3: group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform template = cls.__parser_template( - info, command, user_id, group_id, adapter, target + info, command, user_id, group_id, adapter, target, platform ) if e: template += f" || 错误 {type(e)}: {e}" @@ -232,9 +245,9 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @overload @classmethod @@ -245,9 +258,9 @@ class logger: *, session: Session | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, - ): - ... + ): ... @classmethod def debug( @@ -259,6 +272,7 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, e: Exception | None = None, ): user_id: str | None = session # type: ignore @@ -266,10 +280,13 @@ class logger: if type(session) == Session: user_id = session.id1 adapter = session.bot_type - if session.id3 or session.id2: + if session.id3: group_id = f"{session.id3}:{session.id2}" + elif session.id2: + group_id = f"{session.id2}" + platform = platform or session.platform template = cls.__parser_template( - info, command, user_id, group_id, adapter, target + info, command, user_id, group_id, adapter, target, platform ) if e: template += f" || 错误 {type(e)}: {e}" @@ -284,12 +301,16 @@ class logger: group_id: int | str | None = None, adapter: str | None = None, target: Any = None, + platform: str | None = None, ) -> str: arg_list = [] template = "" if adapter is not None: template += cls.TEMPLATE_ADAPTER arg_list.append(adapter) + if platform is not None: + template += cls.TEMPLATE_PLATFORM + arg_list.append(platform) if group_id is not None: template += cls.TEMPLATE_GROUP arg_list.append(group_id) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index 6649e542..bcb83dc7 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -1,8 +1,9 @@ import base64 import math +import uuid from io import BytesIO from pathlib import Path -from typing import List, Literal, Tuple, TypeAlias, overload +from typing import Literal, Tuple, TypeAlias, overload from nonebot.utils import run_sync from PIL import Image, ImageDraw, ImageFilter, ImageFont @@ -40,12 +41,13 @@ class BuildImage: self, width: int = 0, height: int = 0, - color: ColorAlias = None, + color: ColorAlias = (255, 255, 255), mode: ModeType = "RGBA", font: str | Path | FreeTypeFont = "HYWenHei-85W.ttf", font_size: int = 20, background: str | BytesIO | Path | None = None, ) -> None: + self.uid = uuid.uuid1() self.width = width self.height = height self.color = color @@ -72,7 +74,7 @@ class BuildImage: async def build_text_image( cls, text: str, - font: str | Path = "HYWenHei-85W.ttf", + font: str | FreeTypeFont | Path = "HYWenHei-85W.ttf", size: int = 10, font_color: str | Tuple[int, int, int] = (0, 0, 0), color: ColorAlias = None, @@ -91,12 +93,16 @@ class BuildImage: 返回: Self: Self """ - _font = cls.load_font(font, size) - width, height = cls.get_text_size(text, _font) - if type(padding) == int: + _font = None + if isinstance(font, FreeTypeFont): + _font = font + elif isinstance(font, (str, Path)): + _font = cls.load_font(font, size) + width, height = cls.get_text_size(text or "A", _font) + if isinstance(padding, int): width += padding * 2 height += padding * 2 - elif type(padding) == tuple: + elif isinstance(padding, tuple): width += padding[1] + padding[3] height += padding[0] + padding[2] markImg = cls(width, height, color) @@ -112,9 +118,9 @@ class BuildImage: row: int, space: int = 10, padding: int = 50, - color: ColorAlias = (255, 255, 255, 0), + color: ColorAlias = (255, 255, 255), background: str | BytesIO | Path | None = None, - ) -> Self | None: + ) -> Self: """自动贴图 参数: @@ -129,11 +135,15 @@ class BuildImage: Self: Self """ if not img_list: - return None + raise ValueError("贴图类别为空...") width, height = img_list[0].size background_width = width * row + space * (row - 1) + padding * 2 - column = math.ceil(len(img_list) / row) - background_height = height * column + space * (column - 1) + padding * 2 + row_count = math.ceil(len(img_list) / row) + if row_count == 1: + background_width = ( + sum([img.width for img in img_list]) + space * (row - 1) + padding * 2 + ) + background_height = height * row_count + space * (row_count - 1) + padding * 2 background_image = cls( background_width, background_height, color=color, background=background ) @@ -141,15 +151,16 @@ class BuildImage: for img in img_list: await background_image.paste(img, (_cur_width, _cur_height)) _cur_width += space + img.width - _cur_height += space + img.height if _cur_width + padding >= background_image.width: + _cur_height += space + img.height _cur_width = padding return background_image @classmethod - def load_font(cls, font: str | Path, font_size: int) -> FreeTypeFont: - """ - 加载字体 + def load_font( + cls, font: str | Path = "HYWenHei-85W.ttf", font_size: int = 10 + ) -> FreeTypeFont: + """加载字体 参数: font: 字体名称 @@ -165,19 +176,20 @@ class BuildImage: @classmethod def get_text_size( cls, text: str, font: FreeTypeFont | None = None - ) -> Tuple[int, int]: - ... + ) -> Tuple[int, int]: ... @overload @classmethod def get_text_size( cls, text: str, font: str | None = None, font_size: int = 10 - ) -> Tuple[int, int]: - ... + ) -> Tuple[int, int]: ... @classmethod def get_text_size( - cls, text: str, font: str | FreeTypeFont | None = None, font_size: int = 10 + cls, + text: str, + font: str | FreeTypeFont | None = "HYWenHei-85W.ttf", + font_size: int = 10, ) -> Tuple[int, int]: """获取该字体下文本需要的长宽 @@ -192,7 +204,7 @@ class BuildImage: _font = font if font and type(font) == str: _font = cls.load_font(font, font_size) - return _font.getsize(text) # type: ignore + return _font.getsize(str(text)) # type: ignore def getsize(self, msg: str) -> Tuple[int, int]: """ @@ -265,7 +277,10 @@ class BuildImage: _image = image.markImg if _image.width and _image.height and center_type: pos = self.__center_xy(pos, _image.width, _image.height, center_type) - self.markImg.paste(_image, pos, _image) # type: ignore + try: + self.markImg.paste(_image, pos, _image) # type: ignore + except ValueError: + self.markImg.paste(_image, pos) # type: ignore return self @run_sync @@ -335,6 +350,7 @@ class BuildImage: 异常: ValueError: 居中类型错误 """ + text = str(text) if center_type and center_type not in ["center", "height", "width"]: raise ValueError("center_type must be 'center', 'width' or 'height'") width, height = 0, 0 @@ -484,7 +500,7 @@ class BuildImage: @run_sync def polygon( self, - xy: List[Tuple[int, int]], + xy: list[Tuple[int, int]], fill: Tuple[int, int, int] = (0, 0, 0), outline: int = 1, ) -> Self: @@ -560,7 +576,7 @@ class BuildImage: def circle_corner( self, radii: int = 30, - point_list: List[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], + point_list: list[Literal["lt", "rt", "lb", "rb"]] = ["lt", "rt", "lb", "rb"], ) -> Self: """ 矩形四角变圆 @@ -654,3 +670,11 @@ class BuildImage: self.markImg = self.markImg.filter(_type) self.draw = ImageDraw.Draw(self.markImg) return self + + def tobytes(self) -> bytes: + """转换为bytes + + 返回: + bytes: bytes + """ + return self.markImg.tobytes() diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py new file mode 100644 index 00000000..3c894cab --- /dev/null +++ b/zhenxun/utils/_image_template.py @@ -0,0 +1,156 @@ +from email.mime import image +from io import BytesIO +from pathlib import Path +from typing import Any, Callable + +from nonebot.plugin import PluginMetadata +from PIL.ImageFont import FreeTypeFont +from pydantic import BaseModel + +from ._build_image import BuildImage + + +class RowStyle(BaseModel): + + font: FreeTypeFont | str | Path | None = "HYWenHei-85W.ttf" + """字体""" + font_size: int = 20 + """字体大小""" + font_color: str | tuple[int, int, int] = (0, 0, 0) + """字体颜色""" + + class Config: + arbitrary_types_allowed = True + + +class ImageTemplate: + + @classmethod + async def table_page( + cls, + head_text: str, + tip_text: str | None, + column_name: list[str], + data_list: list[list[str]], + row_space: int = 35, + column_space: int = 30, + padding: int = 5, + text_style: Callable[[str, str], RowStyle] | None = None, + ) -> BuildImage: + """表格页 + + 参数: + head_text: 标题文本. + tip_text: 标题注释. + column_name: 表头列表. + data_list: 数据列表. + row_space: 行间距. + column_space: 列间距. + padding: 文本内间距. + text_style: 文本样式. + + 返回: + BuildImage: 表格图片 + """ + table = await cls.table( + column_name, data_list, row_space, column_space, padding, text_style + ) + await table.circle_corner() + table_bk = BuildImage(table.width + 100, table.height + 50, "#EAEDF2") + await table_bk.paste(table, center_type="center") + height = table_bk.height + 200 + background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50) + await background.paste(table_bk, (0, 200)) + await background.text((0, 50), head_text, "#334762", center_type="width") + if tip_text: + text_image = await BuildImage.build_text_image(tip_text, size=22) + await background.paste(text_image, (0, 110), center_type="width") + return background + + @classmethod + async def table( + cls, + column_name: list[str], + data_list: list[list[str | tuple[Path, int, int]]], + row_space: int = 25, + column_space: int = 10, + padding: int = 5, + text_style: Callable[[str, str], RowStyle] | None = None, + ) -> BuildImage: + """表格 + + 参数: + column_name: 表头列表 + data_list: 数据列表 + row_space: 行间距. + column_space: 列间距. + padding: 文本内间距. + text_style: 文本样式. + + 返回: + BuildImage: 表格图片 + """ + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + column_num = max([len(l) for l in data_list]) + list_data = [] + column_data = [] + for i in range(len(column_name)): + c = [] + for l in data_list: + if len(l) > i: + c.append(l[i]) + else: + c.append("") + column_data.append(c) + build_data_list = [] + _, base_h = BuildImage.get_text_size("A", font) + for i, column_list in enumerate(column_data): + name_width, name_height = BuildImage.get_text_size(column_name[i], font) + _temp = {"width": name_width, "data": column_list} + for s in column_list: + if isinstance(s, tuple): + w = s[1] + else: + w, _ = BuildImage.get_text_size(s, font) + if w > _temp["width"]: + _temp["width"] = w + build_data_list.append(_temp) + column_image_list = [] + for i, data in enumerate(build_data_list): + width = data["width"] + padding * 2 + height = (base_h + row_space) * (len(data["data"]) + 1) + padding * 2 + background = BuildImage(width, height, (255, 255, 255)) + column_name_image = await BuildImage.build_text_image( + column_name[i], font, 12, "#C8CCCF" + ) + await background.paste(column_name_image, (0, 20), center_type="width") + cur_h = column_name_image.height + row_space + 20 + for item in data["data"]: + style = RowStyle(font=font) + if text_style: + style = text_style(column_name[i], item) + if isinstance(item, tuple): + """图片""" + data, width, height = item + if isinstance(data, Path): + image_ = BuildImage(width, height, background=data) + elif isinstance(data, bytes): + image_ = BuildImage(width, height, background=BytesIO(data)) + elif isinstance(data, BuildImage): + image_ = data + await background.paste(image_, (padding, cur_h)) + else: + await background.text( + (padding, cur_h), + item if item is not None else "", + style.font_color, + font=style.font, + font_size=style.font_size, + ) + cur_h += base_h + row_space + column_image_list.append(background) + height = max([bk.height for bk in column_image_list]) + width = sum([bk.width for bk in column_image_list]) + return await BuildImage.auto_paste( + column_image_list, len(column_image_list), column_space + ) diff --git a/zhenxun/utils/browser.py b/zhenxun/utils/browser.py index 88e252b0..c7d89727 100644 --- a/zhenxun/utils/browser.py +++ b/zhenxun/utils/browser.py @@ -1,14 +1,12 @@ -from typing import Optional - from nonebot import get_driver from playwright.async_api import Browser, Playwright, async_playwright -from services.log import logger +from zhenxun.services.log import logger driver = get_driver() -_playwright: Optional[Playwright] = None -_browser: Optional[Browser] = None +_playwright: Playwright | None = None +_browser: Browser | None = None @driver.on_startup diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py new file mode 100644 index 00000000..5105e4c2 --- /dev/null +++ b/zhenxun/utils/decorator/shop.py @@ -0,0 +1,204 @@ +from typing import Callable, Union, Tuple, Optional +from nonebot.adapters.onebot.v11 import MessageSegment, Message +from nonebot.plugin import require + + +class ShopRegister(dict): + def __init__(self, *args, **kwargs): + super(ShopRegister, self).__init__(*args, **kwargs) + self._data = {} + self._flag = True + + def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): + """ + 说明: + 使用前检查方法 + 参数: + :param name: 道具名称 + :param load_status: 加载状态 + """ + def register_before_handle(name_list: Tuple[str, ...], func: Callable): + if load_status: + for name_ in name_list: + if not self._data[name_]: + self._data[name_] = {} + if not self._data[name_].get('before_handle'): + self._data[name_]['before_handle'] = [] + self._data[name]['before_handle'].append(func) + _name = (name,) if isinstance(name, str) else name + return lambda func: register_before_handle(_name, func) + + def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): + """ + 说明: + 使用后执行方法 + 参数: + :param name: 道具名称 + :param load_status: 加载状态 + """ + def register_after_handle(name_list: Tuple[str, ...], func: Callable): + if load_status: + for name_ in name_list: + if not self._data[name_]: + self._data[name_] = {} + if not self._data[name_].get('after_handle'): + self._data[name_]['after_handle'] = [] + self._data[name_]['after_handle'].append(func) + _name = (name,) if isinstance(name, str) else name + return lambda func: register_after_handle(_name, func) + + def register( + self, + name: Tuple[str, ...], + price: Tuple[float, ...], + des: Tuple[str, ...], + discount: Tuple[float, ...], + limit_time: Tuple[int, ...], + load_status: Tuple[bool, ...], + daily_limit: Tuple[int, ...], + is_passive: Tuple[bool, ...], + icon: Tuple[str, ...], + **kwargs, + ): + def add_register_item(func: Callable): + if name in self._data.keys(): + raise ValueError("该商品已注册,请替换其他名称!") + for n, p, d, dd, l, s, dl, pa, i in zip( + name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon + ): + if s: + _temp_kwargs = {} + for key, value in kwargs.items(): + if key.startswith(f"{n}_"): + _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value + else: + _temp_kwargs[key] = value + temp = self._data.get(n, {}) + temp.update({ + "price": p, + "des": d, + "discount": dd, + "limit_time": l, + "daily_limit": dl, + "icon": i, + "is_passive": pa, + "func": func, + "kwargs": _temp_kwargs, + }) + self._data[n] = temp + return func + + return lambda func: add_register_item(func) + + async def load_register(self): + require("use") + require("shop_handle") + from basic_plugins.shop.use.data_source import register_use, func_manager + from basic_plugins.shop.shop_handle.data_source import register_goods + # 统一进行注册 + if self._flag: + # 只进行一次注册 + self._flag = False + for name in self._data.keys(): + await register_goods( + name, + self._data[name]["price"], + self._data[name]["des"], + self._data[name]["discount"], + self._data[name]["limit_time"], + self._data[name]["daily_limit"], + self._data[name]["is_passive"], + self._data[name]["icon"], + ) + register_use( + name, self._data[name]["func"], **self._data[name]["kwargs"] + ) + func_manager.register_use_before_handle(name, self._data[name].get('before_handle', [])) + func_manager.register_use_after_handle(name, self._data[name].get('after_handle', [])) + + def __call__( + self, + name: Union[str, Tuple[str, ...]], # 名称 + price: Union[float, Tuple[float, ...]], # 价格 + des: Union[str, Tuple[str, ...]], # 简介 + discount: Union[float, Tuple[float, ...]] = 1, # 折扣 + limit_time: Union[int, Tuple[int, ...]] = 0, # 限时 + load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态 + daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购 + is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗) + icon: Union[str, Tuple[str, ...]] = False, # 图标 + **kwargs, + ): + _tuple_list = [] + _current_len = -1 + for x in [name, price, des, discount, limit_time, load_status]: + if isinstance(x, tuple): + if _current_len == -1: + _current_len = len(x) + if _current_len != len(x): + raise ValueError( + f"注册商品 {name} 中 name,price,des,discount,limit_time,load_status,daily_limit 数量不符!" + ) + _current_len = _current_len if _current_len > -1 else 1 + _name = self.__get(name, _current_len) + _price = self.__get(price, _current_len) + _discount = self.__get(discount, _current_len) + _limit_time = self.__get(limit_time, _current_len) + _des = self.__get(des, _current_len) + _load_status = self.__get(load_status, _current_len) + _daily_limit = self.__get(daily_limit, _current_len) + _is_passive = self.__get(is_passive, _current_len) + _icon = self.__get(icon, _current_len) + return self.register( + _name, + _price, + _des, + _discount, + _limit_time, + _load_status, + _daily_limit, + _is_passive, + _icon, + **kwargs, + ) + + def __get(self, value, _current_len): + return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)]) + + def __setitem__(self, key, value): + self._data[key] = value + + def __getitem__(self, key): + return self._data[key] + + def __contains__(self, key): + return key in self._data + + def __str__(self): + return str(self._data) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + +class NotMeetUseConditionsException(Exception): + + """ + 不满足条件异常类 + """ + + def __init__(self, info: Optional[Union[str, MessageSegment, Message]]): + super().__init__(self) + self._info = info + + def get_info(self): + return self._info + + +shop_register = ShopRegister() diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 5de75bf4..32270316 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -1,15 +1,38 @@ from strenum import StrEnum +class GoldHandle(StrEnum): + """ + 金币处理 + """ + + BUY = "BUY" + """购买""" + GET = "GET" + """获取""" + + +class PropHandle(StrEnum): + """ + 道具处理 + """ + + BUY = "BUY" + """购买""" + USE = "USE" + """使用""" + + class PluginType(StrEnum): """ 插件类型 """ - SUPERUSER = "超级管理员插件" - ADMIN = "管理员插件" - NORMAL = "普通插件" - HIDDEN = "被动插件" + SUPERUSER = "SUPERUSER" + ADMIN = "ADMIN" + SUPER_AND_ADMIN = "ADMIN_SUPER" + NORMAL = "NORMAL" + HIDDEN = "HIDDEN" class BlockType(StrEnum): diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py index c901a5c9..0998e776 100644 --- a/zhenxun/utils/exception.py +++ b/zhenxun/utils/exception.py @@ -1,2 +1,14 @@ class NotFoundError(Exception): pass + + +class GroupInfoNotFound(Exception): + pass + + +class EmptyError(Exception): + pass + + +class UserAndGroupIsNone(Exception): + pass diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py new file mode 100644 index 00000000..ea9516ef --- /dev/null +++ b/zhenxun/utils/http_utils.py @@ -0,0 +1,379 @@ +import asyncio +from asyncio.exceptions import TimeoutError +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any, AsyncGenerator, Dict, Literal + +import aiofiles +import httpx +import rich +from httpx import ConnectTimeout, Response +from nonebot import require +from playwright.async_api import Page +from retrying import retry + +from zhenxun.configs.config import SYSTEM_PROXY +from zhenxun.services.log import logger +from zhenxun.utils.user_agent import get_user_agent + +from .browser import get_browser + +require("nonebot_plugin_saa") + +from nonebot_plugin_saa import Image + + +class AsyncHttpx: + + proxy = {"http://": SYSTEM_PROXY, "https://": SYSTEM_PROXY} + + @classmethod + @retry(stop_max_attempt_number=3) + async def get( + cls, + url: str, + *, + params: Dict[str, Any] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + verify: bool = True, + use_proxy: bool = True, + proxy: Dict[str, str] | None = None, + timeout: int = 30, + **kwargs, + ) -> Response: + """Get + + 参数: + url: url + params: params + headers: 请求头 + cookies: cookies + verify: verify + use_proxy: 使用默认代理 + proxy: 指定代理 + timeout: 超时时间 + """ + if not headers: + headers = get_user_agent() + _proxy = proxy if proxy else cls.proxy if use_proxy else None + async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore + return await client.get( + url, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + **kwargs, + ) + + @classmethod + async def post( + cls, + url: str, + *, + data: Dict[str, str] | None = None, + content: Any = None, + files: Any = None, + verify: bool = True, + use_proxy: bool = True, + proxy: Dict[str, str] | None = None, + json: Dict[str, Any] | None = None, + params: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, + **kwargs, + ) -> Response: + """ + 说明: + Post + 参数: + url: url + data: data + content: content + files: files + use_proxy: 是否默认代理 + proxy: 指定代理 + json: json + params: params + headers: 请求头 + cookies: cookies + timeout: 超时时间 + """ + if not headers: + headers = get_user_agent() + _proxy = proxy if proxy else cls.proxy if use_proxy else None + async with httpx.AsyncClient(proxies=_proxy, verify=verify) as client: # type: ignore + return await client.post( + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + **kwargs, + ) + + @classmethod + async def download_file( + cls, + url: str, + path: str | Path, + *, + params: Dict[str, str] | None = None, + verify: bool = True, + use_proxy: bool = True, + proxy: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, + stream: bool = False, + **kwargs, + ) -> bool: + """下载文件 + + 参数: + url: url + path: 存储路径 + params: params + verify: verify + use_proxy: 使用代理 + proxy: 指定代理 + headers: 请求头 + cookies: cookies + timeout: 超时时间 + stream: 是否使用流式下载(流式写入+进度条,适用于下载大文件) + """ + if isinstance(path, str): + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + try: + for _ in range(3): + if not stream: + try: + content = ( + await cls.get( + url, + params=params, + headers=headers, + cookies=cookies, + use_proxy=use_proxy, + proxy=proxy, + timeout=timeout, + **kwargs, + ) + ).content + async with aiofiles.open(path, "wb") as wf: + await wf.write(content) + logger.info(f"下载 {url} 成功.. Path:{path.absolute()}") + return True + except (TimeoutError, ConnectTimeout): + pass + else: + if not headers: + headers = get_user_agent() + _proxy = proxy if proxy else cls.proxy if use_proxy else None + try: + async with httpx.AsyncClient( + proxies=_proxy, verify=verify # type: ignore + ) as client: + async with client.stream( + "GET", + url, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + **kwargs, + ) as response: + logger.info( + f"开始下载 {path.name}.. Path: {path.absolute()}" + ) + async with aiofiles.open(path, "wb") as wf: + total = int(response.headers["Content-Length"]) + with rich.progress.Progress( # type: ignore + rich.progress.TextColumn(path.name), # type: ignore + "[progress.percentage]{task.percentage:>3.0f}%", # type: ignore + rich.progress.BarColumn(bar_width=None), # type: ignore + rich.progress.DownloadColumn(), # type: ignore + rich.progress.TransferSpeedColumn(), # type: ignore + ) as progress: + download_task = progress.add_task( + "Download", total=total + ) + async for chunk in response.aiter_bytes(): + await wf.write(chunk) + await wf.flush() + progress.update( + download_task, + completed=response.num_bytes_downloaded, + ) + logger.info( + f"下载 {url} 成功.. Path:{path.absolute()}" + ) + return True + except (TimeoutError, ConnectTimeout): + pass + else: + logger.error(f"下载 {url} 下载超时.. Path:{path.absolute()}") + except Exception as e: + logger.error(f"下载 {url} 错误 Path:{path.absolute()}", e=e) + return False + + @classmethod + async def gather_download_file( + cls, + url_list: list[str], + path_list: list[str | Path], + *, + limit_async_number: int | None = None, + params: Dict[str, str] | None = None, + use_proxy: bool = True, + proxy: Dict[str, str] | None = None, + headers: Dict[str, str] | None = None, + cookies: Dict[str, str] | None = None, + timeout: int = 30, + **kwargs, + ) -> list[bool]: + """分组同时下载文件 + + 参数: + url_list: url列表 + path_list: 存储路径列表 + limit_async_number: 限制同时请求数量 + params: params + use_proxy: 使用代理 + proxy: 指定代理 + headers: 请求头 + cookies: cookies + timeout: 超时时间 + """ + if n := len(url_list) != len(path_list): + raise UrlPathNumberNotEqual( + f"Url数量与Path数量不对等,Url:{len(url_list)},Path:{len(path_list)}" + ) + if limit_async_number and n > limit_async_number: + m = float(n) / limit_async_number + x = 0 + j = limit_async_number + _split_url_list = [] + _split_path_list = [] + for _ in range(int(m)): + _split_url_list.append(url_list[x:j]) + _split_path_list.append(path_list[x:j]) + x += limit_async_number + j += limit_async_number + if int(m) < m: + _split_url_list.append(url_list[j:]) + _split_path_list.append(path_list[j:]) + else: + _split_url_list = [url_list] + _split_path_list = [path_list] + tasks = [] + result_ = [] + for x, y in zip(_split_url_list, _split_path_list): + for url, path in zip(x, y): + tasks.append( + asyncio.create_task( + cls.download_file( + url, + path, + params=params, + headers=headers, + cookies=cookies, + use_proxy=use_proxy, + timeout=timeout, + proxy=proxy, + **kwargs, + ) + ) + ) + _x = await asyncio.gather(*tasks) + result_ = result_ + list(_x) + tasks.clear() + return result_ + + +class AsyncPlaywright: + @classmethod + @asynccontextmanager + async def new_page(cls, **kwargs) -> AsyncGenerator[Page, None]: + """获取一个新页面 + + 参数: + user_agent: 请求头 + """ + browser = get_browser() + ctx = await browser.new_context(**kwargs) + page = await ctx.new_page() + try: + yield page + finally: + await page.close() + await ctx.close() + + @classmethod + async def screenshot( + cls, + url: str, + path: Path | str, + element: str | list[str], + *, + wait_time: int | None = None, + viewport_size: Dict[str, int] | None = None, + wait_until: ( + Literal["domcontentloaded", "load", "networkidle"] | None + ) = "networkidle", + timeout: float | None = None, + type_: Literal["jpeg", "png"] | None = None, + user_agent: str | None = None, + **kwargs, + ) -> Image | None: + """截图,该方法仅用于简单快捷截图,复杂截图请操作 page + + 参数: + url: 网址 + path: 存储路径 + element: 元素选择 + wait_time: 等待截取超时时间 + viewport_size: 窗口大小 + wait_until: 等待类型 + timeout: 超时限制 + type_: 保存类型 + """ + if viewport_size is None: + viewport_size = dict(width=2560, height=1080) + if isinstance(path, str): + path = Path(path) + wait_time = wait_time * 1000 if wait_time else None + if isinstance(element, str): + element_list = [element] + else: + element_list = element + async with cls.new_page( + viewport=viewport_size, + user_agent=user_agent, + **kwargs, + ) as page: + await page.goto(url, timeout=timeout, wait_until=wait_until) + card = page + for e in element_list: + if not card: + return None + card = await card.wait_for_selector(e, timeout=wait_time) + if card: + await card.screenshot(path=path, timeout=timeout, type=type_) + return Image(path) + return None + + +class UrlPathNumberNotEqual(Exception): + pass + + +class BrowserIsNone(Exception): + pass diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index fe09e627..b09e9d92 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -1,2 +1,339 @@ -from ._build_image import BuildImage +import os +import random +import re +from pathlib import Path +from typing import Awaitable, Callable + +from nonebot.utils import is_coroutine_callable + +from ._build_image import BuildImage, ColorAlias from ._build_mat import BuildMat +from ._image_template import ImageTemplate, RowStyle + +# TODO: text2image 长度错误 + + +async def text2image( + text: str, + auto_parse: bool = True, + font_size: int = 20, + color: str | tuple[int, int, int] = (255, 255, 255), + font: str = "HYWenHei-85W.ttf", + font_color: str | tuple[int, int, int] = (0, 0, 0), + padding: int | tuple[int, int, int, int] = 0, + _add_height: float = 0, +) -> BuildImage: + """解析文本并转为图片 + 使用标签 + + 可选配置项 + font: str -> 特殊文本字体 + fs / font_size: int -> 特殊文本大小 + fc / font_color: Union[str, Tuple[int, int, int]] -> 特殊文本颜色 + 示例 + 在不在,HibiKi小姐, + 你最近还好吗,我非常想你,这段时间我非常不好过, + 抽卡抽不到金色,这让我很痛苦 + 参数: + text: 文本 + auto_parse: 是否自动解析,否则原样发送 + font_size: 普通字体大小 + color: 背景颜色 + font: 普通字体 + font_color: 普通字体颜色 + padding: 文本外边距,元组类型时为 (上,左,下,右) + _add_height: 由于get_size无法返回正确的高度,采用手动方式额外添加高度 + """ + if not text: + raise ValueError("文本转图片 text 不能为空...") + pw = ph = top_padding = left_padding = 0 + if padding: + if isinstance(padding, int): + pw = padding * 2 + ph = padding * 2 + top_padding = left_padding = padding + elif isinstance(padding, tuple): + pw = padding[0] + padding[2] + ph = padding[1] + padding[3] + top_padding = padding[0] + left_padding = padding[1] + _font = BuildImage.load_font(font, font_size) + if auto_parse and re.search(r"(.*)", text): + _data = [] + new_text = "" + placeholder_index = 0 + for s in text.split(""): + r = re.search(r"(.*)", s) + if r: + start, end = r.span() + if start != 0 and (t := s[:start]): + new_text += t + _data.append( + [ + (start, end), + f"[placeholder_{placeholder_index}]", + r.group(1).strip(), + r.group(2), + ] + ) + new_text += f"[placeholder_{placeholder_index}]" + placeholder_index += 1 + new_text += text.split("")[-1] + image_list = [] + current_placeholder_index = 0 + # 切分换行,每行为单张图片 + for s in new_text.split("\n"): + _tmp_text = s + img_width = 0 + img_height = BuildImage.get_text_size("正", _font)[1] + _tmp_index = current_placeholder_index + for _ in range(s.count("[placeholder_")): + placeholder = _data[_tmp_index] + if "font_size" in placeholder[2]: + r = re.search(r"font_size=['\"]?(\d+)", placeholder[2]) + if r: + w, h = BuildImage.get_text_size( + placeholder[3], font, int(r.group(1)) + ) + img_height = img_height if img_height > h else h + img_width += w + else: + img_width += BuildImage.get_text_size(placeholder[3], _font)[0] + _tmp_text = _tmp_text.replace(f"[placeholder_{_tmp_index}]", "") + _tmp_index += 1 + img_width += BuildImage.get_text_size(_tmp_text, _font)[0] + # 开始画图 + A = BuildImage( + img_width, img_height, color=color, font=font, font_size=font_size + ) + basic_font_h = A.getsize("正")[1] + current_width = 0 + # 遍历占位符 + for _ in range(s.count("[placeholder_")): + if not s.startswith(f"[placeholder_{current_placeholder_index}]"): + slice_ = s.split(f"[placeholder_{current_placeholder_index}]") + await A.text( + (current_width, A.height - basic_font_h - 1), + slice_[0], + font_color, + ) + current_width += A.getsize(slice_[0])[0] + placeholder = _data[current_placeholder_index] + # 解析配置 + _font = font + _font_size = font_size + _font_color = font_color + for e in placeholder[2].split(): + if e.startswith("font="): + _font = e.split("=")[-1] + if e.startswith("font_size=") or e.startswith("fs="): + _font_size = int(e.split("=")[-1]) + if _font_size > 1000: + _font_size = 1000 + if _font_size < 1: + _font_size = 1 + if e.startswith("font_color") or e.startswith("fc="): + _font_color = e.split("=")[-1] + text_img = await BuildImage.build_text_image( + placeholder[3], font=_font, size=_font_size, font_color=_font_color + ) + _img_h = ( + int(A.height / 2 - text_img.height / 2) + if new_text == "[placeholder_0]" + else A.height - text_img.height + ) + await A.paste(text_img, (current_width, _img_h - 1)) + current_width += text_img.width + s = s[ + s.index(f"[placeholder_{current_placeholder_index}]") + + len(f"[placeholder_{current_placeholder_index}]") : + ] + current_placeholder_index += 1 + if s: + slice_ = s.split(f"[placeholder_{current_placeholder_index}]") + await A.text((current_width, A.height - basic_font_h), slice_[0]) + current_width += A.getsize(slice_[0])[0] + await A.crop((0, 0, current_width, A.height)) + # A.show() + image_list.append(A) + height = 0 + width = 0 + for img in image_list: + height += img.h + width = width if width > img.w else img.w + width += pw + height += ph + A = BuildImage(width + left_padding, height + top_padding, color=color) + current_height = top_padding + for img in image_list: + await A.paste(img, (left_padding, current_height)) + current_height += img.h + else: + width = 0 + height = 0 + _, h = BuildImage.get_text_size("正", _font) + line_height = int(font_size / 3) + image_list = [] + for s in text.split("\n"): + w, _ = BuildImage.get_text_size(s.strip() or "正", _font) + height += h + line_height + width = width if width > w else w + image_list.append( + await BuildImage.build_text_image( + s.strip(), font, font_size, font_color + ) + ) + width += pw + height += ph + A = BuildImage( + width + left_padding, + height + top_padding + 2, + color=color, + ) + cur_h = ph + for img in image_list: + await A.paste(img, (pw, cur_h)) + cur_h += img.height + line_height + return A + + +def group_image(image_list: list[BuildImage]) -> tuple[list[list[BuildImage]], int]: + """ + 说明: + 根据图片大小进行分组 + 参数: + image_list: 排序图片列表 + """ + image_list.sort(key=lambda x: x.height, reverse=True) + max_image = max(image_list, key=lambda x: x.height) + + image_list.remove(max_image) + max_h = max_image.height + total_w = 0 + + # 图片分组 + image_group = [[max_image]] + is_use = [] + surplus_list = image_list[:] + + for image in image_list: + if image.uid not in is_use: + group = [image] + is_use.append(image.uid) + curr_h = image.height + while True: + surplus_list = [x for x in surplus_list if x.uid not in is_use] + for tmp in surplus_list: + temp_h = curr_h + tmp.height + 10 + if temp_h < max_h or abs(max_h - temp_h) < 100: + curr_h += tmp.height + 15 + is_use.append(tmp.uid) + group.append(tmp) + break + else: + break + total_w += max([x.width for x in group]) + 15 + image_group.append(group) + while surplus_list: + surplus_list = [x for x in surplus_list if x.uid not in is_use] + if not surplus_list: + break + surplus_list.sort(key=lambda x: x.height, reverse=True) + for img in surplus_list: + if img.uid not in is_use: + _w = 0 + index = -1 + for i, ig in enumerate(image_group): + if s := sum([x.height for x in ig]) > _w: + _w = s + index = i + if index != -1: + image_group[index].append(img) + is_use.append(img.uid) + + max_h = 0 + max_w = 0 + for ig in image_group: + if (_h := sum([x.height + 15 for x in ig])) > max_h: + max_h = _h + max_w += max([x.width for x in ig]) + 30 + is_use.clear() + while abs(max_h - max_w) > 200 and len(image_group) - 1 >= len(image_group[-1]): + for img in image_group[-1]: + _min_h = 999999 + _min_index = -1 + for i, ig in enumerate(image_group): + # if i not in is_use and (_h := sum([x.h for x in ig]) + img.h) > _min_h: + if (_h := sum([x.height for x in ig]) + img.height) < _min_h: + _min_h = _h + _min_index = i + is_use.append(_min_index) + image_group[_min_index].append(img) + max_w -= max([x.width for x in image_group[-1]]) - 30 + image_group.pop(-1) + max_h = max([sum([x.height + 15 for x in ig]) for ig in image_group]) + return image_group, max(max_h + 250, max_w + 70) + + +async def build_sort_image( + image_group: list[list[BuildImage]], + h: int | None = None, + padding_top: int = 200, + color: ColorAlias = ( + 255, + 255, + 255, + ), + background_path: Path | None = None, + background_handle: Callable[[BuildImage], Awaitable] | None = None, +) -> BuildImage: + """ + 说明: + 对group_image的图片进行组装 + 参数: + image_group: 分组图片列表 + h: max(宽,高),一般为group_image的返回值,有值时,图片必定为正方形 + padding_top: 图像列表与最顶层间距 + color: 背景颜色 + background_path: 背景图片文件夹路径(随机) + background_handle: 背景图额外操作 + """ + bk_file = None + if background_path: + random_bk = os.listdir(background_path) + if random_bk: + bk_file = random.choice(random_bk) + image_w = 0 + image_h = 0 + if not h: + for ig in image_group: + _w = max([x.width + 30 for x in ig]) + image_w += _w + 30 + _h = sum([x.height + 10 for x in ig]) + if _h > image_h: + image_h = _h + image_h += padding_top + else: + image_w = h + image_h = h + A = BuildImage( + image_w, + image_h, + font_size=24, + font="CJGaoDeGuo.otf", + color=color, + background=(background_path / bk_file) if background_path and bk_file else None, + ) + if background_handle: + if is_coroutine_callable(background_handle): + await background_handle(A) + else: + background_handle(A) + curr_w = 50 + for ig in image_group: + curr_h = padding_top - 20 + for img in ig: + await A.paste(img, (curr_w, curr_h)) + curr_h += img.height + 10 + curr_w += max([x.width for x in ig]) + 30 + return A diff --git a/zhenxun/utils/user_agent.py b/zhenxun/utils/user_agent.py new file mode 100644 index 00000000..3047da33 --- /dev/null +++ b/zhenxun/utils/user_agent.py @@ -0,0 +1,50 @@ +import random + +user_agent = [ + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11", + "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", + "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + "MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + "Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10", + "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13", + "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 Mobile Safari/534.1+", + "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/233.70 Safari/534.6 TouchPad/1.0", + "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)", + "UCWEB7.0.2.37/28/999", + "NOKIA5700/ UCWEB7.0.2.37/28/999", + "Openwave/ UCWEB7.0.2.37/28/999", + "Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999", + # iPhone 6: + "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25", +] + + +def get_user_agent(): + return {"User-Agent": random.choice(user_agent)} + + +def get_user_agent_str(): + return random.choice(user_agent) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 660b0327..86e8c4e4 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -1,11 +1,42 @@ import os +import time +from collections import defaultdict from pathlib import Path +from typing import Any import httpx from zhenxun.services.log import logger +class WithdrawManager: + """ + 消息撤回 + """ + + _data = {} + + @classmethod + def append(cls, message_id: str, second: int): + """添加一个撤回消息id和时间 + + 参数: + message_id: 撤回消息id + time: 延迟时间 + """ + cls._data[message_id] = second + + @classmethod + def remove(cls, message_id: str): + """删除一个数据 + + 参数: + message_id: 撤回消息id + """ + if message_id in cls._data: + del cls._data[message_id] + + class ResourceDirManager: """ 临时文件管理器 @@ -45,6 +76,69 @@ class ResourceDirManager: cls.__tree_append(path) +class CountLimiter: + """ + 次数检测工具,检测调用次数是否超过设定值 + """ + + def __init__(self, max_count: int): + self.count = defaultdict(int) + self.max_count = max_count + + def add(self, key: Any): + self.count[key] += 1 + + def check(self, key: Any) -> bool: + if self.count[key] >= self.max_count: + self.count[key] = 0 + return True + return False + + +class UserBlockLimiter: + """ + 检测用户是否正在调用命令 + """ + + def __init__(self): + self.flag_data = defaultdict(bool) + self.time = time.time() + + def set_true(self, key: Any): + self.time = time.time() + self.flag_data[key] = True + + def set_false(self, key: Any): + self.flag_data[key] = False + + def check(self, key: Any) -> bool: + if time.time() - self.time > 30: + self.set_false(key) + return False + return self.flag_data[key] + + +class FreqLimiter: + """ + 命令冷却,检测用户是否处于冷却状态 + """ + + def __init__(self, default_cd_seconds: int): + self.next_time = defaultdict(float) + self.default_cd = default_cd_seconds + + def check(self, key: Any) -> bool: + return time.time() >= self.next_time[key] + + def start_cd(self, key: Any, cd_time: int = 0): + self.next_time[key] = time.time() + ( + cd_time if cd_time > 0 else self.default_cd + ) + + def left_time(self, key: Any) -> float: + return self.next_time[key] - time.time() + + async def get_user_avatar(uid: int | str) -> bytes | None: """快捷获取用户头像 From 7b3793728adda464ed7173246893b375aeae592b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 26 Feb 2024 03:04:32 +0800 Subject: [PATCH 003/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0hook?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=85=B6=E4=BB=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + poetry.lock | 22 +- pyproject.toml | 2 +- .../admin/plugin_switch/__init__.py | 132 +++-- .../admin/plugin_switch/_data_source.py | 39 +- .../admin/plugin_switch/command.py | 94 ++++ zhenxun/builtin_plugins/help/_utils.py | 2 +- .../builtin_plugins/hooks/_auth_checker.py | 455 ++++++++++++++++++ zhenxun/builtin_plugins/hooks/auth_hook.py | 35 ++ zhenxun/builtin_plugins/init/init_plugin.py | 2 +- zhenxun/builtin_plugins/shop/_data_source.py | 12 +- .../builtin_plugins/sign_in/_data_source.py | 9 +- .../builtin_plugins/sign_in/goods_register.py | 21 +- .../builtin_plugins/superuser/group_manage.py | 154 ++++++ zhenxun/models/group_console.py | 63 +++ zhenxun/models/plugin_limit.py | 2 +- zhenxun/models/user_console.py | 57 ++- zhenxun/utils/_build_image.py | 14 +- zhenxun/utils/enum.py | 5 +- zhenxun/utils/exception.py | 24 + zhenxun/utils/rules.py | 4 +- zhenxun/utils/utils.py | 32 +- 22 files changed, 1064 insertions(+), 117 deletions(-) create mode 100644 zhenxun/builtin_plugins/admin/plugin_switch/command.py create mode 100644 zhenxun/builtin_plugins/hooks/_auth_checker.py create mode 100644 zhenxun/builtin_plugins/hooks/auth_hook.py create mode 100644 zhenxun/builtin_plugins/superuser/group_manage.py diff --git a/.vscode/settings.json b/.vscode/settings.json index b23be7bc..1a22fedc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "arclet", "Arparma", "displayname", + "flmt", "getbbox", "httpx", "kaiheila", diff --git a/poetry.lock b/poetry.lock index 58022200..0a13029c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -96,13 +96,13 @@ reference = "ali" [[package]] name = "arclet-alconna" -version = "1.7.42" +version = "1.7.44" description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." optional = false python-versions = ">=3.8" files = [ - {file = "arclet_alconna-1.7.42-py3-none-any.whl", hash = "sha256:fa78944121d4afa4e2c0247a98967ddb1e76cf63b94c8c3f4f393c52f6d23e75"}, - {file = "arclet_alconna-1.7.42.tar.gz", hash = "sha256:a5a1cca37d0c3d58607ee22485e636fa0b01d40eb43194e542b2c3d6a5d2e70b"}, + {file = "arclet_alconna-1.7.44-py3-none-any.whl", hash = "sha256:e5751a2aa854b7b2c01cac87986ad11b397986a725c9536d5f9ff81a84e85614"}, + {file = "arclet_alconna-1.7.44.tar.gz", hash = "sha256:9c8a70a3f75e8358fa9c71befd3687c8c9781a19b1d28cb53cbe08fbc36cf720"}, ] [package.dependencies] @@ -1356,20 +1356,20 @@ reference = "ali" [[package]] name = "nonebot-plugin-alconna" -version = "0.36.3" +version = "0.37.1" description = "Alconna Adapter for Nonebot" optional = false python-versions = ">=3.8" files = [ - {file = "nonebot_plugin_alconna-0.36.3-py3-none-any.whl", hash = "sha256:8f26f96c711d3adadc538ebf40d51ba2249c18fe1689bf36baed0e4d1e05246a"}, - {file = "nonebot_plugin_alconna-0.36.3.tar.gz", hash = "sha256:ed8e4f2fd845d0c3d8becdd68678c203ee76109b9104a3b1c18f63525e85c6d4"}, + {file = "nonebot_plugin_alconna-0.37.1-py3-none-any.whl", hash = "sha256:fcc46f04ac89bf43730afebd97fa46e5910bc404a9e24cab7950da58be36246d"}, + {file = "nonebot_plugin_alconna-0.37.1.tar.gz", hash = "sha256:5e9989ee7debd79d61c97aa41c88aac5fe452cc9c47f2d48b829d81d26dfe130"}, ] [package.dependencies] -arclet-alconna = ">=1.7.42,<2.0.0" -arclet-alconna-tools = ">=0.6.11,<0.7.0" -nepattern = ">=0.5.14,<0.6.0" -nonebot2 = ">=2.1.0" +arclet-alconna = ">=1.7.44" +arclet-alconna-tools = ">=0.6.11" +nepattern = ">=0.5.15" +nonebot2 = ">=2.2.0" [package.source] type = "legacy" @@ -2982,4 +2982,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "2e5c4963196533949601dff69762b6f5586056a8775419c2ee1aef0df91b016a" +content-hash = "858e616442c77d1a328e37af331056a7b870611b22247fcebfe5dbe41a3fd4f0" diff --git a/pyproject.toml b/pyproject.toml index 41f7f799..33e40319 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ url = "https://mirrors.aliyun.com/pypi/simple/" [tool.poetry.dependencies] python = "^3.10" -nonebot-plugin-alconna = "^0.36.0" playwright = "^1.41.1" nonebot-adapter-onebot = "^2.3.1" nonebot-plugin-apscheduler = "^0.3.0" @@ -33,6 +32,7 @@ retrying = "^1.3.4" aiofiles = "^23.2.1" nonebot-plugin-htmlrender = "^0.3.0" nonebot-plugin-userinfo = "^0.1.3" +nonebot-plugin-alconna = "^0.37.1" [tool.poetry.dev-dependencies] diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 42da314f..c0af3544 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -21,6 +21,7 @@ from zhenxun.utils.enum import BlockType, PluginType from zhenxun.utils.rules import admin_check, ensure_group from ._data_source import PluginManage, build_plugin, build_task +from .command import _group_status_matcher, _status_matcher base_config = Config.get("admin_bot_manage") @@ -53,60 +54,70 @@ __plugin_meta__ = PluginMetadata( ) -_status_matcher = on_alconna( - Alconna( - "switch", - Option("-t|--task", action=store_true, help_text="被动技能"), - Subcommand( - "open", - Args["name", str], - Option( - "-g|--group", - Args["group_id", str], - ), - ), - Subcommand( - "close", - Args["name", str], - Option( - "-t|--type", - Args["block_type", ["all", "a", "private", "p", "group", "g"]], - ), - Option( - "-g|--group", - Args["group_id", str], - ), - ), - ), - rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), - priority=5, - block=True, -) +# _status_matcher = on_alconna( +# Alconna( +# "switch", +# Option("-t|--task", action=store_true, help_text="被动技能"), +# Subcommand( +# "open", +# Args["name", str], +# Option( +# "-g|--group", +# Args["group_id", str], +# ), +# ), +# Subcommand( +# "close", +# Args["name", str], +# Option( +# "-t|--type", +# Args["block_type", ["all", "a", "private", "p", "group", "g"]], +# ), +# Option( +# "-g|--group", +# Args["group_id", str], +# ), +# ), +# ), +# rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), +# priority=5, +# block=True, +# ) -# TODO: shortcut +# # TODO: shortcut -_group_status_matcher = on_alconna( - Alconna("group-status", Args["status", ["sleep", "wake"]]), - rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group, - priority=5, - block=True, -) +# _group_status_matcher = on_alconna( +# Alconna("group-status", Args["status", ["sleep", "wake"]]), +# rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group, +# priority=5, +# block=True, +# ) @_status_matcher.assign("$main") async def _(bot: Bot, session: EventSession, arparma: Arparma): image = None - if arparma.find("task"): - image = await build_task(session.id3 or session.id2) - elif session.id1 in bot.config.superusers: + if session.id1 in bot.config.superusers: image = await build_plugin() if image: await Image(image.pic2bs4()).send(reply=True) - logger.info( - f"查看{'被动' if arparma.find('task') else '功能'}列表", - arparma.header_result, - session=session, - ) + logger.info( + f"查看功能列表", + arparma.header_result, + session=session, + ) + + +@_status_matcher.assign("task") +async def _(bot: Bot, session: EventSession, arparma: Arparma): + image = None + if image := await build_task(session.id3 or session.id2): + await Image(image.pic2bs4()).send(reply=True) + logger.info( + f"查看被动列表", + arparma.header_result, + session=session, + ) @_status_matcher.assign("open") @@ -122,13 +133,14 @@ async def _( await Text(result).send(reply=True) logger.info(f"开启功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: - result = await PluginManage.superuser_block(name, None, group.result) + group_id = group.result if group.available else None + result = await PluginManage.superuser_block(name, None, group_id) await Text(result).send(reply=True) logger.info( f"超级用户开启功能 {name}", arparma.header_result, session=session, - target=group.result, + target=group_id, ) @@ -146,17 +158,39 @@ async def _( await Text(result).send(reply=True) logger.info(f"关闭功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: + group_id = group.result if group.available else None _type = BlockType.ALL if block_type.available: if block_type.result in ["p", "private"]: - _type = BlockType.FRIEND + _type = BlockType.PRIVATE elif block_type.result in ["g", "group"]: _type = BlockType.GROUP - result = await PluginManage.superuser_block(name, _type, group.result) + result = await PluginManage.superuser_block(name, _type, group_id) await Text(result).send(reply=True) logger.info( f"超级用户关闭功能 {name}, 禁用类型: {_type}", arparma.header_result, session=session, - target=group.result, + target=group_id, ) + + +@_group_status_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + status: str, +): + if gid := session.id3 or session.id2: + if status == "sleep": + await PluginManage.sleep(gid) + logger.info("进行休眠", arparma.header_result, session=session) + await Text("那我先睡觉了...").finish() + else: + if PluginManage.is_wake(gid): + await Text("我还醒着呢!").finish() + await PluginManage.wake(gid) + logger.info("醒来", arparma.header_result, session=session) + await Text("呜..醒来了...").finish() + return Text("群组id为空...").send() diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index bd7fd937..8137206f 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -119,7 +119,7 @@ async def build_task(group_id: str | None) -> BuildImage: task.name, "开启" if task.module not in group.block_task else "关闭", "开启" if task.status else "关闭", - task.run_time, + task.run_time or "-", ] ) else: @@ -129,7 +129,7 @@ async def build_task(group_id: str | None) -> BuildImage: task.module, task.name, "开启" if task.status else "关闭", - task.run_time, + task.run_time or "-", ] ) return await ImageTemplate.table_page( @@ -143,6 +143,20 @@ async def build_task(group_id: str | None) -> BuildImage: class PluginManage: + @classmethod + async def is_wake(cls, group_id: str) -> bool: + if c := await GroupConsole.get_or_none(group_id=group_id): + return c.status + return False + + @classmethod + async def sleep(cls, group_id: str): + await GroupConsole.filter(group_id=group_id).update(status=False) + + @classmethod + async def wake(cls, group_id: str): + await GroupConsole.filter(group_id=group_id).update(status=True) + @classmethod async def block(cls, module: str): await PluginInfo.filter(module=module).update(status=False) @@ -191,8 +205,13 @@ class PluginManage: 返回: str: 返回信息 """ + + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) status_str = "开启" if status else "关闭" - if plugin := await PluginInfo.get_or_none(name=plugin_name): + if plugin: group, _ = await GroupConsole.get_or_create(group_id=group_id) if status: if plugin.module in group.block_plugin: @@ -200,12 +219,12 @@ class PluginManage: f"{plugin.module},", "" ) await group.save(update_fields=["block_plugin"]) - return f"已成功{status_str} {plugin_name} 功能!" + return f"已成功{status_str} {plugin.name} 功能!" else: if plugin.module not in group.block_plugin: group.block_plugin += f"{plugin.module}," await group.save(update_fields=["block_plugin"]) - return f"已成功{status_str} {plugin_name} 功能!" + return f"已成功{status_str} {plugin.name} 功能!" return f"该功能已经{status_str}了喔,不要重复{status_str}..." return "没有找到这个功能喔..." @@ -223,7 +242,11 @@ class PluginManage: 返回: str: 返回信息 """ - if plugin := await PluginInfo.get_or_none(name=plugin_name): + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) + if plugin: if group_id: if group := await GroupConsole.get_or_none(group_id=group_id): if f"super:{plugin_name}," not in group.block_plugin: @@ -238,7 +261,7 @@ class PluginManage: plugin.status = not bool(block_type) await plugin.save(update_fields=["status", "block_type"]) if not block_type: - return f"已成功将 {plugin_name} 全局启用!" + return f"已成功将 {plugin.name} 全局启用!" else: - return f"已成功将 {plugin_name} 全局关闭!" + return f"已成功将 {plugin.name} 全局关闭!" return "没有找到这个功能喔..." diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py new file mode 100644 index 00000000..ec0c8513 --- /dev/null +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -0,0 +1,94 @@ +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + Args, + Option, + Subcommand, + on_alconna, + store_true, +) + +from zhenxun.utils.rules import admin_check, ensure_group + +_status_matcher = on_alconna( + Alconna( + "switch", + Option("-t|--task", action=store_true, help_text="被动技能"), + Subcommand( + "open", + Args["name", [str, int]], + Option( + "-g|--group", + Args["group_id", str], + ), + ), + Subcommand( + "close", + Args["name", [str, int]], + Option( + "-t|--type", + Args["block_type", ["all", "a", "private", "p", "group", "g"]], + ), + Option( + "-g|--group", + Args["group_id", str], + ), + ), + ), + rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), + priority=5, + block=True, +) + +# TODO: shortcut + +_group_status_matcher = on_alconna( + Alconna("group-status", Args["status", ["sleep", "wake"]]), + rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") + & ensure_group + & to_me(), + priority=5, + block=True, +) + +_status_matcher.shortcut( + r"插件列表", + command="switch", + arguments=[], + prefix=True, +) + +_status_matcher.shortcut( + r"群被动状态", + command="switch", + arguments=["--task"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启(?P.+)", + command="switch", + arguments=["open", "{name}"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭(?P.+)", + command="switch", + arguments=["close", "{name}"], + prefix=True, +) + +_group_status_matcher.shortcut( + r"醒来", + command="group-status", + arguments=["wake"], + prefix=True, +) + +_group_status_matcher.shortcut( + r"休息吧", + command="group-status", + arguments=["sleep"], + prefix=True, +) diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py index b3f8d3a4..82aae886 100644 --- a/zhenxun/builtin_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -84,7 +84,7 @@ class HelpImageBuild: sta = 2 if not group_id and plugin.block_type in [ BlockType.ALL, - BlockType.FRIEND, + BlockType.PRIVATE, ]: sta = 2 if group_id and ( diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py new file mode 100644 index 00000000..03a33ebd --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -0,0 +1,455 @@ +from typing import Dict +from unittest import result + +from nonebot.adapters import Bot, Event +from nonebot.exception import IgnoredException +from nonebot.matcher import Matcher +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession +from pydantic import BaseModel + +from zhenxun.configs.config import Config +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.level_user import LevelUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.plugin_limit import PluginLimit +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import ( + BlockType, + GoldHandle, + LimitWatchType, + PluginLimitType, + PluginType, +) +from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter + + +class Limit(BaseModel): + + limit: PluginLimit + limiter: FreqLimiter | UserBlockLimiter | CountLimiter + + class Config: + arbitrary_types_allowed = True + + +class LimitManage: + + add_module = [] + + cd_limit: Dict[str, Limit] = {} + block_limit: Dict[str, Limit] = {} + count_limit: Dict[str, Limit] = {} + + @classmethod + def add_limit(cls, limit: PluginLimit): + """添加限制 + + 参数: + limit: PluginLimit + """ + if limit.module not in cls.add_module: + cls.add_module.append(limit.module) + if limit.limit_type == PluginLimitType.BLOCK: + cls.block_limit[limit.module] = Limit( + limit=limit, limiter=UserBlockLimiter() + ) + elif limit.limit_type == PluginLimitType.CD: + cls.cd_limit[limit.module] = Limit( + limit=limit, limiter=FreqLimiter(limit.cd) + ) + elif limit.limit_type == PluginLimitType.COUNT: + cls.count_limit[limit.module] = Limit( + limit=limit, limiter=CountLimiter(limit.max_count) + ) + + @classmethod + def unblock( + cls, module: str, user_id: str, group_id: str | None, channel_id: str | None + ): + """解除插件block + + 参数: + module: 模块名 + user_id: 用户id + group_id: 群组id + channel_id: 频道id + """ + if limit_model := cls.block_limit.get(module): + limit = limit_model.limit + limiter: UserBlockLimiter = limit_model.limiter # type: ignore + key_type = user_id + if group_id and limit.watch_type == LimitWatchType.GROUP: + key_type = channel_id or group_id + limiter.set_false(key_type) + + @classmethod + async def check( + cls, + module: str, + user_id: str, + group_id: str | None, + channel_id: str | None, + session: EventSession, + ): + """检测限制 + + 参数: + module: 模块名 + user_id: 用户id + group_id: 群组id + channel_id: 频道id + session: Session + + 异常: + IgnoredException: IgnoredException + """ + if limit_model := cls.cd_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + if limit_model := cls.block_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + if limit_model := cls.count_limit.get(module): + await cls.__check(limit_model, user_id, group_id, channel_id, session) + + @classmethod + async def __check( + cls, + limit_model: Limit, + user_id: str, + group_id: str | None, + channel_id: str | None, + session: EventSession, + ): + """检测限制 + + 参数: + limit_model: Limit + user_id: 用户id + group_id: 群组id + channel_id: 频道id + session: Session + + 异常: + IgnoredException: IgnoredException + """ + if limit_model: + limit = limit_model.limit + limiter = limit_model.limiter + is_limit = ( + LimitWatchType.ALL + or (group_id and limit.watch_type == LimitWatchType.GROUP) + or (not group_id and limit.watch_type == LimitWatchType.USER) + ) + key_type = user_id + if group_id and limit.watch_type == LimitWatchType.GROUP: + key_type = channel_id or group_id + if is_limit and limiter.check(key_type): + if limit.result: + await Text(limit.result).send() + logger.debug( + f"{limit.module}({limit.limit_type}) 正在限制中...", + "HOOK", + session=session, + ) + raise IgnoredException(f"{limit.module} 正在cd中...") + else: + if isinstance(limiter, FreqLimiter): + limiter.start_cd(key_type) + if isinstance(limiter, UserBlockLimiter): + limiter.set_true(key_type) + if isinstance(limiter, CountLimiter): + limiter.increase(key_type) + + +class IsSuperuserException(Exception): + pass + + +class AuthChecker: + """ + 权限检查 + """ + + def __init__(self): + check_notice_info_cd = Config.get_config("hook", "CHECK_NOTICE_INFO_CD") + if check_notice_info_cd is None or check_notice_info_cd < 0: + raise ValueError("模块: [hook], 配置项: [CHECK_NOTICE_INFO_CD] 为空或小于0") + self._flmt = FreqLimiter(check_notice_info_cd) + self._flmt_g = FreqLimiter(check_notice_info_cd) + self._flmt_s = FreqLimiter(check_notice_info_cd) + self._flmt_c = FreqLimiter(check_notice_info_cd) + + async def auth( + self, + matcher: Matcher, + bot: Bot, + session: EventSession, + message: UniMsg, + ): + """权限检查 + + 参数: + matcher: matcher + bot: bot + session: EventSession + message: UniMsg + """ + is_ignore = False + cost_gold = 0 + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id and matcher.plugin and (module := matcher.plugin.name): + user = await UserConsole.get_user(user_id, session.platform) + if plugin := await PluginInfo.get_or_none(module=module): + try: + cost_gold = await self.auth_cost(user, plugin, session) + if session.id1 in bot.config.superusers: + if plugin.plugin_type == PluginType.SUPERUSER: + raise IsSuperuserException() + if not plugin.limit_superuser: + cost_gold = 0 + raise IsSuperuserException() + await self.auth_group(plugin, session, message) + await self.auth_admin(plugin, session) + await self.auth_plugin(plugin, session) + await self.auth_limit(plugin, session) + except IsSuperuserException: + logger.debug( + f"超级用户或被ban跳过权限检测...", "HOOK", session=session + ) + except IgnoredException: + is_ignore = True + LimitManage.unblock( + matcher.plugin.name, user_id, group_id, channel_id + ) + if cost_gold and user_id: + """花费金币""" + await UserConsole.reduce_gold( + user_id, + cost_gold, + GoldHandle.PLUGIN, + matcher.plugin.name if matcher.plugin else "", + session.platform, + ) + logger.debug(f"调用功能花费金币: {cost_gold}", "HOOK", session=session) + if is_ignore: + raise IgnoredException("权限检测 ignore") + + async def auth_limit(self, plugin: PluginInfo, session: EventSession): + """插件限制 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + limit_list: list[PluginLimit] = await plugin.plugin_limit.all() # type: ignore + for limit in limit_list: + LimitManage.add_limit(limit) + if user_id: + await LimitManage.check( + plugin.module, user_id, group_id, channel_id, session + ) + + async def auth_plugin(self, plugin: PluginInfo, session: EventSession): + """插件状态 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id: + if group_id: + if await GroupConsole.is_block_plugin( + group_id, plugin.module, channel_id + ): + """群组插件状态""" + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await Text("该群未开启此功能...").send(reply=True) + logger.debug( + f"{plugin.name}({plugin.module}) 未开启此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("该群未开启此功能...") + if await GroupConsole.is_super_block_plugin( + group_id, plugin.module, channel_id + ): + """群组插件状态""" + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await Text("超级管理员禁用了该群此功能...").send(reply=True) + logger.debug( + f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("超级管理员禁用了该群此功能...") + # 群聊禁用 + if not plugin.status and plugin.block_type == BlockType.GROUP: + try: + if self._flmt_c.check(group_id): + self._flmt_c.start_cd(group_id) + await Text("该功能在群聊中已被禁用...").send(reply=True) + except Exception: + pass + logger.debug( + f"{plugin.name}({plugin.module}) 该插件在群聊中已被禁用...", + "HOOK", + session=session, + ) + raise IgnoredException("该插件在群聊中已被禁用...") + else: + # 私聊禁用 + if not plugin.status and plugin.block_type == BlockType.PRIVATE: + try: + if self._flmt_c.check(user_id): + self._flmt_c.start_cd(user_id) + await Text("该功能在私聊中已被禁用...").send() + except Exception: + pass + logger.debug( + f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...", + "HOOK", + session=session, + ) + raise IgnoredException("该插件在私聊中已被禁用...") + if not plugin.status and plugin.block_type == BlockType.ALL: + """全局状态""" + if group_id: + if await GroupConsole.is_super_group(group_id, channel_id): + raise IsSuperuserException() + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await Text("全局未开启此功能...").send() + logger.debug( + f"{plugin.name}({plugin.module}) 全局未开启此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("全局未开启此功能...") + + async def auth_admin(self, plugin: PluginInfo, session: EventSession): + """管理员命令 个人权限 + + 参数: + plugin: PluginInfo + session: EventSession + """ + user_id = session.id1 + group_id = session.id3 or session.id2 + if user_id and group_id and plugin.admin_level: + if group_id: + if await LevelUser.check_level(user_id, group_id, plugin.admin_level): + try: + if self._flmt.check(user_id): + self._flmt.start_cd(user_id) + await MessageFactory( + [ + Mention(user_id), + Text( + f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" + ), + ] + ).finish(reply=True) + except Exception: + pass + logger.debug( + f"{plugin.name}({plugin.module}) 管理员权限不足...", + "HOOK", + session=session, + ) + raise IgnoredException("管理员权限不足...") + else: + if not await LevelUser.check_level(user_id, "", plugin.admin_level): + try: + await Text( + f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" + ).finish() + except Exception: + pass + logger.debug( + f"{plugin.name}({plugin.module}) 管理员权限不足...", + "HOOK", + session=session, + ) + raise IgnoredException("权限不足") + + async def auth_group( + self, plugin: PluginInfo, session: EventSession, message: UniMsg + ): + """群黑名单检测 群总开关检测 + + 参数: + plugin: PluginInfo + session: EventSession + message: UniMsg + """ + if group_id := session.id3 or session.id2: + text = message.extract_plain_text() + group, _ = await GroupConsole.get_or_create(group_id=group_id) + if group.level < -1: + """群权限小于0""" + logger.debug( + f"{plugin.name}({plugin.module}) 群黑名单, 群权限-1...", + "HOOK", + session=session, + ) + raise IgnoredException("群黑名单") + if not group.status: + """群休眠""" + if text.strip() != "醒来": + logger.debug( + f"{plugin.name}({plugin.module}) 功能总开关关闭状态...", + "HOOK", + session=session, + ) + raise IgnoredException("功能总开关关闭状态") + + async def auth_cost( + self, user: UserConsole, plugin: PluginInfo, session: EventSession + ) -> int: + """检测是否满足金币条件 + + 参数: + user: UserConsole + plugin: PluginInfo + session: EventSession + + 返回: + int: 需要消耗的金币 + """ + if user.gold < plugin.cost_gold: + """插件消耗金币不足""" + try: + await Text(f"金币不足..该功能需要{plugin.cost_gold}金币..").send() + except Exception: + pass + logger.debug( + f"{plugin.name}({plugin.module}) 金币限制..该功能需要{plugin.cost_gold}金币..", + "HOOK", + session=session, + ) + raise IgnoredException(f"{plugin.name}({plugin.module}) 金币限制...") + return plugin.cost_gold + + +checker = AuthChecker() diff --git a/zhenxun/builtin_plugins/hooks/auth_hook.py b/zhenxun/builtin_plugins/hooks/auth_hook.py new file mode 100644 index 00000000..938f8222 --- /dev/null +++ b/zhenxun/builtin_plugins/hooks/auth_hook.py @@ -0,0 +1,35 @@ +from typing import Optional + +from nonebot.adapters.onebot.v11 import Bot, Event, MessageEvent +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor, run_preprocessor +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from ._auth_checker import LimitManage, checker + + +# # 权限检测 +@run_preprocessor +async def _(matcher: Matcher, bot: Bot, session: EventSession, message: UniMsg): + await checker.auth(matcher, bot, session, message) + + +# 解除命令block阻塞 +@run_postprocessor +async def _( + matcher: Matcher, + exception: Optional[Exception], + bot: Bot, + event: Event, + session: EventSession, +): + user_id = session.id1 + group_id = session.id3 + channel_id = session.id2 + if not group_id: + group_id = channel_id + channel_id = None + if user_id and matcher.plugin: + module = matcher.plugin.name + LimitManage.unblock(module, user_id, group_id, channel_id) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index e80fc8f6..1f8caf39 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -136,7 +136,7 @@ async def _(): create_list = [] update_list = [] for task in task_list: - if task.module not in module_list: + if task.module not in module_dict: create_list.append(task) else: task.id = module_dict[task.module] diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 96bed3f0..6ecea8da 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -36,9 +36,7 @@ class ShopManage: goods = filter_goods[0] else: return "道具名称不存在..." - user, _ = await UserConsole.get_or_create( - user_id=user_id, defaults={"platform": platform} - ) + user = await UserConsole.get_user(user_id, platform) price = goods.goods_price * num * goods.goods_discount if user.gold < price: return "糟糕! 您的金币好像不太够哦..." @@ -77,9 +75,7 @@ class ShopManage: 返回: BuildImage | None: 道具背包图片 """ - user, _ = await UserConsole.get_or_create( - user_id=user_id, defaults={"platform": platform} - ) + user = await UserConsole.get_user(user_id, platform) if not user.props: return None result = await GoodsInfo.filter(uuid__in=user.props.keys()).all() @@ -113,9 +109,7 @@ class ShopManage: 返回: int: 金币数量 """ - user, _ = await UserConsole.get_or_create( - user_id=user_id, defaults={"platform": platform} - ) + user = await UserConsole.get_user(user_id, platform) return user.gold @classmethod diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 07f3fcbe..6fe4ce76 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -94,13 +94,7 @@ class SignManage: if not session.id1: return None now = datetime.now(pytz.timezone("Asia/Shanghai")) - user_console, _ = await UserConsole.get_or_create( - user_id=session.id1, - defaults={ - "uid": await UserConsole.get_new_uid(), - "platform": session.platform, - }, - ) + user_console = await UserConsole.get_user(session.id1, session.platform) user, _ = await SignUser.get_or_create( user_id=session.id1, defaults={"user_console": user_console, "platform": session.platform}, @@ -112,7 +106,6 @@ class SignManage: or (new_log and now > new_log.create_time) or file_name in os.listdir(SIGN_TODAY_CARD_PATH) ): - user_console, _ = await UserConsole.get_or_create(user_id=session.id1) path = await get_card(user, nickname, -1, user_console.gold, "") else: path = await cls._handle_sign_in(user, nickname, session, is_view_card) diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py index 3af6514f..0fc3b921 100644 --- a/zhenxun/builtin_plugins/sign_in/goods_register.py +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -35,19 +35,14 @@ async def _(): **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore ) async def _(session: EventSession, user_id: int, group_id: int, prob: float): - user_console, _ = await UserConsole.get_or_create( - user_id=session.id1, - defaults={ - "uid": await UserConsole.get_new_uid(), - "platform": session.platform, - }, - ) - user, _ = await SignUser.get_or_create( - user_id=user_id, - defaults={"platform": session.platform, "user_console": user_console}, - ) - user.add_probability = Decimal(prob) - await user.save(update_fields=["add_probability"]) + if session.id1: + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=user_id, + defaults={"platform": session.platform, "user_console": user_console}, + ) + user.add_probability = Decimal(prob) + await user.save(update_fields=["add_probability"]) @shop_register( name="测试道具A", diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py new file mode 100644 index 00000000..1e7c9f14 --- /dev/null +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -0,0 +1,154 @@ +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.params import Depends +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + Subcommand, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="管理群操作", + description="管理群操作", + usage=""" + 群权限 | 群白名单 | 退出群 操作 + 退群,添加/删除群白名单,添加/删除群认证,当在群组中这五个命令且没有指定群号时,默认指定当前群组 + 指令: + 退群 ?[group_id] + 修改群权限 [group_id] [等级] + 修改群权限 [等级]: 该命令仅在群组时生效,默认修改当前群组 + 添加群白名单 ?*[group_id] + 删除群白名单 ?*[group_id] + 添加群认证 ?*[group_id] + 删除群认证 ?*[group_id] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "group-manage", + Subcommand( + "modify-level", Args["level", int]["group_id?", int], help_text="修改群权限" + ), + Subcommand( + "super-handle", + Option("--del", action=store_true, help_text="删除"), + Args["group_id", int], + help_text="添加/删除群白名单", + ), + Subcommand( + "auth-handle", + Option("--del", action=store_true, help_text="删除"), + Args["group_id", int], + help_text="添加群白名单", + ), + Subcommand("del-group", Args["group_id", int], help_text="退出群组"), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + + +def CheckGroupId(): + """ + 检测群组id + """ + + async def dependency( + session: EventSession, + group_id: Match[int], + state: T_State, + ): + gid = session.id3 or session.id2 + if group_id.available: + gid = group_id.result + if not gid: + await Text("群组id不能为空...").finish() + state["group_id"] = gid + + return Depends(dependency) + + +@_matcher.assign("modify-level", parameterless=[]) +async def _(session: EventSession, arparma: Arparma, state: T_State, level: int): + gid = state["group_id"] + group, _ = await GroupConsole.get_or_create(group_id=gid) + old_level = group.level + group.level = level + await group.save(update_fields=["level"]) + await Text("群权限修改成功!").send(reply=True) + logger.info( + f"修改群权限: {old_level} -> {level}", + arparma.header_result, + session=session, + target=gid, + ) + + +@_matcher.assign("super-handle") +async def _(session: EventSession, arparma: Arparma, state: T_State): + gid = state["group_id"] + group = await GroupConsole.get_or_none(group_id=gid) + if not group: + await Text("群组信息不存在, 请更新群组信息...").finish() + s = "删除" if arparma.find("del") else "添加" + group.is_super = not arparma.find("del") + await group.save(update_fields=["is_super"]) + await Text(f"{s}群白名单成功!").send(reply=True) + logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) + + +@_matcher.assign("auth-handle") +async def _(session: EventSession, arparma: Arparma, state: T_State): + gid = state["group_id"] + await GroupConsole.update_or_create( + group_id=gid, defaults={"group_flag": 0 if arparma.find("del") else 1} + ) + s = "删除" if arparma.find("del") else "添加" + await Text(f"{s}群认证成功!").send(reply=True) + logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) + + +@_matcher.assign("del-group") +async def _(bot: Bot, session: EventSession, arparma: Arparma, group_id: int): + if isinstance(bot, v11Bot): + group_list = [g["group_id"] for g in await bot.get_group_list()] + if group_id not in group_list: + logger.debug("群组不存在", "退群", session=session, target=group_id) + await Text(f"{NICKNAME}未在该群组中...").finish() + try: + await bot.set_group_leave(group_id=group_id) + logger.info( + f"{NICKNAME}退出群组成功", "退群", session=session, target=group_id + ) + await Text(f"退出群组 {group_id} 成功!").send() + await GroupConsole.filter(group_id=group_id).delete() + except Exception as e: + logger.error(f"退出群组失败", "退群", session=session, target=group_id, e=e) + await Text(f"退出群组 {group_id} 失败...").send() + else: + # TODO: 其他平台的退群操作 + await Text(f"暂未支持退群操作...").send() diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py index a0ff26fe..0e8858de 100644 --- a/zhenxun/models/group_console.py +++ b/zhenxun/models/group_console.py @@ -17,6 +17,14 @@ class GroupConsole(Model): """最大人数""" member_count = fields.IntField(default=0, description="当前人数") """当前人数""" + status = fields.BooleanField(default=True, description="群状态") + """群状态""" + level = fields.IntField(default=5, description="群权限") + """群权限""" + is_super = fields.BooleanField( + default=False, description="超级用户指定,可以使用全局关闭的功能" + ) + """超级用户指定群,可以使用全局关闭的功能""" group_flag = fields.IntField(default=0, description="群认证标记") """群认证标记""" block_plugin = fields.TextField(default="", description="禁用插件") @@ -31,6 +39,61 @@ class GroupConsole(Model): table_description = "群组信息表" unique_together = ("group_id", "channel_id") + @classmethod + async def is_super_group(cls, group_id: str, channel_id: str | None = None) -> bool: + """是否超级用户指定群 + + 参数: + group_id: 群组id + channel_id: 频道id. + + 返回: + bool: 是否超级用户指定群 + """ + if group := await cls.get_or_none(group_id=group_id): + return group.is_super + return False + + @classmethod + async def is_super_block_plugin( + cls, group_id: str, module: str, channel_id: str | None = None + ) -> bool: + """查看群组是否超级用户禁用功能 + + 参数: + group_id: 群组id + module: 模块名称 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists( + group_id=group_id, + channel_id=channel_id, + block_plugin__contains=f"super:{module},", + ) + + @classmethod + async def is_block_plugin( + cls, group_id: str, module: str, channel_id: str | None = None + ) -> bool: + """查看群组是否禁用功能 + + 参数: + group_id: 群组id + module: 模块名称 + channel_id: 频道id + + 返回: + bool: 是否禁用被动 + """ + return await cls.exists( + group_id=group_id, + channel_id=channel_id, + block_plugin__contains=f"{module},", + ) + @classmethod async def is_block_task( cls, group_id: str, task: str, channel_id: str | None = None diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py index 9bf4259f..96538b64 100644 --- a/zhenxun/models/plugin_limit.py +++ b/zhenxun/models/plugin_limit.py @@ -26,7 +26,7 @@ class PluginLimit(Model): limit_type = fields.CharEnumField(PluginLimitType, description="限制类型") """限制类型""" watch_type = fields.CharEnumField(LimitWatchType, description="监听类型") - """限制类型""" + """监听类型""" status = fields.BooleanField(default=True, description="限制的开关状态") """限制的开关状态""" check_type = fields.CharEnumField( diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py index 59b643f4..32695923 100644 --- a/zhenxun/models/user_console.py +++ b/zhenxun/models/user_console.py @@ -4,6 +4,7 @@ from tortoise import fields from zhenxun.services.db_context import Model from zhenxun.utils.enum import GoldHandle +from zhenxun.utils.exception import InsufficientGold from .user_gold_log import UserGoldLog @@ -32,7 +33,29 @@ class UserConsole(Model): table_description = "用户数据表" @classmethod - async def get_new_uid(cls): + async def get_user(cls, user_id: str, platform: str | None = None) -> "UserConsole": + """获取用户 + + 参数: + user_id: 用户id + platform: 平台. + + 返回: + UserConsole: UserConsole + """ + user, _ = await UserConsole.get_or_create( + user_id=user_id, + defaults={"platform": platform, "uid": await cls.get_new_uid()}, + ) + return user + + @classmethod + async def get_new_uid(cls) -> int: + """获取最新uid + + 返回: + int: 最新uid + """ if user := await cls.annotate().order_by("uid").first(): return user.uid + 1 return 1 @@ -58,6 +81,38 @@ class UserConsole(Model): user_id=user_id, gold=gold, handle=GoldHandle.GET, source=source ) + @classmethod + async def reduce_gold( + cls, + user_id: str, + gold: int, + handle: GoldHandle, + plugin_module: str, + platform: str | None = None, + ): + """消耗金币 + + 参数: + user_id: 用户id + gold: 金币 + handle: 金币处理 + plugin_name: 插件模块 + platform: 平台. + + 异常: + InsufficientGold: 金币不足 + """ + user, _ = await cls.get_or_create( + user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + ) + if user.gold < gold: + raise InsufficientGold() + user.gold -= gold + await user.save(update_fields=["gold"]) + await UserGoldLog.create( + user_id=user_id, gold=gold, handle=handle, source=plugin_module + ) + @classmethod async def add_props( cls, user_id: str, goods_uuid: str, num: int = 1, platform: str | None = None diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index bcb83dc7..757bc5bc 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -453,14 +453,24 @@ class BuildImage: return self def pic2bs4(self) -> str: - """ - BuildImage 转 base64 + """BuildImage 转 base64 + + 返回: + str: base64 """ buf = BytesIO() self.markImg.save(buf, format="PNG") base64_str = base64.b64encode(buf.getvalue()).decode() return "base64://" + base64_str + def pic2io(self) -> BytesIO: + """图片转 BytesIO + + 返回: + BytesIO: BytesIO + """ + return BytesIO(self.tobytes()) + def convert(self, type_: ModeType) -> Self: """ 修改图片类型 diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 32270316..437c4f0d 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -10,6 +10,8 @@ class GoldHandle(StrEnum): """购买""" GET = "GET" """获取""" + PLUGIN = "PLUGIN" + """插件花费""" class PropHandle(StrEnum): @@ -40,7 +42,7 @@ class BlockType(StrEnum): 禁用状态 """ - FRIEND = "PRIVATE" + PRIVATE = "PRIVATE" GROUP = "GROUP" ALL = "ALL" @@ -72,6 +74,7 @@ class LimitWatchType(StrEnum): USER = "USER" GROUP = "GROUP" + ALL = "ALL" class RequestType(StrEnum): diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py index 0998e776..4b1cbe17 100644 --- a/zhenxun/utils/exception.py +++ b/zhenxun/utils/exception.py @@ -1,14 +1,38 @@ class NotFoundError(Exception): + """ + 未发现 + """ + pass class GroupInfoNotFound(Exception): + """ + 群组未找到 + """ + pass class EmptyError(Exception): + """ + 空错误 + """ + pass class UserAndGroupIsNone(Exception): + """ + 用户和群组为空 + """ + + pass + + +class InsufficientGold(Exception): + """ + 金币不足 + """ + pass diff --git a/zhenxun/utils/rules.py b/zhenxun/utils/rules.py index f48c1106..0508f21f 100644 --- a/zhenxun/utils/rules.py +++ b/zhenxun/utils/rules.py @@ -27,7 +27,9 @@ def admin_check(a: int | str, key: str | None = None) -> Rule: if type(a) == str and key: level = Config.get_config(a, key) if level is not None: - return bool(LevelUser.check_level(session.id1, session.id2, int(level))) + return bool( + await LevelUser.check_level(session.id1, session.id2, int(level)) + ) return False return Rule(_rule) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 86e8c4e4..5fe35211 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -1,10 +1,12 @@ import os import time from collections import defaultdict +from datetime import datetime from pathlib import Path from typing import Any import httpx +import pytz from zhenxun.services.log import logger @@ -78,21 +80,31 @@ class ResourceDirManager: class CountLimiter: """ - 次数检测工具,检测调用次数是否超过设定值 + 每日调用命令次数限制 """ - def __init__(self, max_count: int): + tz = pytz.timezone("Asia/Shanghai") + + def __init__(self, max_num): + self.today = -1 self.count = defaultdict(int) - self.max_count = max_count + self.max = max_num - def add(self, key: Any): - self.count[key] += 1 + def check(self, key) -> bool: + day = datetime.now(self.tz).day + if day != self.today: + self.today = day + self.count.clear() + return bool(self.count[key] < self.max) - def check(self, key: Any) -> bool: - if self.count[key] >= self.max_count: - self.count[key] = 0 - return True - return False + def get_num(self, key): + return self.count[key] + + def increase(self, key, num=1): + self.count[key] += num + + def reset(self, key): + self.count[key] = 0 class UserBlockLimiter: From 2501a72bb85ada0ad33b8321df93cda3d99e5af1 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 27 Feb 2024 01:14:49 +0800 Subject: [PATCH 004/132] =?UTF-8?q?feat=E2=9C=A8:=20=20test=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 52 +----------- .../admin/plugin_switch/_data_source.py | 11 ++- .../admin/plugin_switch/command.py | 4 +- .../builtin_plugins/hooks/_auth_checker.py | 84 +++++++++++-------- .../builtin_plugins/superuser/group_manage.py | 8 +- zhenxun/models/level_user.py | 8 +- 6 files changed, 69 insertions(+), 98 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index c0af3544..81a7bff8 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -1,24 +1,13 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import ( - Alconna, - Args, - Arparma, - Match, - Option, - Subcommand, - on_alconna, - store_true, -) +from nonebot_plugin_alconna import Arparma, Match from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from requests import session from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import BlockType, PluginType -from zhenxun.utils.rules import admin_check, ensure_group from ._data_source import PluginManage, build_plugin, build_task from .command import _group_status_matcher, _status_matcher @@ -54,44 +43,7 @@ __plugin_meta__ = PluginMetadata( ) -# _status_matcher = on_alconna( -# Alconna( -# "switch", -# Option("-t|--task", action=store_true, help_text="被动技能"), -# Subcommand( -# "open", -# Args["name", str], -# Option( -# "-g|--group", -# Args["group_id", str], -# ), -# ), -# Subcommand( -# "close", -# Args["name", str], -# Option( -# "-t|--type", -# Args["block_type", ["all", "a", "private", "p", "group", "g"]], -# ), -# Option( -# "-g|--group", -# Args["group_id", str], -# ), -# ), -# ), -# rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), -# priority=5, -# block=True, -# ) - -# # TODO: shortcut - -# _group_status_matcher = on_alconna( -# Alconna("group-status", Args["status", ["sleep", "wake"]]), -# rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group, -# priority=5, -# block=True, -# ) +# TODO: shortcut @_status_matcher.assign("$main") diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index 8137206f..b88cd607 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -249,8 +249,8 @@ class PluginManage: if plugin: if group_id: if group := await GroupConsole.get_or_none(group_id=group_id): - if f"super:{plugin_name}," not in group.block_plugin: - group.block_plugin += f"super:{plugin_name}," + if f"super:{plugin.module}," not in group.block_plugin: + group.block_plugin += f"super:{plugin.module}," await group.save(update_fields=["block_plugin"]) return ( f"已成功关闭群组 {group.group_name} 的 {plugin_name} 功能!" @@ -263,5 +263,10 @@ class PluginManage: if not block_type: return f"已成功将 {plugin.name} 全局启用!" else: - return f"已成功将 {plugin.name} 全局关闭!" + if block_type == BlockType.ALL: + return f"已成功将 {plugin.name} 全局关闭!" + if block_type == BlockType.GROUP: + return f"已成功将 {plugin.name} 全局群组关闭!" + if block_type == BlockType.PRIVATE: + return f"已成功将 {plugin.name} 全局私聊关闭!" return "没有找到这个功能喔..." diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index ec0c8513..0ed95b66 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -19,7 +19,7 @@ _status_matcher = on_alconna( Args["name", [str, int]], Option( "-g|--group", - Args["group_id", str], + Args["group", str], ), ), Subcommand( @@ -31,7 +31,7 @@ _status_matcher = on_alconna( ), Option( "-g|--group", - Args["group_id", str], + Args["group", str], ), ), ), diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 03a33ebd..2aef17c1 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -145,7 +145,7 @@ class LimitManage: key_type = user_id if group_id and limit.watch_type == LimitWatchType.GROUP: key_type = channel_id or group_id - if is_limit and limiter.check(key_type): + if is_limit and not limiter.check(key_type): if limit.result: await Text(limit.result).send() logger.debug( @@ -153,7 +153,7 @@ class LimitManage: "HOOK", session=session, ) - raise IgnoredException(f"{limit.module} 正在cd中...") + raise IgnoredException(f"{limit.module} 正在限制中...") else: if isinstance(limiter, FreqLimiter): limiter.start_cd(key_type) @@ -207,6 +207,8 @@ class AuthChecker: if user_id and matcher.plugin and (module := matcher.plugin.name): user = await UserConsole.get_user(user_id, session.platform) if plugin := await PluginInfo.get_or_none(module=module): + if plugin.plugin_type == PluginType.HIDDEN: + return try: cost_gold = await self.auth_cost(user, plugin, session) if session.id1 in bot.config.superusers: @@ -277,6 +279,19 @@ class AuthChecker: channel_id = None if user_id: if group_id: + if await GroupConsole.is_super_block_plugin( + group_id, plugin.module, channel_id + ): + """超级用户群组插件状态""" + if self._flmt_s.check(group_id or user_id): + self._flmt_s.start_cd(group_id or user_id) + await Text("超级管理员禁用了该群此功能...").send(reply=True) + logger.debug( + f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", + "HOOK", + session=session, + ) + raise IgnoredException("超级管理员禁用了该群此功能...") if await GroupConsole.is_block_plugin( group_id, plugin.module, channel_id ): @@ -290,42 +305,33 @@ class AuthChecker: session=session, ) raise IgnoredException("该群未开启此功能...") - if await GroupConsole.is_super_block_plugin( - group_id, plugin.module, channel_id - ): - """群组插件状态""" - if self._flmt_s.check(group_id or user_id): - self._flmt_s.start_cd(group_id or user_id) - await Text("超级管理员禁用了该群此功能...").send(reply=True) - logger.debug( - f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", - "HOOK", - session=session, - ) - raise IgnoredException("超级管理员禁用了该群此功能...") - # 群聊禁用 if not plugin.status and plugin.block_type == BlockType.GROUP: + """全局群组禁用""" try: if self._flmt_c.check(group_id): self._flmt_c.start_cd(group_id) - await Text("该功能在群聊中已被禁用...").send(reply=True) - except Exception: - pass + await Text("该功能在群组中已被禁用...").send(reply=True) + except Exception as e: + logger.error( + "auth_plugin 发送消息失败", "HOOK", session=session, e=e + ) logger.debug( - f"{plugin.name}({plugin.module}) 该插件在群聊中已被禁用...", + f"{plugin.name}({plugin.module}) 该插件在群组中已被禁用...", "HOOK", session=session, ) - raise IgnoredException("该插件在群聊中已被禁用...") + raise IgnoredException("该插件在群组中已被禁用...") else: - # 私聊禁用 if not plugin.status and plugin.block_type == BlockType.PRIVATE: + """全局私聊禁用""" try: if self._flmt_c.check(user_id): self._flmt_c.start_cd(user_id) await Text("该功能在私聊中已被禁用...").send() - except Exception: - pass + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) logger.debug( f"{plugin.name}({plugin.module}) 该插件在私聊中已被禁用...", "HOOK", @@ -356,9 +362,11 @@ class AuthChecker: """ user_id = session.id1 group_id = session.id3 or session.id2 - if user_id and group_id and plugin.admin_level: + if user_id and plugin.admin_level: if group_id: - if await LevelUser.check_level(user_id, group_id, plugin.admin_level): + if not await LevelUser.check_level( + user_id, group_id, plugin.admin_level + ): try: if self._flmt.check(user_id): self._flmt.start_cd(user_id) @@ -369,9 +377,11 @@ class AuthChecker: f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" ), ] - ).finish(reply=True) - except Exception: - pass + ).send(reply=True) + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) logger.debug( f"{plugin.name}({plugin.module}) 管理员权限不足...", "HOOK", @@ -379,13 +389,15 @@ class AuthChecker: ) raise IgnoredException("管理员权限不足...") else: - if not await LevelUser.check_level(user_id, "", plugin.admin_level): + if not await LevelUser.check_level(user_id, None, plugin.admin_level): try: await Text( f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" - ).finish() - except Exception: - pass + ).send() + except Exception as e: + logger.error( + "auth_admin 发送消息失败", "HOOK", session=session, e=e + ) logger.debug( f"{plugin.name}({plugin.module}) 管理员权限不足...", "HOOK", @@ -406,7 +418,7 @@ class AuthChecker: if group_id := session.id3 or session.id2: text = message.extract_plain_text() group, _ = await GroupConsole.get_or_create(group_id=group_id) - if group.level < -1: + if group.level < 0: """群权限小于0""" logger.debug( f"{plugin.name}({plugin.module}) 群黑名单, 群权限-1...", @@ -441,8 +453,8 @@ class AuthChecker: """插件消耗金币不足""" try: await Text(f"金币不足..该功能需要{plugin.cost_gold}金币..").send() - except Exception: - pass + except Exception as e: + logger.error("auth_cost 发送消息失败", "HOOK", session=session, e=e) logger.debug( f"{plugin.name}({plugin.module}) 金币限制..该功能需要{plugin.cost_gold}金币..", "HOOK", diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py index 1e7c9f14..0531962e 100644 --- a/zhenxun/builtin_plugins/superuser/group_manage.py +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -71,6 +71,8 @@ _matcher = on_alconna( block=True, ) +# TODO: shortcut + def CheckGroupId(): """ @@ -92,7 +94,7 @@ def CheckGroupId(): return Depends(dependency) -@_matcher.assign("modify-level", parameterless=[]) +@_matcher.assign("modify-level", parameterless=[CheckGroupId()]) async def _(session: EventSession, arparma: Arparma, state: T_State, level: int): gid = state["group_id"] group, _ = await GroupConsole.get_or_create(group_id=gid) @@ -108,7 +110,7 @@ async def _(session: EventSession, arparma: Arparma, state: T_State, level: int) ) -@_matcher.assign("super-handle") +@_matcher.assign("super-handle", parameterless=[CheckGroupId()]) async def _(session: EventSession, arparma: Arparma, state: T_State): gid = state["group_id"] group = await GroupConsole.get_or_none(group_id=gid) @@ -121,7 +123,7 @@ async def _(session: EventSession, arparma: Arparma, state: T_State): logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) -@_matcher.assign("auth-handle") +@_matcher.assign("auth-handle", parameterless=[CheckGroupId()]) async def _(session: EventSession, arparma: Arparma, state: T_State): gid = state["group_id"] await GroupConsole.update_or_create( diff --git a/zhenxun/models/level_user.py b/zhenxun/models/level_user.py index da229216..c2c4fc31 100644 --- a/zhenxun/models/level_user.py +++ b/zhenxun/models/level_user.py @@ -82,7 +82,7 @@ class LevelUser(Model): return False @classmethod - async def check_level(cls, user_id: str, group_id: str, level: int) -> bool: + async def check_level(cls, user_id: str, group_id: str | None, level: int) -> bool: """检查用户权限等级是否大于 level 参数: @@ -97,9 +97,9 @@ class LevelUser(Model): if user := await cls.get_or_none(user_id=user_id, group_id=group_id): return user.user_level >= level else: - user_list = await cls.filter(user_id=user_id).all() - user = max(user_list, key=lambda x: x.user_level) - return user.user_level >= level + if user_list := await cls.filter(user_id=user_id).all(): + user = max(user_list, key=lambda x: x.user_level) + return user.user_level >= level return False @classmethod From 499e51e996fbd7e2a33af9c3605184438dda4b39 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 27 Feb 2024 02:30:01 +0800 Subject: [PATCH 005/132] =?UTF-8?q?perf=F0=9F=91=8C:=20=E6=B7=BB=E5=8A=A0s?= =?UTF-8?q?hortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + zhenxun/builtin_plugins/admin/ban/__init__.py | 96 ++++++++++++++----- .../builtin_plugins/admin/ban/_data_source.py | 40 ++++---- .../admin/plugin_switch/__init__.py | 30 ++++-- .../admin/plugin_switch/command.py | 2 - .../chat_history/chat_message_handle.py | 15 ++- zhenxun/models/chat_history.py | 21 ++-- 7 files changed, 140 insertions(+), 65 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a22fedc..6d295644 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,7 @@ "nonebot", "onebot", "tobytes", + "unban", "userinfo", "zhenxun" ], diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 289ef6b6..0aa3c57d 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -29,12 +29,31 @@ __plugin_meta__ = PluginMetadata( name="封禁用户/群组", description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息", usage=""" - .ban [at] ?[小时] ?[分钟] - .unban - 示例:.ban @user - 示例:.ban @user 6 - 示例:.ban @user 3 10 - 示例:.unban @user + 普通管理员 + 格式: + ban [At用户] [时长] + + 示例: + ban @用户 : 永久拉黑用户 + ban @用户 100 : 拉黑用户100分钟 + unban @用户 : 从小黑屋中拉出来 + + 超级管理员额外命令 + 格式: + ban [At用户/用户Id] [时长] + ban列表: 获取所有Ban数据 + 群组ban列表: 获取群组Ban数据 + 用户ban列表: 获取用户Ban数据 + + 私聊下: + 示例: + ban 123456789 : 永久拉黑用户123456789 + ban 123456789 100 : 拉黑用户123456789 100分钟 + + ban -g 999999 : 拉黑群组为999999的群组 + + unban 123456789 : 从小黑屋中拉出来 + unban -g 999999 : 将群组9999999从小黑屋中拉出来 """.strip(), extra=PluginExtraData( author="HibiKier", @@ -54,19 +73,22 @@ __plugin_meta__ = PluginMetadata( ) -_matcher = on_alconna( +_ban_matcher = on_alconna( Alconna( - "ban-console", - Subcommand( - "ban", - Args["user?", [str, At]]["duration?", int], - Option("-g|--group", Args["group_id", str]), - ), - Subcommand( - "unban", - Args["user?", [str, At]], - Option("-g|--group", Args["group_id", str]), - ), + "ban", + Args["user?", [str, At]]["duration?", int], + Option("-g|--group", Args["group_id", str]), + ), + rule=admin_check("ban", "BAN_LEVEL"), + priority=5, + block=True, +) + +_unban_matcher = on_alconna( + Alconna( + "unban", + Args["user?", [str, At]], + Option("-g|--group", Args["group_id", str]), ), rule=admin_check("ban", "BAN_LEVEL"), priority=5, @@ -76,14 +98,35 @@ _matcher = on_alconna( _status_matcher = on_alconna( Alconna( "ban-status", - Option("-u|--user", Args["user_id", str]), - Option("-g|--group", Args["group_id", str]), + Option("-u|--user", action=store_true, help_text="过滤用户"), + Option("-g|--group", action=store_true, help_text="过滤群组"), ), permission=SUPERUSER, priority=1, block=True, ) -# TODO: shortcut + +_status_matcher.shortcut( + "ban列表", + command="ban-status", + arguments=[], + prefix=True, +) + + +_status_matcher.shortcut( + "用户ban列表", + command="ban-status", + arguments=["--user"], + prefix=True, +) + +_status_matcher.shortcut( + "群组ban列表", + command="ban-status", + arguments=["--group"], + prefix=True, +) @_status_matcher.handle() @@ -94,15 +137,20 @@ async def _( user_id: Match[str], group_id: Match[str], ): + filter_type = None + if arparma.find("user"): + filter_type = "user" + if arparma.find("group"): + filter_type = "group" _user_id = user_id.result if user_id.available else None _group_id = group_id.result if group_id.available else None - if image := await BanManage.build_ban_image(_user_id, _group_id): + if image := await BanManage.build_ban_image(filter_type): await Image(image.pic2bs4()).finish(reply=True) else: await Text("数据为空捏...").finish(reply=True) -@_matcher.assign("ban") +@_ban_matcher.handle() async def _( bot: Bot, session: EventSession, @@ -150,7 +198,7 @@ async def _( await Text(f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!").finish(reply=True) -@_matcher.assign("unban") +@_unban_matcher.handle() async def _( bot: Bot, session: EventSession, diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py index 7418eb47..4264258d 100644 --- a/zhenxun/builtin_plugins/admin/ban/_data_source.py +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -1,33 +1,35 @@ import time +from typing import Literal from nonebot_plugin_session import EventSession from zhenxun.models.ban_console import BanConsole from zhenxun.models.level_user import LevelUser -from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.image_utils import BuildImage, ImageTemplate class BanManage: @classmethod - async def build_ban_image(cls, user_id: str | None, group_id: str | None): + async def build_ban_image( + cls, + filter_type: Literal["group", "user"] | None, + ) -> BuildImage | None: + """构造Ban列表图片 + + 参数: + filter_type: 过滤类型 + + 返回: + BuildImage | None: Ban列表图片 + """ data_list = None - if not user_id and not group_id: + if not filter_type: data_list = await BanConsole.all() - elif user_id: - if group_id: - data_list = await BanConsole.filter( - user_id=user_id, group_id=group_id - ).all() - else: - data_list = await BanConsole.filter( - user_id=user_id, group_id__isnull=True - ).all() - else: - if group_id: - data_list = await BanConsole.filter( - user_id__isnull=True, group_id=group_id - ).all() + elif filter_type == "user": + data_list = await BanConsole.filter(group_id__isnull=True).all() + elif filter_type == "group": + data_list = await BanConsole.filter(user_id__isnull=True).all() if not data_list: return None column_name = [ @@ -41,8 +43,8 @@ class BanManage: row_data = [] for data in data_list: duration = int((data.ban_time + data.duration - time.time()) / 60) - if duration < 0: - duration = 0 + if data.duration < 0: + duration = "∞" row_data.append( [ data.id, diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 81a7bff8..f183c354 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -19,11 +19,28 @@ __plugin_meta__ = PluginMetadata( name="功能开关", description="对群组内的功能限制,超级用户可以对群组以及全局的功能被动开关限制", usage=""" - 开启/关闭[功能] - 群被动状态 - 开启全部被动 - 关闭全部被动 - 醒来/休息吧 + 普通管理员 + 格式: + 开启/关闭[功能名称] : 开关功能 + 群被动状态 : 查看被动技能开关状态 + 醒来 : 结束休眠 + 休息吧 : 群组休眠, 不会再响应命令 + + 示例: + 开启签到 : 开启签到 + 关闭签到 : 关闭签到 + + 超级管理员额外命令 + 格式: + 插件列表 + 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] + + 私聊下: + 示例: + 开启签到 : 全局开启签到 + 关闭签到 : 全局关闭签到 + 关闭签到 p : 全局私聊关闭签到 + 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) """.strip(), extra=PluginExtraData( author="HibiKier", @@ -43,9 +60,6 @@ __plugin_meta__ = PluginMetadata( ) -# TODO: shortcut - - @_status_matcher.assign("$main") async def _(bot: Bot, session: EventSession, arparma: Arparma): image = None diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index 0ed95b66..41e1811a 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -40,8 +40,6 @@ _status_matcher = on_alconna( block=True, ) -# TODO: shortcut - _group_status_matcher = on_alconna( Alconna("group-status", Args["status", ["sleep", "wake"]]), rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index 8b8cb697..d9e17111 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -5,10 +5,12 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import ( Alconna, + AlconnaMatch, Args, Arparma, Match, Option, + Query, on_alconna, store_true, ) @@ -39,13 +41,20 @@ __plugin_meta__ = PluginMetadata( _matcher = on_alconna( Alconna( "消息排行", - Option("--des", default=False, action=store_true), + Option("--des", action=store_true, help_text="逆序"), Args["type?", ["日", "周", "月", "年"]]["count?", int, 10], ), priority=5, block=True, ) +_matcher.shortcut( + r"(?P.+)?消息排行", + command="消息排行", + arguments=["type", "{type}"], + prefix=True, +) + @_matcher.handle() async def _( @@ -53,11 +62,9 @@ async def _( session: EventSession, arparma: Arparma, type: Match[str], - count: Match[int], + count: Query[int] = Query("count", 10), ): group_id = session.id3 or session.id2 - if not group_id: - await Text("群组id为空...").finish() time_now = datetime.now() date_scope = None zero_today = time_now - timedelta( diff --git a/zhenxun/models/chat_history.py b/zhenxun/models/chat_history.py index 117397ce..02425987 100644 --- a/zhenxun/models/chat_history.py +++ b/zhenxun/models/chat_history.py @@ -34,7 +34,7 @@ class ChatHistory(Model): @classmethod async def get_group_msg_rank( cls, - gid: str, + gid: str | None, limit: int = 10, order: str = "DESC", date_scope: tuple[datetime, datetime] | None = None, @@ -48,7 +48,7 @@ class ChatHistory(Model): date_scope: 日期范围 """ o = "-" if order == "DESC" else "" - query = cls.filter(group_id=gid) + query = cls.filter(group_id=gid) if gid else cls if date_scope: query = query.filter(create_time__range=date_scope) return list( @@ -60,18 +60,23 @@ class ChatHistory(Model): ) # type: ignore @classmethod - async def get_group_first_msg_datetime(cls, group_id: str) -> datetime | None: + async def get_group_first_msg_datetime( + cls, group_id: str | None + ) -> datetime | None: """获取群第一条记录消息时间 参数: group_id: 群组id """ - if ( - message := await cls.filter(group_id=group_id) - .order_by("create_time") - .first() - ): + if group_id: + message = ( + await cls.filter(group_id=group_id).order_by("create_time").first() + ) + else: + message = await cls.all().order_by("create_time").first() + if message: return message.create_time + return None @classmethod async def get_message( From 993ff8113050dfcf13e840225de4d6bc2c36cea8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 27 Feb 2024 16:12:56 +0800 Subject: [PATCH 006/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0shor?= =?UTF-8?q?tcut=E5=92=8Cusage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_history/chat_message_handle.py | 18 ++- zhenxun/builtin_plugins/help/_utils.py | 6 +- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 2 +- zhenxun/builtin_plugins/sign_in/__init__.py | 17 ++- .../superuser/broadcast/_data_source.py | 1 + zhenxun/builtin_plugins/superuser/exec_sql.py | 89 +++++++------ .../builtin_plugins/superuser/group_manage.py | 70 +++++++++-- zhenxun/utils/_image_template.py | 117 +++++++++++++++++- 8 files changed, 255 insertions(+), 65 deletions(-) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index d9e17111..22e97cf8 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -25,9 +25,20 @@ from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import ImageTemplate __plugin_meta__ = PluginMetadata( - name="消息统计查询", + name="消息统计", description="消息统计查询", - usage="", + usage=""" + 格式: + 消息排行 ?[type [日,周,月,年]] ?[--des] + + 快捷: + [日,周,月,年]消息排行 + + 示例: + 消息排行 : 所有记录排行 + 日消息排行 : 今日记录排行 + 消息排行 周 --des : 逆序周记录排行 + """.strip(), extra=PluginExtraData( author="HibiKier", version="0.1", @@ -36,7 +47,6 @@ __plugin_meta__ = PluginMetadata( ).dict(), ) -# TODO: shortcut _matcher = on_alconna( Alconna( @@ -51,7 +61,7 @@ _matcher = on_alconna( _matcher.shortcut( r"(?P.+)?消息排行", command="消息排行", - arguments=["type", "{type}"], + arguments=["{type}"], prefix=True, ) diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py index 82aae886..9c49d2a2 100644 --- a/zhenxun/builtin_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -54,7 +54,7 @@ class HelpImageBuild: self._sort_data[menu_type] = [] self._sort_data[menu_type].append(plugin) - async def build_image(self, group_id: int | None): + async def build_image(self, group_id: str | None): if group_id: help_image = GROUP_HELP_PATH / f"{group_id}.png" else: @@ -68,7 +68,7 @@ class HelpImageBuild: img = await self.build_pil_image(group_id) await img.save(help_image) - async def build_html_image(self, group_id: int | None) -> bytes: + async def build_html_image(self, group_id: str | None) -> bytes: from nonebot_plugin_htmlrender import template_to_pic await self.sort_type() @@ -133,7 +133,7 @@ class HelpImageBuild: ) return pic - async def build_pil_image(self, group_id: int | None) -> BuildImage: + async def build_pil_image(self, group_id: str | None) -> BuildImage: """构造帮助图片 参数: diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index fffc1b80..f64b740e 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -77,7 +77,7 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") if user_id: command = state["_prefix"]["raw_command"] - if state["_alc_result"]: + if state.get("_alc_result"): command = state["_alc_result"].source.command if command: if _blmt.check(f"{user_id}__{command}"): diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index cb1155f2..a9452cb5 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -25,7 +25,8 @@ __plugin_meta__ = PluginMetadata( usage=""" 每日签到 会影响色图概率和开箱次数,以及签到的随机道具获取 - 指令: + 指令: + 签到 我的签到 好感度排行 * 签到时有 3% 概率 * 2 * @@ -88,7 +89,19 @@ _sign_matcher = on_alconna( block=True, ) -# TODO: shortcut +_sign_matcher.shortcut( + "我的签到", + command="签到", + arguments=["--my"], + prefix=True, +) + +_sign_matcher.shortcut( + "好感度排行", + command="签到", + arguments=["--list"], + prefix=True, +) @_sign_matcher.assign("$main") diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 967fee82..1833aae3 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -107,6 +107,7 @@ class BroadcastManage: ] return channel_id_list if isinstance(bot, KaiheilaBot): + # TODO: kaiheila获取群组列表 pass # group_list = await bot.guild_list() # if group_list.guilds: diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py index e4f81915..471ee775 100644 --- a/zhenxun/builtin_plugins/superuser/exec_sql.py +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -1,18 +1,9 @@ +from nonebot import on_command from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import ( - Alconna, - AlconnaQuery, - Args, - Arparma, - Match, - Option, - Query, - on_alconna, - store_true, -) -from nonebot_plugin_saa import Text +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from tortoise import Tortoise @@ -20,6 +11,7 @@ from zhenxun.configs.utils import PluginExtraData from zhenxun.services.db_context import TestSQL from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import ImageTemplate __plugin_meta__ = PluginMetadata( name="数据库操作", @@ -35,44 +27,59 @@ __plugin_meta__ = PluginMetadata( ).dict(), ) - -_matcher = on_alconna( - Alconna( - "exec", - Args["sql?", str], - Option("-l|--list", action=store_true, help_text="查看数据表"), - ), +_matcher = on_command( + "exec", rule=to_me(), permission=SUPERUSER, priority=1, block=True, ) +_table_matcher = on_command( + "查看所有表", + rule=to_me(), + permission=SUPERUSER, + priority=1, + block=True, +) + +SELECT_TABLE_SQL = """ +select a.tablename as name,d.description as desc from pg_tables a + left join pg_class c on relname=tablename + left join pg_description d on oid=objoid and objsubid=0 where a.schemaname = 'public' +""" + @_matcher.handle() -async def _( - sql: Match[str], - session: EventSession, - arparma: Arparma, - query_list: Query[bool] = AlconnaQuery("list.value", False), -): - db = Tortoise.get_connection("default") - if query_list.result: - query = await db.execute_query_dict( - "select tablename from pg_tables where schemaname = 'public'" - ) - msg = "数据库中的所有表名:\n" - for tablename in query: - msg += str(tablename["tablename"]) + "\n" - logger.info("查看数据库所有表", arparma.header_result, session=session) - await Text(msg[:-1]).finish() - else: - if not sql.available: - await Text("必须带有需要执行的 SQL 语句...").finish() - sql_text = sql.result +async def _(session: EventSession, message: UniMsg): + sql_text = message.extract_plain_text().strip() + if sql_text.startswith("exec"): + sql_text = sql_text[4:] + logger.info(f"执行SQL语句: {sql_text}", "exec", session=session) + try: if not sql_text.lower().startswith("select"): await TestSQL.raw(sql_text) - await Text("执行 SQL 语句成功!").finish() else: + db = Tortoise.get_connection("default") res = await db.execute_query_dict(sql_text) - # TODO: Alconna空格sql无法接收 + except Exception as e: + logger.error("执行 SQL 语句失败...", session=session, e=e) + await Text(f"执行 SQL 语句失败... {type(e)}").finish() + await Text("执行 SQL 语句成功!").finish() + + +@_table_matcher.handle() +async def _(session: EventSession): + try: + db = Tortoise.get_connection("default") + query = await db.execute_query_dict(SELECT_TABLE_SQL) + column_name = ["表名", "简介"] + data_list = [] + for table in query: + data_list.append([table["name"], table["desc"]]) + logger.info("查看数据库所有表", "查看所有表", session=session) + table = await ImageTemplate.table_page("数据库表", "", column_name, data_list) + await Image(table.pic2bs4()).send() + except Exception as e: + logger.error("获取表数据失败...", session=session, e=e) + await Text(f"获取表数据失败... {type(e)}").send() diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py index 0531962e..c63118c1 100644 --- a/zhenxun/builtin_plugins/superuser/group_manage.py +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -30,13 +30,25 @@ __plugin_meta__ = PluginMetadata( 群权限 | 群白名单 | 退出群 操作 退群,添加/删除群白名单,添加/删除群认证,当在群组中这五个命令且没有指定群号时,默认指定当前群组 指令: - 退群 ?[group_id] - 修改群权限 [group_id] [等级] - 修改群权限 [等级]: 该命令仅在群组时生效,默认修改当前群组 - 添加群白名单 ?*[group_id] - 删除群白名单 ?*[group_id] - 添加群认证 ?*[group_id] - 删除群认证 ?*[group_id] + 格式: + group-manage modify-level [权限等级] ?[群组Id] : 修改群权限 + group-manage super-handle [群组Id] [--del 删除操作] : 添加/删除群白名单 + group-manage auth-handle [群组Id] [--del 删除操作] : 添加/删除群认证 + group-manage del-group [群组Id] : 退出指定群 + + 快捷: + group-manage modify-level : 修改群权限 + group-manage super-handle : 添加/删除群白名单 + group-manage auth-handle : 添加/删除群认证 + group-manage del-group : 退群 + + 示例: + 修改群权限 7 : 在群组中修改当前群组权限为7 + group-manage modify-level 7 : 在群组中修改当前群组权限为7 + group-manage modify-level 7 1234556 : 修改 123456 群组的权限等级为7 + 添加/删除群白名单 1234567 : 添加/删除 1234567 为群白名单 + 添加/删除群认证 1234567 : 添加/删除 1234567 为群认证 + 退群 12344566 : 退出指定群组 """.strip(), extra=PluginExtraData( author="HibiKier", @@ -62,7 +74,7 @@ _matcher = on_alconna( "auth-handle", Option("--del", action=store_true, help_text="删除"), Args["group_id", int], - help_text="添加群白名单", + help_text="添加/删除群认证", ), Subcommand("del-group", Args["group_id", int], help_text="退出群组"), ), @@ -71,7 +83,47 @@ _matcher = on_alconna( block=True, ) -# TODO: shortcut +_matcher.shortcut( + "修改群权限", + command="group-manage", + arguments=["modify-level", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "添加群白名单", + command="group-manage", + arguments=["super-handle", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "删除群白名单", + command="group-manage", + arguments=["super-handle", "{%0}", "--del"], + prefix=True, +) + +_matcher.shortcut( + "添加群认证", + command="group-manage", + arguments=["auth-handle", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "删除群认证", + command="group-manage", + arguments=["auth-handle", "{%0}", "--del"], + prefix=True, +) + +_matcher.shortcut( + "退群", + command="group-manage", + arguments=["del-group", "{%0}"], + prefix=True, +) def CheckGroupId(): diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py index 3c894cab..a5ea836a 100644 --- a/zhenxun/utils/_image_template.py +++ b/zhenxun/utils/_image_template.py @@ -1,9 +1,9 @@ -from email.mime import image +import random from io import BytesIO from pathlib import Path -from typing import Any, Callable +from typing import Any, Callable, Dict -from nonebot.plugin import PluginMetadata +from fastapi import background from PIL.ImageFont import FreeTypeFont from pydantic import BaseModel @@ -25,13 +25,60 @@ class RowStyle(BaseModel): class ImageTemplate: + color_list = ["#C2CEFE", "#FFA94C", "#3FE6A0", "#D1D4F5"] + + @classmethod + async def hl_page( + cls, + head_text: str, + items: Dict[str, str], + row_space: int = 10, + padding: int = 30, + ) -> BuildImage: + font = BuildImage.load_font("HYWenHei-85W.ttf", 20) + width, height = BuildImage.get_text_size(head_text, font) + for title, item in items.items(): + title_width, title_height = await cls.__get_text_size(title, font) + it_width, it_height = await cls.__get_text_size(item, font) + width = max([width, title_width, it_width]) + height += title_height + it_height + width = max([width + padding * 2 + 100, 300]) + height = max([height + padding * 2 + 150, 100]) + A = BuildImage(width + padding * 2, height + padding * 2, color="#FAF9FE") + top_head = BuildImage(width, 100, color="#FFFFFF", font_size=40) + await top_head.line((0, 1, width, 1), "#C2CEFE", 2) + await top_head.text((15, 20), "签到", "#9FA3B2", "center") + await top_head.circle_corner() + await A.paste(top_head, (0, 20), "width") + _min_width = top_head.width - 60 + cur_h = top_head.height + 35 + row_space * len(items) + for title, item in items.items(): + title_width, title_height = BuildImage.get_text_size(title, font) + title_background = BuildImage( + title_width + 6, title_height + 10, font=font, color="#C1CDFF" + ) + await title_background.text((3, 5), title) + await title_background.circle_corner(5) + _text_width, _text_height = await cls.__get_text_size(item, font) + _width = max([title_background.width, _text_width, _min_width]) + text_image = await cls.__build_text_image( + item, _width, _text_height, font, color="#FDFCFA" + ) + B = BuildImage(_width + 20, title_height + text_image.height + 40) + await B.paste(title_background, (10, 10)) + await B.paste(text_image, (10, 20 + title_background.height)) + await B.line((0, 0, 0, B.height), random.choice(cls.color_list)) + await A.paste(B, (0, cur_h), "width") + cur_h += B.height + row_space + return A + @classmethod async def table_page( cls, head_text: str, tip_text: str | None, column_name: list[str], - data_list: list[list[str]], + data_list: list[list[str | tuple[Path | BuildImage, int, int]]], row_space: int = 35, column_space: int = 30, padding: int = 5, @@ -71,7 +118,7 @@ class ImageTemplate: async def table( cls, column_name: list[str], - data_list: list[list[str | tuple[Path, int, int]]], + data_list: list[list[str | tuple[Path | BuildImage, int, int]]], row_space: int = 25, column_space: int = 10, padding: int = 5, @@ -154,3 +201,63 @@ class ImageTemplate: return await BuildImage.auto_paste( column_image_list, len(column_image_list), column_space ) + + @classmethod + async def __build_text_image( + cls, + text: str, + width: int, + height: int, + font: FreeTypeFont, + font_color: str | tuple[int, int, int] = (0, 0, 0), + color: str | tuple[int, int, int] = (255, 255, 255), + ) -> BuildImage: + """文本转图片 + + 参数: + text: 文本 + width: 宽度 + height: 长度 + font: 字体 + font_color: 文本颜色 + color: 背景颜色 + + 返回: + BuildImage: 文本转图片 + """ + _, h = BuildImage.get_text_size("A", font) + A = BuildImage(width, height, color=color) + cur_h = 0 + for s in text.split("\n"): + text_image = await BuildImage.build_text_image( + s, font, font_color=font_color + ) + await A.paste(text_image, (0, cur_h)) + cur_h += h + return A + + @classmethod + async def __get_text_size( + cls, + text: str, + font: FreeTypeFont, + ) -> tuple[int, int]: + """获取文本所占大小 + + 参数: + text: 文本 + font: 字体 + + 返回: + tuple[int, int]: 宽, 高 + """ + width = 0 + height = 0 + _, h = BuildImage.get_text_size("A", font) + image_list = [] + for s in text.split("\n"): + s = s.strip() or "A" + w, _ = BuildImage.get_text_size(s, font) + width = width if width > w else w + height += h + return width, height From f6bc6f3481dc91d2f9b67ceafc1d7b07b93f8cb8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 27 Feb 2024 16:13:06 +0800 Subject: [PATCH 007/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8D=95=E4=B8=AA=E6=8F=92=E4=BB=B6=E5=B8=AE=E5=8A=A9=E5=9B=BE?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help/__init__.py | 13 +++++++------ zhenxun/builtin_plugins/help/_data_source.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 41c190b2..4f2c87a1 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -8,6 +8,7 @@ from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage from ._data_source import create_help_img, get_plugin_help from ._utils import GROUP_HELP_PATH @@ -47,20 +48,20 @@ _matcher = on_alconna( block=True, ) -# TODO: 插件使用详情 图片形式的帮助回复 - @_matcher.handle() async def _( name: Match[str], session: EventSession, ): - if name.available: - if text := await get_plugin_help(name.result): - await Text(text).send(reply=True) + if result := await get_plugin_help(name.result): + if isinstance(result, BuildImage): + await Image(result.pic2bs4()).send(reply=True) + else: + await Text(result).send(reply=True) else: - await Text("没有此功能的帮助信息...").send() + await Text("没有此功能的帮助信息...").send(reply=True) logger.info( f"查看帮助详情: {name.result}", "帮助", diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py index 75d8b66e..0f6a010f 100644 --- a/zhenxun/builtin_plugins/help/_data_source.py +++ b/zhenxun/builtin_plugins/help/_data_source.py @@ -2,7 +2,7 @@ import nonebot from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.models.plugin_info import PluginInfo -from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.image_utils import BuildImage, ImageTemplate from ._utils import HelpImageBuild @@ -11,7 +11,7 @@ random_bk_path = IMAGE_PATH / "background" / "help" / "simple_help" background = IMAGE_PATH / "background" / "0.png" -async def create_help_img(group_id: int | None): +async def create_help_img(group_id: str | None): """ 说明: 生成帮助图片 @@ -21,7 +21,7 @@ async def create_help_img(group_id: int | None): await HelpImageBuild().build_image(group_id) -async def get_plugin_help(name: str) -> str: +async def get_plugin_help(name: str) -> str | BuildImage: """获取功能的帮助信息 参数: @@ -30,6 +30,10 @@ async def get_plugin_help(name: str) -> str: if plugin := await PluginInfo.get_or_none(name=name): _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) if _plugin and _plugin.metadata: - return _plugin.metadata.usage + items = { + "简介": _plugin.metadata.description, + "用法": _plugin.metadata.usage, + } + return await ImageTemplate.hl_page(name, items) return "糟糕! 该功能没有帮助喔..." return "没有查找到这个功能噢..." From 9f17a525f1f1cc233f14f714d2c5c5aacc926f17 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 27 Feb 2024 21:43:40 +0800 Subject: [PATCH 008/132] =?UTF-8?q?perf=F0=9F=91=8C:=20=E6=B7=BB=E5=8A=A0s?= =?UTF-8?q?hop=E7=9A=84shortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/shop/__init__.py | 42 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py index 9ea6a6cb..00e9c7e0 100644 --- a/zhenxun/builtin_plugins/shop/__init__.py +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -16,13 +16,10 @@ __plugin_meta__ = PluginMetadata( usage=""" 商品操作 指令: - 添加商品 name:[名称] price:[价格] des:[描述] ?discount:[折扣](小数) ?limit_time:[限时时间](小时) - 删除商品 [名称或序号] - 修改商品 name:[名称或序号] price:[价格] des:[描述] discount:[折扣] limit_time:[限时] - 示例:添加商品 name:萝莉酒杯 price:9999 des:普通的酒杯,但是里面.. discount:0.4 limit_time:90 - 示例:添加商品 name:可疑的药 price:5 des:效果未知 - 示例:删除商品 2 - 示例:修改商品 name:1 price:900 修改序号为1的商品的价格为900 + 我的金币 + 我的道具 + 使用道具 [名称/Id] + 购买道具 [名称/Id] * 修改商品只需添加需要值即可 * """.strip(), extra=PluginExtraData( @@ -34,11 +31,10 @@ __plugin_meta__ = PluginMetadata( ).dict(), ) -# TODO: 修改操作,shortcut _matcher = on_alconna( Alconna( - "shop", + "商店", Subcommand("my-cost", help_text="我的金币"), Subcommand("my-props", help_text="我的道具"), Subcommand("buy", Args["name", str]["num", int, 1], help_text="购买道具"), @@ -48,6 +44,34 @@ _matcher = on_alconna( block=True, ) +_matcher.shortcut( + "我的金币", + command="商店", + arguments=["my-cost"], + prefix=True, +) + +_matcher.shortcut( + "我的道具", + command="商店", + arguments=["my-props"], + prefix=True, +) + +_matcher.shortcut( + "购买道具", + command="商店", + arguments=["buy", "{%0}"], + prefix=True, +) + +_matcher.shortcut( + "使用道具", + command="商店", + arguments=["use", "{%0}"], + prefix=True, +) + @_matcher.assign("$main") async def _(session: EventSession, arparma: Arparma): From aa68553539fac403c3c420286ba576acc3bfce20 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 28 Feb 2024 00:38:54 +0800 Subject: [PATCH 009/132] =?UTF-8?q?fix=F0=9F=90=9B:=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=9B=BE=E7=89=87bytes=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/ban/__init__.py | 2 +- .../admin/plugin_switch/__init__.py | 4 +- .../chat_history/chat_message_handle.py | 2 +- zhenxun/builtin_plugins/help/__init__.py | 4 +- zhenxun/builtin_plugins/help/_utils.py | 65 +++++++++---------- zhenxun/builtin_plugins/nickname.py | 2 +- zhenxun/builtin_plugins/shop/__init__.py | 5 +- zhenxun/builtin_plugins/sign_in/__init__.py | 2 +- zhenxun/builtin_plugins/superuser/exec_sql.py | 2 +- .../superuser/request_manage.py | 4 +- zhenxun/utils/_build_image.py | 10 +-- zhenxun/utils/_image_template.py | 13 +++- 12 files changed, 64 insertions(+), 51 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 0aa3c57d..11a0608b 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -145,7 +145,7 @@ async def _( _user_id = user_id.result if user_id.available else None _group_id = group_id.result if group_id.available else None if image := await BanManage.build_ban_image(filter_type): - await Image(image.pic2bs4()).finish(reply=True) + await Image(image.pic2bytes()).finish(reply=True) else: await Text("数据为空捏...").finish(reply=True) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index f183c354..1165dead 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -66,7 +66,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma): if session.id1 in bot.config.superusers: image = await build_plugin() if image: - await Image(image.pic2bs4()).send(reply=True) + await Image(image.pic2bytes()).send(reply=True) logger.info( f"查看功能列表", arparma.header_result, @@ -78,7 +78,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma): async def _(bot: Bot, session: EventSession, arparma: Arparma): image = None if image := await build_task(session.id3 or session.id2): - await Image(image.pic2bs4()).send(reply=True) + await Image(image.pic2bytes()).send(reply=True) logger.info( f"查看被动列表", arparma.header_result, diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index 22e97cf8..c7766eee 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -119,7 +119,7 @@ async def _( logger.info( f"查看消息排行 数量={count.result}", arparma.header_result, session=session ) - await Image(A.pic2bs4()).finish(reply=True) + await Image(A.pic2bytes()).finish(reply=True) await Text("群组消息记录为空...").finish() diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 4f2c87a1..8f6fd606 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -1,3 +1,5 @@ +from pathlib import Path + from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna @@ -57,7 +59,7 @@ async def _( if name.available: if result := await get_plugin_help(name.result): if isinstance(result, BuildImage): - await Image(result.pic2bs4()).send(reply=True) + await Image(result.pic2bytes()).send(reply=True) else: await Text(result).send(reply=True) else: diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py index 9c49d2a2..9b2e783d 100644 --- a/zhenxun/builtin_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -171,39 +171,38 @@ class HelpImageBuild: color="white" if not idx % 2 else "black", ) curr_h = 10 - if group := await GroupConsole.get_or_none(group_id=group_id): - for i, plugin in enumerate(plugin_list): - text_color = (255, 255, 255) if idx % 2 else (0, 0, 0) - if f"{plugin.module}," in group.block_plugin: - text_color = (252, 75, 13) - pos = None - # 禁用状态划线 - if ( - plugin.block_type in [BlockType.ALL, BlockType.GROUP] - or f"{plugin.module}:super," in group.block_plugin - ): - w = curr_h + int(B.getsize(plugin.name)[1] / 2) + 2 - pos = ( - 7, - w, - B.getsize(plugin.name)[0] + 35, - w, - ) - if build_type == "VV": - name_image = await self.build_name_image( # type: ignore - max_width, - plugin.name, - "black" if not idx % 2 else "white", - text_color, - pos, - ) - await B.paste(name_image, (0, curr_h), center_type="width") - curr_h += name_image.h + 5 - else: - await B.text((10, curr_h), f"{i + 1}.{plugin.name}", text_color) - if pos: - await B.line(pos, (236, 66, 7), 3) - curr_h += font_size + 5 + group = await GroupConsole.get_or_none(group_id=group_id) + for i, plugin in enumerate(plugin_list): + text_color = (255, 255, 255) if idx % 2 else (0, 0, 0) + if group and f"{plugin.module}," in group.block_plugin: + text_color = (252, 75, 13) + pos = None + # 禁用状态划线 + if plugin.block_type in [BlockType.ALL, BlockType.GROUP] or ( + group and f"super:{plugin.module}," in group.block_plugin + ): + w = curr_h + int(B.getsize(plugin.name)[1] / 2) + 2 + pos = ( + 7, + w, + B.getsize(plugin.name)[0] + 35, + w, + ) + if build_type == "VV": + name_image = await self.build_name_image( # type: ignore + max_width, + plugin.name, + "black" if not idx % 2 else "white", + text_color, + pos, + ) + await B.paste(name_image, (0, curr_h), center_type="width") + curr_h += name_image.h + 5 + else: + await B.text((10, curr_h), f"{i + 1}.{plugin.name}", text_color) + if pos: + await B.line(pos, (236, 66, 7), 3) + curr_h += font_size + 5 if menu_type == "normal": menu_type = "功能" await bk.text((0, 14), menu_type, center_type="width") diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py index d672db69..7a67c38c 100644 --- a/zhenxun/builtin_plugins/nickname.py +++ b/zhenxun/builtin_plugins/nickname.py @@ -34,7 +34,7 @@ __plugin_meta__ = PluginMetadata( author="HibiKier", version="0.1", plugin_type=PluginType.NORMAL, - menu_type="商店", + menu_type="其他", configs=[ RegisterConfig( key="BLACK_WORD", diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py index 00e9c7e0..1d71d1f6 100644 --- a/zhenxun/builtin_plugins/shop/__init__.py +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -20,7 +20,6 @@ __plugin_meta__ = PluginMetadata( 我的道具 使用道具 [名称/Id] 购买道具 [名称/Id] - * 修改商品只需添加需要值即可 * """.strip(), extra=PluginExtraData( author="HibiKier", @@ -77,7 +76,7 @@ _matcher.shortcut( async def _(session: EventSession, arparma: Arparma): image = await ShopManage.build_shop_image() logger.info("查看商店", arparma.header_result, session=session) - await Image(image.pic2bs4()).send() + await Image(image.pic2bytes()).send() @_matcher.assign("my-cost") @@ -101,7 +100,7 @@ async def _( user_info.user_displayname or user_info.user_name, session.platform, ): - await Image(image.pic2bs4()).finish(reply=True) + await Image(image.pic2bytes()).finish(reply=True) return await Text(f"你的道具为空捏...").send(reply=True) else: await Text(f"用户id为空...").send(reply=True) diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index a9452cb5..d6c1e40a 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -145,7 +145,7 @@ async def _( if session.id1: if image := await SignManage.rank(session.id1, num): logger.info("查看签到排行", arparma.header_result, session=session) - await Image(image.pic2bs4()).finish() + await Image(image.pic2bytes()).finish() return Text("用户id为空...").send() diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py index 471ee775..86118baa 100644 --- a/zhenxun/builtin_plugins/superuser/exec_sql.py +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -79,7 +79,7 @@ async def _(session: EventSession): data_list.append([table["name"], table["desc"]]) logger.info("查看数据库所有表", "查看所有表", session=session) table = await ImageTemplate.table_page("数据库表", "", column_name, data_list) - await Image(table.pic2bs4()).send() + await Image(table.pic2bytes()).send() except Exception as e: logger.error("获取表数据失败...", session=session, e=e) await Text(f"获取表数据失败... {type(e)}").send() diff --git a/zhenxun/builtin_plugins/superuser/request_manage.py b/zhenxun/builtin_plugins/superuser/request_manage.py index 24188197..71d6e37f 100644 --- a/zhenxun/builtin_plugins/superuser/request_manage.py +++ b/zhenxun/builtin_plugins/superuser/request_manage.py @@ -227,7 +227,7 @@ async def _( if not req_image_list: await Text("没有任何请求喔...").finish(reply=True) if len(req_image_list) == 1: - await Image(req_image_list[0].pic2bs4()).finish() + await Image(req_image_list[0].pic2bytes()).finish() width = sum([img.width for img in req_image_list]) height = max([img.height for img in req_image_list]) background = BuildImage(width, height) @@ -235,7 +235,7 @@ async def _( await req_image_list[1].line((0, 10, 1, req_image_list[1].height - 10), width=1) await background.paste(req_image_list[1], (req_image_list[1].width, 0)) logger.info("查看请求", arparma.header_result, session=session) - await Image(background.pic2bs4()).finish() + await Image(background.pic2bytes()).finish() await Text("没有任何请求喔...").finish(reply=True) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index 757bc5bc..5cf26e89 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -463,13 +463,15 @@ class BuildImage: base64_str = base64.b64encode(buf.getvalue()).decode() return "base64://" + base64_str - def pic2io(self) -> BytesIO: - """图片转 BytesIO + def pic2bytes(self) -> bytes: + """获取bytes 返回: - BytesIO: BytesIO + bytes: bytes """ - return BytesIO(self.tobytes()) + buf = BytesIO() + self.markImg.save(buf, format="PNG") + return buf.getvalue() def convert(self, type_: ModeType) -> Self: """ diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py index a5ea836a..6f160090 100644 --- a/zhenxun/utils/_image_template.py +++ b/zhenxun/utils/_image_template.py @@ -35,6 +35,17 @@ class ImageTemplate: row_space: int = 10, padding: int = 30, ) -> BuildImage: + """列文档 (如插件帮助) + + 参数: + head_text: 头标签文本 + items: 列内容 + row_space: 列间距. + padding: 间距. + + 返回: + BuildImage: 图片 + """ font = BuildImage.load_font("HYWenHei-85W.ttf", 20) width, height = BuildImage.get_text_size(head_text, font) for title, item in items.items(): @@ -47,7 +58,7 @@ class ImageTemplate: A = BuildImage(width + padding * 2, height + padding * 2, color="#FAF9FE") top_head = BuildImage(width, 100, color="#FFFFFF", font_size=40) await top_head.line((0, 1, width, 1), "#C2CEFE", 2) - await top_head.text((15, 20), "签到", "#9FA3B2", "center") + await top_head.text((15, 20), head_text, "#9FA3B2", "center") await top_head.circle_corner() await A.paste(top_head, (0, 20), "width") _min_width = top_head.width - 60 From 88bda9ce2c87ec9318f881e590014779ab659802 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 28 Feb 2024 13:51:16 +0800 Subject: [PATCH 010/132] =?UTF-8?q?perf=F0=9F=91=8C:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E7=BE=A4=E7=BB=84=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/auto_update_group.py | 36 ++------------ .../__init__.py => update_fg_info.py} | 9 ++-- .../_data_source.py => utils/platform.py} | 49 ++++++++++++------- 3 files changed, 38 insertions(+), 56 deletions(-) rename zhenxun/builtin_plugins/superuser/{update_fg_info/__init__.py => update_fg_info.py} (88%) rename zhenxun/{builtin_plugins/superuser/update_fg_info/_data_source.py => utils/platform.py} (77%) diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py index 1cfdacb3..f399248c 100644 --- a/zhenxun/builtin_plugins/scheduler/auto_update_group.py +++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py @@ -4,8 +4,7 @@ from nonebot_plugin_apscheduler import scheduler from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger - -# TODO: 其他平台更新 +from zhenxun.utils.platform import PlatformManage # 自动更新群组信息 @@ -19,21 +18,7 @@ async def _(): _used_group = [] for bot in bots.values(): try: - group_list = await bot.get_group_list() - gl = [g["group_id"] for g in group_list if g["group_id"] not in _used_group] - for g in gl: - _used_group.append(g) - group_info = await bot.get_group_info(group_id=g) - await GroupConsole.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) - logger.debug("自动更新群组信息成功", "自动更新群组", group_id=g) + await PlatformManage.update_group(bot) except Exception as e: logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) logger.info("自动更新群组成员信息成功...") @@ -47,22 +32,9 @@ async def _(): ) async def _(): bots = nonebot.get_bots() - for key in bots: + for bot in bots.values(): try: - bot = bots[key] - fl = await bot.get_friend_list() - for f in fl: - if FriendUser.exists(user_id=str(f["user_id"])): - await FriendUser.create( - user_id=str(f["user_id"]), user_name=f["nickname"] - ) - logger.debug( - f"更新好友信息成功", "自动更新好友", session=f["user_id"] - ) - else: - logger.debug( - f"好友信息已存在", "自动更新好友", session=f["user_id"] - ) + await PlatformManage.update_friend(bot) except Exception as e: logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) logger.info("自动更新好友信息成功...") diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py similarity index 88% rename from zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py rename to zhenxun/builtin_plugins/superuser/update_fg_info.py index 2c8bcca0..3c96a8e8 100644 --- a/zhenxun/builtin_plugins/superuser/update_fg_info/__init__.py +++ b/zhenxun/builtin_plugins/superuser/update_fg_info.py @@ -1,5 +1,4 @@ from nonebot.adapters import Bot -from nonebot.adapters.kaiheila.exception import ApiNotAvailable from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me @@ -8,11 +7,9 @@ from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData -from zhenxun.models.friend_user import FriendUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType - -from ._data_source import FgUpdateManage +from zhenxun.utils.platform import PlatformManage __plugin_meta__ = PluginMetadata( name="更新群组/好友信息", @@ -57,7 +54,7 @@ async def _( arparma: Arparma, ): try: - num = await FgUpdateManage.update_group(bot, session.platform) + num = await PlatformManage.update_group(bot) logger.info( f"更新群聊信息完成,共更新了 {num} 个群组的信息!", arparma.header_result, @@ -78,7 +75,7 @@ async def _( arparma: Arparma, ): try: - num = await FgUpdateManage.update_friend(bot, session.platform) + num = await PlatformManage.update_friend(bot, session.platform) logger.info( f"更新好友信息完成,共更新了 {num} 个好友的信息!", arparma.header_result, diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py b/zhenxun/utils/platform.py similarity index 77% rename from zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py rename to zhenxun/utils/platform.py index 646a1136..fc18a5f6 100644 --- a/zhenxun/builtin_plugins/superuser/update_fg_info/_data_source.py +++ b/zhenxun/utils/platform.py @@ -10,21 +10,21 @@ from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -class FgUpdateManage: +class PlatformManage: @classmethod - async def update_group(cls, bot: Bot, platform: str) -> int: + async def update_group(cls, bot: Bot) -> int: """更新群组信息 参数: bot: Bot - platform: 平台 返回: int: 更新个数 """ create_list = [] - if group_list := await cls.__get_group_list(bot): + group_list, platform = await cls.__get_group_list(bot) + if group_list: exists_group_list = await GroupConsole.all().values_list( "group_id", "channel_id" ) @@ -42,7 +42,7 @@ class FgUpdateManage: return len(create_list) @classmethod - async def __get_group_list(cls, bot: Bot) -> list[GroupConsole]: + async def __get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: """获取群组列表 参数: @@ -61,7 +61,7 @@ class FgUpdateManage: member_count=g["member_count"], ) for g in group_list - ] + ], "qq" if isinstance(bot, v12Bot): group_list = await bot.get_group_list() return [ @@ -70,7 +70,7 @@ class FgUpdateManage: user_name=g.group_name, # type: ignore ) for g in group_list - ] + ], "qq" if isinstance(bot, DodoBot): island_list = await bot.get_island_list() source_id_list = [ @@ -88,28 +88,41 @@ class FgUpdateManage: ) for c in channel_list ] - return group_list + return group_list, "dodo" if isinstance(bot, KaiheilaBot): - # TODO: kaiheila群组列表 - pass + group_list = [] + guilds = await bot.guild_list() + if guilds.guilds: + for guild_id, name in [(g.id_, g.name) for g in guilds.guilds if g.id_]: + view = await bot.guild_view(guild_id=guild_id) + group_list.append(GroupConsole(group_id=guild_id, group_name=name)) + if view.channels: + group_list += [ + GroupConsole( + group_id=guild_id, group_name=c.name, channel_id=c.id_ + ) + for c in view.channels + if c.type != 0 + ] + return group_list, "kaiheila" if isinstance(bot, DiscordBot): # TODO: discord群组列表 pass - return [] + return [], "" @classmethod - async def update_friend(cls, bot: Bot, platform: str) -> int: + async def update_friend(cls, bot: Bot) -> int: """更新好友信息 参数: bot: Bot - platform: 平台 返回: int: 更新个数 """ create_list = [] - if friend_list := await cls.__get_friend_list(bot): + friend_list, platform = await cls.__get_friend_list(bot) + if friend_list: user_id_list = await FriendUser.all().values_list("user_id", flat=True) for friend in friend_list: friend.platform = platform @@ -120,7 +133,7 @@ class FgUpdateManage: return len(create_list) @classmethod - async def __get_friend_list(cls, bot: Bot) -> list[FriendUser]: + async def __get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]: """获取好友列表 参数: @@ -134,7 +147,7 @@ class FgUpdateManage: return [ FriendUser(user_id=str(f["user_id"]), user_name=f["nickname"]) for f in friend_list - ] + ], "qq" if isinstance(bot, v12Bot): friend_list = await bot.get_friend_list() return [ @@ -143,7 +156,7 @@ class FgUpdateManage: user_name=f.user_displayname or f.user_remark or f.user_name, # type: ignore ) for f in friend_list - ] + ], "qq" if isinstance(bot, DodoBot): # TODO: dodo好友列表 pass @@ -153,4 +166,4 @@ class FgUpdateManage: if isinstance(bot, DiscordBot): # TODO: discord好友列表 pass - return [] + return [], "" From a2d6c7f951d25061e55d074740ed7f4c655b5c72 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 29 Feb 2024 03:07:31 +0800 Subject: [PATCH 011/132] =?UTF-8?q?perf=F0=9F=91=8C:=20=E5=B9=BF=E6=92=AD?= =?UTF-8?q?=E4=B8=8E=E5=85=A8=E5=B1=80=E6=8F=92=E4=BB=B6/=E8=A2=AB?= =?UTF-8?q?=E5=8A=A8=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 113 ++++++++----- .../admin/plugin_switch/_data_source.py | 112 ++++++++++++- .../admin/plugin_switch/command.py | 17 ++ zhenxun/builtin_plugins/scheduler/morning.py | 91 ++++++++--- .../superuser/broadcast/__init__.py | 8 +- .../superuser/broadcast/_data_source.py | 74 ++------- zhenxun/models/group_console.py | 6 + zhenxun/utils/platform.py | 150 +++++++++++++++++- 8 files changed, 433 insertions(+), 138 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 1165dead..59295c44 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -1,6 +1,6 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Arparma, Match +from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession @@ -21,14 +21,16 @@ __plugin_meta__ = PluginMetadata( usage=""" 普通管理员 格式: - 开启/关闭[功能名称] : 开关功能 - 群被动状态 : 查看被动技能开关状态 - 醒来 : 结束休眠 - 休息吧 : 群组休眠, 不会再响应命令 + 开启/关闭[功能名称] : 开关功能 + 开启/关闭群被动[被动名称] : 群被动开关 + 群被动状态 : 查看被动技能开关状态 + 醒来 : 结束休眠 + 休息吧 : 群组休眠, 不会再响应命令 示例: 开启签到 : 开启签到 关闭签到 : 关闭签到 + 开启群被动早晚安 : 关闭被动任务早晚安 超级管理员额外命令 格式: @@ -61,26 +63,21 @@ __plugin_meta__ = PluginMetadata( @_status_matcher.assign("$main") -async def _(bot: Bot, session: EventSession, arparma: Arparma): +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + task: Query[bool] = AlconnaQuery("task.value", False), +): image = None - if session.id1 in bot.config.superusers: + if task.result: + image = await build_task(session.id3 or session.id2) + elif session.id1 in bot.config.superusers: image = await build_plugin() if image: await Image(image.pic2bytes()).send(reply=True) logger.info( - f"查看功能列表", - arparma.header_result, - session=session, - ) - - -@_status_matcher.assign("task") -async def _(bot: Bot, session: EventSession, arparma: Arparma): - image = None - if image := await build_task(session.id3 or session.id2): - await Image(image.pic2bytes()).send(reply=True) - logger.info( - f"查看被动列表", + f"查看{'功能' if arparma.find('task') else '被动'}列表", arparma.header_result, session=session, ) @@ -93,21 +90,35 @@ async def _( arparma: Arparma, name: str, group: Match[str], + task: Query[bool] = AlconnaQuery("task.value", False), ): if gid := session.id3 or session.id2: - result = await PluginManage.block_group_plugin(name, gid) + if task.result: + result = await PluginManage.unblock_group_task(name, gid) + else: + result = await PluginManage.block_group_plugin(name, gid) await Text(result).send(reply=True) logger.info(f"开启功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None - result = await PluginManage.superuser_block(name, None, group_id) - await Text(result).send(reply=True) - logger.info( - f"超级用户开启功能 {name}", - arparma.header_result, - session=session, - target=group_id, - ) + if task.result: + result = await PluginManage.superuser_task_handle(name, group_id, True) + await Text(result).send(reply=True) + logger.info( + f"超级用户开启被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + result = await PluginManage.superuser_block(name, None, group_id) + await Text(result).send(reply=True) + logger.info( + f"超级用户开启功能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) @_status_matcher.assign("close") @@ -118,27 +129,41 @@ async def _( name: str, block_type: Match[str], group: Match[str], + task: Query[bool] = AlconnaQuery("task.value", False), ): if gid := session.id3 or session.id2: - result = await PluginManage.unblock_group_plugin(name, gid) + if task.result: + result = await PluginManage.block_group_task(name, gid) + else: + result = await PluginManage.unblock_group_plugin(name, gid) await Text(result).send(reply=True) logger.info(f"关闭功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None - _type = BlockType.ALL - if block_type.available: - if block_type.result in ["p", "private"]: - _type = BlockType.PRIVATE - elif block_type.result in ["g", "group"]: - _type = BlockType.GROUP - result = await PluginManage.superuser_block(name, _type, group_id) - await Text(result).send(reply=True) - logger.info( - f"超级用户关闭功能 {name}, 禁用类型: {_type}", - arparma.header_result, - session=session, - target=group_id, - ) + if task.result: + result = await PluginManage.superuser_task_handle(name, group_id, False) + await Text(result).send(reply=True) + logger.info( + f"超级用户关闭被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + _type = BlockType.ALL + if block_type.available: + if block_type.result in ["p", "private"]: + _type = BlockType.PRIVATE + elif block_type.result in ["g", "group"]: + _type = BlockType.GROUP + result = await PluginManage.superuser_block(name, _type, group_id) + await Text(result).send(reply=True) + logger.info( + f"超级用户关闭功能 {name}, 禁用类型: {_type}", + arparma.header_result, + session=session, + target=group_id, + ) @_group_status_matcher.handle() diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index b88cd607..5894cfbc 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -104,7 +104,9 @@ async def build_task(group_id: str | None) -> BuildImage: column_name = ["ID", "模块", "名称", "群组状态", "全局状态", "运行时间"] group = None if group_id: - group = await GroupConsole.get_or_none(group_id=group_id) + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) if not group: raise GroupInfoNotFound() else: @@ -145,17 +147,23 @@ class PluginManage: @classmethod async def is_wake(cls, group_id: str) -> bool: - if c := await GroupConsole.get_or_none(group_id=group_id): + if c := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): return c.status return False @classmethod async def sleep(cls, group_id: str): - await GroupConsole.filter(group_id=group_id).update(status=False) + await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( + status=False + ) @classmethod async def wake(cls, group_id: str): - await GroupConsole.filter(group_id=group_id).update(status=True) + await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( + status=True + ) @classmethod async def block(cls, module: str): @@ -178,6 +186,32 @@ class PluginManage: """ return await cls._change_group_plugin(plugin_name, group_id, True) + @classmethod + async def unblock_group_task(cls, task_name: str, group_id: str) -> str: + """启用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task(task_name, group_id, False) + + @classmethod + async def block_group_task(cls, task_name: str, group_id: str) -> str: + """禁用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task(task_name, group_id, True) + @classmethod async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: """启用群组插件 @@ -191,6 +225,33 @@ class PluginManage: """ return await cls._change_group_plugin(plugin_name, group_id, False) + @classmethod + async def _change_group_task( + cls, task_name: str, group_id: str, status: bool + ) -> str: + """改变群组被动技能状态 + + 参数: + task_name: 被动技能名称 + group_id: 群组Id + status: 状态 + + 返回: + str: 返回信息 + """ + if task := await TaskInfo.get_or_none(name=task_name): + status_str = "关闭" if status else "开启" + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task += f"{task.module}," + else: + group.block_task = group.block_task.replace(f"{task.module},", "") + await group.save(update_fields=["block_task"]) + return f"已成功{status_str} {task_name} 被动技能!" + return "没有找到这个被动技能喔..." + @classmethod async def _change_group_plugin( cls, plugin_name: str, group_id: str, status: bool @@ -212,7 +273,9 @@ class PluginManage: plugin = await PluginInfo.get_or_none(name=plugin_name) status_str = "开启" if status else "关闭" if plugin: - group, _ = await GroupConsole.get_or_create(group_id=group_id) + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) if status: if plugin.module in group.block_plugin: group.block_plugin = group.block_plugin.replace( @@ -228,11 +291,44 @@ class PluginManage: return f"该功能已经{status_str}了喔,不要重复{status_str}..." return "没有找到这个功能喔..." + @classmethod + async def superuser_task_handle( + cls, task_name: str, group_id: str | None, status: bool + ) -> str: + """超级用户禁用被动技能 + + 参数: + task_name: 被动技能名称 + group_id: 群组id + status: 状态 + + 返回: + str: 返回信息 + """ + if task := await TaskInfo.get_or_none(name=task_name): + status_str = "开启" if status else "关闭" + if group_id: + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task = group.block_task.replace( + f"super:{task.module},", "" + ) + else: + group.block_task += f"super:{task.module}," + await group.save(update_fields=["block_task"]) + else: + task.status = status + await task.save(update_fields=["status"]) + return f"已成功将被动技能 {task_name} 全局{status_str}!" + return "没有找到这个功能喔..." + @classmethod async def superuser_block( cls, plugin_name: str, block_type: BlockType | None, group_id: str | None ) -> str: - """超级用户禁用 + """超级用户禁用插件 参数: plugin_name: 插件名称 @@ -248,7 +344,9 @@ class PluginManage: plugin = await PluginInfo.get_or_none(name=plugin_name) if plugin: if group_id: - if group := await GroupConsole.get_or_none(group_id=group_id): + if group := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): if f"super:{plugin.module}," not in group.block_plugin: group.block_plugin += f"super:{plugin.module}," await group.save(update_fields=["block_plugin"]) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index 41e1811a..f1323227 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -63,6 +63,15 @@ _status_matcher.shortcut( prefix=True, ) + +_status_matcher.shortcut( + r"开启群被动(?P.+)", + command="switch", + arguments=["open", "{name}", "--task"], + prefix=True, +) + + _status_matcher.shortcut( r"开启(?P.+)", command="switch", @@ -70,6 +79,13 @@ _status_matcher.shortcut( prefix=True, ) +_status_matcher.shortcut( + r"关闭群被动(?P.+)", + command="switch", + arguments=["close", "{name}", "--task"], + prefix=True, +) + _status_matcher.shortcut( r"关闭(?P.+)", command="switch", @@ -77,6 +93,7 @@ _status_matcher.shortcut( prefix=True, ) + _group_status_matcher.shortcut( r"醒来", command="group-status", diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index 62964462..6ed2ff1c 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -1,28 +1,75 @@ +import nonebot +from nonebot.plugin import PluginMetadata from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Image -# TODO: 消息发送 +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData, Task +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.platform import broadcast_group -# # 早上好 -# @scheduler.scheduled_job( -# "cron", -# hour=6, -# minute=1, -# ) -# async def _(): -# img = image(IMAGE_PATH / "zhenxun" / "zao.jpg") -# await broadcast_group("[[_task|zwa]]早上好" + img, log_cmd="被动早晚安") -# logger.info("每日早安发送...") +__plugin_meta__ = PluginMetadata( + name="早晚安被动技能", + description="早晚安被动技能", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + tasks=[ + Task(module="group_welcome", name="进群欢迎"), + Task(module="refund_group_remind", name="退群提醒"), + ], + ).dict(), +) + +driver = nonebot.get_driver() + + +@driver.on_startup +async def _(): + if not await TaskInfo.exists(module="morning_goodnight"): + await TaskInfo.create( + module="morning_goodnight", + name="早晚安", + status=True, + ) + + +async def check(group_id: str, channel_id: str | None) -> bool: + task = await TaskInfo.get_or_none(module="morning_goodnight") + if not task or not task.status: + return False + return await GroupConsole.is_block_task(group_id, "morning_goodnight") + + +# 早上好 +@scheduler.scheduled_job( + "cron", + hour=6, + minute=1, +) +async def _(): + img = Image(IMAGE_PATH / "zhenxun" / "zao.jpg") + await broadcast_group("早上好" + img, log_cmd="被动早晚安", check_func=check) + logger.info("每日早安发送...") # # 睡觉了 -# @scheduler.scheduled_job( -# "cron", -# hour=23, -# minute=59, -# ) -# async def _(): -# img = image(IMAGE_PATH / "zhenxun" / "sleep.jpg") -# await broadcast_group( -# f"[[_task|zwa]]{NICKNAME}要睡觉了,你们也要早点睡呀" + img, log_cmd="被动早晚安" -# ) -# logger.info("每日晚安发送...") +@scheduler.scheduled_job( + "cron", + hour=23, + minute=59, +) +async def _(): + img = Image(IMAGE_PATH / "zhenxun" / "sleep.jpg") + await broadcast_group( + f"{NICKNAME}要睡觉了,你们也要早点睡呀" + img, + log_cmd="被动早晚安", + check_func=check, + ) + logger.info("每日晚安发送...") diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py index 271652a7..9b823f54 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py @@ -5,6 +5,7 @@ from nonebot.adapters import Bot from nonebot.params import Command from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Text as alcText from nonebot_plugin_alconna import UniMsg from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession @@ -51,8 +52,11 @@ async def _( message: UniMsg, command: Annotated[tuple[str, ...], Command()], ): - message[0].text = message[0].text.replace(command[0], "").strip() - # await Text("正在发送..请等一下哦!").send() + for msg in message: + if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]): + msg.text = msg.text.replace(command[0], "", 1).strip() + break + await Text("正在发送..请等一下哦!").send() count, error_count = await BroadcastManage.send(bot, message, session) result = f"成功广播 {count} 个群组" if error_count: diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 1833aae3..4781aff3 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -9,23 +9,13 @@ from nonebot_plugin_alconna import UniMsg from nonebot_plugin_saa import ( Image, MessageFactory, - TargetDoDoChannel, - TargetQQGroup, Text, ) from nonebot_plugin_session import EventSession -from pydantic import BaseModel from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger - - -class GroupChannel(BaseModel): - - group_id: str - """群组id""" - channel_id: str | None = None - """频道id""" +from zhenxun.utils.platform import PlatformManage class BroadcastManage: @@ -50,24 +40,27 @@ class BroadcastManage: message_list.append(Image(msg.url)) elif isinstance(msg, alc.Text): message_list.append(Text(msg.text)) - if group_list := await cls.__get_group_list(bot): + group_list, _ = await PlatformManage.get_group_list(bot) + if group_list: error_count = 0 for group in group_list: try: if not await GroupConsole.is_block_task( group.group_id, "broadcast", group.channel_id ): - if isinstance(bot, (v11Bot, v12Bot)): - target = TargetQQGroup(group_id=int(group.group_id)) - elif isinstance(bot, DodoBot): - target = TargetDoDoChannel(channel_id=group.channel_id) # type: ignore - await MessageFactory(message_list).send_to(target, bot) - logger.debug( - "发送成功", - "广播", - session=session, - target=f"{group.group_id}:{group.channel_id}", + target = PlatformManage.get_target( + bot, group.group_id, group.channel_id ) + if target: + await MessageFactory(message_list).send_to(target, bot) + logger.debug( + "发送成功", + "广播", + session=session, + target=f"{group.group_id}:{group.channel_id}", + ) + else: + logger.warning("target为空", "广播", session=session) except Exception as e: error_count += 1 logger.error( @@ -79,40 +72,3 @@ class BroadcastManage: ) return len(group_list) - error_count, error_count return 0, 0 - - @classmethod - async def __get_group_list(cls, bot: Bot) -> list[GroupChannel]: - """获取群组id列表 - - 参数: - bot: Bot - - 返回: - list[str]: 群组id列表 - """ - if isinstance(bot, (v11Bot, v12Bot)): - group_list = await bot.get_group_list() - return [GroupChannel(group_id=str(g["group_id"])) for g in group_list] - if isinstance(bot, DodoBot): - island_list = await bot.get_island_list() - source_id_list = [ - g.island_source_id for g in island_list if g.island_source_id - ] - channel_id_list = [] - for id in source_id_list: - channel_list = await bot.get_channel_list(island_source_id=id) - channel_id_list += [ - GroupChannel(group_id=id, channel_id=c.channel_id) - for c in channel_list - ] - return channel_id_list - if isinstance(bot, KaiheilaBot): - # TODO: kaiheila获取群组列表 - pass - # group_list = await bot.guild_list() - # if group_list.guilds: - # return [g.open_id for g in group_list.guilds if g.open_id] - if isinstance(bot, DiscordBot): - # TODO: discord获取群组列表 - pass - return [] diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py index 0e8858de..d5946396 100644 --- a/zhenxun/models/group_console.py +++ b/zhenxun/models/group_console.py @@ -108,6 +108,12 @@ class GroupConsole(Model): 返回: bool: 是否禁用被动 """ + if not channel_id: + return await cls.exists( + group_id=group_id, + channel_id__isnull=True, + block_task__contains=f"{task},", + ) return await cls.exists( group_id=group_id, channel_id=channel_id, block_task__contains=f"{task}," ) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index fc18a5f6..fb34e81f 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -1,9 +1,20 @@ +from typing import Awaitable, Callable, Literal, Set + +import nonebot from nonebot.adapters import Bot from nonebot.adapters.discord import Bot as DiscordBot from nonebot.adapters.dodo import Bot as DodoBot from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.utils import is_coroutine_callable +from nonebot_plugin_saa import ( + MessageFactory, + TargetDoDoChannel, + TargetKaiheilaChannel, + TargetQQGroup, + Text, +) from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole @@ -23,7 +34,7 @@ class PlatformManage: int: 更新个数 """ create_list = [] - group_list, platform = await cls.__get_group_list(bot) + group_list, platform = await cls.get_group_list(bot) if group_list: exists_group_list = await GroupConsole.all().values_list( "group_id", "channel_id" @@ -42,7 +53,25 @@ class PlatformManage: return len(create_list) @classmethod - async def __get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: + def get_platform(cls, bot: Bot) -> str | None: + """获取平台 + + 参数: + bot: Bot + + 返回: + str | None: 平台 + """ + if isinstance(bot, (v11Bot, v12Bot)): + return "qq" + if isinstance(bot, DodoBot): + return "dodo" + if isinstance(bot, KaiheilaBot): + return "kaiheila" + return None + + @classmethod + async def get_group_list(cls, bot: Bot) -> tuple[list[GroupConsole], str]: """获取群组列表 参数: @@ -121,7 +150,7 @@ class PlatformManage: int: 更新个数 """ create_list = [] - friend_list, platform = await cls.__get_friend_list(bot) + friend_list, platform = await cls.get_friend_list(bot) if friend_list: user_id_list = await FriendUser.all().values_list("user_id", flat=True) for friend in friend_list: @@ -133,7 +162,7 @@ class PlatformManage: return len(create_list) @classmethod - async def __get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]: + async def get_friend_list(cls, bot: Bot) -> tuple[list[FriendUser], str]: """获取好友列表 参数: @@ -167,3 +196,116 @@ class PlatformManage: # TODO: discord好友列表 pass return [], "" + + @classmethod + def get_target(cls, bot: Bot, group_id: str | None, channel_id: str | None): + """获取发生Target + + 参数: + bot: Bot + group_id: 群组id + channel_id: 频道id + + 返回: + target: 对应平台Target + """ + target = None + if isinstance(bot, (v11Bot, v12Bot)): + if group_id: + target = TargetQQGroup(group_id=int(group_id)) + if channel_id: + if isinstance(bot, DodoBot): + target = TargetDoDoChannel(channel_id=channel_id) + elif isinstance(bot, KaiheilaBot): + target = TargetKaiheilaChannel(channel_id=channel_id) + return target + + +async def broadcast_group( + message: str | MessageFactory, + bot: Bot | list[Bot] | None = None, + bot_id: str | Set[str] | None = None, + ignore_group: Set[int] | None = None, + check_func: Callable[[str, str | None], Awaitable] | None = None, + log_cmd: str | None = None, + platform: Literal["qq", "dodo", "kaiheila"] | None = None, +): + """获取所有Bot或指定Bot对象广播群聊 + + Args: + message: 广播消息内容 + bot: 指定bot对象. + bot_id: 指定bot id. + ignore_group: 忽略群聊列表. + check_func: 发送前对群聊检测方法,判断是否发送. + log_cmd: 日志标记. + platform: 指定平台 + """ + if platform and platform not in ["qq", "dodo", "kaiheila"]: + raise ValueError("指定平台不支持") + if not message: + raise ValueError("群聊广播消息不能为空") + bot_dict = nonebot.get_bots() + bot_list: list[Bot] = [] + if bot: + if isinstance(bot, list): + bot_list = bot + else: + bot_list.append(bot) + elif bot_id: + _bot_id_list = bot_id + if isinstance(bot_id, str): + _bot_id_list = [bot_id] + for id_ in _bot_id_list: + if bot_id in bot_dict: + bot_list.append(bot_dict[bot_id]) + else: + logger.warning(f"Bot:{id_} 对象未连接或不存在") + else: + bot_list = list(bot_dict.values()) + _used_group = [] + for _bot in bot_list: + try: + if platform and platform != PlatformManage.get_platform(_bot): + continue + group_list, _ = await PlatformManage.get_group_list(_bot) + if group_list: + for group in group_list: + key = f"{group.group_id}:{group.channel_id}" + try: + if ( + ignore_group + and ( + group.group_id in ignore_group + or group.channel_id in ignore_group + ) + ) or key in _used_group: + continue + is_continue = False + if check_func: + if is_coroutine_callable(check_func): + is_continue = not await check_func( + group.group_id, group.channel_id + ) + else: + is_continue = not check_func( + group.group_id, group.channel_id + ) + if is_continue: + continue + target = PlatformManage.get_target( + _bot, group.group_id, group.channel_id + ) + if target: + _used_group.append(key) + message_list = message + if isinstance(message, str): + message_list = MessageFactory([Text(message)]) + await MessageFactory(message_list).send_to(target, _bot) + logger.debug("发送成功", log_cmd, target=key) + else: + logger.warning("target为空", log_cmd, target=key) + except Exception as e: + logger.error("发送失败", log_cmd, target=key, e=e) + except Exception as e: + logger.error(f"Bot: {_bot.self_id} 获取群聊列表失败", command=log_cmd, e=e) From db96f46dcbf3d6f9cb71abadae0b4c43f204f37e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 4 Mar 2024 23:27:05 +0800 Subject: [PATCH 012/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 105 +++++++ zhenxun/builtin_plugins/hooks/__init__.py | 2 +- zhenxun/builtin_plugins/init/init_plugin.py | 1 + zhenxun/builtin_plugins/scripts.py | 13 +- zhenxun/builtin_plugins/shop/_data_source.py | 77 +++++- zhenxun/models/bag_user.py | 277 ++++++++++--------- zhenxun/models/goods_info.py | 7 +- zhenxun/models/sign_group_user.py | 81 ++++++ zhenxun/models/user_console.py | 2 +- zhenxun/plugins/__init__.py | 105 +++++++ zhenxun/services/db_context.py | 2 +- zhenxun/utils/decorator/shop.py | 240 ++++++++++------ 12 files changed, 682 insertions(+), 230 deletions(-) create mode 100644 zhenxun/models/sign_group_user.py create mode 100644 zhenxun/plugins/__init__.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 38776d55..d8ef9dba 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -1,6 +1,13 @@ import os from nonebot import require +from tortoise import Tortoise + +from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.decorator.shop import shop_register require("nonebot_plugin_apscheduler") require("nonebot_plugin_alconna") @@ -13,7 +20,105 @@ enable_auto_select_bot() from pathlib import Path import nonebot +import ujson as json path = Path(__file__).parent / "platform" for d in os.listdir(path): nonebot.load_plugins(str((path / d).resolve())) + + +driver = nonebot.get_driver() + +flag = True + +SIGN_SQL = """ +select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability, t1.specify_probability, t1.impression +from public.sign_group_users t1 + join ( + select user_id, max(t2.impression) as max_impression + from public.sign_group_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_impression = t1.impression +""" + +BAG_SQL = """ +select t1.user_id, t1.gold, t1.property +from public.bag_users t1 + join ( + select user_id, max(t2.gold) as max_gold + from public.bag_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_gold = t1.gold +""" + + +@driver.on_bot_connect +async def _(): + global flag + await shop_register.load_register() + if ( + flag + and not await UserConsole.annotate().count() + and not await SignUser.annotate().count() + ): + flag = False + db = Tortoise.get_connection("default") + old_sign_list = await db.execute_query_dict(SIGN_SQL) + old_bag_list = await db.execute_query_dict(BAG_SQL) + goods = { + g["goods_name"]: g["uuid"] + for g in await GoodsInfo.annotate().values("goods_name", "uuid") + } + create_list = [] + sign_id_list = [] + uid = await UserConsole.get_new_uid() + for old_sign in old_sign_list: + sign_id_list.append(old_sign["user_id"]) + old_bag = [b for b in old_bag_list if b["user_id"] == old_sign["user_id"]] + if old_bag: + old_bag = old_bag[0] + property = json.loads(old_bag["property"]) + props = {} + if property: + for name, num in property.items(): + if name in goods: + props[goods[name]] = num + create_list.append( + UserConsole( + user_id=old_sign["user_id"], + platform="qq", + uid=uid, + props=props, + gold=old_bag["gold"], + ) + ) + else: + create_list.append( + UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) + ) + uid += 1 + if create_list: + logger.info("开始迁移用户数据...") + await UserConsole.bulk_create(create_list, 10) + logger.info("迁移用户数据完成!") + create_list.clear() + uc_dict = {u.user_id: u for u in await UserConsole.all()} + for old_sign in old_sign_list: + user_console = uc_dict.get(old_sign["user_id"]) + if not user_console: + user_console = await UserConsole.get_user(old_sign["user_id"], "qq") + create_list.append( + SignUser( + user_id=old_sign["user_id"], + user_console=user_console, + platform="qq", + sign_count=old_sign["checkin_count"], + impression=old_sign["impression"], + add_probability=old_sign["add_probability"], + specify_probability=old_sign["specify_probability"], + ) + ) + if create_list: + logger.info("开始迁移签到数据...") + await SignUser.bulk_create(create_list, 10) + logger.info("迁移签到数据完成!") diff --git a/zhenxun/builtin_plugins/hooks/__init__.py b/zhenxun/builtin_plugins/hooks/__init__.py index 80aa7181..41912be2 100644 --- a/zhenxun/builtin_plugins/hooks/__init__.py +++ b/zhenxun/builtin_plugins/hooks/__init__.py @@ -40,4 +40,4 @@ Config.add_plugin_config( type=int, ) -nonebot.load_plugins(str(Path(__file__).parent.resolve())) +# nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 1f8caf39..7459d766 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -39,6 +39,7 @@ async def _handle_setting( setting = extra_data.setting or PluginSetting() if metadata.type == "library": extra_data.plugin_type = PluginType.HIDDEN + extra_data.menu_type = "" plugin_list.append( PluginInfo( module=plugin.name, diff --git a/zhenxun/builtin_plugins/scripts.py b/zhenxun/builtin_plugins/scripts.py index 13589454..d8abefe2 100644 --- a/zhenxun/builtin_plugins/scripts.py +++ b/zhenxun/builtin_plugins/scripts.py @@ -1,7 +1,6 @@ from asyncio.exceptions import TimeoutError import nonebot -import ujson as json from nonebot.drivers import Driver from nonebot_plugin_apscheduler import scheduler @@ -9,6 +8,12 @@ from zhenxun.configs.path_config import TEXT_PATH from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +try: + import ujson as json +except ModuleNotFoundError: + import json + + driver: Driver = nonebot.get_driver() @@ -40,13 +45,13 @@ async def update_city(): data[provinces_data[province]].append(city_data[city]) with open(china_city, "w", encoding="utf8") as f: json.dump(data, f, indent=4, ensure_ascii=False) - logger.info("自动更新城市列表完成...") + logger.info("自动更新城市列表完成.....") except TimeoutError as e: logger.warning("自动更新城市列表超时...", e=e) except ValueError as e: - logger.warning("自动城市列表失败...", e=e) + logger.warning("自动城市列表失败.....", e=e) except Exception as e: - logger.error(f"自动城市列表未知错误...", e=e) + logger.error(f"自动城市列表未知错误", e=e) # 自动更新城市列表 diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 6ecea8da..9c006965 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -1,5 +1,8 @@ import time -from typing import Dict +from typing import Any, Callable, Dict + +from nonebot.adapters import Event +from pydantic import BaseModel, create_model from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.models.goods_info import GoodsInfo @@ -13,8 +16,80 @@ from zhenxun.utils.image_utils import BuildImage, ImageTemplate, text2image ICON_PATH = IMAGE_PATH / "shop_icon" +class Goods(BaseModel): + + before_handle: list[Callable] = [] + after_handle: list[Callable] = [] + func: Callable | None = None + params: Any | None = None + send_success_msg: bool = True + max_num_limit: int = 1 + model: Any | None = None + + +class ShopParam(BaseModel): + + goods_name: str + """商品名称""" + user_id: int + """用户id""" + group_id: int + """群聊id""" + bot: Any + """bot""" + event: Event + """event""" + num: int + """道具单次使用数量""" + message: str + """message""" + text: str + """text""" + send_success_msg: bool = True + """是否发送使用成功信息""" + max_num_limit: int = 1 + """单次使用最大次数""" + + class ShopManage: + uuid2goods: Dict[str, Goods] = {} + + @classmethod + async def register_use( + cls, + uuid: str, + func: Callable, + send_success_msg: bool = True, + max_num_limit: int = 1, + before_handle: list[Callable] = [], + after_handle: list[Callable] = [], + **kwargs, + ): + """注册使用方法 + + 参数: + uuid: uuid + func: 使用函数 + send_success_msg: 使用成功时发送消息. + max_num_limit: 单次最大使用限制. + before_handle: 使用前函数. + after_handle: 使用后函数. + + 异常: + ValueError: 该商品使用函数已被注册! + """ + if uuid in cls.uuid2goods: + raise ValueError("该商品使用函数已被注册!") + kwargs["send_success_msg"] = send_success_msg + kwargs["max_num_limit"] = max_num_limit + cls.uuid2func = Goods( + model=create_model(f"{uuid}_model", __base__=ShopParam, **kwargs), + params=kwargs, + before_handle=before_handle, + after_handle=after_handle, + ) + @classmethod async def buy_prop( cls, user_id: str, name: str, num: int = 1, platform: str | None = None diff --git a/zhenxun/models/bag_user.py b/zhenxun/models/bag_user.py index 7f3b0322..711de8f7 100644 --- a/zhenxun/models/bag_user.py +++ b/zhenxun/models/bag_user.py @@ -1,160 +1,161 @@ -# from typing import Dict +from typing import Dict -# from services.db_context import Model -# from tortoise import fields +from tortoise import fields -# from .goods_info import GoodsInfo +from zhenxun.services.db_context import Model + +from .goods_info import GoodsInfo -# class BagUser(Model): +class BagUser(Model): -# id = fields.IntField(pk=True, generated=True, auto_increment=True) -# """自增id""" -# user_id = fields.CharField(255) -# """用户id""" -# group_id = fields.CharField(255) -# """群聊id""" -# gold = fields.IntField(default=100) -# """金币数量""" -# spend_total_gold = fields.IntField(default=0) -# """花费金币总数""" -# get_total_gold = fields.IntField(default=0) -# """获取金币总数""" -# get_today_gold = fields.IntField(default=0) -# """今日获取金币""" -# spend_today_gold = fields.IntField(default=0) -# """今日获取金币""" -# property: Dict[str, int] = fields.JSONField(default={}) # type: ignore -# """道具""" + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + gold = fields.IntField(default=100) + """金币数量""" + spend_total_gold = fields.IntField(default=0) + """花费金币总数""" + get_total_gold = fields.IntField(default=0) + """获取金币总数""" + get_today_gold = fields.IntField(default=0) + """今日获取金币""" + spend_today_gold = fields.IntField(default=0) + """今日获取金币""" + property: Dict[str, int] = fields.JSONField(default={}) # type: ignore + """道具""" -# class Meta: -# table = "bag_users" -# table_description = "用户道具数据表" -# unique_together = ("user_id", "group_id") + class Meta: + table = "bag_users" + table_description = "用户道具数据表" + unique_together = ("user_id", "group_id") -# @classmethod -# async def get_gold(cls, user_id: str, group_id: str) -> int: -# """获取当前金币 + @classmethod + async def get_gold(cls, user_id: str, group_id: str) -> int: + """获取当前金币 -# 参数: -# user_id: 用户id -# group_id: 所在群组id + 参数: + user_id: 用户id + group_id: 所在群组id -# 返回: -# int: 金币数量 -# """ -# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) -# return user.gold + 返回: + int: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + return user.gold -# @classmethod -# async def get_property( -# cls, user_id: str, group_id: str, only_active: bool = False -# ) -> Dict[str, int]: -# """获取当前道具 + @classmethod + async def get_property( + cls, user_id: str, group_id: str, only_active: bool = False + ) -> Dict[str, int]: + """获取当前道具 -# 参数: -# user_id: 用户id -# group_id: 所在群组id -# only_active: 仅仅获取主动使用的道具 + 参数: + user_id: 用户id + group_id: 所在群组id + only_active: 仅仅获取主动使用的道具 -# 返回: -# Dict[str, int]: 道具名称与数量 -# """ -# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) -# if only_active and user.property: -# data = {} -# name_list = [ -# x.goods_name -# for x in await GoodsInfo.get_all_goods() -# if not x.is_passive -# ] -# for key in [x for x in user.property if x in name_list]: -# data[key] = user.property[key] -# return data -# return user.property + 返回: + Dict[str, int]: 道具名称与数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + if only_active and user.property: + data = {} + name_list = [ + x.goods_name + for x in await GoodsInfo.get_all_goods() + if not x.is_passive + ] + for key in [x for x in user.property if x in name_list]: + data[key] = user.property[key] + return data + return user.property -# @classmethod -# async def add_gold(cls, user_id: str, group_id: str, num: int): -# """增加金币 + @classmethod + async def add_gold(cls, user_id: str, group_id: str, num: int): + """增加金币 -# 参数: -# user_id: 用户id -# group_id: 所在群组id -# num: 金币数量 -# """ -# user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) -# user.gold = user.gold + num -# user.get_total_gold = user.get_total_gold + num -# user.get_today_gold = user.get_today_gold + num -# await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) + 参数: + user_id: 用户id + group_id: 所在群组id + num: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + user.gold = user.gold + num + user.get_total_gold = user.get_total_gold + num + user.get_today_gold = user.get_today_gold + num + await user.save(update_fields=["gold", "get_today_gold", "get_total_gold"]) -# @classmethod -# async def spend_gold(cls, user_id: str, group_id: str, num: int): -# """花费金币 + @classmethod + async def spend_gold(cls, user_id: str, group_id: str, num: int): + """花费金币 -# 参数: -# user_id: 用户id -# group_id: 所在群组id -# num: 金币数量 -# """ -# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) -# user.gold = user.gold - num -# user.spend_total_gold = user.spend_total_gold + num -# user.spend_today_gold = user.spend_today_gold + num -# await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) + 参数: + user_id: 用户id + group_id: 所在群组id + num: 金币数量 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + user.gold = user.gold - num + user.spend_total_gold = user.spend_total_gold + num + user.spend_today_gold = user.spend_today_gold + num + await user.save(update_fields=["gold", "spend_total_gold", "spend_today_gold"]) -# @classmethod -# async def add_property(cls, user_id: str, group_id: str, name: str, num: int = 1): -# """增加道具 + @classmethod + async def add_property(cls, user_id: str, group_id: str, name: str, num: int = 1): + """增加道具 -# 参数: -# user_id: 用户id -# group_id: 所在群组id -# name: 道具名称 -# num: 道具数量 -# """ -# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) -# property_ = user.property -# if property_.get(name) is None: -# property_[name] = 0 -# property_[name] += num -# user.property = property_ -# await user.save(update_fields=["property"]) + 参数: + user_id: 用户id + group_id: 所在群组id + name: 道具名称 + num: 道具数量 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + property_ = user.property + if property_.get(name) is None: + property_[name] = 0 + property_[name] += num + user.property = property_ + await user.save(update_fields=["property"]) -# @classmethod -# async def delete_property( -# cls, user_id: str, group_id: str, name: str, num: int = 1 -# ) -> bool: -# """使用/删除 道具 + @classmethod + async def delete_property( + cls, user_id: str, group_id: str, name: str, num: int = 1 + ) -> bool: + """使用/删除 道具 -# 参数: -# user_id: 用户id -# group_id: 所在群组id -# name: 道具名称 -# num: 使用个数 + 参数: + user_id: 用户id + group_id: 所在群组id + name: 道具名称 + num: 使用个数 -# 返回: -# bool: 是否使用/删除成功 -# """ -# user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) -# property_ = user.property -# if name in property_: -# if (n := property_.get(name, 0)) < num: -# return False -# if n == num: -# del property_[name] -# else: -# property_[name] -= num -# await user.save(update_fields=["property"]) -# return True -# return False + 返回: + bool: 是否使用/删除成功 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=str(group_id)) + property_ = user.property + if name in property_: + if (n := property_.get(name, 0)) < num: + return False + if n == num: + del property_[name] + else: + property_[name] -= num + await user.save(update_fields=["property"]) + return True + return False -# @classmethod -# async def _run_script(cls): -# return [ -# "ALTER TABLE bag_users DROP props;", # 删除 props 字段 -# "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id -# "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", -# # 将user_id字段类型改为character varying(255) -# "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);", -# ] + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE bag_users DROP props;", # 删除 props 字段 + "ALTER TABLE bag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE bag_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE bag_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py index 60f64c4b..b4b346f1 100644 --- a/zhenxun/models/goods_info.py +++ b/zhenxun/models/goods_info.py @@ -46,7 +46,7 @@ class GoodsInfo(Model): daily_limit: int = 0, is_passive: bool = False, icon: str | None = None, - ): + ) -> str | None: """添加商品 参数: @@ -60,8 +60,9 @@ class GoodsInfo(Model): icon: 图标 """ if not await cls.exists(goods_name=goods_name): + uuid_ = uuid.uuid1() await cls.create( - uuid=uuid.uuid1(), + uuid=uuid_, goods_name=goods_name, goods_price=goods_price, goods_description=goods_description, @@ -71,6 +72,8 @@ class GoodsInfo(Model): is_passive=is_passive, icon=icon, ) + return str(uuid_) + return None @classmethod async def delete_goods(cls, goods_name: str) -> bool: diff --git a/zhenxun/models/sign_group_user.py b/zhenxun/models/sign_group_user.py new file mode 100644 index 00000000..ca397270 --- /dev/null +++ b/zhenxun/models/sign_group_user.py @@ -0,0 +1,81 @@ +from datetime import datetime +from typing import List, Literal, Optional, Tuple, Union + +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class SignGroupUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + checkin_count = fields.IntField(default=0) + """签到次数""" + checkin_time_last = fields.DatetimeField(default=datetime.min) + """最后签到时间""" + impression = fields.DecimalField(10, 3, default=0) + """好感度""" + add_probability = fields.DecimalField(10, 3, default=0) + """双倍签到增加概率""" + specify_probability = fields.DecimalField(10, 3, default=0) + """使用指定双倍概率""" + # specify_probability = fields.DecimalField(10, 3, default=0) + + class Meta: + table = "sign_group_users" + table_description = "群员签到数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def sign(cls, user: "SignGroupUser", impression: float): + """ + 说明: + 签到 + 说明: + :param user: 用户 + :param impression: 增加的好感度 + """ + user.checkin_time_last = datetime.now() + user.checkin_count = user.checkin_count + 1 + user.add_probability = 0 + user.specify_probability = 0 + user.impression = float(user.impression) + impression + await user.save() + + @classmethod + async def get_all_impression( + cls, group_id: Union[int, str] + ) -> Tuple[List[str], List[float], List[str]]: + """ + 说明: + 获取该群所有用户 id 及对应 好感度 + 参数: + :param group_id: 群号 + """ + if group_id: + query = cls.filter(group_id=str(group_id)) + else: + query = cls + value_list = await query.all().values_list("user_id", "group_id", "impression") # type: ignore + user_list = [] + group_list = [] + impression_list = [] + for value in value_list: + user_list.append(value[0]) + group_list.append(value[1]) + impression_list.append(float(value[2])) + return user_list, impression_list, group_list + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE sign_group_users RENAME COLUMN user_qq TO user_id;", # 将user_id改为user_id + "ALTER TABLE sign_group_users ALTER COLUMN user_id TYPE character varying(255);", + # 将user_id字段类型改为character varying(255) + "ALTER TABLE sign_group_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py index 32695923..bd5e81cc 100644 --- a/zhenxun/models/user_console.py +++ b/zhenxun/models/user_console.py @@ -15,7 +15,7 @@ class UserConsole(Model): """自增id""" user_id = fields.CharField(255, unique=True, description="用户id") """用户id""" - uid = fields.IntField(description="UID") + uid = fields.IntField(description="UID", unique=True) """UID""" gold = fields.IntField(default=100, description="金币数量") """金币数量""" diff --git a/zhenxun/plugins/__init__.py b/zhenxun/plugins/__init__.py new file mode 100644 index 00000000..cb1d0368 --- /dev/null +++ b/zhenxun/plugins/__init__.py @@ -0,0 +1,105 @@ +import os + +import nonebot +import ujson as json +from tortoise import Tortoise + +from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger + +driver = nonebot.get_driver() + +flag = True + +SIGN_SQL = """ +select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability, t1.specify_probability, t1.impression +from public.sign_group_users t1 + join ( + select user_id, max(t2.impression) as max_impression + from public.sign_group_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_impression = t1.impression +""" + +BAG_SQL = """ +select t1.user_id, t1.gold, t1.property +from public.bag_users t1 + join ( + select user_id, max(t2.gold) as max_gold + from public.bag_users t2 + group by user_id + ) t on t.user_id = t1.user_id and t.max_gold = t1.gold +""" + + +@driver.on_startup +async def _test(): + global flag + if ( + flag + and not await UserConsole.annotate().count() + and not await SignUser.annotate().count() + ): + flag = False + db = Tortoise.get_connection("default") + old_sign_list = await db.execute_query_dict(SIGN_SQL) + old_bag_list = await db.execute_query_dict(BAG_SQL) + goods = { + g["goods_name"]: g["uuid"] + for g in await GoodsInfo.annotate().values("goods_name", "uuid") + } + create_list = [] + sign_id_list = [] + uid = await UserConsole.get_new_uid() + for old_sign in old_sign_list: + sign_id_list.append(old_sign["user_id"]) + old_bag = [b for b in old_bag_list if b["user_id"] == old_sign["user_id"]] + if old_bag: + old_bag = old_bag[0] + property = json.loads(old_bag["property"]) + props = {} + if property: + for name, num in property.items(): + if name in goods: + props[goods[name]] = num + create_list.append( + UserConsole( + user_id=old_sign["user_id"], + platform="qq", + uid=uid, + props=props, + gold=old_bag["gold"], + ) + ) + else: + create_list.append( + UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) + ) + uid += 1 + if create_list: + logger.info("开始迁移用户数据...") + await UserConsole.bulk_create(create_list, 10) + logger.info("迁移用户数据完成!") + create_list.clear() + uc_dict = {u.user_id: u for u in await UserConsole.all()} + for old_sign in old_sign_list: + user_console = uc_dict.get(old_sign["user_id"]) + if not user_console: + user_console = await UserConsole.get_user(old_sign["user_id"], "qq") + create_list.append( + SignUser( + user_id=old_sign["user_id"], + user_console=user_console, + platform="qq", + sign_count=old_sign["checkin_count"], + impression=old_sign["impression"], + add_probability=old_sign["add_probability"], + specify_probability=old_sign["specify_probability"], + ) + ) + if create_list: + logger.info("开始迁移签到数据...") + await SignUser.bulk_create(create_list, 10) + logger.info("迁移签到数据完成!") diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 3e2e4649..34e0264c 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -47,7 +47,7 @@ class TestSQL(Model): async def init(): if not bind and not any([user, password, address, port, database]): - raise ValueError("\n数据库配置未填写.......") + raise ValueError("\n数据库配置未填写...") i_bind = bind if not i_bind: i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py index 5105e4c2..f62f36a0 100644 --- a/zhenxun/utils/decorator/shop.py +++ b/zhenxun/utils/decorator/shop.py @@ -1,70 +1,116 @@ -from typing import Callable, Union, Tuple, Optional -from nonebot.adapters.onebot.v11 import MessageSegment, Message +from typing import Any, Callable, Dict + +from nonebot.adapters.onebot.v11 import Message, MessageSegment from nonebot.plugin import require +from pydantic import BaseModel + +from zhenxun.models.goods_info import GoodsInfo + + +class Goods(BaseModel): + + before_handle: list[Callable] = [] + after_handle: list[Callable] = [] + price: int + des: str = "" + discount: float + limit_time: int + daily_limit: int + icon: str | None = None + is_passive: bool + func: Callable + kwargs: Dict[str, str] = {} + send_success_msg: bool + max_num_limit: int class ShopRegister(dict): + def __init__(self, *args, **kwargs): super(ShopRegister, self).__init__(*args, **kwargs) - self._data = {} + self._data: Dict[str, Goods] = {} self._flag = True - def before_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用前检查方法 + def before_handle(self, name: str | tuple[str, ...], load_status: bool = True): + """使用前检查方法 + 参数: - :param name: 道具名称 - :param load_status: 加载状态 + name: 道具名称 + load_status: 加载状态 """ - def register_before_handle(name_list: Tuple[str, ...], func: Callable): + + def register_before_handle(name_list: tuple[str, ...], func: Callable): if load_status: for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('before_handle'): - self._data[name_]['before_handle'] = [] - self._data[name]['before_handle'].append(func) + if goods := self._data.get(name_): + self._data[name_].before_handle.append(func) + _name = (name,) if isinstance(name, str) else name return lambda func: register_before_handle(_name, func) - def after_handle(self, name: Union[str, Tuple[str, ...]], load_status: bool = True): - """ - 说明: - 使用后执行方法 + def after_handle(self, name: str | tuple[str, ...], load_status: bool = True): + """使用后执行方法 + 参数: - :param name: 道具名称 - :param load_status: 加载状态 + name: 道具名称 + load_status: 加载状态 """ - def register_after_handle(name_list: Tuple[str, ...], func: Callable): + + def register_after_handle(name_list: tuple[str, ...], func: Callable): if load_status: for name_ in name_list: - if not self._data[name_]: - self._data[name_] = {} - if not self._data[name_].get('after_handle'): - self._data[name_]['after_handle'] = [] - self._data[name_]['after_handle'].append(func) + if goods := self._data.get(name_): + self._data[name_].after_handle.append(func) + _name = (name,) if isinstance(name, str) else name return lambda func: register_after_handle(_name, func) def register( self, - name: Tuple[str, ...], - price: Tuple[float, ...], - des: Tuple[str, ...], - discount: Tuple[float, ...], - limit_time: Tuple[int, ...], - load_status: Tuple[bool, ...], - daily_limit: Tuple[int, ...], - is_passive: Tuple[bool, ...], - icon: Tuple[str, ...], + name: tuple[str, ...], + price: tuple[float, ...], + des: tuple[str, ...], + discount: tuple[float, ...], + limit_time: tuple[int, ...], + load_status: tuple[bool, ...], + daily_limit: tuple[int, ...], + is_passive: tuple[bool, ...], + icon: tuple[str, ...], + send_success_msg: tuple[bool, ...], + max_num_limit: tuple[int, ...], **kwargs, ): + """注册商品 + + 参数: + name: 商品名称 + price: 价格 + des: 简介 + discount: 折扣 + limit_time: 售卖限时时间 + load_status: 是否加载 + daily_limit: 每日限购 + is_passive: 是否被动道具 + icon: 图标 + send_success_msg: 成功时发送消息 + max_num_limit: 单次最大使用次数 + """ + def add_register_item(func: Callable): if name in self._data.keys(): raise ValueError("该商品已注册,请替换其他名称!") - for n, p, d, dd, l, s, dl, pa, i in zip( - name, price, des, discount, limit_time, load_status, daily_limit, is_passive, icon + for n, p, d, dd, l, s, dl, pa, i, ssm, mnl in zip( + name, + price, + des, + discount, + limit_time, + load_status, + daily_limit, + is_passive, + icon, + send_success_msg, + max_num_limit, ): if s: _temp_kwargs = {} @@ -73,62 +119,89 @@ class ShopRegister(dict): _temp_kwargs[key.split("_", maxsplit=1)[-1]] = value else: _temp_kwargs[key] = value - temp = self._data.get(n, {}) - temp.update({ - "price": p, - "des": d, - "discount": dd, - "limit_time": l, - "daily_limit": dl, - "icon": i, - "is_passive": pa, - "func": func, - "kwargs": _temp_kwargs, - }) - self._data[n] = temp + goods = self._data.get(n) or Goods( + price=p, + des=d, + discount=dd, + limit_time=l, + daily_limit=dl, + is_passive=pa, + func=func, + send_success_msg=ssm, + max_num_limit=mnl, + ) + goods.price = p + goods.des = d + goods.discount = dd + goods.limit_time = l + goods.daily_limit = dl + goods.icon = i + goods.is_passive = pa + goods.func = func + goods.kwargs = _temp_kwargs + goods.send_success_msg = ssm + goods.max_num_limit = mnl return func return lambda func: add_register_item(func) async def load_register(self): - require("use") - require("shop_handle") - from basic_plugins.shop.use.data_source import register_use, func_manager - from basic_plugins.shop.shop_handle.data_source import register_goods + require("shop") + from zhenxun.builtin_plugins.shop._data_source import ShopManage + # 统一进行注册 if self._flag: # 只进行一次注册 self._flag = False for name in self._data.keys(): - await register_goods( - name, - self._data[name]["price"], - self._data[name]["des"], - self._data[name]["discount"], - self._data[name]["limit_time"], - self._data[name]["daily_limit"], - self._data[name]["is_passive"], - self._data[name]["icon"], - ) - register_use( - name, self._data[name]["func"], **self._data[name]["kwargs"] - ) - func_manager.register_use_before_handle(name, self._data[name].get('before_handle', [])) - func_manager.register_use_after_handle(name, self._data[name].get('after_handle', [])) + if goods := self._data.get(name): + uuid = await GoodsInfo.add_goods( + name, + goods.price, + goods.des, + goods.discount, + goods.limit_time, + goods.daily_limit, + goods.is_passive, + goods.icon, + ) + if uuid: + await ShopManage.register_use( + uuid, + goods.func, + goods.send_success_msg, + goods.max_num_limit, + goods.before_handle, + goods.after_handle, + **self._data[name].kwargs, + ) def __call__( self, - name: Union[str, Tuple[str, ...]], # 名称 - price: Union[float, Tuple[float, ...]], # 价格 - des: Union[str, Tuple[str, ...]], # 简介 - discount: Union[float, Tuple[float, ...]] = 1, # 折扣 - limit_time: Union[int, Tuple[int, ...]] = 0, # 限时 - load_status: Union[bool, Tuple[bool, ...]] = True, # 加载状态 - daily_limit: Union[int, Tuple[int, ...]] = 0, # 每日限购 - is_passive: Union[bool, Tuple[bool, ...]] = False, # 被动道具(无法被'使用道具'命令消耗) - icon: Union[str, Tuple[str, ...]] = False, # 图标 + name: str | tuple[str, ...], + price: float | tuple[float, ...], + des: str | tuple[str, ...], + discount: float | tuple[float, ...] = 1, + limit_time: int | tuple[int, ...] = 0, + load_status: bool | tuple[bool, ...] = True, + daily_limit: int | tuple[int, ...] = 0, + is_passive: bool | tuple[bool, ...] = False, + icon: str | tuple[str, ...] = "", **kwargs, ): + """注册商品 + + 参数: + name: 商品名称 + price: 价格 + des: 简介 + discount: 折扣 + limit_time: 售卖限时时间 + load_status: 是否加载 + daily_limit: 每日限购 + is_passive: 是否被动道具 + icon: 图标 + """ _tuple_list = [] _current_len = -1 for x in [name, price, des, discount, limit_time, load_status]: @@ -163,7 +236,11 @@ class ShopRegister(dict): ) def __get(self, value, _current_len): - return value if isinstance(value, tuple) else tuple([value for _ in range(_current_len)]) + return ( + value + if isinstance(value, tuple) + else tuple([value for _ in range(_current_len)]) + ) def __setitem__(self, key, value): self._data[key] = value @@ -188,12 +265,11 @@ class ShopRegister(dict): class NotMeetUseConditionsException(Exception): - """ 不满足条件异常类 """ - def __init__(self, info: Optional[Union[str, MessageSegment, Message]]): + def __init__(self, info: str | MessageSegment | Message | None): super().__init__(self) self._info = info From 5a50a2bff4c7e24bf8a926e4846f7f0b2ec23218 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 5 Mar 2024 08:29:46 +0800 Subject: [PATCH 013/132] =?UTF-8?q?perf=F0=9F=91=8C:=20=E9=81=93=E5=85=B7?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=B8=8E=E7=AD=BE=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 5 +- zhenxun/builtin_plugins/shop/__init__.py | 29 ++- zhenxun/builtin_plugins/shop/_data_source.py | 244 +++++++++++++++++- .../builtin_plugins/sign_in/_data_source.py | 1 + .../builtin_plugins/sign_in/goods_register.py | 105 ++++---- zhenxun/models/goods_info.py | 5 +- zhenxun/models/user_console.py | 16 +- zhenxun/plugins/__init__.py | 105 -------- zhenxun/utils/decorator/shop.py | 10 + 9 files changed, 342 insertions(+), 178 deletions(-) delete mode 100644 zhenxun/plugins/__init__.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index d8ef9dba..89e9e62b 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -1,6 +1,7 @@ import os from nonebot import require +from nonebot.drivers import Driver from tortoise import Tortoise from zhenxun.models.goods_info import GoodsInfo @@ -27,7 +28,7 @@ for d in os.listdir(path): nonebot.load_plugins(str((path / d).resolve())) -driver = nonebot.get_driver() +driver: Driver = nonebot.get_driver() flag = True @@ -52,7 +53,7 @@ from public.bag_users t1 """ -@driver.on_bot_connect +@driver.on_startup async def _(): global flag await shop_register.load_register() diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py index 1d71d1f6..041ca6e8 100644 --- a/zhenxun/builtin_plugins/shop/__init__.py +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -1,6 +1,14 @@ +from nonebot.adapters import Bot, Event from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Alconna, Args, Arparma, Subcommand, on_alconna -from nonebot_plugin_saa import Image, Text +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Subcommand, + UniMsg, + on_alconna, +) +from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo @@ -121,5 +129,18 @@ async def _(session: EventSession, arparma: Arparma, name: str, num: int): @_matcher.assign("use") -async def _(session: EventSession, arparma: Arparma, name: str, num: int): - pass +async def _( + bot: Bot, + event: Event, + message: UniMsg, + session: EventSession, + arparma: Arparma, + name: str, + num: int, +): + result = await ShopManage.use(bot, event, session, message, name, num, "") + logger.info(f"使用道具 {name}, 数量: {num}", arparma.header_result, session=session) + if isinstance(result, str): + await Text(result).send(reply=True) + elif isinstance(result, MessageFactory): + await result.finish(reply=True) diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 9c006965..71a0f8f1 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -1,7 +1,13 @@ +import asyncio +import inspect import time -from typing import Any, Callable, Dict +from types import MappingProxyType +from typing import Any, Callable, Dict, Literal -from nonebot.adapters import Event +from nonebot.adapters import Bot, Event +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import MessageFactory +from nonebot_plugin_session import EventSession from pydantic import BaseModel, create_model from zhenxun.configs.path_config import IMAGE_PATH @@ -18,13 +24,24 @@ ICON_PATH = IMAGE_PATH / "shop_icon" class Goods(BaseModel): + name: str + """商品名称""" before_handle: list[Callable] = [] + """使用前函数""" after_handle: list[Callable] = [] + """使用后函数""" func: Callable | None = None - params: Any | None = None + """使用函数""" + params: Any = None + """参数""" send_success_msg: bool = True + """使用成功是否发送消息""" max_num_limit: int = 1 - model: Any | None = None + """单次使用最大次数""" + model: Any = None + """model""" + session: EventSession | None = None + """EventSession""" class ShopParam(BaseModel): @@ -41,23 +58,221 @@ class ShopParam(BaseModel): """event""" num: int """道具单次使用数量""" - message: str - """message""" text: str """text""" send_success_msg: bool = True """是否发送使用成功信息""" max_num_limit: int = 1 """单次使用最大次数""" + session: EventSession | None = None + """EventSession""" class ShopManage: uuid2goods: Dict[str, Goods] = {} + @classmethod + def __build_params( + cls, + bot: Bot, + event: Event, + session: EventSession, + message: UniMsg, + goods: Goods, + num: int, + text: str, + ) -> tuple[ShopParam, Dict[str, Any]]: + """构造参数 + + 参数: + bot: bot + event: event + goods_name: 商品名称 + num: 数量 + text: 其他信息 + """ + _kwargs = goods.params + model = goods.model( + **{ + "goods_name": goods.name, + "bot": bot, + "event": event, + "user_id": session.id1, + "group_id": session.id3 or session.id2, + "num": num, + "text": text, + "session": session, + } + ) + return model, { + **_kwargs, + "_bot": bot, + "event": event, + "user_id": session.id1, + "group_id": session.id3 or session.id2, + "num": num, + "text": text, + "goods_name": goods.name, + } + + @classmethod + def __parse_args( + cls, + args: MappingProxyType, + param: ShopParam, + session: EventSession, + message: UniMsg, + **kwargs, + ) -> list[Any]: + """解析参数 + + 参数: + args: MappingProxyType + param: ShopParam + + 返回: + list[Any]: 参数 + """ + param_list = [] + _bot = param.bot + param.bot = None + param_json = param.dict() + param_json["bot"] = _bot + for par in args.keys(): + if par in ["shop_param"]: + param_list.append(param) + elif par in ["session"]: + param_list.append(session) + elif par in ["message"]: + param_list.append(message) + elif par not in ["args", "kwargs"]: + param_list.append(param_json.get(par)) + if kwargs.get(par) is not None: + del kwargs[par] + return param_list + + @classmethod + async def run_before_after( + cls, + goods: Goods, + param: ShopParam, + run_type: Literal["after", "before"], + **kwargs, + ): + """运行使用前使用后函数 + + 参数: + goods: Goods + param: 参数 + run_type: 运行类型 + """ + fun_list = goods.before_handle if run_type == "before" else goods.after_handle + if fun_list: + for func in fun_list: + args = inspect.signature(func).parameters + if args and list(args.keys())[0] != "kwargs": + if asyncio.iscoroutinefunction(func): + await func(*cls.__parse_args(args, param, **kwargs)) + else: + func(*cls.__parse_args(args, param, **kwargs)) + else: + if asyncio.iscoroutinefunction(func): + await func(**kwargs) + else: + func(**kwargs) + + @classmethod + async def __run( + cls, + goods: Goods, + param: ShopParam, + session: EventSession, + message: UniMsg, + **kwargs, + ) -> str | MessageFactory | None: + """运行道具函数 + + 参数: + goods: Goods + param: ShopParam + + 返回: + str | MessageFactory | None: 使用完成后返回信息 + """ + args = inspect.signature(goods.func).parameters # type: ignore + if goods.func: + if args and list(args.keys())[0] != "kwargs": + if asyncio.iscoroutinefunction(goods.func): + return await goods.func( + *cls.__parse_args(args, param, session, message, **kwargs) + ) + else: + return goods.func( + *cls.__parse_args(args, param, session, message, **kwargs) + ) + else: + if asyncio.iscoroutinefunction(goods.func): + return await goods.func( + **kwargs, + ) + else: + return goods.func(**kwargs) + + @classmethod + async def use( + cls, + bot: Bot, + event: Event, + session: EventSession, + message: UniMsg, + goods_name: str, + num: int, + text: str, + ) -> str | MessageFactory | None: + """使用道具 + + 参数: + bot: Bot + event: Event + session: Session + message: 消息 + goods_name: 商品名称 + num: 使用数量 + text: 其他信息 + + 返回: + str | MessageFactory | None: 使用完成后返回信息 + """ + if goods_name.isdigit(): + user = await UserConsole.get_user(user_id=session.id1) # type: ignore + uuid = list(user.props.keys())[int(goods_name)] + goods_info = await GoodsInfo.get_or_none(uuid=uuid) + else: + goods_info = await GoodsInfo.get_or_none(goods_name=goods_name) + if not goods_info: + return f"{goods_name} 不存在..." + if goods_info.is_passive: + return f"{goods_name} 是被动道具, 无法使用..." + goods = cls.uuid2goods.get(goods_info.uuid) + if not goods or not goods.func: + return f"{goods_name} 未注册使用函数, 无法使用..." + param, kwargs = cls.__build_params( + bot, event, session, message, goods, num, text + ) + if num > param.max_num_limit: + return f"{goods_name} 单次使用最大数量为{param.max_num_limit}..." + await cls.run_before_after(goods, param, "before", **kwargs) + result = await cls.__run(goods, param, session, message, **kwargs) + await cls.run_before_after(goods, param, "after", **kwargs) + if not result and param.send_success_msg: + result = f"使用道具 {goods.name} {num} 次成功!" + return result + @classmethod async def register_use( cls, + name: str, uuid: str, func: Callable, send_success_msg: bool = True, @@ -83,22 +298,35 @@ class ShopManage: raise ValueError("该商品使用函数已被注册!") kwargs["send_success_msg"] = send_success_msg kwargs["max_num_limit"] = max_num_limit - cls.uuid2func = Goods( + cls.uuid2goods[uuid] = Goods( model=create_model(f"{uuid}_model", __base__=ShopParam, **kwargs), params=kwargs, before_handle=before_handle, after_handle=after_handle, + name=name, + func=func, ) @classmethod async def buy_prop( cls, user_id: str, name: str, num: int = 1, platform: str | None = None ) -> str: + """购买道具 + + 参数: + user_id: 用户id + name: 道具名称 + num: 购买数量. + platform: 平台. + + 返回: + str: 返回小 + """ if name == "神秘药水": return "你们看看就好啦,这是不可能卖给你们的~" if num < 0: return "购买的数量要大于0!" - goods_list = await GoodsInfo.annotate().order_by("-id").all() + goods_list = await GoodsInfo.annotate().order_by("id").all() goods_list = [ goods for goods in goods_list diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 6fe4ce76..2affe19d 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -19,6 +19,7 @@ from zhenxun.utils.image_utils import BuildImage, ImageTemplate from zhenxun.utils.utils import get_user_avatar from ._random_event import random_event +from .goods_register import driver from .utils import SIGN_TODAY_CARD_PATH, get_card ICON_PATH = IMAGE_PATH / "_icon" diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py index 0fc3b921..80beafec 100644 --- a/zhenxun/builtin_plugins/sign_in/goods_register.py +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -12,59 +12,62 @@ from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_reg driver: Driver = nonebot.get_driver() -@driver.on_startup -async def _(): - """ - 导入内置的三个商品 - """ +# @driver.on_startup +# async def _(): +# """ +# 导入内置的三个商品 +# """ - @shop_register( - name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), - price=(30, 150, 250), - des=( - "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", - "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", - ), - load_status=bool(Config.get_config("shop", "IMPORT_DEFAULT_SHOP_GOODS")), - icon=( - "favorability_card_1.png", - "favorability_card_2.png", - "favorability_card_3.png", - ), - **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore - ) - async def _(session: EventSession, user_id: int, group_id: int, prob: float): - if session.id1: - user_console = await UserConsole.get_user(session.id1, session.platform) - user, _ = await SignUser.get_or_create( - user_id=user_id, - defaults={"platform": session.platform, "user_console": user_console}, - ) - user.add_probability = Decimal(prob) - await user.save(update_fields=["add_probability"]) - @shop_register( - name="测试道具A", - price=99, - des="随便侧而出", - load_status=False, - icon="sword.png", - ) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "使用测试道具") +@shop_register( + name=("好感度双倍加持卡Ⅰ", "好感度双倍加持卡Ⅱ", "好感度双倍加持卡Ⅲ"), + price=(30, 150, 250), + des=( + "下次签到双倍好感度概率 + 10%(谁才是真命天子?)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 20%(平平庸庸)(同类商品将覆盖)", + "下次签到双倍好感度概率 + 30%(金币才是真命天子!)(同类商品将覆盖)", + ), + load_status=True, + icon=( + "favorability_card_1.png", + "favorability_card_2.png", + "favorability_card_3.png", + ), + **{"好感度双倍加持卡Ⅰ_prob": 0.1, "好感度双倍加持卡Ⅱ_prob": 0.2, "好感度双倍加持卡Ⅲ_prob": 0.3}, # type: ignore +) +async def _(session: EventSession, user_id: int, group_id: int, prob: float): + if session.id1: + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=user_id, + defaults={"platform": session.platform, "user_console": user_console}, + ) + user.add_probability = Decimal(prob) + await user.save(update_fields=["add_probability"]) - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用前函数(before handle)") - @shop_register.before_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第二个使用前函数(before handle)222") - raise NotMeetUseConditionsException( - "太笨了!" - ) # 抛出异常,阻断使用,并返回信息 +@shop_register( + name="测试道具A", + price=99, + des="随便侧而出", + load_status=False, + icon="sword.png", +) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "使用测试道具") - @shop_register.after_handle(name="测试道具A", load_status=False) - async def _(user_id: int, group_id: int): - print(user_id, group_id, "第一个使用后函数(after handle)") + +@shop_register.before_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用前函数(before handle)") + + +@shop_register.before_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第二个使用前函数(before handle)222") + raise NotMeetUseConditionsException("太笨了!") # 抛出异常,阻断使用,并返回信息 + + +@shop_register.after_handle(name="测试道具A", load_status=False) +async def _(user_id: int, group_id: int): + print(user_id, group_id, "第一个使用后函数(after handle)") diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py index b4b346f1..53aefe61 100644 --- a/zhenxun/models/goods_info.py +++ b/zhenxun/models/goods_info.py @@ -46,7 +46,7 @@ class GoodsInfo(Model): daily_limit: int = 0, is_passive: bool = False, icon: str | None = None, - ) -> str | None: + ) -> str: """添加商品 参数: @@ -73,7 +73,8 @@ class GoodsInfo(Model): icon=icon, ) return str(uuid_) - return None + else: + return (await cls.get(goods_name=goods_name)).uuid @classmethod async def delete_goods(cls, goods_name: str) -> bool: diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py index bd5e81cc..a492c317 100644 --- a/zhenxun/models/user_console.py +++ b/zhenxun/models/user_console.py @@ -43,11 +43,15 @@ class UserConsole(Model): 返回: UserConsole: UserConsole """ - user, _ = await UserConsole.get_or_create( - user_id=user_id, - defaults={"platform": platform, "uid": await cls.get_new_uid()}, - ) - return user + if not await cls.exists(user_id=user_id): + await cls.create( + user_id=user_id, platform=platform, uid=await cls.get_new_uid() + ) + # user, _ = await UserConsole.get_or_create( + # user_id=user_id, + # defaults={"platform": platform, "uid": await cls.get_new_uid()}, + # ) + return await cls.get(user_id=user_id) @classmethod async def get_new_uid(cls) -> int: @@ -56,7 +60,7 @@ class UserConsole(Model): 返回: int: 最新uid """ - if user := await cls.annotate().order_by("uid").first(): + if user := await cls.annotate().order_by("-uid").first(): return user.uid + 1 return 1 diff --git a/zhenxun/plugins/__init__.py b/zhenxun/plugins/__init__.py deleted file mode 100644 index cb1d0368..00000000 --- a/zhenxun/plugins/__init__.py +++ /dev/null @@ -1,105 +0,0 @@ -import os - -import nonebot -import ujson as json -from tortoise import Tortoise - -from zhenxun.models.goods_info import GoodsInfo -from zhenxun.models.sign_user import SignUser -from zhenxun.models.user_console import UserConsole -from zhenxun.services.log import logger - -driver = nonebot.get_driver() - -flag = True - -SIGN_SQL = """ -select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability, t1.specify_probability, t1.impression -from public.sign_group_users t1 - join ( - select user_id, max(t2.impression) as max_impression - from public.sign_group_users t2 - group by user_id - ) t on t.user_id = t1.user_id and t.max_impression = t1.impression -""" - -BAG_SQL = """ -select t1.user_id, t1.gold, t1.property -from public.bag_users t1 - join ( - select user_id, max(t2.gold) as max_gold - from public.bag_users t2 - group by user_id - ) t on t.user_id = t1.user_id and t.max_gold = t1.gold -""" - - -@driver.on_startup -async def _test(): - global flag - if ( - flag - and not await UserConsole.annotate().count() - and not await SignUser.annotate().count() - ): - flag = False - db = Tortoise.get_connection("default") - old_sign_list = await db.execute_query_dict(SIGN_SQL) - old_bag_list = await db.execute_query_dict(BAG_SQL) - goods = { - g["goods_name"]: g["uuid"] - for g in await GoodsInfo.annotate().values("goods_name", "uuid") - } - create_list = [] - sign_id_list = [] - uid = await UserConsole.get_new_uid() - for old_sign in old_sign_list: - sign_id_list.append(old_sign["user_id"]) - old_bag = [b for b in old_bag_list if b["user_id"] == old_sign["user_id"]] - if old_bag: - old_bag = old_bag[0] - property = json.loads(old_bag["property"]) - props = {} - if property: - for name, num in property.items(): - if name in goods: - props[goods[name]] = num - create_list.append( - UserConsole( - user_id=old_sign["user_id"], - platform="qq", - uid=uid, - props=props, - gold=old_bag["gold"], - ) - ) - else: - create_list.append( - UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) - ) - uid += 1 - if create_list: - logger.info("开始迁移用户数据...") - await UserConsole.bulk_create(create_list, 10) - logger.info("迁移用户数据完成!") - create_list.clear() - uc_dict = {u.user_id: u for u in await UserConsole.all()} - for old_sign in old_sign_list: - user_console = uc_dict.get(old_sign["user_id"]) - if not user_console: - user_console = await UserConsole.get_user(old_sign["user_id"], "qq") - create_list.append( - SignUser( - user_id=old_sign["user_id"], - user_console=user_console, - platform="qq", - sign_count=old_sign["checkin_count"], - impression=old_sign["impression"], - add_probability=old_sign["add_probability"], - specify_probability=old_sign["specify_probability"], - ) - ) - if create_list: - logger.info("开始迁移签到数据...") - await SignUser.bulk_create(create_list, 10) - logger.info("迁移签到数据完成!") diff --git a/zhenxun/utils/decorator/shop.py b/zhenxun/utils/decorator/shop.py index f62f36a0..3e46db6a 100644 --- a/zhenxun/utils/decorator/shop.py +++ b/zhenxun/utils/decorator/shop.py @@ -141,6 +141,7 @@ class ShopRegister(dict): goods.kwargs = _temp_kwargs goods.send_success_msg = ssm goods.max_num_limit = mnl + self._data[n] = goods return func return lambda func: add_register_item(func) @@ -167,6 +168,7 @@ class ShopRegister(dict): ) if uuid: await ShopManage.register_use( + name, uuid, goods.func, goods.send_success_msg, @@ -187,6 +189,8 @@ class ShopRegister(dict): daily_limit: int | tuple[int, ...] = 0, is_passive: bool | tuple[bool, ...] = False, icon: str | tuple[str, ...] = "", + send_success_msg: bool | tuple[bool, ...] = True, + max_num_limit: int | tuple[int, ...] = 1, **kwargs, ): """注册商品 @@ -201,6 +205,8 @@ class ShopRegister(dict): daily_limit: 每日限购 is_passive: 是否被动道具 icon: 图标 + send_success_msg: 成功时发送消息 + max_num_limit: 单次最大使用次数 """ _tuple_list = [] _current_len = -1 @@ -222,6 +228,8 @@ class ShopRegister(dict): _daily_limit = self.__get(daily_limit, _current_len) _is_passive = self.__get(is_passive, _current_len) _icon = self.__get(icon, _current_len) + _send_success_msg = self.__get(send_success_msg, _current_len) + _max_num_limit = self.__get(max_num_limit, _current_len) return self.register( _name, _price, @@ -232,6 +240,8 @@ class ShopRegister(dict): _daily_limit, _is_passive, _icon, + _send_success_msg, + _max_num_limit, **kwargs, ) From 960b665ba4799edf6ccfb6e80395e251e856cd18 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 9 Mar 2024 23:42:59 +0800 Subject: [PATCH 014/132] =?UTF-8?q?feat=E2=9C=A8:=20Ai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 1 + zhenxun/plugins/ai/__init__.py | 114 ++++++++++++++ zhenxun/plugins/ai/data_source.py | 245 ++++++++++++++++++++++++++++++ zhenxun/plugins/ai/utils.py | 153 +++++++++++++++++++ zhenxun/utils/depends/__init__.py | 29 ++++ 5 files changed, 542 insertions(+) create mode 100644 zhenxun/plugins/ai/__init__.py create mode 100644 zhenxun/plugins/ai/data_source.py create mode 100644 zhenxun/plugins/ai/utils.py create mode 100644 zhenxun/utils/depends/__init__.py diff --git a/bot.py b/bot.py index 71a794b0..b3009b5c 100644 --- a/bot.py +++ b/bot.py @@ -20,6 +20,7 @@ driver.on_shutdown(disconnect) nonebot.load_builtin_plugins("echo") # 内置插件 nonebot.load_plugins("zhenxun/builtin_plugins") +nonebot.load_plugins("zhenxun/plugins") if __name__ == "__main__": diff --git a/zhenxun/plugins/ai/__init__.py b/zhenxun/plugins/ai/__init__.py new file mode 100644 index 00000000..c66be3ed --- /dev/null +++ b/zhenxun/plugins/ai/__init__.py @@ -0,0 +1,114 @@ +from typing import List + +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName + +from .data_source import get_chat_result, hello, no_result + +__zx_plugin_name__ = "AI" +__plugin_usage__ = f""" +usage: + 与{NICKNAME}普普通通的对话吧! +""" +__plugin_version__ = 0.1 +__plugin_author__ = "HibiKier" +__plugin_settings__ = { + "level": 5, + "cmd": ["Ai", "ai", "AI", "aI"], +} +__plugin_configs__ = { + "TL_KEY": {"value": [], "help": "图灵Key", "type": List[str]}, + "ALAPI_AI_CHECK": { + "value": False, + "help": "是否检测青云客骂娘回复", + "default_value": False, + "type": bool, + }, + "TEXT_FILTER": { + "value": ["鸡", "口交"], + "help": "文本过滤器,将敏感词更改为*", + "default_value": [], + "type": List[str], + }, +} + +__plugin_meta__ = PluginMetadata( + name="AI", + description="屑Ai", + usage=""" + 与{NICKNAME}普普通通的对话吧! + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + module="alapi", + key="ALAPI_TOKEN", + value=None, + help="在 https://admin.alapi.cn/user/login 登录后获取token", + ), + RegisterConfig(key="TL_KEY", value=[], help="图灵Key", type=List[str]), + RegisterConfig( + key="ALAPI_AI_CHECK", + value=False, + help="是否检测青云客骂娘回复", + default_value=False, + type=bool, + ), + RegisterConfig( + key="TEXT_FILTER", + value=["鸡", "口交"], + help="文本过滤器,将敏感词更改为*", + type=List[str], + ), + ], + ).dict(), +) + + +ai = on_message(rule=to_me(), priority=998) + + +@ai.handle() +async def _(message: UniMsg, session: EventSession, uname: str = UserName()): + if not message or message.extract_plain_text() in [ + "你好啊", + "你好", + "在吗", + "在不在", + "您好", + "您好啊", + "你好", + "在", + ]: + await hello().finish() + if not session.id1: + await Text("用户id不存在...").finish() + gid = session.id3 or session.id2 + if gid: + nickname = await GroupInfoUser.get_user_nickname(session.id1, gid) + else: + nickname = await FriendUser.get_user_nickname(session.id1) + if not nickname: + nickname = uname + result = await get_chat_result(message, session.id1, nickname) + logger.info(f"问题:{message} ---- 回答:{result}", "ai", session=session) + if result: + result = str(result) + for t in Config.get_config("ai", "TEXT_FILTER"): + result = result.replace(t, "*") + await Text(result).finish() + else: + await no_result().finish() diff --git a/zhenxun/plugins/ai/data_source.py b/zhenxun/plugins/ai/data_source.py new file mode 100644 index 00000000..4b9136c4 --- /dev/null +++ b/zhenxun/plugins/ai/data_source.py @@ -0,0 +1,245 @@ +import os +import random +import re + +import ujson as json +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Image, MessageFactory, Text + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +from .utils import ai_message_manager + +url = "http://openapi.tuling123.com/openapi/api/v2" + +check_url = "https://v2.alapi.cn/api/censor/text" + +index = 0 + +anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) + + +async def get_chat_result( + message: UniMsg, user_id: str, nickname: str +) -> Text | MessageFactory: + """获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 + + 参数: + text: 问题 + img_url: 图片链接 + user_id: 用户id + nickname: 用户昵称 + + 返回 + str: 回答 + """ + global index + text = message.extract_plain_text() + ai_message_manager.add_message(user_id, text) + special_rst = await ai_message_manager.get_result(user_id, nickname) + if special_rst: + ai_message_manager.add_result(user_id, special_rst) + return Text(special_rst) + if index == 5: + index = 0 + if len(text) < 6 and random.random() < 0.6: + keys = anime_data.keys() + for key in keys: + if text.find(key) != -1: + return random.choice(anime_data[key]).replace("你", nickname) + rst = await tu_ling(text, "", user_id) + if not rst: + rst = await xie_ai(text) + if not rst: + return no_result() + if nickname: + if len(nickname) < 5: + if random.random() < 0.5: + nickname = "~".join(nickname) + "~" + if random.random() < 0.2: + if nickname.find("大人") == -1: + nickname += "大~人~" + rst = str(rst).replace("小主人", nickname).replace("小朋友", nickname) + ai_message_manager.add_result(user_id, rst) + for t in Config.get_config("ai", "TEXT_FILTER"): + rst = rst.replace(t, "*") + return Text(rst) + + +# 图灵接口 +async def tu_ling(text: str, img_url: str, user_id: str) -> str | None: + """获取图灵接口的回复 + + 参数: + text: 问题 + img_url: 图片链接 + user_id: 用户id + + 返回 + str: 图灵回复 + """ + global index + TL_KEY = Config.get_config("ai", "TL_KEY") + req = None + if not TL_KEY: + return None + try: + if text: + req = { + "perception": { + "inputText": {"text": text}, + "selfInfo": { + "location": { + "city": "陨石坑", + "province": "火星", + "street": "第5坑位", + } + }, + }, + "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, + } + elif img_url: + req = { + "reqType": 1, + "perception": { + "inputImage": {"url": img_url}, + "selfInfo": { + "location": { + "city": "陨石坑", + "province": "火星", + "street": "第5坑位", + } + }, + }, + "userInfo": {"apiKey": TL_KEY[index], "userId": str(user_id)}, + } + except IndexError: + index = 0 + return None + text = "" + response = await AsyncHttpx.post(url, json=req) + if response.status_code != 200: + return None + resp_payload = json.loads(response.text) + if int(resp_payload["intent"]["code"]) in [4003]: + return None + if resp_payload["results"]: + for result in resp_payload["results"]: + if result["resultType"] == "text": + text = result["values"]["text"] + if "请求次数超过" in text: + text = "" + return text + + +# 屑 AI +async def xie_ai(text: str) -> str: + """获取青云客回复 + + 参数: + text: 问题 + + 返回: + str: 青云可回复 + """ + res = await AsyncHttpx.get( + f"http://api.qingyunke.com/api.php?key=free&appid=0&msg={text}" + ) + content = "" + try: + data = json.loads(res.text) + if data["result"] == 0: + content = data["content"] + if "菲菲" in content: + content = content.replace("菲菲", NICKNAME) + if "艳儿" in content: + content = content.replace("艳儿", NICKNAME) + if "公众号" in content: + content = "" + if "{br}" in content: + content = content.replace("{br}", "\n") + if "提示" in content: + content = content[: content.find("提示")] + if "淘宝" in content or "taobao.com" in content: + return "" + while True: + r = re.search("{face:(.*)}", content) + if r: + id_ = r.group(1) + content = content.replace("{" + f"face:{id_}" + "}", "") + else: + break + return ( + content + if not content and not Config.get_config("ai", "ALAPI_AI_CHECK") + else await check_text(content) + ) + except Exception as e: + logger.error(f"Ai xie_ai 发生错误", e=e) + return "" + + +def hello() -> MessageFactory: + """一些打招呼的内容""" + result = random.choice( + ( + "哦豁?!", + "你好!Ov<", + f"库库库,呼唤{NICKNAME}做什么呢", + "我在呢!", + "呼呼,叫俺干嘛", + ) + ) + img = random.choice(os.listdir(IMAGE_PATH / "zai")) + return MessageFactory([Image(IMAGE_PATH / "zai" / img), Text(result)]) + + +def no_result() -> MessageFactory: + """ + 没有回答时的回复 + """ + return MessageFactory( + [ + Text( + random.choice( + [ + "你在说啥子?", + f"纯洁的{NICKNAME}没听懂", + "下次再告诉你(下次一定)", + "你觉得我听懂了吗?嗯?", + "我!不!知!道!", + ] + ) + ), + Image( + IMAGE_PATH + / "noresult" + / random.choice(os.listdir(IMAGE_PATH / "noresult")) + ), + ] + ) + + +async def check_text(text: str) -> str: + """ALAPI文本检测,主要针对青云客API,检测为恶俗文本改为无回复的回答 + + 参数: + text: 回复 + + 返回: + str: 检测文本 + """ + if not Config.get_config("alapi", "ALAPI_TOKEN"): + return text + params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} + try: + data = (await AsyncHttpx.get(check_url, timeout=2, params=params)).json() + if data["code"] == 200: + if data["data"]["conclusion_type"] == 2: + return "" + except Exception as e: + logger.error(f"检测违规文本错误...", e=e) + return text diff --git a/zhenxun/plugins/ai/utils.py b/zhenxun/plugins/ai/utils.py new file mode 100644 index 00000000..946bc234 --- /dev/null +++ b/zhenxun/plugins/ai/utils.py @@ -0,0 +1,153 @@ +import random +import time + +from zhenxun.configs.config import NICKNAME +from zhenxun.models.ban_console import BanConsole + + +class AiMessageManager: + def __init__(self): + self._data = {} + self._same_message = [ + "为什么要发一样的话?", + "请不要再重复对我说一句话了,不然我就要生气了!", + "别再发这句话了,我已经知道了...", + "你是只会说这一句话吗?", + "[*],你发我也发!", + "[uname],[*]", + f"救命!有笨蛋一直给{NICKNAME}发一样的话!", + "这句话你已经给我发了{}次了,再发就生气!", + ] + self._repeat_message = [ + f"请不要学{NICKNAME}说话", + f"为什么要一直学{NICKNAME}说话?", + "你再学!你再学我就生气了!", + f"呜呜,你是想欺负{NICKNAME}嘛..", + "[uname]不要再学我说话了!", + "再学我说话,我就把你拉进黑名单(生气", + "你再学![uname]是个笨蛋!", + "你已经学我说话{}次了!别再学了!", + ] + + def add_message(self, user_id: str, message: str): + """添加用户消息 + + 参数: + user_id: 用户id + message: 消息内容 + """ + if message: + if self._data.get(user_id) is None: + self._data[user_id] = { + "time": time.time(), + "message": [], + "result": [], + "repeat_count": 0, + } + if time.time() - self._data[user_id]["time"] > 60 * 10: + self._data[user_id]["message"].clear() + self._data[user_id]["time"] = time.time() + self._data[user_id]["message"].append(message.strip()) + + def add_result(self, user_id: str, message: str): + """添加回复用户的消息 + + 参数: + user_id: 用户id + message: 回复消息内容 + """ + if message: + if self._data.get(user_id) is None: + self._data[user_id] = { + "time": time.time(), + "message": [], + "result": [], + "repeat_count": 0, + } + if time.time() - self._data[user_id]["time"] > 60 * 10: + self._data[user_id]["result"].clear() + self._data[user_id]["repeat_count"] = 0 + self._data[user_id]["time"] = time.time() + self._data[user_id]["result"].append(message.strip()) + + async def get_result(self, user_id: str, nickname: str) -> str | None: + """特殊消息特殊回复 + + 参数: + user_id: 用户id + nickname: 用户昵称 + + 返回: + str | None: 回答 + """ + try: + if len(self._data[user_id]["message"]) < 2: + return None + except KeyError: + return None + msg = await self._get_user_repeat_message_result(user_id) + if not msg: + msg = await self._get_user_same_message_result(user_id) + if msg: + if "[uname]" in msg: + msg = msg.replace("[uname]", nickname) + if not msg.startswith("生气了!你好烦,闭嘴!") and "[*]" in msg: + msg = msg.replace("[*]", self._data[user_id]["message"][-1]) + return msg + + async def _get_user_same_message_result(self, user_id: str) -> str | None: + """重复消息回复 + + 参数: + user_id: 用户id + + 返回: + str | None: 回答 + """ + msg = self._data[user_id]["message"][-1] + cnt = 0 + _tmp = self._data[user_id]["message"][:-1] + _tmp.reverse() + for s in _tmp: + if s == msg: + cnt += 1 + else: + break + if cnt > 1: + if random.random() < 0.5 and cnt > 3: + rand = random.randint(60, 300) + await BanConsole.ban(user_id, None, 9, rand, None) + self._data[user_id]["message"].clear() + return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" + return random.choice(self._same_message).format(cnt) + return None + + async def _get_user_repeat_message_result(self, user_id: str) -> str | None: + """复读真寻的消息回复 + + 参数: + user_id: 用户id + + 返回: + str | None: 回答 + """ + msg = self._data[user_id]["message"][-1] + if self._data[user_id]["result"]: + rst = self._data[user_id]["result"][-1] + else: + return None + if msg == rst: + self._data[user_id]["repeat_count"] += 1 + cnt = self._data[user_id]["repeat_count"] + if cnt > 1: + if random.random() < 0.5 and cnt > 3: + rand = random.randint(60, 300) + await BanConsole.ban(user_id, None, 9, rand, None) + self._data[user_id]["result"].clear() + self._data[user_id]["repeat_count"] = 0 + return f"生气了!你好烦,闭嘴!给我老实安静{rand}秒" + return random.choice(self._repeat_message).format(cnt) + return None + + +ai_message_manager = AiMessageManager() diff --git a/zhenxun/utils/depends/__init__.py b/zhenxun/utils/depends/__init__.py new file mode 100644 index 00000000..a993ee45 --- /dev/null +++ b/zhenxun/utils/depends/__init__.py @@ -0,0 +1,29 @@ +from nonebot.internal.params import Depends +from nonebot.params import Command +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + + +def OneCommand(): + """ + 获取单个命令Command + """ + + async def dependency( + cmd: tuple[str, ...] = Command(), + ): + return cmd[0] if cmd else None + + return Depends(dependency) + + +def UserName(): + """ + 用户名称 + """ + + async def dependency(user_info: UserInfo = EventUserInfo()): + return ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) + + return Depends(dependency) From 913811b90dc13c4c921941397b8a703ce74511cc Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 10 Mar 2024 00:50:20 +0800 Subject: [PATCH 015/132] =?UTF-8?q?feat=E2=9C=A8:=20ALAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/alapi/__init__.py | 14 +++++++ zhenxun/plugins/alapi/_data_source.py | 29 +++++++++++++ zhenxun/plugins/alapi/comments_163.py | 60 +++++++++++++++++++++++++++ zhenxun/plugins/alapi/cover.py | 45 ++++++++++++++++++++ zhenxun/plugins/alapi/jitang.py | 48 +++++++++++++++++++++ zhenxun/plugins/alapi/poetry.py | 55 ++++++++++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100644 zhenxun/plugins/alapi/__init__.py create mode 100644 zhenxun/plugins/alapi/_data_source.py create mode 100644 zhenxun/plugins/alapi/comments_163.py create mode 100644 zhenxun/plugins/alapi/cover.py create mode 100644 zhenxun/plugins/alapi/jitang.py create mode 100644 zhenxun/plugins/alapi/poetry.py diff --git a/zhenxun/plugins/alapi/__init__.py b/zhenxun/plugins/alapi/__init__.py new file mode 100644 index 00000000..3efe4113 --- /dev/null +++ b/zhenxun/plugins/alapi/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "alapi", + "ALAPI_TOKEN", + None, + help="在https://admin.alapi.cn/user/login登录后获取token", +) + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/alapi/_data_source.py b/zhenxun/plugins/alapi/_data_source.py new file mode 100644 index 00000000..61037ab9 --- /dev/null +++ b/zhenxun/plugins/alapi/_data_source.py @@ -0,0 +1,29 @@ +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx + + +async def get_data(url: str, params: dict | None = None) -> tuple[dict | str, int]: + """获取ALAPI数据 + + 参数: + url: 请求链接 + params: 参数 + + 返回: + tuple[dict | str, int]: 返回信息 + """ + if not params: + params = {} + params["token"] = Config.get_config("alapi", "ALAPI_TOKEN") + try: + data = (await AsyncHttpx.get(url, params=params, timeout=5)).json() + if data["code"] == 200: + if not data["data"]: + return "没有搜索到...", 997 + return data, 200 + else: + if data["code"] == 101: + return "缺失ALAPI TOKEN,请在配置文件中填写!", 999 + return f'发生了错误...code:{data["code"]}', 999 + except TimeoutError: + return "超时了....", 998 diff --git a/zhenxun/plugins/alapi/comments_163.py b/zhenxun/plugins/alapi/comments_163.py new file mode 100644 index 00000000..5b29ab5c --- /dev/null +++ b/zhenxun/plugins/alapi/comments_163.py @@ -0,0 +1,60 @@ +from nonebot import on_regex +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import get_data + +comments_163 = on_regex( + "^(网易云热评|网易云评论|到点了|12点了)$", priority=5, block=True +) + + +comments_163_url = "https://v2.alapi.cn/api/comment" + +__plugin_meta__ = PluginMetadata( + name="网易云热评", + description="生了个人,我很抱歉", + usage=""" + 到点了,还是防不了下塔 + 指令: + 网易云热评/到点了/12点了 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("网易云热评"), + priority=5, + block=True, +) + +_matcher.shortcut( + "(到点了|12点了)", + command="网易云热评", + arguments=[], + prefix=True, +) + + +@comments_163.handle() +async def _(session: EventSession, arparma: Arparma): + data, code = await get_data(comments_163_url) + if code != 200 and isinstance(data, str): + await Text(data).finish(reply=True) + data = data["data"] # type: ignore + comment = data["comment_content"] # type: ignore + song_name = data["title"] # type: ignore + await Text(f"{comment}\n\t——《{song_name}》").send(reply=True) + logger.info( + f" 发送网易云热评: {comment} \n\t\t————{song_name}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/alapi/cover.py b/zhenxun/plugins/alapi/cover.py new file mode 100644 index 00000000..9a832bff --- /dev/null +++ b/zhenxun/plugins/alapi/cover.py @@ -0,0 +1,45 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import get_data + +cover_url = "https://v2.alapi.cn/api/bilibili/cover" + +__plugin_meta__ = PluginMetadata( + name="b封面", + description="快捷的b站视频封面获取方式", + usage=""" + b封面 [链接/av/bv/cv/直播id] + 示例:b封面 av86863038 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("b封面", Args["url", str]), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, url: str): + params = {"c": url} + data, code = await get_data(cover_url, params) + if code != 200 and isinstance(data, str): + await Text(data).finish(reply=True) + data = data["data"] # type: ignore + title = data["title"] # type: ignore + img = data["cover"] # type: ignore + await MessageFactory([Text(f"title:{title}\n"), Image(img)]).send(reply=True) + logger.info( + f" 获取b站封面: {title} url:{img}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/alapi/jitang.py b/zhenxun/plugins/alapi/jitang.py new file mode 100644 index 00000000..a0a29e5f --- /dev/null +++ b/zhenxun/plugins/alapi/jitang.py @@ -0,0 +1,48 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import get_data + +url = "https://v2.alapi.cn/api/soul" + +__plugin_meta__ = PluginMetadata( + name="鸡汤", + description="喏,亲手为你煮的鸡汤", + usage=""" + 不喝点什么感觉有点不舒服 + 指令: + 鸡汤 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("鸡汤"), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + try: + data, code = await get_data(url) + if code != 200 and isinstance(data, str): + await Text(data).finish(reply=True) + await Text(data["data"]["content"]).send(reply=True) # type: ignore + logger.info( + f" 发送鸡汤:" + data["data"]["content"], # type:ignore + arparma.header_result, + session=session, + ) + except Exception as e: + await Text("鸡汤煮坏掉了...").send() + logger.error(f"鸡汤煮坏掉了", e=e) diff --git a/zhenxun/plugins/alapi/poetry.py b/zhenxun/plugins/alapi/poetry.py new file mode 100644 index 00000000..f315999f --- /dev/null +++ b/zhenxun/plugins/alapi/poetry.py @@ -0,0 +1,55 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import get_data + +__plugin_meta__ = PluginMetadata( + name="古诗", + description="为什么突然文艺起来了!", + usage=""" + 平白无故念首诗 + 示例:念诗/来首诗/念首诗 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("念诗"), + priority=5, + block=True, +) + +_matcher.shortcut( + "(来首诗|念首诗)", + command="念诗", + arguments=[], + prefix=True, +) + + +poetry_url = "https://v2.alapi.cn/api/shici" + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + data, code = await get_data(poetry_url) + if code != 200 and isinstance(data, str): + await Text(data).finish(reply=True) + data = data["data"] # type: ignore + content = data["content"] # type: ignore + title = data["origin"] # type: ignore + author = data["author"] # type: ignore + await Text(f"{content}\n\t——{author}《{title}》").send(reply=True) + logger.info( + f" 发送古诗: f'{content}\n\t--{author}《{title}》'", + arparma.header_result, + session=session, + ) From 5dd03bb0cafefb98fc42e171fc1a490d1c22d3bb Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 18 Mar 2024 16:10:44 +0800 Subject: [PATCH 016/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20black=5Fword?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 18 +- pyproject.toml | 1 + zhenxun/builtin_plugins/admin/ban/__init__.py | 2 +- zhenxun/builtin_plugins/hooks/__init__.py | 2 +- zhenxun/builtin_plugins/hooks/ban_hook.py | 14 +- .../superuser/broadcast/_data_source.py | 8 +- zhenxun/models/ban_console.py | 6 +- zhenxun/plugins/alapi/comments_163.py | 7 +- zhenxun/plugins/black_word/__init__.py | 281 +++++++++++++ zhenxun/plugins/black_word/data_source.py | 103 +++++ zhenxun/plugins/black_word/model.py | 154 ++++++++ zhenxun/plugins/black_word/utils.py | 374 ++++++++++++++++++ zhenxun/utils/platform.py | 58 ++- zhenxun/utils/utils.py | 13 + 14 files changed, 1011 insertions(+), 30 deletions(-) create mode 100644 zhenxun/plugins/black_word/__init__.py create mode 100644 zhenxun/plugins/black_word/data_source.py create mode 100644 zhenxun/plugins/black_word/model.py create mode 100644 zhenxun/plugins/black_word/utils.py diff --git a/poetry.lock b/poetry.lock index 0a13029c..e41ee7de 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1859,6 +1859,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pypinyin" +version = "0.51.0" +description = "汉字拼音转换模块/工具." +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" +files = [ + {file = "pypinyin-0.51.0-py2.py3-none-any.whl", hash = "sha256:ae8878f08fee15d0c5c11053a737e68a4158c22c63dc632b4de060af5c95bf84"}, + {file = "pypinyin-0.51.0.tar.gz", hash = "sha256:cede34fc35a79ef6c799f161e2c280e7b6755ee072fb741cae5ce2a60c4ae0c5"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-dateutil" version = "2.8.2" @@ -2982,4 +2998,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "858e616442c77d1a328e37af331056a7b870611b22247fcebfe5dbe41a3fd4f0" +content-hash = "535f64938d522045aff2fa03ec967470085477b9d5bf1b9b803bcfceac60c7b6" diff --git a/pyproject.toml b/pyproject.toml index 33e40319..f3363021 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ aiofiles = "^23.2.1" nonebot-plugin-htmlrender = "^0.3.0" nonebot-plugin-userinfo = "^0.1.3" nonebot-plugin-alconna = "^0.37.1" +pypinyin = "^0.51.0" [tool.poetry.dev-dependencies] diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 11a0608b..330eaeb5 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -31,7 +31,7 @@ __plugin_meta__ = PluginMetadata( usage=""" 普通管理员 格式: - ban [At用户] [时长] + ban [At用户] [时长(分钟)] 示例: ban @用户 : 永久拉黑用户 diff --git a/zhenxun/builtin_plugins/hooks/__init__.py b/zhenxun/builtin_plugins/hooks/__init__.py index 41912be2..80aa7181 100644 --- a/zhenxun/builtin_plugins/hooks/__init__.py +++ b/zhenxun/builtin_plugins/hooks/__init__.py @@ -40,4 +40,4 @@ Config.add_plugin_config( type=int, ) -# nonebot.load_plugins(str(Path(__file__).parent.resolve())) +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py index 3f10e078..74d456f3 100644 --- a/zhenxun/builtin_plugins/hooks/ban_hook.py +++ b/zhenxun/builtin_plugins/hooks/ban_hook.py @@ -38,10 +38,8 @@ async def _( ban_result = Config.get_config("hook", "BAN_RESULT") if user_id in bot.config.superusers: return - if await BanConsole.is_ban(user_id) or await BanConsole.is_ban( - user_id, group_id - ): - time = await BanConsole.check_ban_time(user_id) + if await BanConsole.is_ban(user_id, group_id): + time = await BanConsole.check_ban_time(user_id, group_id) if time == -1: time_str = "∞" else: @@ -49,7 +47,13 @@ async def _( if time < 60: time_str = str(time) + " 秒" else: - time_str = str(int(time / 60)) + " 分钟" + minute = int(time / 60) + if minute > 60: + hours = int(minute / 60) + minute = minute % 60 + time_str = f"{hours} 小时 {minute}分钟" + else: + time_str = f"{minute} 分钟" if ban_result and _flmt.check(user_id): _flmt.start_cd(user_id) await MessageFactory( diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 4781aff3..66a6033b 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -6,11 +6,7 @@ from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import ( - Image, - MessageFactory, - Text, -) +from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.models.group_console import GroupConsole @@ -49,7 +45,7 @@ class BroadcastManage: group.group_id, "broadcast", group.channel_id ): target = PlatformManage.get_target( - bot, group.group_id, group.channel_id + bot, None, group.group_id, group.channel_id ) if target: await MessageFactory(message_list).send_to(target, bot) diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index 5996e55f..d15c5401 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -116,6 +116,8 @@ class BanConsole(Model): if await cls.check_ban_time(user_id, group_id): return True else: + if await cls.check_ban_time(user_id): + return True await cls.unban(user_id, group_id) return False @@ -126,7 +128,7 @@ class BanConsole(Model): group_id: str | None, ban_level: int, duration: int, - operator: str | None, + operator: str | None = None, ): """ban掉目标用户 @@ -134,7 +136,7 @@ class BanConsole(Model): user_id: 用户id group_id: 群组id ban_level: 使用命令者的权限等级 - duration: 时长,秒 + duration: 时长,分钟,-1时为永久 operator: 操作者id """ logger.debug( diff --git a/zhenxun/plugins/alapi/comments_163.py b/zhenxun/plugins/alapi/comments_163.py index 5b29ab5c..bac2587e 100644 --- a/zhenxun/plugins/alapi/comments_163.py +++ b/zhenxun/plugins/alapi/comments_163.py @@ -9,11 +9,6 @@ from zhenxun.services.log import logger from ._data_source import get_data -comments_163 = on_regex( - "^(网易云热评|网易云评论|到点了|12点了)$", priority=5, block=True -) - - comments_163_url = "https://v2.alapi.cn/api/comment" __plugin_meta__ = PluginMetadata( @@ -44,7 +39,7 @@ _matcher.shortcut( ) -@comments_163.handle() +@_matcher.handle() async def _(session: EventSession, arparma: Arparma): data, code = await get_data(comments_163_url) if code != 200 and isinstance(data, str): diff --git a/zhenxun/plugins/black_word/__init__.py b/zhenxun/plugins/black_word/__init__.py new file mode 100644 index 00000000..6af840a7 --- /dev/null +++ b/zhenxun/plugins/black_word/__init__.py @@ -0,0 +1,281 @@ +from datetime import datetime +from typing import Any, List + +from nonebot import on_message +from nonebot.adapters import Bot, Event +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + UniMsg, + on_alconna, +) +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage + +from .data_source import set_user_punish, show_black_text_image +from .utils import black_word_manager + +__plugin_meta__ = PluginMetadata( + name="敏感词检测", + description="请注意你的发言!!", + usage=""" + 惩罚机制: 检测内容提示 + 设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取 + 记录名单: 查看检测记录名单 + 记录名单: + -u [uid] 指定用户记录名单 + -g [gid] 指定群组记录名单 + -d [date] 指定日期 + -dt ['=', '>', '<'] 大于小于等于指定日期 + + 示例: + 设置惩罚 123123123 0 1 + 记录名单 -u 123123123 + 记录名单 -g 333333 + 记录名单 -d 2022-11-11 + 记录名单 -d 2022-11-11 -dt > + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + menu_type="其他", + configs=[ + RegisterConfig( + key="CYCLE_DAYS", + value=30, + help="黑名单词汇记录周期", + default_value=30, + type=int, + ), + RegisterConfig( + key="TOLERATE_COUNT", + value=[5, 1, 1, 1, 1], + help="各个级别惩罚的容忍次数, 依次为: 1, 2, 3, 4, 5", + default_value=[5, 1, 1, 1, 1], + type=List[int], + ), + RegisterConfig( + key="AUTO_PUNISH", + value=True, + help="是否启动自动惩罚机制", + default_value=True, + type=bool, + ), + RegisterConfig( + key="BAN_4_DURATION", + value=360, + help="Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", + default_value=360, + type=int, + ), + RegisterConfig( + key="BAN_3_DURATION", + value=7, + help="Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", + default_value=7, + type=int, + ), + RegisterConfig( + key="WARNING_RESULT", + value=f"请注意对{NICKNAME}的发言内容", + help="口头警告内容", + default_value=None, + ), + RegisterConfig( + key="AUTO_ADD_PUNISH_LEVEL", + value=360, + help="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", + default_value=360, + type=int, + ), + RegisterConfig( + key="ADD_PUNISH_LEVEL_TO_COUNT", + value=3, + help="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", + default_value=3, + type=int, + ), + RegisterConfig( + key="ALAPI_CHECK_FLAG", + value=False, + help="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", + default_value=False, + type=bool, + ), + RegisterConfig( + key="CONTAIN_BLACK_STOP_PROPAGATION", + value=True, + help="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +_message_matcher = on_message(priority=1, block=False) + +_punish_matcher = on_alconna( + Alconna("设置惩罚", Args["uid", str]["id", int]["punish_level", int]), + priority=1, + permission=SUPERUSER, + block=True, +) + + +_show_matcher = on_alconna( + Alconna( + "记录名单", + Option("-u|--uid", Args["uid", str]), + Option("-g|--group", Args["gid", str]), + Option("-d|--date", Args["date", str]), + Option("-dt|--type", Args["date_type", ["=", ">", "<"]], default="="), + ), + priority=1, + permission=SUPERUSER, + block=True, +) + +_show_punish_matcher = on_alconna( + Alconna("惩罚机制"), aliases={"敏感词检测"}, priority=1, block=True +) + + +# 黑名单词汇检测 +@run_preprocessor +async def _( + bot: Bot, message: UniMsg, matcher: Matcher, event: Event, session: EventSession +): + gid = session.id3 or session.id2 + if session.id1: + if ( + event.is_tome() + and matcher.plugin_name == "black_word" + and not await BanConsole.is_ban(session.id1, gid) + ): + msg = message.extract_plain_text() + if session.id1 in bot.config.superusers: + return logger.debug( + f"超级用户跳过黑名单词汇检查 Message: {msg}", target=session.id1 + ) + if gid: + """屏蔽群权限-1的群""" + group, _ = await GroupConsole.get_or_create( + group_id=gid, channel_id__isnull=True + ) + if group.level < 0: + return + if await black_word_manager.check(bot, session, msg) and Config.get_config( + "black_word", "CONTAIN_BLACK_STOP_PROPAGATION" + ): + matcher.stop_propagation() + + +@_show_matcher.handle() +async def _( + bot: Bot, uid: Match[str], gid: Match[str], date: Match[str], date_type: Match[str] +): + user_id = None + group_id = None + date_ = None + date_str = None + date_type_ = "=" + if uid.available: + user_id = uid.result + if gid.available: + group_id = gid.result + if date.available: + date_str = date.result + if date_type.available: + date_type_ = date_type.result + if date_str: + try: + date_ = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + await Text("日期格式错误,需要:年-月-日").finish() + result = await show_black_text_image( + user_id, + group_id, + date_, + date_type_, + ) + await Image(result.pic2bytes()).send() + + +@_show_punish_matcher.handle() +async def _(): + text = f""" + ** 惩罚机制 ** + + 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 + + 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 + + 目前公开的惩罚等级: + + 1级:永久ban + + 2级:删除好友 + + 3级:ban指定/随机天数 + + 4级:ban指定/随机时长 + + 5级:警告 + + 备注: + + 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 + + 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 + + 关于敏感词: + + 记住不要骂{NICKNAME}就对了! + """.strip() + max_width = 0 + for m in text.split("\n"): + max_width = len(m) * 20 if len(m) * 20 > max_width else max_width + max_height = len(text.split("\n")) * 24 + A = BuildImage( + max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" + ) + await A.text((10, 10), text) + await Image(A.pic2bytes()).send() + + +@_punish_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + uid: str, + id: int, + punish_level: int, +): + result = await set_user_punish( + bot, uid, session.id2 or session.id3, id, punish_level + ) + await Text(result).send(reply=True) + logger.info( + f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/black_word/data_source.py b/zhenxun/plugins/black_word/data_source.py new file mode 100644 index 00000000..e985facc --- /dev/null +++ b/zhenxun/plugins/black_word/data_source.py @@ -0,0 +1,103 @@ +from datetime import datetime + +from nonebot.adapters import Bot + +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.utils.image_utils import BuildImage, ImageTemplate + +from .model import BlackWord +from .utils import Config, _get_punish + + +async def show_black_text_image( + user_id: str | None, + group_id: str | None, + date: datetime | None, + data_type: str = "=", +) -> BuildImage: + """展示记录名单 + + 参数: + bot: bot + user: 用户id + group_id: 群组id + date: 日期 + data_type: 日期搜索类型 + + 返回: + BuildImage: 数据图片 + """ + data_list = await BlackWord.get_black_data(user_id, group_id, date, data_type) + column_name = [ + "ID", + "昵称", + "UID", + "GID", + "文本", + "检测内容", + "检测等级", + "惩罚", + "平台", + "记录日期", + ] + column_list = [] + uid_list = [u for u in data_list] + uid2name = { + u.user_id: u.user_name for u in await FriendUser.filter(user_id__in=uid_list) + } + for i, data in enumerate(data_list): + uname = uid2name.get(data.user_id) + if not uname: + if u := await GroupInfoUser.get_or_none( + user_id=data.user_id, group_id=data.group_id + ): + uname = u.user_name + if len(data.plant_text) > 30: + data.plant_text = data.plant_text[:30] + "..." + column_list.append( + [ + i, + uname or data.user_id, + data.user_id, + data.group_id, + data.plant_text, + data.black_word, + data.punish_level, + data.punish, + data.platform, + data.create_time, + ] + ) + A = await ImageTemplate.table_page( + "记录名单", "一个都不放过!", column_name, column_list + ) + return A + + +async def set_user_punish( + bot: Bot, user_id: str, group_id: str | None, id_: int, punish_level: int +) -> str: + """设置惩罚 + + 参数: + user_id: 用户id + group_id: 群组id或频道id + id_: 记录下标 + punish_level: 惩罚等级 + + 返回: + str: 结果 + """ + result = await _get_punish(bot, punish_level, user_id, group_id) + punish = { + 1: "永久ban", + 2: "删除好友", + 3: f"ban {result} 天", + 4: f"ban {result} 分钟", + 5: "口头警告", + } + if await BlackWord.set_user_punish(user_id, punish[punish_level], id_=id_): + return f"已对 USER {user_id} 进行 {punish[punish_level]} 处罚。" + else: + return "操作失败,可能未找到用户,id或敏感词" diff --git a/zhenxun/plugins/black_word/model.py b/zhenxun/plugins/black_word/model.py new file mode 100644 index 00000000..ef81c0ba --- /dev/null +++ b/zhenxun/plugins/black_word/model.py @@ -0,0 +1,154 @@ +from datetime import datetime, timedelta +from email.policy import default + +import pytz +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class BlackWord(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255, null=True) + """群聊id""" + plant_text = fields.TextField() + """检测文本""" + black_word = fields.TextField() + """黑名单词语""" + punish = fields.TextField(default="") + """惩罚内容""" + punish_level = fields.IntField() + """惩罚等级""" + create_time = fields.DatetimeField(auto_now_add=True) + """创建时间""" + platform = fields.CharField(255, null=True) + """平台""" + + class Meta: + table = "black_word" + table_description = "惩罚机制数据表" + + @classmethod + async def set_user_punish( + cls, + user_id: str, + punish: str, + black_word: str | None = None, + id_: int | None = None, + ) -> bool: + """设置处罚 + + 参数: + user_id: 用户id + punish: 处罚 + black_word: 黑名单词汇 + id_: 记录下标 + """ + user = None + if (not black_word and id_ is None) or not punish: + return False + if black_word: + user = ( + await cls.filter(user_id=user_id, black_word=black_word, punish="") + .order_by("id") + .first() + ) + elif id_ is not None: + user_list = await cls.filter(user_id=user_id).order_by("id").all() + if len(user_list) == 0 or (id_ < 0 or id_ > len(user_list)): + return False + user = user_list[id_] + if not user: + return False + user.punish = f"{user.punish}{punish} " + await user.save(update_fields=["punish"]) + return True + + @classmethod + async def get_user_count( + cls, user_id: str, days: int = 7, punish_level: int | None = None + ) -> int: + """获取用户规定周期内的犯事次数 + + 参数: + user_id: 用户id + days: 周期天数 + punish_level: 惩罚等级 + """ + query = cls.filter( + user_id=user_id, + create_time__gte=datetime.now() - timedelta(days=days), + punish_level__not_in=[-1], + ) + if punish_level is not None: + query = query.filter(punish_level=punish_level) + return await query.count() + + @classmethod + async def get_user_punish_level(cls, user_id: str, days: int = 7) -> int | None: + """获取用户最近一次的惩罚记录等级 + + 参数: + user_id: 用户id + days: 周期天数 + """ + if ( + user := await cls.filter( + user_id=user_id, + create_time__gte=datetime.now() - timedelta(days=days), + ) + .order_by("id") + .first() + ): + return user.punish_level + return None + + @classmethod + async def get_black_data( + cls, + user_id: str | None, + group_id: str | None, + date: datetime | None, + date_type: str = "=", + ) -> list["BlackWord"]: + """通过指定条件查询数据 + + 参数: + user_id: 用户id + group_id: 群号 + date: 日期 + date_type: 日期查询类型 + """ + query = cls + if user_id: + query = query.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + if date: + if date_type == "=": + query = query.filter( + create_time__range=[date, date + timedelta(days=1)] + ) + elif date_type == ">": + query = query.filter(create_time__gte=date) + elif date_type == "<": + query = query.filter(create_time__lte=date) + data_list = await query.all().order_by("id") + for data in data_list: + data.create_time = data.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ) + return data_list # type: ignore + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE black_word RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE black_word ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE black_word ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE black_word ADD COLUMN platform character varying(255);", + ] diff --git a/zhenxun/plugins/black_word/utils.py b/zhenxun/plugins/black_word/utils.py new file mode 100644 index 00000000..29145dfb --- /dev/null +++ b/zhenxun/plugins/black_word/utils.py @@ -0,0 +1,374 @@ +import random +from pathlib import Path + +import ujson as json +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import ActionFailed +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.platform import PlatformManage +from zhenxun.utils.utils import cn2py + +from .model import BlackWord + + +class BlackWordManager: + """ + 敏感词管理( 拒绝恶意 + """ + + def __init__(self, word_file: Path, py_file: Path): + self._word_list = { + "1": [], + "2": [], + "3": [], + "4": ["sb", "nmsl", "mdzz", "2b", "jb", "操", "废物", "憨憨", "cnm", "rnm"], + "5": [], + } + self._py_list = { + "1": [], + "2": [], + "3": [], + "4": [ + "shabi", + "wocaonima", + "sima", + "sabi", + "zhizhang", + "naocan", + "caonima", + "rinima", + "simadongxi", + "simawanyi", + "hanbi", + "hanpi", + "laji", + "fw", + ], + "5": [], + } + word_file.parent.mkdir(parents=True, exist_ok=True) + if word_file.exists(): + # 清空默认配置 + with open(word_file, "r", encoding="utf8") as f: + self._word_list = json.load(f) + else: + with open(word_file, "w", encoding="utf8") as f: + json.dump( + self._word_list, + f, + ensure_ascii=False, + indent=4, + ) + if py_file.exists(): + # 清空默认配置 + with open(py_file, "r", encoding="utf8") as f: + self._py_list = json.load(f) + else: + with open(py_file, "w", encoding="utf8") as f: + json.dump( + self._py_list, + f, + ensure_ascii=False, + indent=4, + ) + + async def check( + self, bot: Bot, session: EventSession, message: str + ) -> str | bool | None: + """检查是否包含黑名单词汇 + + 参数: + bot: Bot + session: EventSession + message: 消息 + """ + logger.debug( + f"检查文本是否含有黑名单词汇: {message}", "敏感词检测", session=session + ) + if session.id1: + if data := self._check(message): + if data[0]: + await _add_user_black_word( + bot, + session.id1, + session.id2 or session.id3, + data[0], + message, + int(data[1]), + ) + return True + if Config.get_config( + "black_word", "ALAPI_CHECK_FLAG" + ) and not await check_text(message): + await send_msg( + bot, + "", + None, + f"用户 {session.id1} 群组 {session.id3 or session.id2} ALAPI 疑似检测:{message}", + ) + return False + + def _check(self, message: str) -> tuple[str | None, int]: + """检测文本是否违规 + + 参数: + message: 检测消息 + """ + # 移除空格 + message = message.replace(" ", "") + py_msg = cn2py(message).lower() + # 完全匹配 + for x in [self._word_list, self._py_list]: + for level in x: + if message in x[level] or py_msg in x[level]: + return message if message in x[level] else py_msg, int(level) + # 模糊匹配 + for x in [self._word_list, self._py_list]: + for level in x: + for m in x[level]: + if m in message or m in py_msg: + return m, -1 + return None, 0 + + +async def _add_user_black_word( + bot: Bot, + user_id: str, + group_id: str | None, + black_word: str, + message: str, + punish_level: int, +): + """添加敏感词数据 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + black_word: 触发的黑名单词汇 + message: 原始文本 + punish_level: 惩罚等级 + """ + cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7 + user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level) + add_punish_level_to_count = Config.get_config( + "black_word", "ADD_PUNISH_LEVEL_TO_COUNT" + ) + # 周期内超过次数直接提升惩罚 + if ( + Config.get_config("black_word", "AUTO_ADD_PUNISH_LEVEL") + and add_punish_level_to_count + ): + punish_level -= 1 + await BlackWord.create( + user_id=user_id, + group_id=group_id, + plant_text=message, + black_word=black_word, + punish_level=punish_level, + platform=PlatformManage.get_platform(bot), + ) + logger.info( + f"已将 USER {user_id} GROUP {group_id} 添加至黑名单词汇记录 Black_word:{black_word} Plant_text:{message}" + ) + # 自动惩罚 + if Config.get_config("black_word", "AUTO_PUNISH") and punish_level != -1: + await _punish_handle(bot, user_id, group_id, punish_level, black_word) + + +async def _punish_handle( + bot: Bot, + user_id: str, + group_id: str | None, + punish_level: int, + black_word: str, +): + """惩罚措施,级别越低惩罚越严 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + black_word: 触发的黑名单词汇 + channel_id: 频道id + """ + logger.info(f"BlackWord USER {user_id} 触发 {punish_level} 级惩罚...") + # 周期天数 + cycle_days = Config.get_config("black_word", "CYCLE_DAYS") or 7 + # 用户周期内触发punish_level级惩罚的次数 + user_count = await BlackWord.get_user_count(user_id, cycle_days, punish_level) + # 获取最近一次的惩罚等级,将在此基础上增加 + punish_level = ( + await BlackWord.get_user_punish_level(user_id, cycle_days) or punish_level + ) + # 容忍次数:List[int] + tolerate_count = Config.get_config("black_word", "TOLERATE_COUNT") + if not tolerate_count or len(tolerate_count) < 5: + tolerate_count = [5, 2, 2, 2, 2] + if punish_level == 1 and user_count > tolerate_count[punish_level - 1]: + # 永久ban + await _get_punish(bot, 1, user_id, group_id) + await BlackWord.set_user_punish(user_id, "永久ban 删除好友", black_word) + elif punish_level == 2 and user_count > tolerate_count[punish_level - 1]: + # 删除好友 + await _get_punish(bot, 2, user_id, group_id) + await BlackWord.set_user_punish(user_id, "删除好友", black_word) + elif punish_level == 3 and user_count > tolerate_count[punish_level - 1]: + # 永久ban + ban_day = await _get_punish(bot, 3, user_id, group_id) + await BlackWord.set_user_punish(user_id, f"ban {ban_day} 天", black_word) + elif punish_level == 4 and user_count > tolerate_count[punish_level - 1]: + # ban指定时长 + ban_time = await _get_punish(bot, 4, user_id, group_id) + await BlackWord.set_user_punish(user_id, f"ban {ban_time} 分钟", black_word) + elif punish_level == 5 and user_count > tolerate_count[punish_level - 1]: + # 口头警告 + warning_result = await _get_punish(bot, 5, user_id, group_id) + await BlackWord.set_user_punish( + user_id, f"口头警告:{warning_result}", black_word + ) + else: + await BlackWord.set_user_punish(user_id, f"提示!", black_word) + await send_msg( + bot, + user_id, + group_id, + f"BlackWordChecker:该条发言已被记录,目前你在{cycle_days}天内的发表{punish_level}级" + f"言论记录次数为:{user_count}次,请注意你的发言\n" + f"* 如果你不清楚惩罚机制,请发送“惩罚机制” *", + ) + + +async def _get_punish( + bot: Bot, + id_: int, + user_id: str, + group_id: str | None = None, +) -> int | str | None: + """通过id_获取惩罚 + + 参数: + bot: Bot + id_: id + user_id: 用户id + group_id: 群组id或频道id + """ + # 忽略的群聊 + # _ignore_group = Config.get_config("black_word", "IGNORE_GROUP") + # 处罚 id 4 ban 时间:int,List[int] + ban_3_duration = Config.get_config("black_word", "BAN_3_DURATION") or 7 + # 处罚 id 4 ban 时间:int,List[int] + ban_4_duration = Config.get_config("black_word", "BAN_4_DURATION") or 360 + # 口头警告内容 + warning_result = Config.get_config("black_word", "WARNING_RESULT") + if user := await GroupInfoUser.get_or_none(user_id=user_id, group_id=group_id): + uname = user.user_name + else: + uname = user_id + # 永久ban + if id_ == 1: + if str(user_id) not in bot.config.superusers: + await BanConsole.ban(user_id, group_id, 10, -1, None) + await send_msg( + bot, + user_id, + group_id, + f"BlackWordChecker 永久ban USER {uname}({user_id})", + ) + logger.info(f"BlackWord 永久封禁 USER {user_id}...") + # 删除好友(有的话 + elif id_ == 2: + if str(user_id) not in bot.config.superusers: + try: + await bot.delete_friend(user_id=user_id) + await send_msg( + bot, + user_id, + group_id, + f"BlackWordChecker 删除好友 USER {uname}({user_id})", + ) + logger.info(f"BlackWord 删除好友 {user_id}...") + except ActionFailed: + pass + # 封禁用户指定时间,默认7天 + elif id_ == 3: + if isinstance(ban_3_duration, list): + ban_3_duration = random.randint(ban_3_duration[0], ban_3_duration[1]) + await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60 * 24) + await send_msg( + bot, + user_id, + group_id, + f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_3_duration} 天处罚。", + ) + logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_3_duration} 天...") + return ban_3_duration + # 封禁用户指定时间,默认360分钟 + elif id_ == 4: + if isinstance(ban_4_duration, list): + ban_4_duration = random.randint(ban_4_duration[0], ban_4_duration[1]) + await BanConsole.ban(user_id, group_id, 9, ban_4_duration * 60) + await send_msg( + bot, + user_id, + group_id, + f"BlackWordChecker 对用户 USER {uname}({user_id}) 进行封禁 {ban_4_duration} 分钟处罚。", + ) + logger.info(f"BlackWord 封禁 USER {uname}({user_id}) {ban_4_duration} 分钟...") + return ban_4_duration + # 口头警告 + elif id_ == 5: + await PlatformManage.send_message(bot, user_id, group_id, warning_result) + logger.info(f"BlackWord 口头警告 USER {user_id}") + return warning_result + return None + + +async def send_msg(bot: Bot, user_id: str, group_id: str | None, message: str): + """发送消息 + + 参数: + bot: Bot + user_id: user_id + group_id: group_id + message: message + """ + if not user_id: + platform = PlatformManage.get_platform(bot) + user_id = bot.config.platform_superusers[platform][0] + await PlatformManage.send_message(bot, user_id, group_id, message) + + +async def check_text(text: str) -> bool: + """ALAPI文本检测,检测输入违规 + + 参数: + text: 回复 + """ + if not Config.get_config("alapi", "ALAPI_TOKEN"): + return True + params = {"token": Config.get_config("alapi", "ALAPI_TOKEN"), "text": text} + try: + data = ( + await AsyncHttpx.get( + "https://v2.alapi.cn/api/censor/text", timeout=4, params=params + ) + ).json() + if data["code"] == 200: + return data["data"]["conclusion_type"] == 2 + except Exception as e: + logger.error(f"检测违规文本错误...", e=e) + return True + + +black_word_manager = BlackWordManager( + DATA_PATH / "black_word" / "black_word.json", + DATA_PATH / "black_word" / "black_py.json", +) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index fb34e81f..dba7bfe3 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -9,10 +9,14 @@ from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.utils import is_coroutine_callable from nonebot_plugin_saa import ( + Image, MessageFactory, TargetDoDoChannel, + TargetDoDoPrivate, TargetKaiheilaChannel, + TargetKaiheilaPrivate, TargetQQGroup, + TargetQQPrivate, Text, ) @@ -23,6 +27,31 @@ from zhenxun.services.log import logger class PlatformManage: + @classmethod + async def send_message( + cls, + bot: Bot, + user_id: str | None, + group_id: str | None, + message: str | Text | MessageFactory | Image, + ) -> bool: + """发送消息 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id或频道id + message: 消息文本 + + 返回: + bool: 是否发送成功 + """ + if target := cls.get_target(bot, user_id, group_id): + send_message = Text(message) if isinstance(message, str) else message + await send_message.send_to(target, bot) + return True + return False + @classmethod async def update_group(cls, bot: Bot) -> int: """更新群组信息 @@ -198,13 +227,18 @@ class PlatformManage: return [], "" @classmethod - def get_target(cls, bot: Bot, group_id: str | None, channel_id: str | None): + def get_target( + cls, + bot: Bot, + user_id: str | None = None, + group_id: str | None = None, + ): """获取发生Target 参数: bot: Bot group_id: 群组id - channel_id: 频道id + channel_id: 频道id或群组id 返回: target: 对应平台Target @@ -213,11 +247,19 @@ class PlatformManage: if isinstance(bot, (v11Bot, v12Bot)): if group_id: target = TargetQQGroup(group_id=int(group_id)) - if channel_id: - if isinstance(bot, DodoBot): - target = TargetDoDoChannel(channel_id=channel_id) - elif isinstance(bot, KaiheilaBot): - target = TargetKaiheilaChannel(channel_id=channel_id) + elif user_id: + target = TargetQQPrivate(user_id=int(user_id)) + elif isinstance(bot, DodoBot): + if group_id: + target = TargetDoDoChannel(channel_id=group_id) + elif user_id: + # target = TargetDoDoPrivate(user_id=user_id) + pass + elif isinstance(bot, KaiheilaBot): + if group_id: + target = TargetKaiheilaChannel(channel_id=group_id) + elif user_id: + target = TargetKaiheilaPrivate(user_id=user_id) return target @@ -294,7 +336,7 @@ async def broadcast_group( if is_continue: continue target = PlatformManage.get_target( - _bot, group.group_id, group.channel_id + _bot, None, group.group_id, group.channel_id ) if target: _used_group.append(key) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 5fe35211..366a5283 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Any import httpx +import pypinyin import pytz from zhenxun.services.log import logger @@ -151,6 +152,18 @@ class FreqLimiter: return self.next_time[key] - time.time() +def cn2py(word: str) -> str: + """将字符串转化为拼音 + + 参数: + word: 文本 + """ + temp = "" + for i in pypinyin.pinyin(word, style=pypinyin.NORMAL): + temp += "".join(i) + return temp + + async def get_user_avatar(uid: int | str) -> bytes | None: """快捷获取用户头像 From 8a073aa7bc4ed57589bfddcbd8bcf417a36b899d Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 18 Mar 2024 16:12:27 +0800 Subject: [PATCH 017/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20black=5Fword?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/black_word/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/plugins/black_word/__init__.py b/zhenxun/plugins/black_word/__init__.py index 6af840a7..79cf4747 100644 --- a/zhenxun/plugins/black_word/__init__.py +++ b/zhenxun/plugins/black_word/__init__.py @@ -32,7 +32,7 @@ from .utils import black_word_manager __plugin_meta__ = PluginMetadata( name="敏感词检测", - description="请注意你的发言!!", + description="请注意你的发言!", usage=""" 惩罚机制: 检测内容提示 设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取 From 606da31851e8e77283addc99f503c1badcb06d7c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 18 Mar 2024 17:50:07 +0800 Subject: [PATCH 018/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20bt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 142 +++++++++++++++++++++++++++++- pyproject.toml | 2 + zhenxun/plugins/ai/__init__.py | 27 ------ zhenxun/plugins/bt/__init__.py | 78 ++++++++++++++++ zhenxun/plugins/bt/data_source.py | 54 ++++++++++++ zhenxun/utils/rules.py | 13 +++ 6 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 zhenxun/plugins/bt/__init__.py create mode 100644 zhenxun/plugins/bt/data_source.py diff --git a/poetry.lock b/poetry.lock index e41ee7de..c5c9a8b9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -264,6 +264,32 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "binaryornot" version = "0.4.4" @@ -926,6 +952,104 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "lxml" +version = "5.1.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, + {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, + {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, + {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, + {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, + {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, + {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, + {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, + {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, + {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, + {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, + {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, + {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, + {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, + {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, + {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, + {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, + {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.8)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "markdown" version = "3.5.2" @@ -2222,6 +2346,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "starlette" version = "0.36.3" @@ -2998,4 +3138,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "535f64938d522045aff2fa03ec967470085477b9d5bf1b9b803bcfceac60c7b6" +content-hash = "28cd8bd2a5d3b00dc4c53dd5d259c5095b741494c7bb17a88927bfacb31cf452" diff --git a/pyproject.toml b/pyproject.toml index f3363021..d75f1afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,8 @@ nonebot-plugin-htmlrender = "^0.3.0" nonebot-plugin-userinfo = "^0.1.3" nonebot-plugin-alconna = "^0.37.1" pypinyin = "^0.51.0" +beautifulsoup4 = "^4.12.3" +lxml = "^5.1.0" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/ai/__init__.py b/zhenxun/plugins/ai/__init__.py index c66be3ed..3b6d694a 100644 --- a/zhenxun/plugins/ai/__init__.py +++ b/zhenxun/plugins/ai/__init__.py @@ -16,33 +16,6 @@ from zhenxun.utils.depends import UserName from .data_source import get_chat_result, hello, no_result -__zx_plugin_name__ = "AI" -__plugin_usage__ = f""" -usage: - 与{NICKNAME}普普通通的对话吧! -""" -__plugin_version__ = 0.1 -__plugin_author__ = "HibiKier" -__plugin_settings__ = { - "level": 5, - "cmd": ["Ai", "ai", "AI", "aI"], -} -__plugin_configs__ = { - "TL_KEY": {"value": [], "help": "图灵Key", "type": List[str]}, - "ALAPI_AI_CHECK": { - "value": False, - "help": "是否检测青云客骂娘回复", - "default_value": False, - "type": bool, - }, - "TEXT_FILTER": { - "value": ["鸡", "口交"], - "help": "文本过滤器,将敏感词更改为*", - "default_value": [], - "type": List[str], - }, -} - __plugin_meta__ = PluginMetadata( name="AI", description="屑Ai", diff --git a/zhenxun/plugins/bt/__init__.py b/zhenxun/plugins/bt/__init__.py new file mode 100644 index 00000000..ee0e8643 --- /dev/null +++ b/zhenxun/plugins/bt/__init__.py @@ -0,0 +1,78 @@ +from asyncio.exceptions import TimeoutError + +from httpx import ConnectTimeout +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.rules import ensure_private + +from .data_source import get_bt_info + +__plugin_meta__ = PluginMetadata( + name="磁力搜索", + description="bt(磁力搜索)[仅支持私聊,懂的都懂]", + usage=""" + * 拒绝反冲斗士! * + 指令: + bt [关键词] ?[页数] + 示例:bt 钢铁侠 + 示例:bt 钢铁侠 3 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + key="BT_MAX_NUM", + value=10, + help="单次BT搜索返回最大消息数量", + default_value=10, + type=int, + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("bt", Args["keyword", str]["page?", int]), + rule=ensure_private, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + keyword: str, + page: Match[int], +): + send_flag = False + try: + async for title, type_, create_time, file_size, link in get_bt_info( + keyword, page.result if page.available else 1 + ): + await Text( + f"标题:{title}\n" + f"类型:{type_}\n" + f"创建时间:{create_time}\n" + f"文件大小:{file_size}\n" + f"种子:{link}" + ).send() + send_flag = True + except (TimeoutError, ConnectTimeout): + await Text(f"搜索 {keyword} 超时...").finish() + except Exception as e: + logger.error(f"bt 错误", arparma.header_result, session=session, e=e) + await Text(f"bt 其他未知错误..").finish() + if not send_flag: + await Text(f"{keyword} 未搜索到...").send() + logger.info( + f"BT搜索 {keyword} 第 {page} 页", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/bt/data_source.py b/zhenxun/plugins/bt/data_source.py new file mode 100644 index 00000000..ad02a5d5 --- /dev/null +++ b/zhenxun/plugins/bt/data_source.py @@ -0,0 +1,54 @@ +from bs4 import BeautifulSoup + +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx, AsyncPlaywright + +url = "http://www.eclzz.ink" + + +async def get_bt_info(keyword: str, page: int): + """获取资源信息 + + 参数: + keyword: 关键词 + page: 页数 + """ + global url + text = (await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30)).text + if "301 Moved Permanently" in text: + async with AsyncPlaywright.new_page() as _page: + await _page.goto(url) + url = _page.url + text = ( + await AsyncHttpx.get(f"{url}/s/{keyword}_rel_{page}.html", timeout=30) + ).text + if "大约0条结果" in text: + return + soup = BeautifulSoup(text, "lxml") + item_lst = soup.find_all("div", {"class": "search-item"}) + bt_max_num = Config.get_config("bt", "BT_MAX_NUM") or 10 + bt_max_num = bt_max_num if bt_max_num < len(item_lst) else len(item_lst) + for item in item_lst[:bt_max_num]: + divs = item.find_all("div") + title = ( + str(divs[0].find("a").text).replace("", "").replace("", "").strip() + ) + spans = divs[2].find_all("span") + type_ = spans[0].text + create_time = spans[1].find("b").text + file_size = spans[2].find("b").text + link = await get_download_link(divs[0].find("a")["href"]) + yield title, type_, create_time, file_size, link + + +async def get_download_link(_url: str) -> str | None: + """获取资源下载地址 + + 参数: + _url: 链接 + """ + text = (await AsyncHttpx.get(f"{url}{_url}")).text + soup = BeautifulSoup(text, "lxml") + if fd := soup.find("a", {"id": "down-url"}): + return fd["href"] # type: ignore + return None diff --git a/zhenxun/utils/rules.py b/zhenxun/utils/rules.py index 0508f21f..f3381a48 100644 --- a/zhenxun/utils/rules.py +++ b/zhenxun/utils/rules.py @@ -46,3 +46,16 @@ def ensure_group(session: EventSession) -> bool: bool: bool """ return session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3] + + +def ensure_private(session: EventSession) -> bool: + """ + 是否在私聊中 + + 参数: + session: session + + 返回: + bool: bool + """ + return not session.id3 and not session.id2 From b74998db875d3d7aa877669d152825cf582c3572 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 19 Mar 2024 00:34:04 +0800 Subject: [PATCH 019/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 35 +++++++++++- pyproject.toml | 1 + zhenxun/plugins/bt/__init__.py | 4 +- zhenxun/plugins/check/__init__.py | 40 ++++++++++++++ zhenxun/plugins/check/data_source.py | 83 ++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 zhenxun/plugins/check/__init__.py create mode 100644 zhenxun/plugins/check/data_source.py diff --git a/poetry.lock b/poetry.lock index c5c9a8b9..a0e7fedb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1813,6 +1813,39 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "psutil-5.9.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73"}, + {file = "psutil-5.9.8-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36"}, + {file = "psutil-5.9.8-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d"}, + {file = "psutil-5.9.8-cp27-none-win32.whl", hash = "sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e"}, + {file = "psutil-5.9.8-cp27-none-win_amd64.whl", hash = "sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631"}, + {file = "psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421"}, + {file = "psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4"}, + {file = "psutil-5.9.8-cp36-cp36m-win32.whl", hash = "sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee"}, + {file = "psutil-5.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2"}, + {file = "psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0"}, + {file = "psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf"}, + {file = "psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"}, + {file = "psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pydantic" version = "1.10.14" @@ -3138,4 +3171,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "28cd8bd2a5d3b00dc4c53dd5d259c5095b741494c7bb17a88927bfacb31cf452" +content-hash = "1d5f9655208fe3bde4ebf3e4f39979dcc961d338d3735d001eb234acfe58f8e3" diff --git a/pyproject.toml b/pyproject.toml index d75f1afd..678d2d0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ nonebot-plugin-alconna = "^0.37.1" pypinyin = "^0.51.0" beautifulsoup4 = "^4.12.3" lxml = "^5.1.0" +psutil = "^5.9.8" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/bt/__init__.py b/zhenxun/plugins/bt/__init__.py index ee0e8643..96d82308 100644 --- a/zhenxun/plugins/bt/__init__.py +++ b/zhenxun/plugins/bt/__init__.py @@ -19,8 +19,8 @@ __plugin_meta__ = PluginMetadata( * 拒绝反冲斗士! * 指令: bt [关键词] ?[页数] - 示例:bt 钢铁侠 - 示例:bt 钢铁侠 3 + 示例: bt 钢铁侠 + 示例: bt 钢铁侠 3 """.strip(), extra=PluginExtraData( author="HibiKier", diff --git a/zhenxun/plugins/check/__init__.py b/zhenxun/plugins/check/__init__.py new file mode 100644 index 00000000..8e8b91d7 --- /dev/null +++ b/zhenxun/plugins/check/__init__.py @@ -0,0 +1,40 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Image +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from .data_source import Check + +__plugin_meta__ = PluginMetadata( + name="服务器自我检查", + description="查看服务器当前状态", + usage=""" + 查看服务器当前状态 + 指令: + 自检 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +check = Check() + + +_matcher = on_alconna( + Alconna("自检"), rule=to_me(), permission=SUPERUSER, block=True, priority=1 +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + image = await check.show() + await Image(image.pic2bytes()).send() + logger.info("自检", arparma.header_result, session=session) diff --git a/zhenxun/plugins/check/data_source.py b/zhenxun/plugins/check/data_source.py new file mode 100644 index 00000000..78fbe7ba --- /dev/null +++ b/zhenxun/plugins/check/data_source.py @@ -0,0 +1,83 @@ +import asyncio +import time +from datetime import datetime + +import psutil + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage + + +class Check: + def __init__(self): + self.cpu = None + self.memory = None + self.disk = None + self.user = None + self.baidu = 200 + self.google = 200 + + async def check_all(self): + await self.check_network() + await asyncio.sleep(0.1) + self.check_system() + self.check_user() + + def check_system(self): + self.cpu = psutil.cpu_percent() + self.memory = psutil.virtual_memory().percent + self.disk = psutil.disk_usage("/").percent + + async def check_network(self): + try: + await AsyncHttpx.get("https://www.baidu.com/", timeout=5) + except Exception as e: + logger.warning(f"访问BaiDu失败... {type(e)}: {e}") + self.baidu = 404 + try: + await AsyncHttpx.get("https://www.google.com/", timeout=5) + except Exception as e: + logger.warning(f"访问Google失败... {type(e)}: {e}") + self.google = 404 + + def check_user(self): + result = "" + for user in psutil.users(): + result += f'[{user.name}] {time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(user.started))}\n' + self.user = result[:-1] + + async def show(self) -> BuildImage: + await self.check_all() + font = BuildImage.load_font(font_size=24) + result = ( + f'[Time] {str(datetime.now()).split(".")[0]}\n' + f"-----System-----\n" + f"[CPU] {self.cpu}%\n" + f"[Memory] {self.memory}%\n" + f"[Disk] {self.disk}%\n" + f"-----Network-----\n" + f"[BaiDu] {self.baidu}\n" + f"[Google] {self.google}\n" + ) + if self.user: + result += "-----User-----\n" + self.user + width = 0 + height = 0 + for x in result.split("\n"): + w, h = BuildImage.get_text_size(x, font) + if w > width: + width = w + height += 30 + A = BuildImage(width + 50, height + 10, font_size=24) + await A.transparent(1) + await A.text((10, 10), result) + max_width = max(width, height) + bk = BuildImage( + max_width + 100, + max_width + 100, + background=IMAGE_PATH / "background" / "check" / "0.jpg", + ) + await bk.paste(A, center_type="center") + return bk From d37280b2964f3af3e203859a4cc49a95376b824c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 27 Mar 2024 11:53:37 +0800 Subject: [PATCH 020/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20coser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtin_plugins/hooks/withdraw_hook.py | 46 +++------- zhenxun/plugins/coser.py | 91 +++++++++++++++++++ zhenxun/utils/utils.py | 28 ------ zhenxun/utils/withdraw_manage.py | 90 ++++++++++++++++++ 4 files changed, 196 insertions(+), 59 deletions(-) create mode 100644 zhenxun/plugins/coser.py create mode 100644 zhenxun/utils/withdraw_manage.py diff --git a/zhenxun/builtin_plugins/hooks/withdraw_hook.py b/zhenxun/builtin_plugins/hooks/withdraw_hook.py index eab5267a..3cb4aadb 100644 --- a/zhenxun/builtin_plugins/hooks/withdraw_hook.py +++ b/zhenxun/builtin_plugins/hooks/withdraw_hook.py @@ -1,46 +1,30 @@ import asyncio -from typing import Optional from nonebot.adapters import Bot -from nonebot.adapters.discord import Bot as DiscordBot -from nonebot.adapters.dodo import Bot as DodoBot -from nonebot.adapters.kaiheila import Bot as KaiheilaBot -from nonebot.adapters.onebot.v11 import Bot as v11Bot -from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.matcher import Matcher from nonebot.message import run_postprocessor -from zhenxun.services.log import logger -from zhenxun.utils.utils import WithdrawManager - -# TODO: 其他平台撤回消息 +from zhenxun.utils.withdraw_manage import WithdrawManager -# 消息撤回 @run_postprocessor async def _( matcher: Matcher, - exception: Optional[Exception], + exception: Exception | None, bot: Bot, ): tasks = [] - for message_id in WithdrawManager._data: - second = WithdrawManager._data[message_id] - tasks.append(asyncio.ensure_future(_withdraw_message(bot, message_id, second))) - WithdrawManager.remove(message_id) + index_list = list(WithdrawManager._data.keys()) + for index in index_list: + ( + bot, + message_id, + time, + ) = WithdrawManager._data[index] + tasks.append( + asyncio.ensure_future( + WithdrawManager.withdraw_message(bot, message_id, time) + ) + ) + WithdrawManager.remove(index) await asyncio.gather(*tasks) - - -async def _withdraw_message(bot: Bot, message_id: str, time: int): - await asyncio.sleep(time) - logger.debug(f"撤回消息ID: {message_id}", "HOOK") - if isinstance(bot, v11Bot): - await bot.delete_msg(message_id=int(message_id)) - elif isinstance(bot, v12Bot): - await bot.delete_message(message_id=message_id) - elif isinstance(bot, DodoBot): - pass - elif isinstance(bot, KaiheilaBot): - pass - elif isinstance(bot, DiscordBot): - pass diff --git a/zhenxun/plugins/coser.py b/zhenxun/plugins/coser.py new file mode 100644 index 00000000..01a2aba7 --- /dev/null +++ b/zhenxun/plugins/coser.py @@ -0,0 +1,91 @@ +import time +from typing import Tuple + +from nonebot.adapters import Bot +from nonebot.params import RegexGroup +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.withdraw_manage import WithdrawManager + +__plugin_meta__ = PluginMetadata( + name="coser", + description="三次元也不戳,嘿嘿嘿", + usage=""" + ?N连cos/coser + 示例: cos + 示例: 5连cos (单次请求张数小于9) + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + configs=[ + RegisterConfig( + key="WITHDRAW_COS_MESSAGE", + value=(0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], + ), + ], + ).dict(), +) + +_matcher = on_alconna(Alconna("get-cos", Args["num", int, 1]), priority=5, block=True) + +_matcher.shortcut( + r"cos", + command="get-cos", + arguments=["1"], + prefix=True, +) + +_matcher.shortcut( + r"(?P\d)(张|个|条|连)cos", + command="get-cos", + arguments=["{num}"], + prefix=True, +) + + +# 纯cos,较慢:https://picture.yinux.workers.dev +# 比较杂,有福利姬,较快:https://api.jrsgslb.cn/cos/url.php?return=img +url = "https://picture.yinux.workers.dev" + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + num: int, +): + withdraw_time = Config.get_config("coser", "WITHDRAW_COS_MESSAGE") + for _ in range(num): + path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg" + try: + await AsyncHttpx.download_file(url, path) + receipt = await Image(path).send() + message_id = receipt.extract_message_id().dict().get("message_id") + if message_id and WithdrawManager.check(session, withdraw_time): + WithdrawManager.append( + bot, + message_id, + withdraw_time[0], + ) + logger.info(f"发送cos", arparma.header_result, session=session) + except Exception as e: + await Text("你cos给我看!").send() + logger.error( + f"cos错误", + arparma.header_result, + session=session, + e=e, + ) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 366a5283..76395fd1 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -12,34 +12,6 @@ import pytz from zhenxun.services.log import logger -class WithdrawManager: - """ - 消息撤回 - """ - - _data = {} - - @classmethod - def append(cls, message_id: str, second: int): - """添加一个撤回消息id和时间 - - 参数: - message_id: 撤回消息id - time: 延迟时间 - """ - cls._data[message_id] = second - - @classmethod - def remove(cls, message_id: str): - """删除一个数据 - - 参数: - message_id: 撤回消息id - """ - if message_id in cls._data: - del cls._data[message_id] - - class ResourceDirManager: """ 临时文件管理器 diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py new file mode 100644 index 00000000..f2310d09 --- /dev/null +++ b/zhenxun/utils/withdraw_manage.py @@ -0,0 +1,90 @@ +import asyncio + +from nonebot.adapters import Bot +from nonebot.adapters.discord import Bot as DiscordBot +from nonebot.adapters.dodo import Bot as DodoBot +from nonebot.adapters.kaiheila import Bot as KaiheilaBot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_session import EventSession + +from zhenxun.services.log import logger + + +class WithdrawManager: + + _data = {} + _index = 0 + + @classmethod + def check(cls, session: EventSession, withdraw_time: tuple[int, int]) -> bool: + """配置项检查 + + 参数: + session: Session + withdraw_time: 配置项数据, (0, 1) + + 返回: + bool: 是否允许撤回 + """ + if withdraw_time[0] and withdraw_time[0] > 0: + if withdraw_time[1] == 2: + return True + if withdraw_time[1] == 1 and (session.id2 or session.id3): + return True + if withdraw_time[1] == 0 and not (session.id2 or session.id3): + return True + return False + + @classmethod + def append(cls, bot: Bot, message_id: str | int, time: int): + """添加消息撤回 + + 参数: + bot: Bot + message_id: 消息Id + time: 延迟时间 + """ + cls._data[cls._index] = ( + bot, + message_id, + time, + ) + cls._index += 1 + + @classmethod + def remove(cls, index: int): + """移除 + + 参数: + index: index + """ + if index in cls._data: + del cls._data[index] + + @classmethod + async def withdraw_message( + cls, bot: Bot, message_id: str | int, time: int | None = None + ): + """消息撤回 + + 参数: + bot: Bot + message_id: 消息Id + time: 延迟时间 + """ + if time: + logger.debug(f"将在 {time}秒 内撤回消息ID: {message_id}", "WithdrawManager") + await asyncio.sleep(time) + if isinstance(bot, v11Bot): + logger.debug(f"v11Bot 撤回消息ID: {message_id}", "WithdrawManager") + await bot.delete_msg(message_id=int(message_id)) + elif isinstance(bot, v12Bot): + logger.debug(f"v12Bot 撤回消息ID: {message_id}", "WithdrawManager") + await bot.delete_message(message_id=str(message_id)) + elif isinstance(bot, KaiheilaBot): + pass + elif isinstance(bot, DodoBot): + pass + elif isinstance(bot, DiscordBot): + pass From d96fd8191d31dd27d797efedfd2af7dc645b7e40 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 27 Mar 2024 20:09:30 +0800 Subject: [PATCH 021/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20dialogue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/auto_update_group.py | 6 +- .../superuser/broadcast/_data_source.py | 6 +- .../superuser/update_fg_info.py | 6 +- zhenxun/plugins/black_word/utils.py | 10 +- zhenxun/plugins/dialogue/__init__.py | 162 ++++++++++++++++++ zhenxun/plugins/dialogue/_data_source.py | 55 ++++++ zhenxun/utils/platform.py | 10 +- 7 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 zhenxun/plugins/dialogue/__init__.py create mode 100644 zhenxun/plugins/dialogue/_data_source.py diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py index f399248c..2a8c5fe2 100644 --- a/zhenxun/builtin_plugins/scheduler/auto_update_group.py +++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py @@ -4,7 +4,7 @@ from nonebot_plugin_apscheduler import scheduler from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -from zhenxun.utils.platform import PlatformManage +from zhenxun.utils.platform import PlatformUtils # 自动更新群组信息 @@ -18,7 +18,7 @@ async def _(): _used_group = [] for bot in bots.values(): try: - await PlatformManage.update_group(bot) + await PlatformUtils.update_group(bot) except Exception as e: logger.error(f"Bot: {bot.self_id} 自动更新群组信息", e=e) logger.info("自动更新群组成员信息成功...") @@ -34,7 +34,7 @@ async def _(): bots = nonebot.get_bots() for bot in bots.values(): try: - await PlatformManage.update_friend(bot) + await PlatformUtils.update_friend(bot) except Exception as e: logger.error(f"自动更新好友信息错误", "自动更新好友", e=e) logger.info("自动更新好友信息成功...") diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 66a6033b..83868088 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -11,7 +11,7 @@ from nonebot_plugin_session import EventSession from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -from zhenxun.utils.platform import PlatformManage +from zhenxun.utils.platform import PlatformUtils class BroadcastManage: @@ -36,7 +36,7 @@ class BroadcastManage: message_list.append(Image(msg.url)) elif isinstance(msg, alc.Text): message_list.append(Text(msg.text)) - group_list, _ = await PlatformManage.get_group_list(bot) + group_list, _ = await PlatformUtils.get_group_list(bot) if group_list: error_count = 0 for group in group_list: @@ -44,7 +44,7 @@ class BroadcastManage: if not await GroupConsole.is_block_task( group.group_id, "broadcast", group.channel_id ): - target = PlatformManage.get_target( + target = PlatformUtils.get_target( bot, None, group.group_id, group.channel_id ) if target: diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py index 3c96a8e8..0cc1be36 100644 --- a/zhenxun/builtin_plugins/superuser/update_fg_info.py +++ b/zhenxun/builtin_plugins/superuser/update_fg_info.py @@ -9,7 +9,7 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType -from zhenxun.utils.platform import PlatformManage +from zhenxun.utils.platform import PlatformUtils __plugin_meta__ = PluginMetadata( name="更新群组/好友信息", @@ -54,7 +54,7 @@ async def _( arparma: Arparma, ): try: - num = await PlatformManage.update_group(bot) + num = await PlatformUtils.update_group(bot) logger.info( f"更新群聊信息完成,共更新了 {num} 个群组的信息!", arparma.header_result, @@ -75,7 +75,7 @@ async def _( arparma: Arparma, ): try: - num = await PlatformManage.update_friend(bot, session.platform) + num = await PlatformUtils.update_friend(bot, session.platform) logger.info( f"更新好友信息完成,共更新了 {num} 个好友的信息!", arparma.header_result, diff --git a/zhenxun/plugins/black_word/utils.py b/zhenxun/plugins/black_word/utils.py index 29145dfb..53526bd0 100644 --- a/zhenxun/plugins/black_word/utils.py +++ b/zhenxun/plugins/black_word/utils.py @@ -12,7 +12,7 @@ from zhenxun.models.ban_console import BanConsole from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx -from zhenxun.utils.platform import PlatformManage +from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.utils import cn2py from .model import BlackWord @@ -173,7 +173,7 @@ async def _add_user_black_word( plant_text=message, black_word=black_word, punish_level=punish_level, - platform=PlatformManage.get_platform(bot), + platform=PlatformUtils.get_platform(bot), ) logger.info( f"已将 USER {user_id} GROUP {group_id} 添加至黑名单词汇记录 Black_word:{black_word} Plant_text:{message}" @@ -325,7 +325,7 @@ async def _get_punish( return ban_4_duration # 口头警告 elif id_ == 5: - await PlatformManage.send_message(bot, user_id, group_id, warning_result) + await PlatformUtils.send_message(bot, user_id, group_id, warning_result) logger.info(f"BlackWord 口头警告 USER {user_id}") return warning_result return None @@ -341,9 +341,9 @@ async def send_msg(bot: Bot, user_id: str, group_id: str | None, message: str): message: message """ if not user_id: - platform = PlatformManage.get_platform(bot) + platform = PlatformUtils.get_platform(bot) user_id = bot.config.platform_superusers[platform][0] - await PlatformManage.send_message(bot, user_id, group_id, message) + await PlatformUtils.send_message(bot, user_id, group_id, message) async def check_text(text: str) -> bool: diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py new file mode 100644 index 00000000..9f14bccd --- /dev/null +++ b/zhenxun/plugins/dialogue/__init__.py @@ -0,0 +1,162 @@ +import nonebot +from nonebot import on_command +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Target +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import DialogueManage + +__plugin_meta__ = PluginMetadata( + name="联系管理员", + description="跨越空间与时间跟管理员对话", + usage=""" + [滴滴滴]/滴滴滴- ?[文本] ?[图片] + 示例:滴滴滴- 我喜欢你 + + 超级管理员额外命令 + /t: 查看当前存储的消息 + /t [user_id] [group_id] [文本]: 在group回复指定用户 + /t [user_id] [文本]: 私聊用户 + /t -1 [group_id] [文本]: 在group内发送消息 + /t [id] [文本]: 回复指定id的对话,id在 /t 中获取 + 示例:/t 73747222 32848432 你好啊 + 示例:/t 73747222 你好不好 + 示例:/t -1 32848432 我不太好 + 示例:/t 0 我收到你的话了 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", menu_type="联系管理员" + ).dict(), +) + +config = nonebot.get_driver().config + + +_dialogue_matcher = on_command("滴滴滴-", priority=5, block=True) +_reply_matcher = on_command("/t", priority=1, permission=SUPERUSER, block=True) + + +@_dialogue_matcher.handle() +async def _( + bot: Bot, + message: UniMsg, + session: EventSession, + user_info: UserInfo = EventUserInfo(), +): + if session.id1: + message[0] = alcText(str(message[0]).replace("滴滴滴-", "", 1)) + platform = PlatformUtils.get_platform(bot) + try: + superuser_id = config.platform_superusers["qq"][0] + if platform == "dodo": + superuser_id = config.platform_superusers["dodo"][0] + if platform == "kaiheila": + superuser_id = config.platform_superusers["kaiheila"][0] + if platform == "discord": + superuser_id = config.platform_superusers["discord"][0] + except IndexError: + await Text("管理员失联啦...").finish() + if not superuser_id: + await Text("管理员失联啦...").finish() + uname = user_info.user_displayname or user_info.user_name + group_name = "" + gid = session.id3 or session.id2 + if gid: + if g := await GroupConsole.get(group_id=gid): + group_name = g.group_name + logger.info( + f"发送消息至{platform}管理员: {message}", "滴滴滴-", session=session + ) + message.insert(0, "消息:\n") + if gid: + message.insert(0, f"群组: {group_name}({gid})\n") + message.insert(0, f"昵称: {uname}({session.id1})\n") + message.insert(0, f"Id: {DialogueManage._index}\n") + message.insert(0, "*****一份交流报告*****\n") + DialogueManage.add(uname, session.id1, gid, group_name, message, platform) + await message.send(bot=bot, target=Target(superuser_id, private=True)) + await Text("已成功发送给管理员啦!").send(reply=True) + else: + await Text("用户id为空...").send() + + +@_reply_matcher.handle() +async def _( + bot: Bot, + message: UniMsg, + session: EventSession, + user_info: UserInfo = EventUserInfo(), +): + message[0] = alcText(str(message[0]).replace("/t", "", 1).strip()) + if session.id1: + msg = message.extract_plain_text() + if not msg: + platform = PlatformUtils.get_platform(bot) + data = DialogueManage._data + if not data: + await Text("暂无待回复消息...").finish() + if platform: + data = [data[d] for d in data if data[d].platform == platform] + for d in data: + await d.message.send( + bot=bot, target=Target(session.id1, private=True) + ) + else: + msg = msg.split() + group_id = "" + user_id = "" + if msg[0].isdigit(): + if len(msg[0]) < 4: + _id = int(msg[0]) + if _id >= 0: + if model := DialogueManage.get(_id): + user_id = model.user_id + group_id = model.group_id + else: + return Text("未获取此id数据").finish() + message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) + else: + user_id = 0 + if msg[1].isdigit(): + group_id = msg[1] + message[0] = alcText( + " ".join(str(message[0]).split(" ")[2:]) + ) + else: + await Text("群组id错误...").finish(at_sender=True) + DialogueManage.remove(_id) + else: + user_id = msg[0] + if msg[1].isdigit() and len(msg[1]) > 5: + group_id = msg[1] + message[0] = alcText(" ".join(str(message[0]).split(" ")[2:])) + else: + group_id = 0 + message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) + else: + await Text("参数错误...").finish(at_sender=True) + if group_id: + if user_id: + message.insert(0, alcAt("user", user_id)) + message.insert(1, "\n管理员回复\n=======\n") + await message.send(Target(group_id), bot) + await Text("消息发送成功!").finish(at_sender=True) + elif user_id: + await message.send(Target(user_id, private=True), bot) + await Text("消息发送成功!").finish(at_sender=True) + else: + await Text("群组id与用户id为空...").send() + else: + await Text("用户id为空...").send() diff --git a/zhenxun/plugins/dialogue/_data_source.py b/zhenxun/plugins/dialogue/_data_source.py new file mode 100644 index 00000000..440c8176 --- /dev/null +++ b/zhenxun/plugins/dialogue/_data_source.py @@ -0,0 +1,55 @@ +from typing import Dict + +from nonebot_plugin_alconna import UniMsg +from pydantic import BaseModel + + +class DialogueData(BaseModel): + + name: str + """用户名称""" + user_id: str + """用户id""" + group_id: str | None + """群组id""" + group_name: str | None + """群组名称""" + message: UniMsg + """UniMsg""" + platform: str | None + """平台""" + + +class DialogueManage: + + _data: Dict[int, DialogueData] = {} + _index = 0 + + @classmethod + def add( + cls, + name: str, + uid: str, + gid: str | None, + group_name: str | None, + message: UniMsg, + platform: str | None, + ): + cls._data[cls._index] = DialogueData( + name=name, + user_id=uid, + group_id=gid, + group_name=group_name, + message=message, + platform=platform, + ) + cls._index += 1 + + @classmethod + def remove(cls, index: int): + if index in cls._data: + del cls._data[index] + + @classmethod + def get(cls, k: int): + return cls._data.get(k) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index dba7bfe3..4cddd1f7 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -25,7 +25,7 @@ from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -class PlatformManage: +class PlatformUtils: @classmethod async def send_message( @@ -97,6 +97,8 @@ class PlatformManage: return "dodo" if isinstance(bot, KaiheilaBot): return "kaiheila" + if isinstance(bot, DiscordBot): + return "discord" return None @classmethod @@ -308,9 +310,9 @@ async def broadcast_group( _used_group = [] for _bot in bot_list: try: - if platform and platform != PlatformManage.get_platform(_bot): + if platform and platform != PlatformUtils.get_platform(_bot): continue - group_list, _ = await PlatformManage.get_group_list(_bot) + group_list, _ = await PlatformUtils.get_group_list(_bot) if group_list: for group in group_list: key = f"{group.group_id}:{group.channel_id}" @@ -335,7 +337,7 @@ async def broadcast_group( ) if is_continue: continue - target = PlatformManage.get_target( + target = PlatformUtils.get_target( _bot, None, group.group_id, group.channel_id ) if target: From 3fcb8af7565f47ec6d449030944215d7a107e913 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 27 Mar 2024 20:12:35 +0800 Subject: [PATCH 022/132] =?UTF-8?q?fix=F0=9F=90=9B:=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/dialogue/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py index 9f14bccd..1ef9cd81 100644 --- a/zhenxun/plugins/dialogue/__init__.py +++ b/zhenxun/plugins/dialogue/__init__.py @@ -117,7 +117,7 @@ async def _( msg = msg.split() group_id = "" user_id = "" - if msg[0].isdigit(): + if msg[0].replace("-", "", 1).isdigit(): if len(msg[0]) < 4: _id = int(msg[0]) if _id >= 0: From afc1dd7377747cd3ff1377a74cbf24de6991ce61 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 29 Mar 2024 23:27:25 +0800 Subject: [PATCH 023/132] =?UTF-8?q?feat=E2=9C=A8:=20add=20epic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/epic/__init__.py | 52 +++++++ zhenxun/plugins/epic/data_source.py | 206 ++++++++++++++++++++++++++++ zhenxun/utils/platform.py | 2 +- 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/epic/__init__.py create mode 100644 zhenxun/plugins/epic/data_source.py diff --git a/zhenxun/plugins/epic/__init__.py b/zhenxun/plugins/epic/__init__.py new file mode 100644 index 00000000..ea32b277 --- /dev/null +++ b/zhenxun/plugins/epic/__init__.py @@ -0,0 +1,52 @@ +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.platform import broadcast_group + +from .data_source import get_epic_free + +__plugin_meta__ = PluginMetadata( + name="epic免费游戏", + description="可以不玩,不能没有,每日白嫖", + usage=""" + epic + """.strip(), + extra=PluginExtraData( + author="AkashiCoin", + version="0.1", + configs=[ + RegisterConfig( + module="_task", + key="DEFAULT_EPIC_FREE_GAME", + value=True, + help="被动 epic免费游戏 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + +_matcher = on_alconna(Alconna("epic"), priority=5, block=True) + + +@_matcher.handle() +async def handle(bot: Bot, session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + type_ = "Group" if gid else "Private" + msg_list, code = await get_epic_free(bot, type_) + if code == 404 and isinstance(msg_list, str): + await Text(msg_list).finish() + elif isinstance(bot, (v11Bot, v12Bot)) and isinstance(msg_list, list): + await bot.send_group_forward_msg(group_id=gid, messages=msg_list) + elif isinstance(msg_list, MessageFactory): + await msg_list.send() + logger.info(f"获取epic免费游戏", arparma.header_result, session=session) diff --git a/zhenxun/plugins/epic/data_source.py b/zhenxun/plugins/epic/data_source.py new file mode 100644 index 00000000..e0666a2b --- /dev/null +++ b/zhenxun/plugins/epic/data_source.py @@ -0,0 +1,206 @@ +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_saa import Image, MessageFactory, Text + +from zhenxun.configs.config import NICKNAME +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + + +# 获取所有 Epic Game Store 促销游戏 +# 方法参考:RSSHub /epicgames 路由 +# https://github.com/DIYgod/RSSHub/blob/master/lib/v2/epicgames/index.js +async def get_epic_game() -> dict | None: + epic_url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN" + headers = { + "Referer": "https://www.epicgames.com/store/zh-CN/", + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", + } + try: + res = await AsyncHttpx.get(epic_url, headers=headers, timeout=10) + res_json = res.json() + games = res_json["data"]["Catalog"]["searchStore"]["elements"] + return games + except Exception as e: + logger.error(f"Epic 访问接口错误", e=e) + return None + + +# 此处用于获取游戏简介 +async def get_epic_game_desp(name) -> dict | None: + desp_url = ( + "https://store-content-ipv4.ak.epicgames.com/api/zh-CN/content/products/" + + str(name) + ) + headers = { + "Referer": "https://store.epicgames.com/zh-CN/p/" + str(name), + "Content-Type": "application/json; charset=utf-8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36", + } + try: + res = await AsyncHttpx.get(desp_url, headers=headers, timeout=10) + res_json = res.json() + gamesDesp = res_json["pages"][0]["data"]["about"] + return gamesDesp + except Exception as e: + logger.error(f"Epic 访问接口错误", e=e) + return None + + +# 获取 Epic Game Store 免费游戏信息 +# 处理免费游戏的信息方法借鉴 pip 包 epicstore_api 示例 +# https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py +async def get_epic_free( + bot: Bot, type_event: str +) -> tuple[MessageFactory | list | str, int]: + games = await get_epic_game() + if not games: + return "Epic 可能又抽风啦,请稍后再试(", 404 + else: + msg_list = [] + for game in games: + game_name = game["title"] + game_corp = game["seller"]["name"] + game_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] + # 赋初值以避免 local variable referenced before assignment + game_thumbnail, game_dev, game_pub = None, game_corp, game_corp + try: + game_promotions = game["promotions"]["promotionalOffers"] + upcoming_promotions = game["promotions"]["upcomingPromotionalOffers"] + if not game_promotions and upcoming_promotions: + # 促销暂未上线,但即将上线 + promotion_data = upcoming_promotions[0]["promotionalOffers"][0] + start_date_iso, end_date_iso = ( + promotion_data["startDate"][:-1], + promotion_data["endDate"][:-1], + ) + # 删除字符串中最后一个 "Z" 使 Python datetime 可处理此时间 + start_date = datetime.fromisoformat(start_date_iso).strftime( + "%b.%d %H:%M" + ) + end_date = datetime.fromisoformat(end_date_iso).strftime( + "%b.%d %H:%M" + ) + if type_event == "Group": + _message = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( + game_corp, game_name, game_price, start_date, end_date + ) + data = { + "type": "node", + "data": { + "name": f"这里是{NICKNAME}酱", + "uin": f"{bot.self_id}", + "content": _message, + }, + } + msg_list.append(data) + else: + msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( + game_corp, game_name, game_price, start_date, end_date + ) + msg_list.append(msg) + else: + for image in game["keyImages"]: + if ( + image.get("url") + and not game_thumbnail + and image["type"] + in [ + "Thumbnail", + "VaultOpened", + "DieselStoreFrontWide", + "OfferImageWide", + ] + ): + game_thumbnail = image["url"] + break + for pair in game["customAttributes"]: + if pair["key"] == "developerName": + game_dev = pair["value"] + if pair["key"] == "publisherName": + game_pub = pair["value"] + if game.get("productSlug"): + if gamesDesp := await get_epic_game_desp(game["productSlug"]): + try: + # 是否存在简短的介绍 + if "shortDescription" in gamesDesp: + game_desp = gamesDesp["shortDescription"] + except KeyError: + game_desp = gamesDesp["description"] + else: + game_desp = game["description"] + try: + end_date_iso = game["promotions"]["promotionalOffers"][0][ + "promotionalOffers" + ][0]["endDate"][:-1] + end_date = datetime.fromisoformat(end_date_iso).strftime( + "%b.%d %H:%M" + ) + except IndexError: + end_date = "未知" + # API 返回不包含游戏商店 URL,此处自行拼接,可能出现少数游戏 404 请反馈 + if game.get("productSlug"): + game_url = "https://store.epicgames.com/zh-CN/p/{}".format( + game["productSlug"].replace("/home", "") + ) + elif game.get("url"): + game_url = game["url"] + else: + slugs = ( + [ + x["pageSlug"] + for x in game.get("offerMappings", []) + if x.get("pageType") == "productHome" + ] + + [ + x["pageSlug"] + for x in game.get("catalogNs", {}).get("mappings", []) + if x.get("pageType") == "productHome" + ] + + [ + x["value"] + for x in game.get("customAttributes", []) + if "productSlug" in x.get("key") + ] + ) + game_url = "https://store.epicgames.com/zh-CN{}".format( + f"/p/{slugs[0]}" if len(slugs) else "" + ) + if isinstance(bot, (v11Bot, v12Bot)) and type_event == "Group": + _message = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( + game_thumbnail, + game_name, + game_price, + game_desp, + game_dev, + game_pub, + end_date, + game_url, + ) + data = { + "type": "node", + "data": { + "name": f"这里是{NICKNAME}酱", + "uin": f"{bot.self_id}", + "content": _message, + }, + } + msg_list.append(data) + else: + _message = [] + if game_thumbnail: + _message.append(Image(game_thumbnail)) + _message.append( + Text( + f"\n\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n" + ) + ) + return MessageFactory(_message), 200 + except TypeError as e: + # logger.info(str(e)) + pass + return msg_list, 200 diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index 4cddd1f7..014e026b 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -276,7 +276,7 @@ async def broadcast_group( ): """获取所有Bot或指定Bot对象广播群聊 - Args: + 参数: message: 广播消息内容 bot: 指定bot对象. bot_id: 指定bot id. From 3fad6fc2ad4cc784c2da541363f3b89661d4a06e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 4 May 2024 13:48:12 +0800 Subject: [PATCH 024/132] =?UTF-8?q?feat=E2=9C=A8:=20=E9=87=91=E5=B8=81?= =?UTF-8?q?=E7=BA=A2=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 118 +++---- zhenxun/configs/utils/__init__.py | 2 + zhenxun/models/user_console.py | 3 +- zhenxun/plugins/epic/__init__.py | 15 +- zhenxun/plugins/gold_redbag/__init__.py | 355 ++++++++++++++++++++ zhenxun/plugins/gold_redbag/config.py | 372 +++++++++++++++++++++ zhenxun/plugins/gold_redbag/data_source.py | 238 +++++++++++++ zhenxun/plugins/gold_redbag/model.py | 63 ++++ zhenxun/utils/depends/__init__.py | 81 ++++- zhenxun/utils/enum.py | 2 +- zhenxun/utils/platform.py | 54 ++- zhenxun/utils/utils.py | 5 +- 12 files changed, 1230 insertions(+), 78 deletions(-) create mode 100644 zhenxun/plugins/gold_redbag/__init__.py create mode 100644 zhenxun/plugins/gold_redbag/config.py create mode 100644 zhenxun/plugins/gold_redbag/data_source.py create mode 100644 zhenxun/plugins/gold_redbag/model.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 89e9e62b..b0ae260a 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -3,6 +3,7 @@ import os from nonebot import require from nonebot.drivers import Driver from tortoise import Tortoise +from tortoise.exceptions import OperationalError from zhenxun.models.goods_info import GoodsInfo from zhenxun.models.sign_user import SignUser @@ -62,64 +63,69 @@ async def _(): and not await UserConsole.annotate().count() and not await SignUser.annotate().count() ): - flag = False - db = Tortoise.get_connection("default") - old_sign_list = await db.execute_query_dict(SIGN_SQL) - old_bag_list = await db.execute_query_dict(BAG_SQL) - goods = { - g["goods_name"]: g["uuid"] - for g in await GoodsInfo.annotate().values("goods_name", "uuid") - } - create_list = [] - sign_id_list = [] - uid = await UserConsole.get_new_uid() - for old_sign in old_sign_list: - sign_id_list.append(old_sign["user_id"]) - old_bag = [b for b in old_bag_list if b["user_id"] == old_sign["user_id"]] - if old_bag: - old_bag = old_bag[0] - property = json.loads(old_bag["property"]) - props = {} - if property: - for name, num in property.items(): - if name in goods: - props[goods[name]] = num + try: + flag = False + db = Tortoise.get_connection("default") + old_sign_list = await db.execute_query_dict(SIGN_SQL) + old_bag_list = await db.execute_query_dict(BAG_SQL) + goods = { + g["goods_name"]: g["uuid"] + for g in await GoodsInfo.annotate().values("goods_name", "uuid") + } + create_list = [] + sign_id_list = [] + uid = await UserConsole.get_new_uid() + for old_sign in old_sign_list: + sign_id_list.append(old_sign["user_id"]) + old_bag = [ + b for b in old_bag_list if b["user_id"] == old_sign["user_id"] + ] + if old_bag: + old_bag = old_bag[0] + property = json.loads(old_bag["property"]) + props = {} + if property: + for name, num in property.items(): + if name in goods: + props[goods[name]] = num + create_list.append( + UserConsole( + user_id=old_sign["user_id"], + platform="qq", + uid=uid, + props=props, + gold=old_bag["gold"], + ) + ) + else: + create_list.append( + UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) + ) + uid += 1 + if create_list: + logger.info("开始迁移用户数据...") + await UserConsole.bulk_create(create_list, 10) + logger.info("迁移用户数据完成!") + create_list.clear() + uc_dict = {u.user_id: u for u in await UserConsole.all()} + for old_sign in old_sign_list: + user_console = uc_dict.get(old_sign["user_id"]) + if not user_console: + user_console = await UserConsole.get_user(old_sign["user_id"], "qq") create_list.append( - UserConsole( + SignUser( user_id=old_sign["user_id"], + user_console=user_console, platform="qq", - uid=uid, - props=props, - gold=old_bag["gold"], + sign_count=old_sign["checkin_count"], + impression=old_sign["impression"], + add_probability=old_sign["add_probability"], + specify_probability=old_sign["specify_probability"], ) ) - else: - create_list.append( - UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) - ) - uid += 1 - if create_list: - logger.info("开始迁移用户数据...") - await UserConsole.bulk_create(create_list, 10) - logger.info("迁移用户数据完成!") - create_list.clear() - uc_dict = {u.user_id: u for u in await UserConsole.all()} - for old_sign in old_sign_list: - user_console = uc_dict.get(old_sign["user_id"]) - if not user_console: - user_console = await UserConsole.get_user(old_sign["user_id"], "qq") - create_list.append( - SignUser( - user_id=old_sign["user_id"], - user_console=user_console, - platform="qq", - sign_count=old_sign["checkin_count"], - impression=old_sign["impression"], - add_probability=old_sign["add_probability"], - specify_probability=old_sign["specify_probability"], - ) - ) - if create_list: - logger.info("开始迁移签到数据...") - await SignUser.bulk_create(create_list, 10) - logger.info("迁移签到数据完成!") + if create_list: + logger.info("开始迁移签到数据...") + await SignUser.bulk_create(create_list, 10) + logger.info("迁移签到数据完成!") + except OperationalError as e: + logger.warning("数据迁移", e=e) diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 7ef9e0f4..abe5d03c 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -161,6 +161,8 @@ class PluginExtraData(BaseModel): """插件限制""" tasks: list[Task] | None = None """技能被动""" + superuser_help: str | None = None + """超级用户帮助""" class NoSuchConfig(Exception): diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py index a492c317..c743dd54 100644 --- a/zhenxun/models/user_console.py +++ b/zhenxun/models/user_console.py @@ -77,7 +77,8 @@ class UserConsole(Model): platform: 平台. """ user, _ = await cls.get_or_create( - user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + user_id=user_id, + defaults={"platform": platform, "uid": await cls.get_new_uid()}, ) user.gold += gold await user.save(update_fields=["gold"]) diff --git a/zhenxun/plugins/epic/__init__.py b/zhenxun/plugins/epic/__init__.py index ea32b277..bc756244 100644 --- a/zhenxun/plugins/epic/__init__.py +++ b/zhenxun/plugins/epic/__init__.py @@ -7,9 +7,8 @@ from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_saa import MessageFactory, Text from nonebot_plugin_session import EventSession -from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger -from zhenxun.utils.platform import broadcast_group from .data_source import get_epic_free @@ -22,16 +21,6 @@ __plugin_meta__ = PluginMetadata( extra=PluginExtraData( author="AkashiCoin", version="0.1", - configs=[ - RegisterConfig( - module="_task", - key="DEFAULT_EPIC_FREE_GAME", - value=True, - help="被动 epic免费游戏 进群默认开关状态", - default_value=True, - type=bool, - ), - ], ).dict(), ) @@ -39,7 +28,7 @@ _matcher = on_alconna(Alconna("epic"), priority=5, block=True) @_matcher.handle() -async def handle(bot: Bot, session: EventSession, arparma: Arparma): +async def _(bot: Bot, session: EventSession, arparma: Arparma): gid = session.id3 or session.id2 type_ = "Group" if gid else "Private" msg_list, code = await get_epic_free(bot, type_) diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py new file mode 100644 index 00000000..6cc29f22 --- /dev/null +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -0,0 +1,355 @@ +import time +import uuid +from datetime import datetime, timedelta +from typing import List + +from apscheduler.jobstores.base import JobLookupError +from nonebot.adapters import Bot +from nonebot.exception import ActionFailed +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Match, Option, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import GetConfig, UserName +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.rules import ensure_group + +from .config import FESTIVE_KEY, FestiveRedBagManage +from .data_source import RedBagManager + +__plugin_meta__ = PluginMetadata( + name="金币红包", + description="运气项目又来了", + usage=""" + 塞红包 [金币数] ?[红包数=5] ?[at指定人]: 塞入红包 + 开/抢: 打开红包 + 退回红包: 退回未开完的红包,必须在一分钟后使用 + + * 不同群组同一个节日红包用户只能开一次 + + 示例: + 塞红包 1000 + 塞红包 1000 10 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 节日红包 [金额] [红包数] ?[指定主题文字] ? -g [群id] + + * 不同群组同一个节日红包用户只能开一次 + + 示例: + 节日红包 10000 20 今日出道贺金 + 节日红包 10000 20 明日出道贺金 -g 123123123 + + """, + configs=[ + RegisterConfig( + key="DEFAULT_TIMEOUT", + value=600, + help="普通红包默认超时时间", + default_value=600, + type=int, + ), + RegisterConfig( + key="DEFAULT_INTERVAL", + value=60, + help="用户发送普通红包最小间隔时间", + default_value=60, + type=int, + ), + RegisterConfig( + key="RANK_NUM", + value=10, + help="结算排行显示前N位", + default_value=10, + type=int, + ), + ], + limits=[PluginCdBlock(result="急什么急什么,待会再发!")], + ).dict(), +) + + +# def rule(session: EventSession) -> bool: +# if gid := session.id3 or session.id2: +# if group_red_bag := RedBagManager.get_group_data(gid): +# return group_red_bag.check_open(gid) +# return False + + +# async def rule_group(session: EventSession): +# return rule(session) and ensure_group(session) + + +_red_bag_matcher = on_alconna( + Alconna("塞红包", Args["amount", int]["num", int, 5]["user?", alcAt]), + aliases={"金币红包"}, + priority=5, + block=True, + rule=ensure_group, +) + +_open_matcher = on_alconna( + Alconna("开"), + aliases={"抢", "开红包", "抢红包"}, + priority=5, + block=True, + rule=ensure_group, +) + +_return_matcher = on_alconna( + Alconna("退回红包"), aliases={"退还红包"}, priority=5, block=True, rule=ensure_group +) + +_festive_matcher = on_alconna( + Alconna( + "节日红包", + Args["amount", int]["num", int]["text?", str], + Option("-g|--group", Args["group_list", str], help_text="指定群"), + ), + priority=1, + block=True, + permission=SUPERUSER, + rule=to_me(), +) + + +@_red_bag_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + amount: int, + num: int, + user: Match[alcAt], + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), + user_name: str = UserName(), +): + at_user = None + if user.available: + at_user = user.result.target + # group_id = session.id3 or session.id2 + group_id = session.id2 + """以频道id为键""" + user_id = session.id1 + if not user_id: + await Text("用户id为空").finish() + if not group_id: + await Text("群组id为空").finish() + group_red_bag = RedBagManager.get_group_data(group_id) + # 剩余过期时间 + time_remaining = group_red_bag.check_timeout(user_id) + if time_remaining != -1: + # 判断用户红包是否存在且是否过时覆盖 + if user_red_bag := group_red_bag.get_user_red_bag(user_id): + now = time.time() + if now < user_red_bag.start_time + default_interval: + await Text( + f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." + f"(或等待{time_remaining}秒红包cd)" + ).finish() + result = await RedBagManager.check_gold(user_id, amount, session.platform) + if result: + await Text(result).finish(at_sender=True) + await group_red_bag.add_red_bag( + f"{user_name}的红包", + int(amount), + 1 if at_user else num, + user_name, + user_id, + assigner=at_user, + platform=session.platform, + ) + image = await RedBagManager.random_red_bag_background( + user_id, platform=session.platform + ) + message_list: list = [ + Text(f"{user_name}发起了金币红包\n金额: {amount}\n数量: {num}\n") + ] + if at_user: + message_list.append(Text("指定人: ")) + message_list.append(Mention(at_user)) + message_list.append(Text("\n")) + message_list.append(Image(image.pic2bytes())) + await MessageFactory(message_list).send() + + logger.info( + f"塞入 {num} 个红包,共 {amount} 金币", arparma.header_result, session=session + ) + + +@_open_matcher.handle() +async def _( + session: EventSession, + rank_num: int = GetConfig(config="RANK_NUM"), +): + # group_id = session.id3 or session.id2 + group_id = session.id2 + """以频道id为键""" + user_id = session.id1 + if not user_id: + await Text("用户id为空").finish() + if not group_id: + await Text("群组id为空").finish() + if group_red_bag := RedBagManager.get_group_data(group_id): + open_data, settlement_list = await group_red_bag.open(user_id, session.platform) + # send_msg = Text("没有红包给你开!") + send_msg = [] + for _, item in open_data.items(): + amount, red_bag = item + result_image = await RedBagManager.build_open_result_image( + red_bag, user_id, amount, session.platform + ) + send_msg.append( + Text(f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n") + ) + send_msg.append(Image(result_image.pic2bytes())) + send_msg.append(Text("\n")) + logger.info( + f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", + "开红包", + session=session, + ) + send_msg = ( + MessageFactory(send_msg[:-1]) if send_msg else Text("没有红包给你开!") + ) + await send_msg.send(reply=True) + if settlement_list: + for red_bag in settlement_list: + result_image = await red_bag.build_amount_rank( + rank_num, session.platform + ) + await MessageFactory( + [Text(f"{red_bag.name}已结算\n"), Image(result_image.pic2bytes())] + ).send() + + +@_return_matcher.handle() +async def _( + session: EventSession, + default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), + rank_num: int = GetConfig(config="RANK_NUM"), +): + group_id = session.id3 or session.id2 + user_id = session.id1 + if not user_id: + await Text("用户id为空").finish() + if not group_id: + await Text("群组id为空").finish() + if group_red_bag := RedBagManager.get_group_data(group_id): + if user_red_bag := group_red_bag.get_user_red_bag(user_id): + now = time.time() + if now - user_red_bag.start_time < default_interval: + await Text( + f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " + f"秒后可以退回..." + ).finish(reply=True) + user_red_bag = group_red_bag.get_user_red_bag(user_id) + if user_red_bag and ( + data := await group_red_bag.settlement(user_id, session.platform) + ): + image_result = await user_red_bag.build_amount_rank( + rank_num, session.platform + ) + logger.info(f"退回了红包 {data[0]} 金币", "红包退回", session=session) + await MessageFactory( + [ + Text(f"已成功退还了 " f"{data[0]} 金币\n"), + Image(image_result.pic2bytes()), + ] + ).finish(reply=True) + await Text("目前没有红包可以退回...").finish(reply=True) + + +@_festive_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + amount: int, + num: int, + text: Match[str], + group_list: Match[str], + user_name: str = UserName(), +): + # TODO: 指定多个群 + greetings = "恭喜发财 大吉大利" + if text.available: + greetings = text.result + gl = [] + if group_list.available: + gl = [group_list.result] + else: + g_l, platform = await PlatformUtils.get_group_list(bot) + gl = [g.channel_id or g.group_id for g in g_l] + _uuid = str(uuid.uuid1()) + FestiveRedBagManage.add(_uuid) + for g in gl: + if target := PlatformUtils.get_target(bot, group_id=g): + group_red_bag = RedBagManager.get_group_data(g) + if festive_red_bag := group_red_bag.get_festive_red_bag(): + group_red_bag.remove_festive_red_bag() + if festive_red_bag.uuid: + FestiveRedBagManage.remove(festive_red_bag.uuid) + rank_image = await festive_red_bag.build_amount_rank(10, platform) + try: + await MessageFactory( + [ + Text( + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" + ), + Image(rank_image.pic2bytes()), + ] + ).send_to(target=target, bot=bot) + except ActionFailed: + pass + try: + scheduler.remove_job(f"{FESTIVE_KEY}_{g}") + await RedBagManager.end_red_bag( + g, is_festive=True, platform=session.platform + ) + except JobLookupError: + pass + await group_red_bag.add_red_bag( + f"{NICKNAME}的红包", + amount, + num, + NICKNAME, + FESTIVE_KEY, + _uuid, + platform=session.platform, + ) + scheduler.add_job( + RedBagManager._auto_end_festive_red_bag, + "date", + run_date=(datetime.now() + timedelta(hours=24)).replace(microsecond=0), + id=f"{FESTIVE_KEY}_{g}", + args=[bot, g, session.platform], + ) + try: + image_result = await RedBagManager.random_red_bag_background( + bot.self_id, greetings, session.platform + ) + await MessageFactory( + [ + Text( + f"{NICKNAME}发起了节日金币红包\n金额: {amount}\n数量: {num}\n" + ), + Image(image_result.pic2bytes()), + ] + ).send_to(target=target, bot=bot) + logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) + except ActionFailed: + logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) diff --git a/zhenxun/plugins/gold_redbag/config.py b/zhenxun/plugins/gold_redbag/config.py new file mode 100644 index 00000000..da8c0d39 --- /dev/null +++ b/zhenxun/plugins/gold_redbag/config.py @@ -0,0 +1,372 @@ +import random +import time +from io import BytesIO +from typing import Dict + +from pydantic import BaseModel + +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import get_user_avatar + +from .model import RedbagUser + +FESTIVE_KEY = "FESTIVE" +"""节日红包KEY""" + + +class FestiveRedBagManage: + + _data: Dict[str, list[str]] = {} + + @classmethod + def add(cls, uuid: str): + cls._data[uuid] = [] + + @classmethod + def open(cls, uuid: str, uid: str): + if uuid in cls._data and uid not in cls._data[uuid]: + cls._data[uuid].append(uid) + + @classmethod + def remove(cls, uuid: str): + if uuid in cls._data: + del cls._data[uuid] + + @classmethod + def check(cls, uuid: str, uid: str): + if uuid in cls._data: + return uid not in cls._data[uuid] + return False + + +class RedBag(BaseModel): + """ + 红包 + """ + + group_id: str + """所属群聊""" + name: str + """红包名称""" + amount: int + """总金币""" + num: int + """红包数量""" + promoter: str + """发起人昵称""" + promoter_id: str + """发起人id""" + is_festival: bool + """是否为节日红包""" + timeout: int + """过期时间""" + assigner: str | None = None + """指定人id""" + start_time: float + """红包发起时间""" + open_user: Dict[str, int] = {} + """开启用户""" + red_bag_list: list[int] + """红包金额列表""" + uuid: str | None + """uuid""" + + async def build_amount_rank(self, num: int, platform: str) -> BuildImage: + """生成结算红包图片 + + 参数: + num: 查看的排名数量. + platform: 平台. + + 返回: + BuildImage: 结算红包图片 + """ + user_image_list = [] + if self.open_user: + sort_data = sorted( + self.open_user.items(), key=lambda item: item[1], reverse=True + ) + num = num if num < len(self.open_user) else len(self.open_user) + user_id_list = [sort_data[i][0] for i in range(num)] + group_user_list = await GroupInfoUser.filter( + group_id=self.group_id, user_id__in=user_id_list + ).all() + for i in range(num): + user_background = BuildImage(600, 100, font_size=30) + user_id, amount = sort_data[i] + user_ava_bytes = await PlatformUtils.get_user_avatar(user_id, platform) + user_ava = None + if user_ava_bytes: + user_ava = BuildImage(80, 80, background=BytesIO(user_ava_bytes)) + else: + user_ava = BuildImage(80, 80) + await user_ava.circle_corner(10) + await user_background.paste(user_ava, (130, 10)) + no_image = BuildImage(100, 100, font_size=65, font="CJGaoDeGuo.otf") + await no_image.text((0, 0), f"{i+1}", center_type="center") + await no_image.line((99, 10, 99, 90), "#b9b9b9") + await user_background.paste(no_image) + name = [ + user.user_name + for user in group_user_list + if user_id == user.user_id + ] + await user_background.text((225, 15), name[0] if name else "") + amount_image = await BuildImage.build_text_image( + f"{amount} 元", size=30, font_color="#cdac72" + ) + await user_background.paste( + amount_image, (user_background.width - amount_image.width - 20, 50) + ) + await user_background.line((225, 99, 590, 99), "#b9b9b9") + user_image_list.append(user_background) + background = BuildImage(600, 150 + len(user_image_list) * 100) + top = BuildImage(600, 100, color="#f55545", font_size=30) + promoter_ava_bytes = await PlatformUtils.get_user_avatar( + self.promoter_id, platform + ) + promoter_ava = None + if promoter_ava_bytes: + promoter_ava = BuildImage(60, 60, background=BytesIO(promoter_ava_bytes)) + else: + promoter_ava = BuildImage(60, 60) + await promoter_ava.circle() + await top.paste(promoter_ava, (10, 0), "height") + await top.text((80, 33), self.name, (255, 255, 255)) + right_text = BuildImage(150, 100, color="#f55545", font_size=30) + await right_text.text((10, 33), "结算排行", (255, 255, 255)) + await right_text.line((4, 10, 4, 90), (255, 255, 255), 2) + await top.paste(right_text, (460, 0)) + await background.paste(top) + cur_h = 110 + for user_image in user_image_list: + await background.paste(user_image, (0, cur_h)) + cur_h += user_image.height + return background + + +class GroupRedBag: + """ + 群组红包管理 + """ + + def __init__(self, group_id: str): + self.group_id = group_id + self._data: Dict[str, RedBag] = {} + """红包列表""" + + def remove_festive_red_bag(self): + """删除节日红包""" + _key = None + for k, red_bag in self._data.items(): + if red_bag.is_festival: + _key = k + break + if _key: + del self._data[_key] + + def get_festive_red_bag(self) -> RedBag | None: + """获取节日红包 + + 返回: + RedBag | None: 节日红包 + """ + for _, red_bag in self._data.items(): + if red_bag.is_festival: + return red_bag + return None + + def get_user_red_bag(self, user_id: str) -> RedBag | None: + """获取用户塞红包数据 + + 参数: + user_id: 用户id + + 返回: + RedBag | None: RedBag + """ + return self._data.get(str(user_id)) + + def check_open(self, user_id: str) -> bool: + """检查是否有可开启的红包 + + 参数: + user_id: 用户id + + 返回: + bool: 是否有可开启的红包 + """ + user_id = str(user_id) + for _, red_bag in self._data.items(): + if red_bag.assigner: + if red_bag.assigner == user_id: + return True + else: + if user_id not in red_bag.open_user: + return True + return False + + def check_timeout(self, user_id: str) -> int: + """判断用户红包是否过期 + + 参数: + user_id: 用户id + + 返回: + int: 距离过期时间 + """ + if user_id in self._data: + reg_bag = self._data[user_id] + now = time.time() + if now < reg_bag.timeout + reg_bag.start_time: + return int(reg_bag.timeout + reg_bag.start_time - now) + return -1 + + async def open( + self, user_id: str, platform: str | None = None + ) -> tuple[Dict[str, tuple[int, RedBag]], list[RedBag]]: + """开启红包 + + 参数: + user_id: 用户id + platform: 所属平台 + + 返回: + Dict[str, tuple[int, RedBag]]: 键为发起者id, 值为开启金额以及对应RedBag + list[RedBag]: 开完的红包 + """ + open_data = {} + settlement_list: list[RedBag] = [] + for _, red_bag in self._data.items(): + if red_bag.num > len(red_bag.open_user): + if red_bag.is_festival and red_bag.uuid: + if not FestiveRedBagManage.check(red_bag.uuid, user_id): + continue + FestiveRedBagManage.open(red_bag.uuid, user_id) + is_open = False + if red_bag.assigner: + is_open = red_bag.assigner == user_id + else: + is_open = user_id not in red_bag.open_user + if is_open: + random_amount = red_bag.red_bag_list.pop() + await RedbagUser.add_redbag_data( + user_id, self.group_id, "get", random_amount + ) + await UserConsole.add_gold( + user_id, random_amount, "gold_redbag", platform + ) + red_bag.open_user[user_id] = random_amount + open_data[red_bag.promoter_id] = (random_amount, red_bag) + if red_bag.num == len(red_bag.open_user): + # 红包开完,结算 + settlement_list.append(red_bag) + if settlement_list: + for uid in [red_bag.promoter_id for red_bag in settlement_list]: + if uid in self._data: + del self._data[uid] + return open_data, settlement_list + + def festive_red_bag_expire(self) -> RedBag | None: + """节日红包过期 + + 返回: + RedBag | None: 过期的节日红包 + """ + if FESTIVE_KEY in self._data: + red_bag = self._data[FESTIVE_KEY] + del self._data[FESTIVE_KEY] + return red_bag + return None + + async def settlement( + self, user_id: str, platform: str | None = None + ) -> tuple[int | None, RedBag | None]: + """红包退回 + + 参数: + user_id: 用户id, 指定id时结算指定用户红包. + platform: 用户平台 + + 返回: + tuple[int | None, RedBag | None]: 退回金币, 红包 + """ + if red_bag := self._data.get(user_id): + del self._data[user_id] + if red_bag.is_festival and red_bag.uuid: + FestiveRedBagManage.remove(red_bag.uuid) + if red_bag.red_bag_list: + """退还剩余金币""" + if amount := sum(red_bag.red_bag_list): + await UserConsole.add_gold(user_id, amount, "gold_redbag", platform) + return amount, red_bag + return None, None + + async def add_red_bag( + self, + name: str, + amount: int, + num: int, + promoter: str, + promoter_id: str, + festival_uuid: str | None = None, + timeout: int = 60, + assigner: str | None = None, + platform: str | None = None, + ): + """添加红包 + + 参数: + name: 红包名称 + amount: 金币数量 + num: 红包数量 + promoter: 发起人昵称 + promoter_id: 发起人id + festival_uuid: 节日红包uuid. + timeout: 超时时间. + assigner: 指定人. + platform: 用户平台. + """ + user = await UserConsole.get_user(promoter_id, platform) + if not festival_uuid and (amount < 1 or user.gold < amount): + raise ValueError("红包金币不足或用户金币不足") + red_bag_list = self._random_red_bag(amount, num) + if not festival_uuid: + user.gold -= amount + await RedbagUser.add_redbag_data(promoter_id, self.group_id, "send", amount) + await user.save(update_fields=["gold"]) + self._data[promoter_id] = RedBag( + group_id=self.group_id, + name=name, + amount=amount, + num=num, + promoter=promoter, + promoter_id=promoter_id, + is_festival=bool(festival_uuid), + timeout=timeout, + start_time=time.time(), + assigner=assigner, + red_bag_list=red_bag_list, + uuid=festival_uuid, + ) + + def _random_red_bag(self, amount: int, num: int) -> list[int]: + """初始化红包金币 + + 参数: + amount: 金币数量 + num: 红包数量 + + 返回: + list[int]: 红包列表 + """ + red_bag_list = [] + for _ in range(num - 1): + tmp = int(amount / random.choice(range(3, num + 3))) + red_bag_list.append(tmp) + amount -= tmp + red_bag_list.append(amount) + return red_bag_list diff --git a/zhenxun/plugins/gold_redbag/data_source.py b/zhenxun/plugins/gold_redbag/data_source.py new file mode 100644 index 00000000..b35dee24 --- /dev/null +++ b/zhenxun/plugins/gold_redbag/data_source.py @@ -0,0 +1,238 @@ +import asyncio +import os +import random +from io import BytesIO +from typing import Dict + +from nonebot.adapters import Bot +from nonebot.exception import ActionFailed +from nonebot_plugin_saa import Image, MessageFactory, Text + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import get_user_avatar + +from .config import FESTIVE_KEY, FestiveRedBagManage, GroupRedBag, RedBag + + +class RedBagManager: + + _data: Dict[str, GroupRedBag] = {} + + @classmethod + def get_group_data(cls, group_id: str) -> GroupRedBag: + """获取群组红包数据 + + 参数: + group_id: 群组id + + 返回: + GroupRedBag | None: GroupRedBag + """ + if group_id not in cls._data: + cls._data[group_id] = GroupRedBag(group_id) + return cls._data[group_id] + + @classmethod + async def _auto_end_festive_red_bag(cls, bot: Bot, group_id: str, platform: str): + """自动结算节日红包 + + 参数: + bot: Bot + group_id: 群组id + platform: 平台 + """ + if target := PlatformUtils.get_target(bot, group_id=group_id): + rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 + group_red_bag = cls.get_group_data(group_id) + red_bag = group_red_bag.get_festive_red_bag() + if not red_bag: + return + rank_image = await red_bag.build_amount_rank(rank_num, platform) + if red_bag.is_festival and red_bag.uuid: + FestiveRedBagManage.remove(red_bag.uuid) + await asyncio.sleep(random.randint(1, 5)) + try: + await MessageFactory( + [ + Text( + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(red_bag.open_user)}" + f" 个红包,共 {sum(red_bag.open_user.values())} 金币\n" + ), + Image(rank_image.pic2bytes()), + ] + ).send_to(target=target, bot=bot) + except ActionFailed: + pass + + @classmethod + async def end_red_bag( + cls, + group_id: str, + user_id: str | None = None, + is_festive: bool = False, + platform: str = "", + ) -> MessageFactory | None: + """结算红包 + + 参数: + group_id: 群组id或频道id + user_id: 用户id + is_festive: 是否节日红包 + platform: 用户平台 + """ + rank_num = Config.get_config("gold_redbag", "RANK_NUM") or 10 + group_red_bag = cls.get_group_data(group_id) + if not group_red_bag: + return None + if is_festive: + if festive_red_bag := group_red_bag.festive_red_bag_expire(): + rank_image = await festive_red_bag.build_amount_rank(rank_num, platform) + return MessageFactory( + [ + Text( + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" + ), + Image(rank_image.pic2bytes()), + ] + ) + else: + if not user_id: + return None + return_gold, red_bag = await group_red_bag.settlement(user_id, platform) + if red_bag: + rank_image = await red_bag.build_amount_rank(rank_num, platform) + return MessageFactory( + [ + Text(f"已成功退还了 " f"{return_gold} 金币\n"), + Image(rank_image.pic2bytes()), + ] + ) + + @classmethod + async def check_gold(cls, user_id: str, amount: int, platform: str) -> str | None: + """检查金币数量是否合法 + + 参数: + user_id: 用户id + amount: 金币数量 + platform: 所属平台 + + 返回: + tuple[bool, str]: 是否合法以及提示语 + """ + user = await UserConsole.get_user(user_id, platform) + if amount < 1: + return "小气鬼,要别人倒贴金币给你嘛!" + if user.gold < amount: + return "没有金币的话请不要发红包..." + return None + + @classmethod + async def random_red_bag_background( + cls, user_id: str, msg: str = "恭喜发财 大吉大利", platform: str = "" + ) -> BuildImage: + """构造发送红包图片 + + 参数: + user_id: 用户id + msg: 红包消息. + platform: 平台. + + 异常: + ValueError: 图片背景列表为空 + + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_2") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) + redbag = BuildImage( + 0, + 0, + font_size=38, + background=IMAGE_PATH / "prts" / "redbag_2" / random_redbag, + ) + ava_byte = await PlatformUtils.get_user_avatar(user_id, platform) + ava = None + if ava_byte: + ava = BuildImage(65, 65, background=BytesIO(ava_byte)) + else: + ava = BuildImage(65, 65, color=(0, 0, 0)) + await ava.circle() + await redbag.text( + (int((redbag.size[0] - redbag.getsize(msg)[0]) / 2), 210), + msg, + (240, 218, 164), + ) + await redbag.paste(ava, (int((redbag.size[0] - ava.size[0]) / 2), 130)) + return redbag + + @classmethod + async def build_open_result_image( + cls, red_bag: RedBag, user_id: str, amount: int, platform: str + ) -> BuildImage: + """构造红包开启图片 + + 参数: + red_bag: RedBag + user_id: 开启红包用户id + amount: 开启红包获取的金额 + platform: 平台 + + 异常: + ValueError: 图片背景列表为空 + + 返回: + BuildImage: 构造后的图片 + """ + background_list = os.listdir(f"{IMAGE_PATH}/prts/redbag_1") + if not background_list: + raise ValueError("prts/redbag_1 背景图列表为空...") + random_redbag = random.choice(background_list) + head = BuildImage( + 1000, + 980, + font_size=30, + background=IMAGE_PATH / "prts" / "redbag_1" / random_redbag, + ) + size = BuildImage.get_text_size(red_bag.name, font_size=50) + ava_bk = BuildImage(100 + size[0], 66, (255, 255, 255, 0), font_size=50) + + ava_byte = await PlatformUtils.get_user_avatar(user_id, platform) + ava = None + if ava_byte: + ava = BuildImage(66, 66, background=BytesIO(ava_byte)) + else: + ava = BuildImage(66, 66, color=(0, 0, 0)) + await ava_bk.paste(ava) + await ava_bk.text((100, 7), red_bag.name) + ava_bk_w, ava_bk_h = ava_bk.size + await head.paste(ava_bk, (int((1000 - ava_bk_w) / 2), 300)) + size = BuildImage.get_text_size(str(amount), font_size=150) + amount_image = BuildImage(size[0], size[1], (255, 255, 255, 0), font_size=150) + await amount_image.text((0, 0), str(amount), fill=(209, 171, 108)) + # 金币中文 + await head.paste(amount_image, (int((1000 - size[0]) / 2) - 50, 460)) + await head.text( + (int((1000 - size[0]) / 2 + size[0]) - 50, 500 + size[1] - 70), + "金币", + fill=(209, 171, 108), + ) + # 剩余数量和金额 + text = ( + f"已领取" + f"{red_bag.num - len(red_bag.open_user)}" + f"/{red_bag.num}个," + f"共{sum(red_bag.open_user.values())}/{red_bag.amount}金币" + ) + await head.text((350, 900), text, (198, 198, 198)) + return head diff --git a/zhenxun/plugins/gold_redbag/model.py b/zhenxun/plugins/gold_redbag/model.py new file mode 100644 index 00000000..a8e9359a --- /dev/null +++ b/zhenxun/plugins/gold_redbag/model.py @@ -0,0 +1,63 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class RedbagUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + send_redbag_count = fields.IntField(default=0) + """发送红包次数""" + get_redbag_count = fields.IntField(default=0) + """开启红包次数""" + spend_gold = fields.IntField(default=0) + """发送红包花费金额""" + get_gold = fields.IntField(default=0) + """开启红包获取金额""" + + class Meta: + table = "redbag_users" + table_description = "红包统计数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def add_redbag_data( + cls, user_id: str, group_id: str, i_type: str, money: int + ): + """添加收发红包数据 + + 参数: + user_id: 用户id + group_id: 群号 + i_type: 收或发 + money: 金钱数量 + """ + + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + if i_type == "get": + user.get_redbag_count = user.get_redbag_count + 1 + user.get_gold = user.get_gold + money + else: + user.send_redbag_count = user.send_redbag_count + 1 + user.spend_gold = user.spend_gold + money + await user.save( + update_fields=[ + "get_redbag_count", + "get_gold", + "send_redbag_count", + "spend_gold", + ] + ) + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE redbag_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE redbag_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE redbag_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/utils/depends/__init__.py b/zhenxun/utils/depends/__init__.py index a993ee45..addcabe6 100644 --- a/zhenxun/utils/depends/__init__.py +++ b/zhenxun/utils/depends/__init__.py @@ -1,7 +1,35 @@ +from typing import Any + from nonebot.internal.params import Depends +from nonebot.matcher import Matcher from nonebot.params import Command +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo +from zhenxun.configs.config import Config + + +def CheckUg(check_user: bool = True, check_group: bool = True): + """检测群组id和用户id是否存在 + + 参数: + check_user: 检查用户id. + check_group: 检查群组id. + """ + + async def dependency(session: EventSession): + if check_user: + user_id = session.id1 + if not user_id: + await Text("用户id为空").finish() + if check_group: + group_id = session.id3 or session.id2 + if not group_id: + await Text("群组id为空").finish() + + return Depends(dependency) + def OneCommand(): """ @@ -24,6 +52,57 @@ def UserName(): async def dependency(user_info: UserInfo = EventUserInfo()): return ( user_info.user_displayname or user_info.user_remark or user_info.user_name - ) + ) or "" + + return Depends(dependency) + + +def GetConfig( + module: str | None = None, + config: str = "", + default_value: Any = None, + prompt: str | None = None, +): + """获取配置项 + + 参数: + module: 模块名,为空时默认使用当前插件模块名 + config: 配置项名称 + default_value: 默认值 + prompt: 为空时提示 + """ + + async def dependency(matcher: Matcher): + module_ = module or matcher.plugin_name + if module_: + value = Config.get_config(module_, config, default_value) + if value is None and prompt: + # await matcher.finish(prompt or f"配置项 {config} 未填写!") + await matcher.finish(prompt) + return value + + return Depends(dependency) + + +def CheckConfig( + module: str | None = None, + config: str | list[str] = "", + prompt: str | None = None, +): + """检测配置项在配置文件中是否填写 + + 参数: + module: 模块名,为空时默认使用当前插件模块名 + config: 需要检查的配置项名称 + prompt: 为空时提示 + """ + + async def dependency(matcher: Matcher): + module_ = module or matcher.plugin_name + if module_: + config_list = [config] if isinstance(config, str) else config + for c in config_list: + if Config.get_config(module_, c) is None: + await matcher.finish(prompt or f"配置项 {c} 未填写!") return Depends(dependency) diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 437c4f0d..681f9768 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -88,7 +88,7 @@ class RequestType(StrEnum): class RequestHandleType(StrEnum): """ - 请求类型 + 请求处理类型 """ APPROVE = "APPROVE" diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index 014e026b..19163cc5 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -1,5 +1,6 @@ from typing import Awaitable, Callable, Literal, Set +import httpx import nonebot from nonebot.adapters import Bot from nonebot.adapters.discord import Bot as DiscordBot @@ -27,6 +28,53 @@ from zhenxun.services.log import logger class PlatformUtils: + @classmethod + async def get_user_avatar(cls, user_id: str, platform: str) -> bytes | None: + """快捷获取用户头像 + + 参数: + user_id: 用户id + platform: 平台 + """ + if platform == "qq": + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=160" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error( + "获取用户头像错误", + "Util", + target=user_id, + platform=platform, + ) + else: + pass + return None + + @classmethod + async def get_group_avatar(cls, gid: str, platform: str) -> bytes | None: + """快捷获取用群头像 + + 参数: + gid: 群组id + platform: 平台 + """ + if platform == "qq": + url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/" + async with httpx.AsyncClient() as client: + for _ in range(3): + try: + return (await client.get(url)).content + except Exception as e: + logger.error( + "获取群头像错误", "Util", target=gid, platform=platform + ) + else: + pass + return None + @classmethod async def send_message( cls, @@ -109,7 +157,7 @@ class PlatformUtils: bot: Bot 返回: - list[GroupConsole]: 群组列表 + tuple[list[GroupConsole], str]: 群组列表, 平台 """ if isinstance(bot, v11Bot): group_list = await bot.get_group_list() @@ -239,8 +287,8 @@ class PlatformUtils: 参数: bot: Bot - group_id: 群组id - channel_id: 频道id或群组id + user_id: 用户id + group_id: 频道id或群组id 返回: target: 对应平台Target diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 76395fd1..9ef46d7d 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -99,8 +99,7 @@ class UserBlockLimiter: def check(self, key: Any) -> bool: if time.time() - self.time > 30: self.set_false(key) - return False - return self.flag_data[key] + return not self.flag_data[key] class FreqLimiter: @@ -156,7 +155,7 @@ async def get_group_avatar(gid: int | str) -> bytes | None: """快捷获取用群头像 参数: - :param gid: 群号 + gid: 群号 """ url = f"http://p.qlogo.cn/gh/{gid}/{gid}/640/" async with httpx.AsyncClient() as client: From ebec169e679f793543987f916ab0d335f30b9e16 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 15 May 2024 23:24:35 +0800 Subject: [PATCH 025/132] =?UTF-8?q?feat=E2=9C=A8:=20=E7=94=BB=E5=BB=8A?= =?UTF-8?q?=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 50 ++--- .vscode/settings.json | 1 + zhenxun/builtin_plugins/admin/ban/__init__.py | 17 +- .../admin/plugin_switch/__init__.py | 24 ++- zhenxun/builtin_plugins/help/__init__.py | 13 +- zhenxun/builtin_plugins/help/_data_source.py | 25 ++- zhenxun/configs/utils/__init__.py | 21 +- zhenxun/plugins/image_management/__init__.py | 69 ++++++ zhenxun/plugins/image_management/_config.py | 14 ++ .../plugins/image_management/_data_source.py | 196 ++++++++++++++++++ .../plugins/image_management/delete_image.py | 109 ++++++++++ .../image_management/image_management_log.py | 27 +++ .../plugins/image_management/move_image.py | 134 ++++++++++++ .../plugins/image_management/send_image.py | 0 .../plugins/image_management/upload_image.py | 196 ++++++++++++++++++ 15 files changed, 841 insertions(+), 55 deletions(-) create mode 100644 zhenxun/plugins/image_management/__init__.py create mode 100644 zhenxun/plugins/image_management/_config.py create mode 100644 zhenxun/plugins/image_management/_data_source.py create mode 100644 zhenxun/plugins/image_management/delete_image.py create mode 100644 zhenxun/plugins/image_management/image_management_log.py create mode 100644 zhenxun/plugins/image_management/move_image.py create mode 100644 zhenxun/plugins/image_management/send_image.py create mode 100644 zhenxun/plugins/image_management/upload_image.py diff --git a/.env.dev b/.env.dev index e7257740..115bcb61 100644 --- a/.env.dev +++ b/.env.dev @@ -21,32 +21,32 @@ PLATFORM_SUPERUSERS = ' DRIVER=~fastapi+~httpx+~websockets # kook adapter toekn -kaiheila_bots =[{"token": ""}] +# kaiheila_bots =[{"token": ""}] -# discode adapter -DISCORD_BOTS=' -[ - { - "token": "", - "intent": { - "guild_messages": true, - "direct_messages": true - }, - "application_commands": {"*": ["*"]} - } -] -' -DISCORD_PROXY='' +# # discode adapter +# DISCORD_BOTS=' +# [ +# { +# "token": "", +# "intent": { +# "guild_messages": true, +# "direct_messages": true +# }, +# "application_commands": {"*": ["*"]} +# } +# ] +# ' +# DISCORD_PROXY='' -# dodo adapter -DODO_BOTS=' -[ - { - "client_id": "", - "token": "" - } -] -' +# # dodo adapter +# DODO_BOTS=' +# [ +# { +# "client_id": "", +# "token": "" +# } +# ] +# ' # application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令 # {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册 @@ -55,3 +55,5 @@ LOG_LEVEL=DEBUG # 服务器和端口 HOST = 127.0.0.1 PORT = 8080 + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6d295644..0d5c4663 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" }, "cSpell.words": [ + "aiofiles", "Alconna", "arclet", "Arparma", diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 330eaeb5..822bcf98 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -26,7 +26,7 @@ from ._data_source import BanManage base_config = Config.get("ban") __plugin_meta__ = PluginMetadata( - name="封禁用户/群组", + name="Ban", description="你被逮捕了!丢进小黑屋!封禁用户以及群组,屏蔽消息", usage=""" 普通管理员 @@ -37,8 +37,13 @@ __plugin_meta__ = PluginMetadata( ban @用户 : 永久拉黑用户 ban @用户 100 : 拉黑用户100分钟 unban @用户 : 从小黑屋中拉出来 - - 超级管理员额外命令 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPER_AND_ADMIN, + superuser_help=""" + 超级管理员额外命令 格式: ban [At用户/用户Id] [时长] ban列表: 获取所有Ban数据 @@ -54,11 +59,7 @@ __plugin_meta__ = PluginMetadata( unban 123456789 : 从小黑屋中拉出来 unban -g 999999 : 将群组9999999从小黑屋中拉出来 - """.strip(), - extra=PluginExtraData( - author="HibiKier", - version="0.1", - plugin_type=PluginType.SUPER_AND_ADMIN, + """, admin_level=base_config.get("BAN_LEVEL", 5), configs=[ RegisterConfig( diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 59295c44..bbaab13b 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -32,22 +32,24 @@ __plugin_meta__ = PluginMetadata( 关闭签到 : 关闭签到 开启群被动早晚安 : 关闭被动任务早晚安 - 超级管理员额外命令 - 格式: - 插件列表 - 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] - - 私聊下: - 示例: - 开启签到 : 全局开启签到 - 关闭签到 : 全局关闭签到 - 关闭签到 p : 全局私聊关闭签到 - 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) """.strip(), extra=PluginExtraData( author="HibiKier", version="0.1", plugin_type=PluginType.SUPER_AND_ADMIN, + superuser_help=""" + 超级管理员额外命令 + 格式: + 插件列表 + 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] + + 私聊下: + 示例: + 开启签到 : 全局开启签到 + 关闭签到 : 全局关闭签到 + 关闭签到 p : 全局私聊关闭签到 + 关闭签到 -g 12345678 : 关闭群组12345678的签到功能(普通管理员无法开启) + """, admin_level=base_config.get("CHANGE_GROUP_SWITCH_LEVEL", 2), configs=[ RegisterConfig( diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 8f6fd606..3aa6021d 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -1,8 +1,9 @@ from pathlib import Path +from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import Alconna, Args, Match, on_alconna +from nonebot_plugin_alconna import Alconna, AlconnaQuery, Args, Match, Option, Query, on_alconna, store_true from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession @@ -43,6 +44,7 @@ _matcher = on_alconna( Alconna( "功能", Args["name?", str], + Option("-s|--superuser", action=store_true, help_text="超级用户帮助") ), aliases={"help", "帮助"}, rule=to_me(), @@ -53,11 +55,18 @@ _matcher = on_alconna( @_matcher.handle() async def _( + bot: Bot, name: Match[str], session: EventSession, + is_superuser: Query[bool] = AlconnaQuery("superuser.value", False) ): + _is_superuser = False + if is_superuser.available: + _is_superuser = is_superuser.result if name.available: - if result := await get_plugin_help(name.result): + if _is_superuser and session.id1 not in bot.config.superusers: + _is_superuser = False + if result := await get_plugin_help(name.result, _is_superuser): if isinstance(result, BuildImage): await Image(result.pic2bytes()).send(reply=True) else: diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py index 0f6a010f..68da9f41 100644 --- a/zhenxun/builtin_plugins/help/_data_source.py +++ b/zhenxun/builtin_plugins/help/_data_source.py @@ -21,19 +21,30 @@ async def create_help_img(group_id: str | None): await HelpImageBuild().build_image(group_id) -async def get_plugin_help(name: str) -> str | BuildImage: +async def get_plugin_help(name: str, is_superuser: bool) -> str | BuildImage: """获取功能的帮助信息 参数: name: 插件名称 + is_superuser: 是否为超级用户 """ - if plugin := await PluginInfo.get_or_none(name=name): + if plugin := await PluginInfo.get_or_none(name__iexact=name): _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) if _plugin and _plugin.metadata: - items = { - "简介": _plugin.metadata.description, - "用法": _plugin.metadata.usage, - } - return await ImageTemplate.hl_page(name, items) + items = None + if is_superuser: + extra = _plugin.metadata.extra + if usage := extra.get("superuser_help"): + items = { + "简介": _plugin.metadata.description, + "用法": usage, + } + else: + items = { + "简介": _plugin.metadata.description, + "用法": _plugin.metadata.usage, + } + if items: + return await ImageTemplate.hl_page(name, items) return "糟糕! 该功能没有帮助喔..." return "没有查找到这个功能噢..." diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index abe5d03c..d6a28180 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -194,6 +194,21 @@ class ConfigsManager: f"**********************************************" ) + def set_name(self, module: str, name: str): + """设置插件配置中文名出 + + 参数: + module: 模块名 + name: 中文名称 + + 异常: + ValueError: module不能为为空 + """ + if not module: + raise ValueError("set_name: module不能为为空") + if data := self._data.get(module): + data.name = name + def add_plugin_config( self, module: str, @@ -219,8 +234,8 @@ class ConfigsManager: _override: 强制覆盖值. 异常: - ValueError: _description_ - ValueError: _description_ + ValueError: module和key不能为为空 + ValueError: 填写错误 """ if not module or not key: @@ -297,7 +312,7 @@ class ConfigsManager: if module in self._data.keys(): config = self._data[module].configs.get(key) if not config: - config = self._data[module].configs.get(f"{key} [LEVEL]") + config = self._data[module].configs.get(key) if not config: raise NoSuchConfig( f"未查询到配置项 MODULE: [ {module} ] | KEY: [ {key} ]" diff --git a/zhenxun/plugins/image_management/__init__.py b/zhenxun/plugins/image_management/__init__.py new file mode 100644 index 00000000..8f24386d --- /dev/null +++ b/zhenxun/plugins/image_management/__init__.py @@ -0,0 +1,69 @@ +from pathlib import Path +from typing import List, Tuple + +import nonebot + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH + +Config.add_plugin_config( + "image_management", + "IMAGE_DIR_LIST", + ["美图", "萝莉", "壁纸"], + help="公开图库列表,可自定义添加 [如果含有send_setu插件,请不要添加色图库]", + default_value=[], + type=List[str], +) + +Config.add_plugin_config( + "image_management", + "WITHDRAW_IMAGE_MESSAGE", + (0, 1), + help="自动撤回,参1:延迟撤回发送图库图片的时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], +) + +Config.add_plugin_config( + "image_management:delete_image", + "DELETE_IMAGE_LEVEL", + 7, + help="删除图库图片需要的管理员等级", + default_value=7, + type=int, +) + +Config.add_plugin_config( + "image_management:move_image", + "MOVE_IMAGE_LEVEL", + 7, + help="移动图库图片需要的管理员等级", + default_value=7, + type=int, +) + +Config.add_plugin_config( + "image_management:upload_image", + "UPLOAD_IMAGE_LEVEL", + 6, + help="上传图库图片需要的管理员等级", + default_value=6, + type=int, +) + +Config.add_plugin_config( + "image_management", + "SHOW_ID", + True, + help="是否消息显示图片下标id", + default_value=True, + type=bool, +) + +Config.set_name("image_management", "图库操作") + + +(IMAGE_PATH / "image_management").mkdir(parents=True, exist_ok=True) + + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/image_management/_config.py b/zhenxun/plugins/image_management/_config.py new file mode 100644 index 00000000..d5e01f58 --- /dev/null +++ b/zhenxun/plugins/image_management/_config.py @@ -0,0 +1,14 @@ +from strenum import StrEnum + + +class ImageHandleType(StrEnum): + """ + 图片处理类型 + """ + + UPLOAD = "UPLOAD" + """上传""" + DELETE = "DELETE" + """删除""" + MOVE = "MOVE" + """移动""" diff --git a/zhenxun/plugins/image_management/_data_source.py b/zhenxun/plugins/image_management/_data_source.py new file mode 100644 index 00000000..bc26b74f --- /dev/null +++ b/zhenxun/plugins/image_management/_data_source.py @@ -0,0 +1,196 @@ +import os +import random +from pathlib import Path + +import aiofiles + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.utils import cn2py + +from .image_management_log import ImageHandleType, ImageManagementLog + +BASE_PATH = IMAGE_PATH / "image_management" + + +class ImageManagementManage: + + @classmethod + async def random_image(cls, name: str, file_id: int | None = None) -> Path | None: + """随机图片 + + 参数: + name: 图库名称 + file_id: 图片id. + + 返回: + Path | None: 图片路径 + """ + path = BASE_PATH / name + file_name = f"{file_id}.jpg" + if file_id is None: + if file_list := os.listdir(path): + file_name = random.choice(file_list) + _file = path / file_name + if not _file.exists(): + return None + return _file + + @classmethod + async def upload_image( + cls, + image_data: bytes | str, + name: str, + user_id: str, + platform: str | None = None, + ) -> str | None: + """上传图片 + + 参数: + image_data: 图片bytes + name: 图库名称 + user_id: 用户id + platform: 所属平台 + + 返回: + str | None: 文件名称 + """ + path = BASE_PATH / cn2py(name) + path.mkdir(exist_ok=True, parents=True) + _file_name = 0 + if file_list := os.listdir(path): + file_list.sort() + _file_name = int(file_list[-1].split(".")[0]) + 1 + _file_path = path / f"{_file_name}.jpg" + try: + await ImageManagementLog.create( + user_id=user_id, + path=_file_path, + handle_type=ImageHandleType.UPLOAD, + platform=platform, + ) + if isinstance(image_data, str): + await AsyncHttpx.download_file(image_data, _file_path) + else: + async with aiofiles.open(_file_path, "wb") as f: + await f.write(image_data) + logger.info( + f"上传图片至 {name}, 路径: {_file_path}", + "上传图片", + session=user_id, + ) + return f"{_file_name}.jpg" + except Exception as e: + logger.error("上传图片错误", "上传图片", e=e) + return None + + @classmethod + async def delete_image( + cls, name: str, file_id: int, user_id: str, platform: str | None = None + ) -> bool: + """删除图片 + + 参数: + name: 图库名称 + file_id: 图片id + user_id: 用户id + platform: 所属平台. + + 返回: + bool: 是否删除成功 + """ + path = BASE_PATH / cn2py(name) + if not path.exists(): + return False + _file_path = path / f"{file_id}.jpg" + if not _file_path.exists(): + return False + try: + await ImageManagementLog.create( + user_id=user_id, + path=_file_path, + handle_type=ImageHandleType.DELETE, + platform=platform, + ) + _file_path.unlink() + logger.info( + f"图库: {name}, 删除图片路径: {_file_path}", "删除图片", session=user_id + ) + if file_list := os.listdir(path): + file_list.sort() + _file_name = file_list[-1].split(".")[0] + _move_file = path / f"{_file_name}.jpg" + _move_file.rename(_file_path) + logger.info( + f"图库: {name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg", + "删除图片", + session=user_id, + ) + except Exception as e: + logger.error("删除图片错误", "删除图片", e=e) + return False + return True + + @classmethod + async def move_image( + cls, + a_name: str, + b_name: str, + file_id: int, + user_id: str, + platform: str | None = None, + ) -> str | None: + """移动图片 + + 参数: + a_name: 源图库 + b_name: 模板图库 + file_id: 图片id + user_id: 用户id + platform: 所属平台. + + 返回: + bool: 是否移动成功 + """ + source_path = BASE_PATH / cn2py(a_name) + if not source_path.exists(): + return None + destination_path = BASE_PATH / cn2py(b_name) + destination_path.mkdir(exist_ok=True, parents=True) + source_file = source_path / f"{file_id}.jpg" + if not source_file.exists(): + return None + _destination_name = 0 + if file_list := os.listdir(destination_path): + file_list.sort() + _destination_name = int(file_list[-1].split(".")[0]) + 1 + destination_file = destination_path / f"{_destination_name}.jpg" + try: + await ImageManagementLog.create( + user_id=user_id, + path=source_file, + move=destination_file, + handle_type=ImageHandleType.MOVE, + platform=platform, + ) + source_file.rename(destination_file) + logger.info( + f"图库: {a_name} -> {b_name}, 移动图片路径: {source_file} -> {destination_file}", + "移动图片", + session=user_id, + ) + if file_list := os.listdir(source_path): + file_list.sort() + _file_name = file_list[-1].split(".")[0] + _move_file = source_path / f"{_file_name}.jpg" + _move_file.rename(source_file) + logger.info( + f"图库: {a_name}, 移动图片名称: {_file_name}.jpg -> {file_id}.jpg", + "移动图片", + session=user_id, + ) + except Exception as e: + logger.error("移动图片错误", "移动图片", e=e) + return None + return f"{source_file} -> {destination_file}" diff --git a/zhenxun/plugins/image_management/delete_image.py b/zhenxun/plugins/image_management/delete_image.py new file mode 100644 index 00000000..bccbe3d2 --- /dev/null +++ b/zhenxun/plugins/image_management/delete_image.py @@ -0,0 +1,109 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="删除图片", + description="不好看的图片删掉删掉!", + usage=""" + 指令: + 删除图片 [图库] [id] + 查看图库 + 示例:删除图片 美图 666 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("DELETE_IMAGE_LEVEL"), + ).dict(), +) + + +_matcher = on_alconna( + Alconna("删除图片", Args["name?", str]["index?", str]), + rule=to_me(), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + name: Match[str], + index: Match[str], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await Text("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _matcher.set_path_arg("name", name.result) + if index.available: + _matcher.set_path_arg("index", index.result) + + +@_matcher.got_path( + "name", + prompt=UniMessage.template( + "请输入要删除的目标图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(name: str): + if name in ["取消", "算了"]: + await Text("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if name.isdigit(): + index = int(name) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("name", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("name", name) + + +@_matcher.got_path("index", "请输入要删除的图片id?【发送'取消', '算了'来取消操作】") +async def _( + session: EventSession, + arparma: Arparma, + index: str, +): + if index in ["取消", "算了"]: + await Text("已取消操作...").finish() + if not index.isdigit(): + await _matcher.reject_path("index", "图片id需要输入数字...") + name = _matcher.get_path_arg("name", None) + if not name: + await Text("图库名称为空...").finish() + if not session.id1: + await Text("用户id为空...").finish() + if file_name := await ImageManagementManage.delete_image( + name, int(index), session.id1, session.platform + ): + logger.info( + f"删除图片成功 图库: {name} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await Text(f"删除图片成功!\n图库: {name}\n名称: {index}.jpg").finish() + await Text("图片删除失败...").finish() diff --git a/zhenxun/plugins/image_management/image_management_log.py b/zhenxun/plugins/image_management/image_management_log.py new file mode 100644 index 00000000..756e58f2 --- /dev/null +++ b/zhenxun/plugins/image_management/image_management_log.py @@ -0,0 +1,27 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + +from ._config import ImageHandleType + + +class ImageManagementLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255, description="用户id") + """用户id""" + path = fields.TextField(description="图片路径") + """图片路径""" + move = fields.TextField(null=True, description="移动路径") + """移动路径""" + handle_type = fields.CharEnumField(ImageHandleType, description="操作类型") + """操作类型""" + create_time = fields.DatetimeField(auto_now_add=True, description="创建时间") + """创建时间""" + platform = fields.CharField(255, null=True, description="平台") + """平台""" + + class Meta: + table = "image_management_log" + table_description = "画廊操作记录" diff --git a/zhenxun/plugins/image_management/move_image.py b/zhenxun/plugins/image_management/move_image.py new file mode 100644 index 00000000..44157f3e --- /dev/null +++ b/zhenxun/plugins/image_management/move_image.py @@ -0,0 +1,134 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="移动图片", + description="图库间的图片移动操作", + usage=""" + 指令: + 移动图片 [源图库] [目标图库] [id] + 查看图库 + 示例:移动图片 萝莉 美图 234 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("MOVE_IMAGE_LEVEL"), + ).dict(), +) + + +_matcher = on_alconna( + Alconna("移动图片", Args["source?", str]["destination?", str]["index?", str]), + rule=to_me(), + priority=5, + block=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + source: Match[str], + destination: Match[str], + index: Match[str], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await Text("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if source.available: + _matcher.set_path_arg("source", source.result) + if destination.available: + _matcher.set_path_arg("destination", destination.result) + if index.available: + _matcher.set_path_arg("index", index.result) + + +@_matcher.got_path( + "source", + prompt=UniMessage.template( + "要从哪个图库移出?【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(source: str): + if source in ["取消", "算了"]: + await Text("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if source.isdigit(): + index = int(source) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("source", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("source", name) + + +@_matcher.got_path( + "destination", + prompt=UniMessage.template( + "要移动到哪个图库?【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(destination: str): + if destination in ["取消", "算了"]: + await Text("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if destination.isdigit(): + index = int(destination) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _matcher.reject_path("destination", "此目录不正确,请重新输入目录!") + _matcher.set_path_arg("destination", name) + + +@_matcher.got_path("index", "要移动的图片id是?【发送'取消', '算了'来取消操作】") +async def _( + session: EventSession, + arparma: Arparma, + index: str, +): + if index in ["取消", "算了"]: + await Text("已取消操作...").finish() + if not index.isdigit(): + await _matcher.reject_path("index", "图片id需要输入数字...") + source = _matcher.get_path_arg("source", None) + destination = _matcher.get_path_arg("destination", None) + if not source: + await Text("图库名称为空...").finish() + if not destination: + await Text("图库名称为空...").finish() + if not session.id1: + await Text("用户id为空...").finish() + if file_name := await ImageManagementManage.move_image( + source, destination, int(index), session.id1, session.platform + ): + logger.info( + f"移动图片成功 图库: {source} -> {destination} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await Text(f"移动图片成功!\n图库: {source} -> {destination}").finish() + await Text("图片删除失败...").finish() diff --git a/zhenxun/plugins/image_management/send_image.py b/zhenxun/plugins/image_management/send_image.py new file mode 100644 index 00000000..e69de29b diff --git a/zhenxun/plugins/image_management/upload_image.py b/zhenxun/plugins/image_management/upload_image.py new file mode 100644 index 00000000..6f5a636a --- /dev/null +++ b/zhenxun/plugins/image_management/upload_image.py @@ -0,0 +1,196 @@ +from nonebot.adapters import Bot +from nonebot.params import Arg, ArgStr, CommandArg +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot.typing import T_State +from nonebot.utils import P +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Match, UniMessage, UniMsg, image_fetch, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx + +from ._data_source import ImageManagementManage + +base_config = Config.get("image_management") + +__plugin_meta__ = PluginMetadata( + name="上传图片", + description="上传图片至指定图库", + usage=""" + 指令: + 查看图库 + 上传图片 [图库] [图片] + 示例:上传图片 美图 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("UPLOAD_IMAGE_LEVEL"), + ).dict(), +) + + +_upload_matcher = on_alconna( + Alconna("上传图片", Args["name?", str]["img?", alcImage]), + rule=to_me(), + priority=5, + block=True, +) + +_continuous_upload_matcher = on_alconna( + Alconna("连续上传图片", Args["name?", str]), + rule=to_me(), + priority=5, + block=True, +) + +_show_matcher = on_alconna(Alconna("查看公开图库"), priority=1, block=True) + + +@_show_matcher.handle() +async def _(): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await Text("未发现任何图库").finish() + text = "公开图库列表:\n" + for i, e in enumerate(image_dir_list): + text += f"\t{i+1}.{e}\n" + await Text(text[:-1]).send() + + +@_upload_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + name: Match[str], + img: Match[bytes], + state: T_State, +): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await Text("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _upload_matcher.set_path_arg("name", name.result) + if img.available: + result = await AsyncHttpx.get(img.result.url) # type: ignore + _upload_matcher.set_path_arg("img", result.content) + + +@_continuous_upload_matcher.handle() +async def _(bot: Bot, state: T_State, name: Match[str]): + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if not image_dir_list: + await Text("未发现任何图库").finish() + _text = "" + for i, dir in enumerate(image_dir_list): + _text += f"{i}. {dir}\n" + state["dir_list"] = _text[:-1] + if name.available: + _upload_matcher.set_path_arg("name", name.result) + + +@_continuous_upload_matcher.got_path( + "name", + prompt=UniMessage.template( + "请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +@_upload_matcher.got_path( + "name", + prompt=UniMessage.template( + "请选择要上传的图库(id 或 名称)【发送'取消', '算了'来取消操作】\n{dir_list}" + ), +) +async def _(name: str, state: T_State): + if name in ["取消", "算了"]: + await Text("已取消操作...").finish() + image_dir_list = base_config.get("IMAGE_DIR_LIST") + if name.isdigit(): + index = int(name) + if index <= len(image_dir_list) - 1: + name = image_dir_list[index] + if name not in image_dir_list: + await _upload_matcher.reject_path("name", "此目录不正确,请重新输入目录!") + _upload_matcher.set_path_arg("name", name) + + +@_upload_matcher.got_path("img", "图呢图呢图呢图呢!GKD!", image_fetch) +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + img: bytes, +): + name = _upload_matcher.get_path_arg("name", None) + if not name: + await Text("图库名称为空...").finish() + if not session.id1: + await Text("用户id为空...").finish() + if file_name := await ImageManagementManage.upload_image( + img, name, session.id1, session.platform + ): + logger.info( + f"图库: {name} --- 名称: {file_name}", + arparma.header_result, + session=session, + ) + await Text(f"上传图片成功!\n图库: {name}\n名称: {file_name}").finish() + await Text("图片上传失败...").finish() + + +@_continuous_upload_matcher.got( + "img", "图呢图呢图呢图呢!GKD!【在最后一张图片中+‘stop’为停止】" +) +async def _( + bot: Bot, + arparma: Arparma, + session: EventSession, + state: T_State, + message: UniMsg, +): + name = _continuous_upload_matcher.get_path_arg("name", None) + if not name: + await Text("图库名称为空...").finish() + if not session.id1: + await Text("用户id为空...").finish() + if not state.get("img_list"): + state["img_list"] = [] + msg = message.extract_plain_text().strip().replace(arparma.header_result, "", 1) + if msg in ["取消", "算了"]: + await Text("已取消操作...").finish() + if msg != "stop": + for msg in message: + if isinstance(msg, alcImage): + state["img_list"].append(msg.url) + await _continuous_upload_matcher.reject("图再来!!【发送‘stop’为停止】") + if state["img_list"]: + await Text("正在下载, 请稍后...").send() + file_list = [] + for img in state["img_list"]: + if file_name := await ImageManagementManage.upload_image( + img, name, session.id1, session.platform + ): + file_list.append(img) + logger.info( + f"图库: {name} --- 名称: {file_name}", + "上传图片", + session=session, + ) + await Text( + f"上传图片成功!共上传了{len(file_list)}张图片\n图库: {name}\n名称: {', '.join(file_list)}" + ).finish() + await Text("图片上传失败...").finish() From e2bb4d2e56457bc5ae14b9393f4320a5eb51ff10 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 16 May 2024 00:29:52 +0800 Subject: [PATCH 026/132] =?UTF-8?q?feat=E2=9C=A8:=20=E9=B2=81=E8=BF=85?= =?UTF-8?q?=E8=AF=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/luxun.py | 74 +++++++++++++++++++++++++++++++++++ zhenxun/utils/_build_image.py | 3 ++ 2 files changed, 77 insertions(+) create mode 100644 zhenxun/plugins/luxun.py diff --git a/zhenxun/plugins/luxun.py b/zhenxun/plugins/luxun.py new file mode 100644 index 00000000..e407cb3a --- /dev/null +++ b/zhenxun/plugins/luxun.py @@ -0,0 +1,74 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +__plugin_meta__ = PluginMetadata( + name="鲁迅说", + description="鲁迅说了啥?", + usage=""" + 鲁迅说 [文本] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + limits=[BaseBlock(result="你的鲁迅正在说,等会")], + ).dict(), +) + +_matcher = on_alconna( + Alconna("luxun", Args["content", str]), + priority=5, + block=True, +) + +_matcher.shortcut( + "鲁迅说", + command="luxun", + arguments=["{%0}"], + prefix=True, +) + + +_sign = None + + +@_matcher.handle() +async def _(content: Match[str]): + if content.available: + _matcher.set_path_arg("content", content.result) + + +@_matcher.got_path("content", prompt="你让鲁迅说点啥?") +async def _(content: str, session: EventSession, arparma: Arparma): + global _sign + if content.startswith(",") or content.startswith(","): + content = content[1:] + A = BuildImage( + font_size=37, background=f"{IMAGE_PATH}/other/luxun.jpg", font="msyh.ttf" + ) + text = "" + if len(content) > 40: + await Text("太长了,鲁迅说不完...").finish() + while A.getsize(content)[0] > A.width - 50: + n = int(len(content) / 2) + text += content[:n] + "\n" + content = content[n:] + text += content + if len(text.split("\n")) > 2: + await Text("太长了,鲁迅说不完...").finish() + await A.text( + (int((480 - A.getsize(text.split("\n")[0])[0]) / 2), 300), text, (255, 255, 255) + ) + if not _sign: + _sign = await BuildImage.build_text_image( + "--鲁迅", "msyh.ttf", 30, (255, 255, 255) + ) + await A.paste(_sign, (320, 400)) + await Image(A.pic2bytes()).send() + logger.info(f"鲁迅说: {content}", arparma.header_result, session=session) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index 5cf26e89..665d4a5b 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -60,6 +60,9 @@ class BuildImage: self.markImg = Image.open(background) if width and height: self.markImg = self.markImg.resize((width, height), Image.LANCZOS) + else: + self.width = self.markImg.width + self.height = self.markImg.height else: if not width or not height: raise ValueError("长度和宽度不能为空...") From f03604d3bcb20acb6bf2aa3017d53488b1070feb Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 16 May 2024 19:54:30 +0800 Subject: [PATCH 027/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=88=91=E6=9C=89?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E6=9C=8B=E5=8F=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/one_friend/__init__.py | 79 ++++++++++ zhenxun/utils/http_utils.py | 2 +- zhenxun/utils/platform.py | 195 +++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/one_friend/__init__.py diff --git a/zhenxun/plugins/one_friend/__init__.py b/zhenxun/plugins/one_friend/__init__.py new file mode 100644 index 00000000..67c5082e --- /dev/null +++ b/zhenxun/plugins/one_friend/__init__.py @@ -0,0 +1,79 @@ +import random +from io import BytesIO + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.platform import PlatformUtils + +__plugin_meta__ = PluginMetadata( + name="我有一个朋友", + description="我有一个朋友想问问...", + usage=""" + 指令: + 我有一个朋友想问问 [文本] ?[at]: 当at时你的朋友就是艾特对象 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_matcher = on_alconna( + Alconna("one-friend", Args["text", str]["at?", alcAt]), priority=5, block=True +) + +_matcher.shortcut( + "^我.{0,4}朋友.{0,2}(?:想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问)(?P.{0,30})$", + command="one-friend", + arguments=["{content}"], + prefix=True, +) + + +@_matcher.handle() +async def _(bot: Bot, text: str, at: Match[alcAt], session: EventSession): + gid = session.id3 or session.id2 + if not gid: + await Text("群组id为空...").finish() + if not session.id1: + await Text("用户id为空...").finish() + at_user = None + if at.available: + at_user = at.result.target + user = None + if at_user: + user = await PlatformUtils.get_user(bot, at_user, gid) + else: + if member_list := await PlatformUtils.get_group_member_list(bot, gid): + text = text.replace("他", "我").replace("她", "我").replace("它", "我") + user = random.choice(member_list) + if user: + ava_data = None + if PlatformUtils.get_platform(bot) == "qq": + ava_data = await PlatformUtils.get_user_avatar(user.user_id, "qq") + elif user.avatar_url: + ava_data = (await AsyncHttpx.get(user.avatar_url)).content + ava_img = BuildImage(200, 100, color=(0, 0, 0, 0)) + if ava_data: + ava_img = BuildImage(200, 100, background=BytesIO(ava_data)) + await ava_img.circle() + user_name = "朋友" + content = BuildImage(400, 30, font_size=30) + await content.text((0, 0), user_name) + A = BuildImage(700, 150, font_size=25, color="white") + await A.paste(ava_img, (30, 25)) + await A.paste(content, (150, 38)) + await A.text((150, 85), text, (125, 125, 125)) + logger.info(f"发送有一个朋友: {text}", "我有一个朋友", session=session) + await Image(A.pic2bytes()).finish() + await Text("获取用户信息失败...").send() diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py index ea9516ef..142d2ceb 100644 --- a/zhenxun/utils/http_utils.py +++ b/zhenxun/utils/http_utils.py @@ -72,7 +72,7 @@ class AsyncHttpx: cls, url: str, *, - data: Dict[str, str] | None = None, + data: Dict[str, Any] | None = None, content: Any = None, files: Any = None, verify: bool = True, diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index 19163cc5..12502fd8 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -20,14 +20,209 @@ from nonebot_plugin_saa import ( TargetQQPrivate, Text, ) +from pydantic import BaseModel from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger +class UserData(BaseModel): + + name: str + """昵称""" + card: str | None = None + """名片/备注""" + user_id: str + """用户id""" + group_id: str | None = None + """群组id""" + role: str | None = None + """角色""" + avatar_url: str | None = None + """头像url""" + join_time: int | None = None + """加入时间""" + + class PlatformUtils: + @classmethod + async def get_group_member_list(cls, bot: Bot, group_id: str) -> list[UserData]: + """获取群组/频道成员列表 + + 参数: + bot: Bot + group_id: 群组/频道id + + 返回: + list[UserData]: 用户数据列表 + """ + if isinstance(bot, v11Bot): + if member_list := await bot.get_group_member_list(group_id=int(group_id)): + return [ + UserData( + name=user["nickname"], + card=user["card"], + user_id=user["user_id"], + group_id=user["group_id"], + role=user["role"], + join_time=user["join_time"], + ) + for user in member_list + ] + if isinstance(bot, v12Bot): + if member_list := await bot.get_group_member_list(group_id=group_id): + return [ + UserData( + name=user["user_name"], + card=user["user_displayname"], + user_id=user["user_id"], + group_id=group_id, + ) + for user in member_list + ] + if isinstance(bot, DodoBot): + if result_data := await bot.get_member_list( + island_source_id=group_id, page_size=100, max_id=0 + ): + max_id = result_data.max_id + result_list = result_data.list + data_list = [] + while max_id == 100: + result_data = await bot.get_member_list( + island_source_id=group_id, page_size=100, max_id=0 + ) + result_list += result_data.list + max_id = result_data.max_id + for user in result_list: + data_list.append( + UserData( + name=user.nick_name, + card=user.personal_nick_name, + avatar_url=user.avatar_url, + user_id=user.dodo_source_id, + group_id=user.island_source_id, + join_time=int(user.join_time.timestamp()), + ) + ) + return data_list + if isinstance(bot, KaiheilaBot): + if result_data := await bot.guild_userList(guild_id=group_id): + if result_data.users: + data_list = [] + for user in result_data.users: + second = None + if user.joined_at: + second = int(user.joined_at / 1000) + data_list.append( + UserData( + name=user.nickname or "", + avatar_url=user.avatar, + user_id=user.id_, # type: ignore + group_id=group_id, + join_time=second, + ) + ) + return data_list + if isinstance(bot, DiscordBot): + # TODO: discord获取用户 + pass + return [] + + @classmethod + async def get_user( + cls, bot: Bot, user_id: str, group_id: str | None = None + ) -> UserData | None: + """获取用户信息 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组/频道id. + + 返回: + UserData | None: 用户数据 + """ + if isinstance(bot, v11Bot): + if group_id: + if user := await bot.get_group_member_info( + group_id=int(group_id), user_id=int(user_id) + ): + return UserData( + name=user["nickname"], + card=user["card"], + user_id=user["user_id"], + group_id=user["group_id"], + role=user["role"], + join_time=user["join_time"], + ) + else: + if friend_list := await bot.get_friend_list(): + for f in friend_list: + if f["user_id"] == int(user_id): + return UserData( + name=f["nickname"], + card=f["remark"], + user_id=f["user_id"], + ) + if isinstance(bot, v12Bot): + if group_id: + if user := await bot.get_group_member_info( + group_id=group_id, user_id=user_id + ): + return UserData( + name=user["user_name"], + card=user["user_displayname"], + user_id=user["user_id"], + group_id=group_id, + ) + else: + if friend_list := await bot.get_friend_list(): + for f in friend_list: + if f["user_id"] == int(user_id): + return UserData( + name=f["user_name"], + card=f["user_remark"], + user_id=f["user_id"], + ) + if isinstance(bot, DodoBot): + if group_id: + if user := await bot.get_member_info( + island_source_id=group_id, dodo_source_id=user_id + ): + return UserData( + name=user.nick_name, + card=user.personal_nick_name, + avatar_url=user.avatar_url, + user_id=user.dodo_source_id, + group_id=user.island_source_id, + join_time=int(user.join_time.timestamp()), + ) + else: + # TODO: DoDo个人数据 + pass + if isinstance(bot, KaiheilaBot): + if group_id: + if user := await bot.user_view(guild_id=group_id, user_id=user_id): + second = None + if user.joined_at: + second = int(user.joined_at / 1000) + return UserData( + name=user.nickname or "", + avatar_url=user.avatar, + user_id=user_id, + group_id=group_id, + join_time=second, + ) + else: + # TODO: kaiheila用户详情 + pass + if isinstance(bot, DiscordBot): + # TODO: discord获取用户 + pass + return None + @classmethod async def get_user_avatar(cls, user_id: str, platform: str) -> bytes | None: """快捷获取用户头像 From c2fd8661b543a299c71bb91cd953f8db1823c55b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 18 May 2024 20:56:23 +0800 Subject: [PATCH 028/132] =?UTF-8?q?feat=E2=9C=A8:=20CSGO=E5=BC=80=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/open_cases/__init__.py | 345 +++++++++ zhenxun/plugins/open_cases/build_image.py | 155 +++++ zhenxun/plugins/open_cases/command.py | 75 ++ zhenxun/plugins/open_cases/config.py | 253 +++++++ zhenxun/plugins/open_cases/models/__init__.py | 0 .../plugins/open_cases/models/buff_prices.py | 22 + .../plugins/open_cases/models/buff_skin.py | 113 +++ .../open_cases/models/buff_skin_log.py | 50 ++ .../open_cases/models/open_cases_log.py | 44 ++ .../open_cases/models/open_cases_user.py | 60 ++ zhenxun/plugins/open_cases/open_cases_c.py | 501 +++++++++++++ zhenxun/plugins/open_cases/utils.py | 656 ++++++++++++++++++ zhenxun/utils/_build_mat.py | 470 ++++++++++++- zhenxun/utils/image_utils.py | 2 +- 14 files changed, 2743 insertions(+), 3 deletions(-) create mode 100644 zhenxun/plugins/open_cases/__init__.py create mode 100644 zhenxun/plugins/open_cases/build_image.py create mode 100644 zhenxun/plugins/open_cases/command.py create mode 100644 zhenxun/plugins/open_cases/config.py create mode 100644 zhenxun/plugins/open_cases/models/__init__.py create mode 100644 zhenxun/plugins/open_cases/models/buff_prices.py create mode 100644 zhenxun/plugins/open_cases/models/buff_skin.py create mode 100644 zhenxun/plugins/open_cases/models/buff_skin_log.py create mode 100644 zhenxun/plugins/open_cases/models/open_cases_log.py create mode 100644 zhenxun/plugins/open_cases/models/open_cases_user.py create mode 100644 zhenxun/plugins/open_cases/open_cases_c.py create mode 100644 zhenxun/plugins/open_cases/utils.py diff --git a/zhenxun/plugins/open_cases/__init__.py b/zhenxun/plugins/open_cases/__init__.py new file mode 100644 index 00000000..e4ee738a --- /dev/null +++ b/zhenxun/plugins/open_cases/__init__.py @@ -0,0 +1,345 @@ +import asyncio +import random +from datetime import datetime, timedelta +from typing import List + +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Arparma, Match +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig, Task +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import text2image + +from .command import ( + _group_open_matcher, + _knifes_matcher, + _multiple_matcher, + _my_open_matcher, + _open_matcher, + _price_matcher, + _reload_matcher, + _show_case_matcher, + _update_image_matcher, + _update_matcher, +) +from .open_cases_c import ( + auto_update, + get_my_knifes, + group_statistics, + open_case, + open_multiple_case, + total_open_statistics, +) +from .utils import ( + CASE2ID, + KNIFE2ID, + CaseManager, + build_case_image, + download_image, + get_skin_case, + init_skin_trends, + reset_count_daily, + update_skin_data, +) + +__plugin_meta__ = PluginMetadata( + name="CSGO开箱", + description="csgo模拟开箱[戒赌]", + usage=""" + 指令: + 开箱 ?[武器箱] + [1-30]连开箱 ?[武器箱] + 我的开箱 + 我的金色 + 群开箱统计 + 查看武器箱?[武器箱] + * 不包含[武器箱]时随机开箱 * + 示例: 查看武器箱 + 示例: 查看武器箱英勇 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 更新皮肤指令 + 重置开箱: 重置今日开箱所有次数 + 指令: + 更新武器箱 ?[武器箱/ALL] + 更新皮肤 ?[名称/ALL1] + 更新皮肤 ?[名称/ALL1] -S: (必定更新罕见皮肤所属箱子) + 更新武器箱图片 + * 不指定武器箱时则全部更新 * + * 过多的爬取会导致账号API被封 * + """.strip(), + menu_type="抽卡相关", + tasks=[Task(module="open_case_reset_remind", name="每日开箱重置提醒")], + limits=[PluginCdBlock(result="着什么急啊,慢慢来!")], + configs=[ + RegisterConfig( + key="INITIAL_OPEN_CASE_COUNT", + value=20, + help="初始每日开箱次数", + default_value=20, + type=int, + ), + RegisterConfig( + key="EACH_IMPRESSION_ADD_COUNT", + value=3, + help="每 * 点好感度额外增加开箱次数", + default_value=3, + type=int, + ), + RegisterConfig(key="COOKIE", value=None, help="BUFF的cookie"), + RegisterConfig( + key="DAILY_UPDATE", + value=None, + help="每日自动更新的武器箱,存在'ALL'时则更新全部武器箱", + type=List[str], + ), + RegisterConfig( + key="DEFAULT_OPEN_CASE_RESET_REMIND", + module="_task", + value=True, + help="被动 每日开箱重置提醒 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +# cases_matcher_group = MatcherGroup(priority=5, permission=GROUP, block=True) + + +# k_open_case = cases_matcher_group.on_command("开箱") +# reload_count = cases_matcher_group.on_command("重置开箱", permission=SUPERUSER) +# total_case_data = cases_matcher_group.on_command( +# "我的开箱", aliases={"开箱统计", "开箱查询", "查询开箱"} +# ) +# group_open_case_statistics = cases_matcher_group.on_command("群开箱统计") +# open_multiple = cases_matcher_group.on_regex("(.*)连开箱(.*)?") +# update_case = on_command( +# "更新武器箱", aliases={"更新皮肤"}, priority=1, permission=SUPERUSER, block=True +# ) +# update_case_image = on_command( +# "更新武器箱图片", priority=1, permission=SUPERUSER, block=True +# ) +# show_case = on_command("查看武器箱", priority=5, block=True) +# my_knifes = on_command("我的金色", priority=1, permission=GROUP, block=True) +# show_skin = on_command("查看皮肤", priority=5, block=True) +# price_trends = on_command("价格趋势", priority=5, block=True) + + +@_price_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + name: str, + skin: str, + abrasion: str, + day: Match[int], +): + name = name.replace("武器箱", "").strip() + _day = 7 + if day.available: + _day = day.result + if _day > 180: + await Text("天数必须大于0且小于180").finish() + result = await init_skin_trends(name, skin, abrasion, _day) + if not result: + await Text("未查询到数据...").finish(reply=True) + await Image(result.pic2bytes()).send() + logger.info( + f"查看 [{name}:{skin}({abrasion})] 价格趋势", + arparma.header_result, + session=session, + ) + + +@_reload_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + await reset_count_daily() + logger.info("重置开箱次数", arparma.header_result, session=session) + + +@_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + case_name = None + if name.available: + case_name = name.result.replace("武器箱", "").strip() + result = await open_case(session.id1, gid, case_name, session) + await result.finish(reply=True) + + +@_my_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + await Text( + await total_open_statistics(session.id1, gid), + ).send(reply=True) + logger.info("查询我的开箱", arparma.header_result, session=session) + + +@_group_open_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not gid: + await Text("群组id为空...").finish() + result = await group_statistics(gid) + await Text(result).send(reply=True) + logger.info("查询群开箱统计", arparma.header_result, session=session) + + +@_knifes_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + result = await get_my_knifes(session.id1, gid) + await result.send(reply=True) + logger.info("查询我的金色", arparma.header_result, session=session) + + +@_multiple_matcher.handle() +async def _(session: EventSession, arparma: Arparma, num: int, name: Match[str]): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + if num > 30: + await Text("开箱次数不要超过30啊笨蛋!").finish() + if num < 0: + await Text("再负开箱就扣你明天开箱数了!").finish() + case_name = None + if name.available: + case_name = name.result.replace("武器箱", "").strip() + result = await open_multiple_case(session.id1, gid, case_name, num, session) + await result.send(reply=True) + logger.info(f"{num}连开箱", arparma.header_result, session=session) + + +@_update_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + if not case_name: + case_list = [] + skin_list = [] + for i, case_name in enumerate(CASE2ID): + if case_name in CaseManager.CURRENT_CASES: + case_list.append(f"{i+1}.{case_name} [已更新]") + else: + case_list.append(f"{i+1}.{case_name}") + for skin_name in KNIFE2ID: + skin_list.append(f"{skin_name}") + text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list) + img = await text2image(text, padding=20, color="#f9f6f2") + await MessageFactory( + [Text("未指定武器箱, 当前已包含武器箱/皮肤\n"), Image(img.pic2bytes())] + ).finish() + if case_name in ["ALL", "ALL1"]: + if case_name == "ALL": + case_list = list(CASE2ID.keys()) + type_ = "武器箱" + else: + case_list = list(KNIFE2ID.keys()) + type_ = "罕见皮肤" + await Text(f"即将更新所有{type_}, 请稍等").send() + for i, case_name in enumerate(case_list): + try: + info = await update_skin_data(case_name, arparma.find("s")) + if "请先登录" in info: + await Text(f"未登录, 已停止更新, 请配置BUFF token...").send() + return + rand = random.randint(300, 500) + result = f"更新全部{type_}完成" + if i < len(case_list) - 1: + next_case = case_list[i + 1] + result = f"将在 {rand} 秒后更新下一{type_}: {next_case}" + await Text(f"{info}, {result}").send() + logger.info(f"info, {result}", "更新武器箱", session=session) + await asyncio.sleep(rand) + except Exception as e: + logger.error(f"更新{type_}: {case_name}", session=session, e=e) + await Text(f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}").send() + await Text(f"更新全部{type_}完成").send() + else: + await Text(f"开始{arparma.header_result}: {case_name}, 请稍等").send() + try: + await Text(await update_skin_data(case_name, arparma.find("s"))).send( + at_sender=True + ) + except Exception as e: + logger.error(f"{arparma.header_result}: {case_name}", session=session, e=e) + await Text( + f"成功{arparma.header_result}: {case_name} 发生错误: {type(e)}: {e}" + ).send() + + +@_show_case_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + result = await build_case_image(case_name) + if isinstance(result, str): + await Text(result).send() + else: + await Image(result.pic2bytes()).send() + logger.info("查看武器箱", arparma.header_result, session=session) + + +@_update_image_matcher.handle() +async def _(session: EventSession, arparma: Arparma, name: Match[str]): + case_name = None + if name.available: + case_name = name.result.strip() + await Text("开始更新图片...").send(reply=True) + await download_image(case_name) + await Text("更新图片完成...").send(at_sender=True) + logger.info("更新武器箱图片", arparma.header_result, session=session) + + +# 重置开箱 +@scheduler.scheduled_job( + "cron", + hour=0, + minute=1, +) +async def _(): + await reset_count_daily() + + +@scheduler.scheduled_job( + "cron", + hour=0, + minute=10, +) +async def _(): + now = datetime.now() + hour = random.choice([0, 1, 2, 3]) + date = now + timedelta(hours=hour) + logger.debug(f"将在 {date} 时自动更新武器箱...", "更新武器箱") + scheduler.add_job( + auto_update, + "date", + run_date=date.replace(microsecond=0), + id=f"auto_update_csgo_cases", + ) diff --git a/zhenxun/plugins/open_cases/build_image.py b/zhenxun/plugins/open_cases/build_image.py new file mode 100644 index 00000000..8b8db8e2 --- /dev/null +++ b/zhenxun/plugins/open_cases/build_image.py @@ -0,0 +1,155 @@ +from datetime import timedelta, timezone + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import cn2py + +from .config import COLOR2COLOR, COLOR2NAME +from .models.buff_skin import BuffSkin + +BASE_PATH = IMAGE_PATH / "csgo_cases" + +ICON_PATH = IMAGE_PATH / "_icon" + + +async def draw_card(skin: BuffSkin, rand: str) -> BuildImage: + """构造抽取图片 + + 参数: + skin (BuffSkin): BuffSkin + rand (str): 磨损 + + 返回: + BuildImage: BuildImage + """ + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" + if not file_path.exists(): + logger.warning(f"皮肤图片: {name} 不存在", "开箱") + skin_bk = BuildImage( + 460, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_image = BuildImage(205, 153, background=file_path) + await skin_bk.paste(skin_image, (10, 30)) + await skin_bk.line((220, 10, 220, 180)) + await skin_bk.text((10, 10), skin.name, (255, 255, 255)) + name_icon = BuildImage(20, 20, background=ICON_PATH / "name_white.png") + await skin_bk.paste(name_icon, (240, 13)) + await skin_bk.text((265, 15), f"名称:", (255, 255, 255), font_size=20) + await skin_bk.text( + (310, 15), + f"{skin.skin_name + ('(St)' if skin.is_stattrak else '')}", + (255, 255, 255), + ) + tone_icon = BuildImage(20, 20, background=ICON_PATH / "tone_white.png") + await skin_bk.paste(tone_icon, (240, 45)) + await skin_bk.text((265, 45), "品质:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 45), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color]) + type_icon = BuildImage(20, 20, background=ICON_PATH / "type_white.png") + await skin_bk.paste(type_icon, (240, 73)) + await skin_bk.text((265, 75), "类型:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 75), skin.weapon_type, (255, 255, 255)) + price_icon = BuildImage(20, 20, background=ICON_PATH / "price_white.png") + await skin_bk.paste(price_icon, (240, 103)) + await skin_bk.text((265, 105), "价格:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 105), str(skin.sell_min_price), (0, 255, 98)) + abrasion_icon = BuildImage(20, 20, background=ICON_PATH / "abrasion_white.png") + await skin_bk.paste(abrasion_icon, (240, 133)) + await skin_bk.text((265, 135), "磨损:", (255, 255, 255), font_size=20) + await skin_bk.text((310, 135), skin.abrasion, (255, 255, 255)) + await skin_bk.text((228, 165), f"({rand})", (255, 255, 255)) + return skin_bk + + +async def generate_skin(skin: BuffSkin, update_count: int) -> BuildImage | None: + """构造皮肤图片 + + 参数: + skin (BuffSkin): BuffSkin + + 返回: + BuildImage | None: 图片 + """ + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + file_path = BASE_PATH / cn2py(skin.case_name.split(",")[0]) / f"{cn2py(name)}.jpg" + if not file_path.exists(): + logger.warning(f"皮肤图片: {name} 不存在", "查看武器箱") + if skin.color == "CASE": + case_bk = BuildImage( + 700, 200, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_img = BuildImage(200, 200, background=file_path) + await case_bk.paste(skin_img, (10, 10)) + await case_bk.line((250, 10, 250, 190)) + await case_bk.line((280, 160, 660, 160)) + name_icon = BuildImage(30, 30, background=ICON_PATH / "box_white.png") + await case_bk.paste(name_icon, (260, 25)) + await case_bk.text((295, 30), "名称:", (255, 255, 255)) + await case_bk.text((345, 30), skin.case_name, (255, 0, 38), font_size=30) + + type_icon = BuildImage(30, 30, background=ICON_PATH / "type_white.png") + await case_bk.paste(type_icon, (260, 70)) + await case_bk.text((295, 75), "类型:", (255, 255, 255)) + await case_bk.text((345, 75), "武器箱", (0, 157, 255), font_size=30) + + price_icon = BuildImage(30, 30, background=ICON_PATH / "price_white.png") + await case_bk.paste(price_icon, (260, 114)) + await case_bk.text((295, 120), "单价:", (255, 255, 255)) + await case_bk.text( + (340, 120), str(skin.sell_min_price), (0, 255, 98), font_size=30 + ) + + update_count_icon = BuildImage( + 40, 40, background=ICON_PATH / "reload_white.png" + ) + await case_bk.paste(update_count_icon, (575, 10)) + await case_bk.text((625, 12), str(update_count), (255, 255, 255), font_size=45) + + num_icon = BuildImage(30, 30, background=ICON_PATH / "num_white.png") + await case_bk.paste(num_icon, (455, 70)) + await case_bk.text((490, 75), "在售:", (255, 255, 255)) + await case_bk.text((535, 75), str(skin.sell_num), (144, 0, 255), font_size=30) + + want_buy_icon = BuildImage(30, 30, background=ICON_PATH / "want_buy_white.png") + await case_bk.paste(want_buy_icon, (455, 114)) + await case_bk.text((490, 120), "求购:", (255, 255, 255)) + await case_bk.text((535, 120), str(skin.buy_num), (144, 0, 255), font_size=30) + + await case_bk.text((275, 165), "更新时间", (255, 255, 255), font_size=22) + date = str( + skin.update_time.replace(microsecond=0).astimezone( + timezone(timedelta(hours=8)) + ) + ).split("+")[0] + await case_bk.text( + (350, 165), + date, + (255, 255, 255), + font_size=30, + ) + return case_bk + else: + skin_bk = BuildImage( + 235, 250, color=(25, 25, 25, 100), font_size=25, font="CJGaoDeGuo.otf" + ) + if file_path.exists(): + skin_image = BuildImage(205, 153, background=file_path) + await skin_bk.paste(skin_image, (10, 30)) + update_count_icon = BuildImage( + 35, 35, background=ICON_PATH / "reload_white.png" + ) + await skin_bk.line((10, 180, 220, 180)) + await skin_bk.text((10, 10), skin.name, (255, 255, 255)) + await skin_bk.paste(update_count_icon, (140, 10)) + await skin_bk.text((175, 15), str(update_count), (255, 255, 255)) + await skin_bk.text((10, 185), f"{skin.skin_name}", (255, 255, 255), "width") + await skin_bk.text((10, 218), "品质:", (255, 255, 255)) + await skin_bk.text( + (55, 218), COLOR2NAME[skin.color][:2], COLOR2COLOR[skin.color] + ) + await skin_bk.text((100, 218), "类型:", (255, 255, 255)) + await skin_bk.text((145, 218), skin.weapon_type, (255, 255, 255)) + return skin_bk diff --git a/zhenxun/plugins/open_cases/command.py b/zhenxun/plugins/open_cases/command.py new file mode 100644 index 00000000..a2f85c38 --- /dev/null +++ b/zhenxun/plugins/open_cases/command.py @@ -0,0 +1,75 @@ +from nonebot.permission import SUPERUSER +from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true + +from zhenxun.utils.rules import ensure_group + +_open_matcher = on_alconna( + Alconna("开箱", Args["name?", str]), priority=5, block=True, rule=ensure_group +) + +_reload_matcher = on_alconna( + Alconna("重置开箱"), priority=5, block=True, permission=SUPERUSER, rule=ensure_group +) + +_my_open_matcher = on_alconna( + Alconna("我的开箱"), + aliases={"开箱统计", "开箱查询", "查询开箱"}, + priority=5, + block=True, + rule=ensure_group, +) + +_group_open_matcher = on_alconna( + Alconna("群开箱统计"), priority=5, block=True, rule=ensure_group +) + +_multiple_matcher = on_alconna( + Alconna("multiple-open", Args["num", int]["name?", str]), + priority=5, + block=True, + rule=ensure_group, +) + +_multiple_matcher.shortcut( + r"(?P\d)连开箱(?P.*?)", + command="multiple-open", + arguments=["{num}", "{name}"], + prefix=True, +) + +_update_matcher = on_alconna( + Alconna( + "更新武器箱", + Args["name?", str], + Option("-s", action=store_true, help_text="是否必定更新所属箱子"), + ), + aliases={"更新皮肤"}, + priority=1, + permission=SUPERUSER, + block=True, +) + +_update_image_matcher = on_alconna( + Alconna("更新武器箱图片", Args["name?", str]), + priority=1, + permission=SUPERUSER, + block=True, +) + +_show_case_matcher = on_alconna( + Alconna("查看武器箱", Args["name?", str]), priority=5, block=True +) + +_knifes_matcher = on_alconna( + Alconna("我的金色"), priority=5, block=True, rule=ensure_group +) + +_show_skin_matcher = on_alconna(Alconna("查看皮肤"), priority=5, block=True) + +_price_matcher = on_alconna( + Alconna( + "价格趋势", Args["name", str]["skin", str]["abrasion", str]["day?", int, 7] + ), + priority=5, + block=True, +) diff --git a/zhenxun/plugins/open_cases/config.py b/zhenxun/plugins/open_cases/config.py new file mode 100644 index 00000000..cefa7384 --- /dev/null +++ b/zhenxun/plugins/open_cases/config.py @@ -0,0 +1,253 @@ +import random +from enum import Enum + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger + +from .models.buff_skin import BuffSkin + +BLUE = 0.7981 +BLUE_ST = 0.0699 +PURPLE = 0.1626 +PURPLE_ST = 0.0164 +PINK = 0.0315 +PINK_ST = 0.0048 +RED = 0.0057 +RED_ST = 0.00021 +KNIFE = 0.0021 +KNIFE_ST = 0.000041 + +# 崭新 +FACTORY_NEW_S = 0 +FACTORY_NEW_E = 0.0699999 +# 略磨 +MINIMAL_WEAR_S = 0.07 +MINIMAL_WEAR_E = 0.14999 +# 久经 +FIELD_TESTED_S = 0.15 +FIELD_TESTED_E = 0.37999 +# 破损 +WELL_WORN_S = 0.38 +WELL_WORN_E = 0.44999 +# 战痕 +BATTLE_SCARED_S = 0.45 +BATTLE_SCARED_E = 0.99999 + + +class UpdateType(Enum): + """ + 更新类型 + """ + + CASE = "case" + WEAPON_TYPE = "weapon_type" + + +NAME2COLOR = { + "消费级": "WHITE", + "工业级": "LIGHTBLUE", + "军规级": "BLUE", + "受限": "PURPLE", + "保密": "PINK", + "隐秘": "RED", + "非凡": "KNIFE", +} + +COLOR2NAME = { + "WHITE": "消费级", + "LIGHTBLUE": "工业级", + "BLUE": "军规级", + "PURPLE": "受限", + "PINK": "保密", + "RED": "隐秘", + "KNIFE": "非凡", +} + +COLOR2COLOR = { + "WHITE": (255, 255, 255), + "LIGHTBLUE": (0, 179, 255), + "BLUE": (0, 85, 255), + "PURPLE": (149, 0, 255), + "PINK": (255, 0, 162), + "RED": (255, 34, 0), + "KNIFE": (255, 225, 0), +} + +ABRASION_SORT = ["崭新出厂", "略有磨损", "久经沙场", "破损不堪", "战横累累"] + +CASE_BACKGROUND = IMAGE_PATH / "csgo_cases" / "_background" / "shu" + +# 刀 +KNIFE2ID = { + "鲍伊猎刀": "weapon_knife_survival_bowie", + "蝴蝶刀": "weapon_knife_butterfly", + "弯刀": "weapon_knife_falchion", + "折叠刀": "weapon_knife_flip", + "穿肠刀": "weapon_knife_gut", + "猎杀者匕首": "weapon_knife_tactical", + "M9刺刀": "weapon_knife_m9_bayonet", + "刺刀": "weapon_bayonet", + "爪子刀": "weapon_knife_karambit", + "暗影双匕": "weapon_knife_push", + "短剑": "weapon_knife_stiletto", + "熊刀": "weapon_knife_ursus", + "折刀": "weapon_knife_gypsy_jackknife", + "锯齿爪刀": "weapon_knife_widowmaker", + "海豹短刀": "weapon_knife_css", + "系绳匕首": "weapon_knife_cord", + "求生匕首": "weapon_knife_canis", + "流浪者匕首": "weapon_knife_outdoor", + "骷髅匕首": "weapon_knife_skeleton", + "血猎手套": "weapon_bloodhound_gloves", + "驾驶手套": "weapon_driver_gloves", + "手部束带": "weapon_hand_wraps", + "摩托手套": "weapon_moto_gloves", + "专业手套": "weapon_specialist_gloves", + "运动手套": "weapon_sport_gloves", + "九头蛇手套": "weapon_hydra_gloves", + "狂牙手套": "weapon_brokenfang_gloves", +} + +WEAPON2ID = {} + +# 武器箱 +CASE2ID = { + "变革": "set_community_32", + "反冲": "set_community_31", + "梦魇": "set_community_30", + "激流": "set_community_29", + "蛇噬": "set_community_28", + "狂牙大行动": "set_community_27", + "裂空": "set_community_26", + "棱彩2号": "set_community_25", + "CS20": "set_community_24", + "裂网大行动": "set_community_23", + "棱彩": "set_community_22", + "头号特训": "set_community_21", + "地平线": "set_community_20", + "命悬一线": "set_community_19", + "光谱2号": "set_community_18", + "九头蛇大行动": "set_community_17", + "光谱": "set_community_16", + "手套": "set_community_15", + "伽玛2号": "set_gamma_2", + "伽玛": "set_community_13", + "幻彩3号": "set_community_12", + "野火大行动": "set_community_11", + "左轮": "set_community_10", + "暗影": "set_community_9", + "弯曲猎手": "set_community_8", + "幻彩2号": "set_community_7", + "幻彩": "set_community_6", + "先锋": "set_community_5", + "电竞2014夏季": "set_esports_iii", + "突围大行动": "set_community_4", + "猎杀者": "set_community_3", + "凤凰": "set_community_2", + "电竞2013冬季": "set_esports_ii", + "冬季攻势": "set_community_1", + "军火交易3号": "set_weapons_iii", + "英勇": "set_bravo_i", + "电竞2013": "set_esports", + "军火交易2号": "set_weapons_ii", + "军火交易": "set_weapons_i", +} + + +def get_wear(rand: float) -> str: + """判断磨损度 + + Args: + rand (float): 随机rand + + Returns: + str: 磨损名称 + """ + if rand <= FACTORY_NEW_E: + return "崭新出厂" + if MINIMAL_WEAR_S <= rand <= MINIMAL_WEAR_E: + return "略有磨损" + if FIELD_TESTED_S <= rand <= FIELD_TESTED_E: + return "久经沙场" + if WELL_WORN_S <= rand <= WELL_WORN_E: + return "破损不堪" + return "战痕累累" + + +def random_color_and_st(rand: float) -> tuple[str, bool]: + """获取皮肤品质及是否暗金 + + 参数: + rand (float): 随机rand + + 返回: + tuple[str, bool]: 品质,是否暗金 + """ + if rand <= KNIFE: + if random.random() <= KNIFE_ST: + return ("KNIFE", True) + return ("KNIFE", False) + elif KNIFE < rand <= RED: + if random.random() <= RED_ST: + return ("RED", True) + return ("RED", False) + elif RED < rand <= PINK: + if random.random() <= PINK_ST: + return ("PINK", True) + return ("PINK", False) + elif PINK < rand <= PURPLE: + if random.random() <= PURPLE_ST: + return ("PURPLE", True) + return ("PURPLE", False) + else: + if random.random() <= BLUE_ST: + return ("BLUE", True) + return ("BLUE", False) + + +async def random_skin(num: int, case_name: str) -> list[tuple[BuffSkin, float]]: + """ + 随机抽取皮肤 + """ + case_name = case_name.replace("武器箱", "").replace(" ", "") + color_map = {} + for _ in range(num): + rand = random.random() + # 尝试降低磨损 + if rand > MINIMAL_WEAR_E: + for _ in range(2): + if random.random() < 0.5: + logger.debug(f"[START]开箱随机磨损触发降低磨损条件: {rand}") + if random.random() < 0.2: + rand /= 3 + else: + rand /= 2 + logger.debug(f"[END]开箱随机磨损触发降低磨损条件: {rand}") + break + abrasion = get_wear(rand) + logger.debug(f"开箱随机磨损: {rand} | {abrasion}") + color, is_stattrak = random_color_and_st(rand) + if not color_map.get(color): + color_map[color] = {} + if is_stattrak: + if not color_map[color].get(f"{abrasion}_st"): + color_map[color][f"{abrasion}_st"] = [] + color_map[color][f"{abrasion}_st"].append(rand) + else: + if not color_map[color].get(abrasion): + color_map[color][f"{abrasion}"] = [] + color_map[color][f"{abrasion}"].append(rand) + skin_list = [] + for color in color_map: + for abrasion in color_map[color]: + rand_list = color_map[color][abrasion] + is_stattrak = "_st" in abrasion + abrasion = abrasion.replace("_st", "") + skin_list_ = await BuffSkin.random_skin( + len(rand_list), color, abrasion, is_stattrak, case_name + ) + skin_list += [(skin, rand) for skin, rand in zip(skin_list_, rand_list)] + return skin_list + + +# M249(StatTrak™) | 等高线 diff --git a/zhenxun/plugins/open_cases/models/__init__.py b/zhenxun/plugins/open_cases/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/zhenxun/plugins/open_cases/models/buff_prices.py b/zhenxun/plugins/open_cases/models/buff_prices.py new file mode 100644 index 00000000..9f53de0e --- /dev/null +++ b/zhenxun/plugins/open_cases/models/buff_prices.py @@ -0,0 +1,22 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + +# 1.狂牙武器箱 + + +class BuffPrice(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + case_id = fields.IntField() + """箱子id""" + skin_name = fields.CharField(255, unique=True) + """皮肤名称""" + skin_price = fields.FloatField() + """皮肤价格""" + update_date = fields.DatetimeField() + + class Meta: + table = "buff_prices" + table_description = "Buff价格数据表" diff --git a/zhenxun/plugins/open_cases/models/buff_skin.py b/zhenxun/plugins/open_cases/models/buff_skin.py new file mode 100644 index 00000000..7f51221a --- /dev/null +++ b/zhenxun/plugins/open_cases/models/buff_skin.py @@ -0,0 +1,113 @@ +from tortoise import fields +from tortoise.contrib.postgres.functions import Random + +from zhenxun.services.db_context import Model + + +class BuffSkin(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + case_name: str = fields.CharField(255) # type: ignore + """箱子名称""" + name: str = fields.CharField(255) # type: ignore + """武器/手套/刀名称""" + skin_name: str = fields.CharField(255) # type: ignore + """皮肤名称""" + is_stattrak = fields.BooleanField(default=False) + """是否暗金(计数)""" + abrasion = fields.CharField(255) + """磨损度""" + color = fields.CharField(255) + """颜色(品质)""" + skin_id = fields.CharField(255, null=True, unique=True) + """皮肤id""" + + img_url = fields.CharField(255) + """图片url""" + steam_price = fields.FloatField(default=0) + """steam价格""" + weapon_type = fields.CharField(255) + """枪械类型""" + buy_max_price = fields.FloatField(default=0) + """最大求购价格""" + buy_num = fields.IntField(default=0) + """求购数量""" + sell_min_price = fields.FloatField(default=0) + """售卖最低价格""" + sell_num = fields.IntField(default=0) + """出售个数""" + sell_reference_price = fields.FloatField(default=0) + """参考价格""" + + create_time = fields.DatetimeField(auto_add_now=True) + """创建日期""" + update_time = fields.DatetimeField(auto_add=True) + """更新日期""" + + class Meta: + table = "buff_skin" + table_description = "Buff皮肤数据表" + # unique_together = ("case_name", "name", "skin_name", "abrasion", "is_stattrak") + + def __eq__(self, other: "BuffSkin"): + + return self.skin_id == other.skin_id + + def __hash__(self): + + return hash(self.case_name + self.name + self.skin_name + str(self.is_stattrak)) + + @classmethod + async def random_skin( + cls, + num: int, + color: str, + abrasion: str, + is_stattrak: bool = False, + case_name: str | None = None, + ) -> list["BuffSkin"]: # type: ignore + """随机皮肤 + + 参数: + num: 数量 + color: 品质 + abrasion: 磨损度 + is_stattrak: 是否暗金 + case_name: 箱子名称 + + 返回: + list["BuffSkin"]: 皮肤列表 + """ + query = cls + if case_name: + query = query.filter(case_name__contains=case_name) + query = query.filter(abrasion=abrasion, is_stattrak=is_stattrak, color=color) + skin_list = await query.annotate(rand=Random()).limit(num) # type:ignore + num_ = num + cnt = 0 + while len(skin_list) < num: + cnt += 1 + num_ = num - len(skin_list) + skin_list += await query.annotate(rand=Random()).limit(num_) + if cnt > 10: + break + return skin_list # type: ignore + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE buff_skin ADD img_url varchar(255);", # 新增img_url + "ALTER TABLE buff_skin ADD skin_id varchar(255);", # 新增skin_id + "ALTER TABLE buff_skin ADD steam_price float DEFAULT 0;", # 新增steam_price + "ALTER TABLE buff_skin ADD weapon_type varchar(255);", # 新增type + "ALTER TABLE buff_skin ADD buy_max_price float DEFAULT 0;", # 新增buy_max_price + "ALTER TABLE buff_skin ADD buy_num Integer DEFAULT 0;", # 新增buy_max_price + "ALTER TABLE buff_skin ADD sell_min_price float DEFAULT 0;", # 新增sell_min_price + "ALTER TABLE buff_skin ADD sell_num Integer DEFAULT 0;", # 新增sell_num + "ALTER TABLE buff_skin ADD sell_reference_price float DEFAULT 0;", # 新增sell_reference_price + "ALTER TABLE buff_skin DROP COLUMN skin_price;", # 删除skin_price + "alter table buff_skin drop constraint if EXISTS uid_buff_skin_case_na_c35c93;", # 删除唯一约束 + "UPDATE buff_skin set case_name='手套' where case_name='手套武器箱'", + "UPDATE buff_skin set case_name='左轮' where case_name='左轮武器箱'", + ] diff --git a/zhenxun/plugins/open_cases/models/buff_skin_log.py b/zhenxun/plugins/open_cases/models/buff_skin_log.py new file mode 100644 index 00000000..ac9fec95 --- /dev/null +++ b/zhenxun/plugins/open_cases/models/buff_skin_log.py @@ -0,0 +1,50 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class BuffSkinLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + case_name = fields.CharField(255) + """箱子名称""" + name = fields.CharField(255) + """武器/手套/刀名称""" + skin_name = fields.CharField(255) + """皮肤名称""" + is_stattrak = fields.BooleanField(default=False) + """是否暗金(计数)""" + abrasion = fields.CharField(255) + """磨损度""" + color = fields.CharField(255) + """颜色(品质)""" + + steam_price = fields.FloatField(default=0) + """steam价格""" + weapon_type = fields.CharField(255) + """枪械类型""" + buy_max_price = fields.FloatField(default=0) + """最大求购价格""" + buy_num = fields.IntField(default=0) + """求购数量""" + sell_min_price = fields.FloatField(default=0) + """售卖最低价格""" + sell_num = fields.IntField(default=0) + """出售个数""" + sell_reference_price = fields.FloatField(default=0) + """参考价格""" + + create_time = fields.DatetimeField(auto_add_now=True) + """创建日期""" + + class Meta: + table = "buff_skin_log" + table_description = "Buff皮肤更新日志表" + + @classmethod + async def _run_script(cls): + return [ + "UPDATE buff_skin_log set case_name='手套' where case_name='手套武器箱'", + "UPDATE buff_skin_log set case_name='左轮' where case_name='左轮武器箱'", + ] diff --git a/zhenxun/plugins/open_cases/models/open_cases_log.py b/zhenxun/plugins/open_cases/models/open_cases_log.py new file mode 100644 index 00000000..0c4f87bb --- /dev/null +++ b/zhenxun/plugins/open_cases/models/open_cases_log.py @@ -0,0 +1,44 @@ +from tortoise import fields +from tortoise.contrib.postgres.functions import Random + +from zhenxun.services.db_context import Model + + +class OpenCasesLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + case_name = fields.CharField(255) + """箱子名称""" + name = fields.CharField(255) + """武器/手套/刀名称""" + skin_name = fields.CharField(255) + """皮肤名称""" + is_stattrak = fields.BooleanField(default=False) + """是否暗金(计数)""" + abrasion = fields.CharField(255) + """磨损度""" + abrasion_value = fields.FloatField() + """磨损数值""" + color = fields.CharField(255) + """颜色(品质)""" + price = fields.FloatField(default=0) + """价格""" + create_time = fields.DatetimeField(auto_add_now=True) + """创建日期""" + + class Meta: + table = "open_cases_log" + table_description = "开箱日志表" + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE open_cases_log RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE open_cases_log ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE open_cases_log ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/plugins/open_cases/models/open_cases_user.py b/zhenxun/plugins/open_cases/models/open_cases_user.py new file mode 100644 index 00000000..3ed43937 --- /dev/null +++ b/zhenxun/plugins/open_cases/models/open_cases_user.py @@ -0,0 +1,60 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class OpenCasesUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + total_count = fields.IntField(default=0) + """总开启次数""" + blue_count = fields.IntField(default=0) + """蓝色""" + blue_st_count = fields.IntField(default=0) + """蓝色暗金""" + purple_count = fields.IntField(default=0) + """紫色""" + purple_st_count = fields.IntField(default=0) + """紫色暗金""" + pink_count = fields.IntField(default=0) + """粉色""" + pink_st_count = fields.IntField(default=0) + """粉色暗金""" + red_count = fields.IntField(default=0) + """紫色""" + red_st_count = fields.IntField(default=0) + """紫色暗金""" + knife_count = fields.IntField(default=0) + """金色""" + knife_st_count = fields.IntField(default=0) + """金色暗金""" + spend_money = fields.IntField(default=0) + """花费金币""" + make_money = fields.FloatField(default=0) + """赚取金币""" + today_open_total = fields.IntField(default=0) + """今日开箱数量""" + open_cases_time_last = fields.DatetimeField() + """最后开箱日期""" + knifes_name = fields.TextField(default="") + """已获取金色""" + + class Meta: + table = "open_cases_users" + table_description = "开箱统计数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def _run_script(cls): + return [ + "alter table open_cases_users alter COLUMN make_money type float;", # 将make_money字段改为float + "alter table open_cases_users alter COLUMN spend_money type float;", # 将spend_money字段改为float + "ALTER TABLE open_cases_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE open_cases_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE open_cases_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/plugins/open_cases/open_cases_c.py b/zhenxun/plugins/open_cases/open_cases_c.py new file mode 100644 index 00000000..8cdd5b32 --- /dev/null +++ b/zhenxun/plugins/open_cases/open_cases_c.py @@ -0,0 +1,501 @@ +import asyncio +import random +import re +from datetime import datetime + +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.models.sign_user import SignUser +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import cn2py + +from .build_image import draw_card +from .config import * +from .models.open_cases_log import OpenCasesLog +from .models.open_cases_user import OpenCasesUser +from .utils import CaseManager, update_skin_data + +RESULT_MESSAGE = { + "BLUE": ["这样看着才舒服", "是自己人,大伙把刀收好", "非常舒适~"], + "PURPLE": ["还行吧,勉强接受一下下", "居然不是蓝色,太假了", "运气-1-1-1-1-1..."], + "PINK": ["开始不适....", "你妈妈买菜必涨价!涨三倍!", "你最近不适合出门,真的"], + "RED": [ + "已经非常不适", + "好兄弟你开的什么箱子啊,一般箱子不是只有蓝色的吗", + "开始拿阳寿开箱子了?", + ], + "KNIFE": [ + "你的好运我收到了,你可以去喂鲨鱼了", + "最近该吃啥就迟点啥吧,哎,好好的一个人怎么就....哎", + "众所周知,欧皇寿命极短.", + ], +} + +COLOR2NAME = { + "BLUE": "军规", + "PURPLE": "受限", + "PINK": "保密", + "RED": "隐秘", + "KNIFE": "罕见", +} + +COLOR2CN = {"BLUE": "蓝", "PURPLE": "紫", "PINK": "粉", "RED": "红", "KNIFE": "金"} + + +def add_count(user: OpenCasesUser, skin: BuffSkin, case_price: float): + if skin.color == "BLUE": + if skin.is_stattrak: + user.blue_st_count += 1 + else: + user.blue_count += 1 + elif skin.color == "PURPLE": + if skin.is_stattrak: + user.purple_st_count += 1 + else: + user.purple_count += 1 + elif skin.color == "PINK": + if skin.is_stattrak: + user.pink_st_count += 1 + else: + user.pink_count += 1 + elif skin.color == "RED": + if skin.is_stattrak: + user.red_st_count += 1 + else: + user.red_count += 1 + elif skin.color == "KNIFE": + if skin.is_stattrak: + user.knife_st_count += 1 + else: + user.knife_count += 1 + user.make_money += skin.sell_min_price + user.spend_money += int(17 + case_price) + + +async def get_user_max_count(user_id: str) -> int: + """获取用户每日最大开箱次数 + + 参数: + user_id: 用户id + + 返回: + int: 最大开箱次数 + """ + user, _ = await SignUser.get_or_create(user_id=user_id) + impression = int(user.impression) + initial_open_case_count = Config.get_config("open_cases", "INITIAL_OPEN_CASE_COUNT") + each_impression_add_count = Config.get_config( + "open_cases", "EACH_IMPRESSION_ADD_COUNT" + ) + return int(initial_open_case_count + impression / each_impression_add_count) # type: ignore + + +async def open_case( + user_id: str, group_id: str, case_name: str | None, session: EventSession +) -> MessageFactory: + """开箱 + + 参数: + user_id: 用户id + group_id : 群号 + case_name: 武器箱名称. Defaults to "狂牙大行动". + session: EventSession + + 返回: + Union[str, Message]: 回复消息 + """ + user_id = str(user_id) + group_id = str(group_id) + if not CaseManager.CURRENT_CASES: + return MessageFactory([Text("未收录任何武器箱")]) + if not case_name: + case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore + if case_name not in CaseManager.CURRENT_CASES: + return "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) # type: ignore + logger.debug( + f"尝试开启武器箱: {case_name}", "开箱", session=user_id, group_id=group_id + ) + case = cn2py(case_name) # type: ignore + user = await OpenCasesUser.get_or_none(user_id=user_id, group_id=group_id) + if not user: + user = await OpenCasesUser.create( + user_id=user_id, group_id=group_id, open_cases_time_last=datetime.now() + ) + max_count = await get_user_max_count(user_id) + # 一天次数上限 + if user.today_open_total >= max_count: + return MessageFactory( + [ + Text( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" + ) + ] + ) + skin_list = await random_skin(1, case_name) # type: ignore + if not skin_list: + return MessageFactory(Text("未抽取到任何皮肤")) + skin, rand = skin_list[0] + rand = str(rand)[:11] + case_price = 0 + if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): + case_price = case_skin.sell_min_price + user.today_open_total += 1 + user.total_count += 1 + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) + add_count(user, skin, case_price) + ridicule_result = random.choice(RESULT_MESSAGE[skin.color]) + price_result = skin.sell_min_price + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + img_path = IMAGE_PATH / "csgo_cases" / case / f"{cn2py(name)}.jpg" + logger.info( + f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand}] 价格: {skin.sell_min_price}", + "开箱", + session=session, + ) + await user.save() + await OpenCasesLog.create( + user_id=user_id, + group_id=group_id, + case_name=case_name, + name=skin.name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + price=skin.sell_min_price, + abrasion_value=rand, + create_time=datetime.now(), + ) + logger.debug(f"添加 1 条开箱日志", "开箱", session=session) + over_count = max_count - user.today_open_total + img = await draw_card(skin, rand) + return MessageFactory( + [ + Text(f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n"), + Image(img.pic2bytes()), + Text( + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}" + ), + ] + ) + + +async def open_multiple_case( + user_id: str, + group_id: str, + case_name: str | None, + num: int = 10, + session: EventSession | None = None, +) -> MessageFactory: + """多连开箱 + + 参数: + user_id (int): 用户id + group_id (int): 群号 + case_name (str): 箱子名称 + num (int, optional): 数量. Defaults to 10. + session: EventSession + + 返回: + _type_: _description_ + """ + user_id = str(user_id) + group_id = str(group_id) + if not CaseManager.CURRENT_CASES: + return MessageFactory([Text("未收录任何武器箱")]) + if not case_name: + case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore + if case_name not in CaseManager.CURRENT_CASES: + return MessageFactory( + [ + Text( + "武器箱未收录, 当前可用武器箱:\n" + + ", ".join(CaseManager.CURRENT_CASES) + ) + ] + ) + user, _ = await OpenCasesUser.get_or_create( + user_id=user_id, + group_id=group_id, + defaults={"open_cases_time_last": datetime.now()}, + ) + max_count = await get_user_max_count(user_id) + if user.today_open_total >= max_count: + return MessageFactory( + [ + Text( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" + ) + ] + ) + if max_count - user.today_open_total < num: + return MessageFactory( + [ + Text( + f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" + f"\n剩余开箱次数:{max_count - user.today_open_total}" + ) + ] + ) + logger.debug(f"尝试开启武器箱: {case_name}", "开箱", session=session) + case = cn2py(case_name) # type: ignore + skin_count = {} + img_list = [] + skin_list = await random_skin(num, case_name) # type: ignore + if not skin_list: + return MessageFactory([Text("未抽取到任何皮肤...")]) + total_price = 0 + log_list = [] + now = datetime.now() + user.today_open_total += num + user.total_count += num + user.open_cases_time_last = datetime.now() + await user.save( + update_fields=["today_open_total", "total_count", "open_cases_time_last"] + ) + case_price = 0 + if case_skin := await BuffSkin.get_or_none(case_name=case_name, color="CASE"): + case_price = case_skin.sell_min_price + img_w, img_h = 0, 0 + for skin, rand in skin_list: + img = await draw_card(skin, str(rand)[:11]) + img_w, img_h = img.size + total_price += skin.sell_min_price + color_name = COLOR2CN[skin.color] + if not skin_count.get(color_name): + skin_count[color_name] = 0 + skin_count[color_name] += 1 + add_count(user, skin, case_price) + img_list.append(img) + logger.info( + f"开启{case_name}武器箱获得 {skin.name}{'(StatTrak™)' if skin.is_stattrak else ''} | {skin.skin_name} ({skin.abrasion}) 磨损: [{rand:.11f}] 价格: {skin.sell_min_price}", + "开箱", + session=session, + ) + log_list.append( + OpenCasesLog( + user_id=user_id, + group_id=group_id, + case_name=case_name, + name=skin.name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + price=skin.sell_min_price, + abrasion_value=rand, + create_time=now, + ) + ) + await user.save() + if log_list: + await OpenCasesLog.bulk_create(log_list, 10) + logger.debug(f"添加 {len(log_list)} 条开箱日志", "开箱", session=session) + img_w += 10 + img_h += 10 + w = img_w * 5 + if num < 5: + h = img_h - 10 + w = img_w * num + elif not num % 5: + h = img_h * int(num / 5) + else: + h = img_h * int(num / 5) + img_h + mark_image = BuildImage(w - 10, h - 10, color=(255, 255, 255)) + mark_image = await mark_image.auto_paste(img_list, 5, padding=20) + over_count = max_count - user.today_open_total + result = "" + for color_name in skin_count: + result += f"[{color_name}:{skin_count[color_name]}] " + return MessageFactory( + [ + Text(f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n"), + Image(mark_image.pic2bytes()), + Text( + f"\nresult[:-1]\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" + ), + ] + ) + + +async def total_open_statistics(user_id: str, group_id: str) -> str: + user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) + return ( + f"开箱总数:{user.total_count}\n" + f"今日开箱:{user.today_open_total}\n" + f"蓝色军规:{user.blue_count}\n" + f"蓝色暗金:{user.blue_st_count}\n" + f"紫色受限:{user.purple_count}\n" + f"紫色暗金:{user.purple_st_count}\n" + f"粉色保密:{user.pink_count}\n" + f"粉色暗金:{user.pink_st_count}\n" + f"红色隐秘:{user.red_count}\n" + f"红色暗金:{user.red_st_count}\n" + f"金色罕见:{user.knife_count}\n" + f"金色暗金:{user.knife_st_count}\n" + f"花费金额:{user.spend_money}\n" + f"获取金额:{user.make_money:.2f}\n" + f"最后开箱日期:{user.open_cases_time_last.date()}" + ) + + +async def group_statistics(group_id: str): + user_list = await OpenCasesUser.filter(group_id=str(group_id)).all() + # lan zi fen hong jin pricei + uplist = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0] + for user in user_list: + uplist[0] += user.blue_count + uplist[1] += user.blue_st_count + uplist[2] += user.purple_count + uplist[3] += user.purple_st_count + uplist[4] += user.pink_count + uplist[5] += user.pink_st_count + uplist[6] += user.red_count + uplist[7] += user.red_st_count + uplist[8] += user.knife_count + uplist[9] += user.knife_st_count + uplist[10] += user.make_money + uplist[11] += user.total_count + uplist[12] += user.today_open_total + return ( + f"群开箱总数:{uplist[11]}\n" + f"群今日开箱:{uplist[12]}\n" + f"蓝色军规:{uplist[0]}\n" + f"蓝色暗金:{uplist[1]}\n" + f"紫色受限:{uplist[2]}\n" + f"紫色暗金:{uplist[3]}\n" + f"粉色保密:{uplist[4]}\n" + f"粉色暗金:{uplist[5]}\n" + f"红色隐秘:{uplist[6]}\n" + f"红色暗金:{uplist[7]}\n" + f"金色罕见:{uplist[8]}\n" + f"金色暗金:{uplist[9]}\n" + f"花费金额:{uplist[11] * 17}\n" + f"获取金额:{uplist[10]:.2f}" + ) + + +async def get_my_knifes(user_id: str, group_id: str) -> MessageFactory: + """获取我的金色 + + 参数: + user_id (str): 用户id + group_id (str): 群号 + + 返回: + MessageFactory: 回复消息或图片 + """ + data_list = await get_old_knife(str(user_id), str(group_id)) + data_list += await OpenCasesLog.filter( + user_id=user_id, group_id=group_id, color="KNIFE" + ).all() + if not data_list: + return MessageFactory([Text("您木有开出金色级别的皮肤喔...")]) + length = len(data_list) + if length < 5: + h = 600 + w = length * 540 + elif length % 5 == 0: + h = 600 * int(length / 5) + w = 540 * 5 + else: + h = 600 * int(length / 5) + 600 + w = 540 * 5 + A = BuildImage(w, h) + image_list = [] + for skin in data_list: + name = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + img_path = ( + IMAGE_PATH / "csgo_cases" / cn2py(skin.case_name) / f"{cn2py(name)}.jpg" + ) + knife_img = BuildImage(470, 600, font_size=20) + await knife_img.paste( + BuildImage(470, 470, background=img_path if img_path.exists() else None), + (0, 0), + ) + await knife_img.text( + (5, 500), f"\t{skin.name}|{skin.skin_name}({skin.abrasion})" + ) + await knife_img.text((5, 530), f"\t磨损:{skin.abrasion_value}") + await knife_img.text((5, 560), f"\t价格:{skin.price}") + image_list.append(knife_img) + A = await A.auto_paste(image_list, 5) + return MessageFactory([Image(A.pic2bytes())]) + + +async def get_old_knife(user_id: str, group_id: str) -> list[OpenCasesLog]: + """获取旧数据字段 + + 参数: + user_id (str): 用户id + group_id (str): 群号 + + 返回: + list[OpenCasesLog]: 旧数据兼容 + """ + user, _ = await OpenCasesUser.get_or_create(user_id=user_id, group_id=group_id) + knifes_name = user.knifes_name + data_list = [] + if knifes_name: + knifes_list = knifes_name[:-1].split(",") + for knife in knifes_list: + try: + if r := re.search( + "(.*)\|\|(.*) \| (.*)\((.*)\) 磨损:(.*), 价格:(.*)", knife + ): + case_name_py = r.group(1) + name = r.group(2) + skin_name = r.group(3) + abrasion = r.group(4) + abrasion_value = r.group(5) + price = r.group(6) + name = name.replace("(StatTrak™)", "") + data_list.append( + OpenCasesLog( + user_id=user_id, + group_id=group_id, + name=name.strip(), + case_name=case_name_py.strip(), + skin_name=skin_name.strip(), + abrasion=abrasion.strip(), + abrasion_value=abrasion_value, + price=price, + ) + ) + except Exception as e: + logger.error( + f"获取兼容旧数据错误: {knife}", + "我的金色", + session=user_id, + group_id=group_id, + e=e, + ) + return data_list + + +async def auto_update(): + """自动更新武器箱""" + if case_list := Config.get_config("open_cases", "DAILY_UPDATE"): + logger.debug("尝试自动更新武器箱", "更新武器箱") + if "ALL" in case_list: + case_list = CASE2ID.keys() + logger.debug(f"预计自动更新武器箱 {len(case_list)} 个", "更新武器箱") + for case_name in case_list: + logger.debug(f"开始自动更新武器箱: {case_name}", "更新武器箱") + try: + await update_skin_data(case_name) + rand = random.randint(300, 500) + logger.info( + f"成功自动更新武器箱: {case_name}, 将在 {rand} 秒后再次更新下一武器箱", + "更新武器箱", + ) + await asyncio.sleep(rand) + except Exception as e: + logger.error(f"自动更新武器箱: {case_name}", e=e) diff --git a/zhenxun/plugins/open_cases/utils.py b/zhenxun/plugins/open_cases/utils.py new file mode 100644 index 00000000..212ef69e --- /dev/null +++ b/zhenxun/plugins/open_cases/utils.py @@ -0,0 +1,656 @@ +import asyncio +import os +import random +import re +import time +from datetime import datetime, timedelta + +import nonebot +from tortoise.functions import Count + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType + +from .build_image import generate_skin +from .config import ( + CASE2ID, + CASE_BACKGROUND, + COLOR2NAME, + KNIFE2ID, + NAME2COLOR, + UpdateType, +) +from .models.buff_skin import BuffSkin +from .models.buff_skin_log import BuffSkinLog +from .models.open_cases_user import OpenCasesUser + +# from zhenxun.utils.utils import broadcast_group, cn2py + + +URL = "https://buff.163.com/api/market/goods" + +SELL_URL = "https://buff.163.com/goods" + + +driver = nonebot.get_driver() + +BASE_PATH = IMAGE_PATH / "csgo_cases" + + +class CaseManager: + + CURRENT_CASES = [] + + @classmethod + async def reload(cls): + cls.CURRENT_CASES = [] + case_list = await BuffSkin.filter(color="CASE").values_list( + "case_name", flat=True + ) + for case_name in ( + await BuffSkin.filter(case_name__not="未知武器箱") + .annotate() + .distinct() + .values_list("case_name", flat=True) + ): + for name in case_name.split(","): # type: ignore + if name not in cls.CURRENT_CASES and name in case_list: + cls.CURRENT_CASES.append(name) + + +async def update_skin_data(name: str, is_update_case_name: bool = False) -> str: + """更新箱子内皮肤数据 + + 参数: + name (str): 箱子名称 + is_update_case_name (bool): 是否必定更新所属箱子 + + 返回: + str: 回复内容 + """ + type_ = None + if name in CASE2ID: + type_ = UpdateType.CASE + if name in KNIFE2ID: + type_ = UpdateType.WEAPON_TYPE + if not type_: + return "未在指定武器箱或指定武器类型内" + session = Config.get_config("open_cases", "COOKIE") + if not session: + return "BUFF COOKIE为空捏!" + weapon2case = {} + if type_ == UpdateType.WEAPON_TYPE: + db_data = await BuffSkin.filter(name__contains=name).all() + weapon2case = { + item.name + item.skin_name: item.case_name + for item in db_data + if item.case_name != "未知武器箱" + } + data_list, total = await search_skin_page(name, 1, type_) + if isinstance(data_list, str): + return data_list + for page in range(2, total + 1): + rand_time = random.randint(20, 50) + logger.debug(f"访问随机等待时间: {rand_time}", "开箱更新") + await asyncio.sleep(rand_time) + data_list_, total = await search_skin_page(name, page, type_) + if isinstance(data_list_, list): + data_list += data_list_ + create_list: list[BuffSkin] = [] + update_list: list[BuffSkin] = [] + log_list = [] + now = datetime.now() + exists_id_list = [] + new_weapon2case = {} + for skin in data_list: + if skin.skin_id in exists_id_list: + continue + if skin.case_name: + skin.case_name = ( + skin.case_name.replace("”", "") + .replace("“", "") + .replace("武器箱", "") + .replace(" ", "") + ) + skin.name = skin.name.replace("(★ StatTrak™)", "").replace("(★)", "") + exists_id_list.append(skin.skin_id) + key = skin.name + skin.skin_name + name_ = skin.name + skin.skin_name + skin.abrasion + skin.create_time = now + skin.update_time = now + if UpdateType.WEAPON_TYPE and not skin.case_name: + if is_update_case_name: + case_name = new_weapon2case.get(key) + else: + case_name = weapon2case.get(key) + if not case_name: + if case_list := await get_skin_case(skin.skin_id): + case_name = ",".join(case_list) + rand = random.randint(10, 20) + logger.debug( + f"获取 {skin.name} | {skin.skin_name} 皮肤所属武器箱: {case_name}, 访问随机等待时间: {rand}", + "开箱更新", + ) + await asyncio.sleep(rand) + if not case_name: + case_name = "未知武器箱" + else: + weapon2case[key] = case_name + new_weapon2case[key] = case_name + if skin.case_name == "反恐精英20周年": + skin.case_name = "CS20" + skin.case_name = case_name + if await BuffSkin.exists(skin_id=skin.skin_id): + update_list.append(skin) + else: + create_list.append(skin) + log_list.append( + BuffSkinLog( + name=skin.name, + case_name=skin.case_name, + skin_name=skin.skin_name, + is_stattrak=skin.is_stattrak, + abrasion=skin.abrasion, + color=skin.color, + steam_price=skin.steam_price, + weapon_type=skin.weapon_type, + buy_max_price=skin.buy_max_price, + buy_num=skin.buy_num, + sell_min_price=skin.sell_min_price, + sell_num=skin.sell_num, + sell_reference_price=skin.sell_reference_price, + create_time=now, + ) + ) + name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + for c_name_ in skin.case_name.split(","): + file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" + if not file_path.exists(): + logger.debug(f"下载皮肤 {name} 图片: {skin.img_url}...", "开箱更新") + await AsyncHttpx.download_file(skin.img_url, file_path) + rand_time = random.randint(1, 10) + await asyncio.sleep(rand_time) + logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱更新") + else: + logger.debug(f"皮肤 {name_} 图片已存在...", "开箱更新") + if create_list: + logger.debug( + f"更新武器箱/皮肤: [{name}], 创建 {len(create_list)} 个皮肤!" + ) + await BuffSkin.bulk_create(set(create_list), 10) + if update_list: + abrasion_list = [] + name_list = [] + skin_name_list = [] + for skin in update_list: + if skin.abrasion not in abrasion_list: + abrasion_list.append(skin.abrasion) + if skin.name not in name_list: + name_list.append(skin.name) + if skin.skin_name not in skin_name_list: + skin_name_list.append(skin.skin_name) + db_data = await BuffSkin.filter( + case_name__contains=name, + skin_name__in=skin_name_list, + name__in=name_list, + abrasion__in=abrasion_list, + ).all() + _update_list = [] + for data in db_data: + for skin in update_list: + if ( + data.name == skin.name + and data.skin_name == skin.skin_name + and data.abrasion == skin.abrasion + ): + data.steam_price = skin.steam_price + data.buy_max_price = skin.buy_max_price + data.buy_num = skin.buy_num + data.sell_min_price = skin.sell_min_price + data.sell_num = skin.sell_num + data.sell_reference_price = skin.sell_reference_price + data.update_time = skin.update_time + _update_list.append(data) + logger.debug( + f"更新武器箱/皮肤: [{name}], 更新 {len(create_list)} 个皮肤!" + ) + await BuffSkin.bulk_update( + _update_list, + [ + "steam_price", + "buy_max_price", + "buy_num", + "sell_min_price", + "sell_num", + "sell_reference_price", + "update_time", + ], + 10, + ) + if log_list: + logger.debug( + f"更新武器箱/皮肤: [{name}], 新增 {len(log_list)} 条皮肤日志!" + ) + await BuffSkinLog.bulk_create(log_list) + if name not in CaseManager.CURRENT_CASES: + CaseManager.CURRENT_CASES.append(name) # type: ignore + return f"更新武器箱/皮肤: [{name}] 成功, 共更新 {len(update_list)} 个皮肤, 新创建 {len(create_list)} 个皮肤!" + + +async def search_skin_page( + s_name: str, page_index: int, type_: UpdateType +) -> tuple[list[BuffSkin] | str, int]: + """查询箱子皮肤 + + 参数: + s_name (str): 箱子/皮肤名称 + page_index (int): 页数 + + 返回: + tuple[list[BuffSkin] | str, int]: BuffSkin + """ + logger.debug( + f"尝试访问武器箱/皮肤: [{s_name}] 页数: [{page_index}]", + "开箱更新", + ) + cookie = {"session": Config.get_config("open_cases", "COOKIE")} + params = { + "game": "csgo", + "page_num": page_index, + "page_size": 80, + "_": time.time(), + "use_suggestio": 0, + } + if type_ == UpdateType.CASE: + params["itemset"] = CASE2ID[s_name] + elif type_ == UpdateType.WEAPON_TYPE: + params["category"] = KNIFE2ID[s_name] + proxy = None + if ip := Config.get_config("open_cases", "BUFF_PROXY"): + proxy = {"http://": ip, "https://": ip} + response = None + error = "" + for i in range(3): + try: + response = await AsyncHttpx.get( + URL, + proxy=proxy, + params=params, + cookies=cookie, # type: ignore + ) + if response.status_code == 200: + break + rand = random.randint(3, 7) + logger.debug( + f"尝试访问武器箱/皮肤第 {i+1} 次访问异常, code: {response.status_code}", + "开箱更新", + ) + await asyncio.sleep(rand) + except Exception as e: + logger.debug( + f"尝试访问武器箱/皮肤第 {i+1} 次访问发生错误 {type(e)}: {e}", "开箱更新" + ) + error = f"{type(e)}: {e}" + if not response: + return f"访问发生异常: {error}", -1 + if response.status_code == 200: + # logger.debug(f"访问BUFF API: {response.text}", "开箱更新") + json_data = response.json() + update_data = [] + if json_data["code"] == "OK": + data_list = json_data["data"]["items"] + for data in data_list: + obj = {} + if type_ == UpdateType.CASE: + obj["case_name"] = s_name + name = data["name"] + try: + logger.debug( + f"武器箱: [{s_name}] 页数: [{page_index}] 正在收录皮肤: [{name}]...", + "开箱更新", + ) + obj["skin_id"] = str(data["id"]) + obj["buy_max_price"] = data["buy_max_price"] # 求购最大金额 + obj["buy_num"] = data["buy_num"] # 当前求购 + goods_info = data["goods_info"] + info = goods_info["info"] + tags = info["tags"] + obj["weapon_type"] = tags["type"]["localized_name"] # 枪械类型 + if obj["weapon_type"] in ["音乐盒", "印花", "探员"]: + continue + elif obj["weapon_type"] in ["匕首", "手套"]: + obj["color"] = "KNIFE" + obj["name"] = data["short_name"].split("(")[0].strip() # 名称 + elif obj["weapon_type"] in ["武器箱"]: + obj["color"] = "CASE" + obj["name"] = data["short_name"] + else: + obj["color"] = NAME2COLOR[tags["rarity"]["localized_name"]] + obj["name"] = tags["weapon"]["localized_name"] # 名称 + if obj["weapon_type"] not in ["武器箱"]: + obj["abrasion"] = tags["exterior"]["localized_name"] # 磨损 + obj["is_stattrak"] = "StatTrak" in tags["quality"]["localized_name"] # type: ignore # 是否暗金 + if not obj["color"]: + obj["color"] = NAME2COLOR[ + tags["rarity"]["localized_name"] + ] # 品质颜色 + else: + obj["abrasion"] = "CASE" + obj["skin_name"] = ( + data["short_name"].split("|")[-1].strip() + ) # 皮肤名称 + obj["img_url"] = goods_info["original_icon_url"] # 图片url + obj["steam_price"] = goods_info["steam_price_cny"] # steam价格 + obj["sell_min_price"] = data["sell_min_price"] # 售卖最低价格 + obj["sell_num"] = data["sell_num"] # 售卖数量 + obj["sell_reference_price"] = data[ + "sell_reference_price" + ] # 参考价格 + update_data.append(BuffSkin(**obj)) + except Exception as e: + logger.error( + f"更新武器箱: [{s_name}] 皮肤: [{s_name}] 错误", + e=e, + ) + logger.debug( + f"访问武器箱: [{s_name}] 页数: [{page_index}] 成功并收录完成", + "开箱更新", + ) + return update_data, json_data["data"]["total_page"] + else: + logger.warning(f'访问BUFF失败: {json_data["error"]}') + return f'访问失败: {json_data["error"]}', -1 + return f"访问失败, 状态码: {response.status_code}", -1 + + +async def build_case_image(case_name: str | None) -> BuildImage | str: + """构造武器箱图片 + + 参数: + case_name (str): 名称 + + 返回: + BuildImage | str: 图片 + """ + background = random.choice(os.listdir(CASE_BACKGROUND)) + background_img = BuildImage(0, 0, background=CASE_BACKGROUND / background) + if case_name: + log_list = ( + await BuffSkinLog.filter(case_name__contains=case_name) + .annotate(count=Count("id")) + .group_by("skin_name") + .values_list("skin_name", "count") + ) + skin_list_ = await BuffSkin.filter(case_name__contains=case_name).all() + skin2count = {item[0]: item[1] for item in log_list} + case = None + skin_list: list[BuffSkin] = [] + exists_name = [] + for skin in skin_list_: + if skin.color == "CASE": + case = skin + else: + name = skin.name + skin.skin_name + if name not in exists_name: + skin_list.append(skin) + exists_name.append(name) + generate_img = {} + for skin in skin_list: + skin_img = await generate_skin(skin, skin2count.get(skin.skin_name, 0)) + if skin_img: + if not generate_img.get(skin.color): + generate_img[skin.color] = [] + generate_img[skin.color].append(skin_img) + skin_image_list = [] + for color in COLOR2NAME: + if generate_img.get(color): + skin_image_list = skin_image_list + generate_img[color] + img = skin_image_list[0] + img_w, img_h = img.size + total_size = (img_w + 25) * (img_h + 10) * len(skin_image_list) # 总面积 + new_size = get_bk_image_size(total_size, background_img.size, img.size, 250) + A = BuildImage( + new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background + ) + await A.filter("GaussianBlur", 2) + if case: + case_img = await generate_skin( + case, skin2count.get(f"{case_name}武器箱", 0) + ) + if case_img: + await A.paste(case_img, (25, 25)) + w = 25 + h = 230 + skin_image_list.reverse() + for image in skin_image_list: + await A.paste(image, (w, h)) + w += image.width + 20 + if w + image.width - 25 > A.width: + h += image.height + 10 + w = 25 + if h + img_h + 100 < A.height: + await A.crop((0, 0, A.width, h + img_h + 100)) + return A + else: + log_list = ( + await BuffSkinLog.filter(color="CASE") + .annotate(count=Count("id")) + .group_by("case_name") + .values_list("case_name", "count") + ) + name2count = {item[0]: item[1] for item in log_list} + skin_list = await BuffSkin.filter(color="CASE").all() + image_list: list[BuildImage] = [] + for skin in skin_list: + if img := await generate_skin(skin, name2count[skin.case_name]): + image_list.append(img) + if not image_list: + return "未收录武器箱" + w = 25 + h = 150 + img = image_list[0] + img_w, img_h = img.size + total_size = (img_w + 25) * (img_h + 10) * len(image_list) # 总面积 + + new_size = get_bk_image_size(total_size, background_img.size, img.size, 155) + A = BuildImage( + new_size[0] + 50, new_size[1], background=CASE_BACKGROUND / background + ) + await A.filter("GaussianBlur", 2) + bk_img = BuildImage( + img_w, 120, color=(25, 25, 25, 100), font_size=60, font="CJGaoDeGuo.otf" + ) + await bk_img.text( + (0, 0), + f"已收录 {len(image_list)} 个武器箱", + (255, 255, 255), + center_type="center", + ) + await A.paste(bk_img, (10, 10), "width") + for image in image_list: + await A.paste(image, (w, h)) + w += image.width + 20 + if w + image.width - 25 > A.width: + h += image.height + 10 + w = 25 + if h + img_h + 100 < A.height: + await A.crop((0, 0, A.width, h + img_h + 100)) + return A + + +def get_bk_image_size( + total_size: int, + base_size: tuple[int, int], + img_size: tuple[int, int], + extra_height: int = 0, +) -> tuple[int, int]: + """获取所需背景大小且不改变图片长宽比 + + 参数: + total_size (int): 总面积 + base_size (Tuple[int, int]): 初始背景大小 + img_size (Tuple[int, int]): 贴图大小 + + 返回: + tuple[int, int]: 满足所有贴图大小 + """ + bk_w, bk_h = base_size + img_w, img_h = img_size + is_add_title_size = False + left_dis = 0 + right_dis = 0 + old_size = (0, 0) + new_size = (0, 0) + ratio = 1.1 + while 1: + w_ = int(ratio * bk_w) + h_ = int(ratio * bk_h) + size = w_ * h_ + if size < total_size: + left_dis = size + else: + right_dis = size + r = w_ / (img_w + 25) + if right_dis and r - int(r) < 0.1: + if not is_add_title_size and extra_height: + total_size = int(total_size + w_ * extra_height) + is_add_title_size = True + right_dis = 0 + continue + if total_size - left_dis > right_dis - total_size: + new_size = (w_, h_) + else: + new_size = old_size + break + old_size = (w_, h_) + ratio += 0.1 + return new_size + + +async def get_skin_case(id_: str) -> list[str] | None: + """获取皮肤所在箱子 + + 参数: + id_ (str): 皮肤id + + 返回: + list[str] | None: 武器箱名称 + """ + url = f"{SELL_URL}/{id_}" + proxy = None + if ip := Config.get_config("open_cases", "BUFF_PROXY"): + proxy = {"http://": ip, "https://": ip} + response = await AsyncHttpx.get( + url, + proxy=proxy, + ) + if response.status_code == 200: + text = response.text + if r := re.search('', text): + case_list = [] + for s in r.group(1).split(","): + if "武器箱" in s: + case_list.append( + s.replace("”", "") + .replace("“", "") + .replace('"', "") + .replace("'", "") + .replace("武器箱", "") + .replace(" ", "") + ) + return case_list + else: + logger.debug(f"访问皮肤所属武器箱异常 url: {url} code: {response.status_code}") + return None + + +async def init_skin_trends( + name: str, skin: str, abrasion: str, day: int = 7 +) -> BuildImage | None: + date = datetime.now() - timedelta(days=day) + log_list = ( + await BuffSkinLog.filter( + name__contains=name.upper(), + skin_name=skin, + abrasion__contains=abrasion, + create_time__gt=date, + is_stattrak=False, + ) + .order_by("create_time") + .limit(day * 5) + .all() + ) + if not log_list: + return None + date_list = [] + price_list = [] + for log in log_list: + date = str(log.create_time.date()) + if date not in date_list: + date_list.append(date) + price_list.append(log.sell_min_price) + graph = BuildMat(MatType.LINE) + graph.data = price_list + graph.title = f"{name}({skin})价格趋势({day})" + graph.x_index = date_list + return await graph.build() + + +async def reset_count_daily(): + """ + 重置每日开箱 + """ + try: + await OpenCasesUser.all().update(today_open_total=0) + # await broadcast_group( + # "[[_task|open_case_reset_remind]]今日开箱次数重置成功", + # log_cmd="开箱重置提醒", + # ) + except Exception as e: + logger.error(f"开箱重置错误", e=e) + + +async def download_image(case_name: str | None = None): + """下载皮肤图片 + + 参数: + case_name: 箱子名称. + """ + skin_list = ( + await BuffSkin.filter(case_name=case_name).all() + if case_name + else await BuffSkin.all() + ) + for skin in skin_list: + name_ = skin.name + "-" + skin.skin_name + "-" + skin.abrasion + for c_name_ in skin.case_name.split(","): + try: + pass + # file_path = BASE_PATH / cn2py(c_name_) / f"{cn2py(name_)}.jpg" + # if not file_path.exists(): + # logger.debug( + # f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}...", + # "开箱图片更新", + # ) + # await AsyncHttpx.download_file(skin.img_url, file_path) + # rand_time = random.randint(1, 5) + # await asyncio.sleep(rand_time) + # logger.debug(f"图片下载随机等待时间: {rand_time}", "开箱图片更新") + # else: + # logger.debug( + # f"皮肤 {c_name_}/{skin.name} 图片已存在...", "开箱图片更新" + # ) + except Exception as e: + logger.error( + f"下载皮肤 {c_name_}/{skin.name} 图片: {skin.img_url}", + "开箱图片更新", + e=e, + ) + + +@driver.on_startup +async def _(): + await CaseManager.reload() diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py index 65b6d371..1506e655 100644 --- a/zhenxun/utils/_build_mat.py +++ b/zhenxun/utils/_build_mat.py @@ -1,3 +1,469 @@ +import random +from io import BytesIO +from pathlib import Path +from re import S + +from pydantic import BaseModel +from strenum import StrEnum + +from ._build_image import BuildImage + + +class MatType(StrEnum): + + LINE = "LINE" + """折线图""" + BAR = "BAR" + """柱状图""" + BARH = "BARH" + """横向柱状图""" + + +class BuildMatData(BaseModel): + + mat_type: MatType + """类型""" + data: list[int | float] = [] + """数据""" + x_name: str | None = None + """X轴坐标名称""" + y_name: str | None = None + """Y轴坐标名称""" + x_index: list[str] = [] + """显示轴坐标值""" + y_index: list[int | float] = [] + """数据轴坐标值""" + space: tuple[int, int] = (15, 15) + """坐标值间隔(X, Y)""" + rotate: tuple[int, int] = (0, 0) + """坐标值旋转(X, Y)""" + title: str | None = None + """标题""" + font: str = "msyh.ttf" + """字体""" + font_size: int = 15 + """字体大小""" + display_num: bool = True + """是否在点与柱状图顶部显示数值""" + is_grid: bool = False + """是否添加栅格""" + background_color: tuple[int, int, int] | str = (255, 255, 255) + """背景颜色""" + background: Path | bytes | None = None + """背景图片""" + bar_color: list[str] = ["*"] + """柱状图柱子颜色, 多个时随机, 使用 * 时七色随机""" + padding: tuple[int, int] = (50, 50) + """图表上下左右边距""" + + class BuildMat: - def pic2bs4(self): - return "" + """ + 针对 折线图/柱状图,基于 BuildImage 编写的 非常难用的 自定义画图工具 + 目前仅支持 正整数 + """ + + class InitGraph(BaseModel): + + mark_image: BuildImage + """BuildImage""" + x_height: int + """横坐标高度""" + x_point: list[int] + """横坐标坐标""" + graph_height: int + """坐标轴高度""" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, mat_type: MatType) -> None: + self.line_length = 760 + self._x_padding = 0 + self._y_padding = 0 + self.build_data = BuildMatData(mat_type=mat_type) + + @property + def x_name(self) -> str | None: + return self.build_data.x_name + + @x_name.setter + def x_name(self, data: str) -> str | None: + self.build_data.x_name = data + + @property + def y_name(self) -> str | None: + return self.build_data.y_name + + @y_name.setter + def y_name(self, data: str) -> str | None: + self.build_data.y_name = data + + @property + def data(self) -> list[int | float]: + return self.build_data.data + + @data.setter + def data(self, data: list[int | float]): + self._check_value(data, self.build_data.y_index) + self.build_data.data = data + + @property + def x_index(self) -> list[str]: + return self.build_data.x_index + + @x_index.setter + def x_index(self, data: list[str]): + self.build_data.x_index = data + + @property + def y_index(self) -> list[int | float]: + return self.build_data.y_index + + @y_index.setter + def y_index(self, data: list[int | float]): + # self._check_value(self.build_data.data, data) + data.sort() + self.build_data.y_index = data + + @property + def space(self) -> tuple[int, int]: + return self.build_data.space + + @space.setter + def space(self, data: tuple[int, int]): + self.build_data.space = data + + @property + def rotate(self) -> tuple[int, int]: + return self.build_data.rotate + + @rotate.setter + def rotate(self, data: tuple[int, int]): + self.build_data.rotate = data + + @property + def title(self) -> str | None: + return self.build_data.title + + @title.setter + def title(self, data: str): + self.build_data.title = data + + @property + def font(self) -> str: + return self.build_data.font + + @font.setter + def font(self, data: str): + self.build_data.font = data + + # @property + # def font_size(self) -> int: + # return self.build_data.font_size + + # @font_size.setter + # def font_size(self, data: int): + # self.build_data.font_size = data + + @property + def display_num(self) -> bool: + return self.build_data.display_num + + @display_num.setter + def display_num(self, data: bool): + self.build_data.display_num = data + + @property + def is_grid(self) -> bool: + return self.build_data.is_grid + + @is_grid.setter + def is_grid(self, data: bool): + self.build_data.is_grid = data + + @property + def background_color(self) -> tuple[int, int, int] | str: + return self.build_data.background_color + + @background_color.setter + def background_color(self, data: tuple[int, int, int] | str): + self.build_data.background_color = data + + @property + def background(self) -> Path | bytes | None: + return self.build_data.background + + @background.setter + def background(self, data: Path | bytes): + self.build_data.background = data + + @property + def bar_color(self) -> list[str]: + return self.build_data.bar_color + + @bar_color.setter + def bar_color(self, data: list[str]): + self.build_data.bar_color = data + + def _check_value( + self, + y: list[int | float], + y_index: list[int | float] | None = None, + x_index: list[int | float] | None = None, + ): + """检查值合法性 + + 参数: + y: 坐标值 + y_index: y轴坐标值 + x_index: x轴坐标值 + """ + if y_index: + _value = x_index if self.build_data.mat_type == "barh" else y_index + if not isinstance(y[0], str): + __y = [float(t_y) for t_y in y] + _y_index = [float(t_y) for t_y in y_index] + if max(__y) > max(_y_index): + raise ValueError("坐标点的值必须小于y轴坐标的最大值...") + i = -9999999999 + for _y in _y_index: + if _y > i: + i = _y + else: + raise ValueError("y轴坐标值必须有序...") + + async def build(self): + """构造图片""" + A = None + bar_color = self.build_data.bar_color + if "*" in bar_color: + bar_color = [ + "#FF0000", + "#FF7F00", + "#FFFF00", + "#00FF00", + "#00FFFF", + "#0000FF", + "#8B00FF", + ] + init_graph = await self._init_graph() + mark_image = None + if self.build_data.mat_type == MatType.LINE: + mark_image = await self._build_line_graph(init_graph, bar_color) + if self.build_data.mat_type == MatType.BAR: + pass + if self.build_data.mat_type == MatType.BARH: + pass + if mark_image: + padding_width, padding_height = self.build_data.padding + width = mark_image.width + padding_width * 2 + height = mark_image.height + padding_height * 2 + if self.build_data.background: + if isinstance(self.build_data.background, bytes): + A = BuildImage( + width, height, background=BytesIO(self.build_data.background) + ) + elif isinstance(self.build_data.background, Path): + A = BuildImage(width, height, background=self.build_data.background) + else: + A = BuildImage(width, height, self.build_data.background_color) + if A: + await A.paste(mark_image, (padding_width, padding_height)) + if self.build_data.title: + font = BuildImage.load_font( + self.build_data.font, self.build_data.font_size + 7 + ) + title_width, title_height = BuildImage.get_text_size( + self.build_data.title, font + ) + pos = ( + int(A.width / 2 - title_width / 2), + int(padding_height / 2 - title_height / 2), + ) + await A.text(pos, self.build_data.title) + if self.build_data.x_name: + font = BuildImage.load_font( + self.build_data.font, self.build_data.font_size + 4 + ) + title_width, title_height = BuildImage.get_text_size( + self.build_data.x_name, font # type: ignore + ) + pos = ( + A.width - title_width - 20, + A.height - int(padding_height / 2 + title_height), + ) + await A.text(pos, self.build_data.x_name) + return A + + async def _init_graph(self) -> InitGraph: + """构造初始化图表 + + 返回: + InitGraph: InitGraph + """ + padding_width = 0 + padding_height = 0 + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + width_list = [] + height_list = [] + for x in self.build_data.x_index: + text_size = BuildImage.get_text_size(x, font) + if text_size[1] > padding_height: + padding_height = text_size[1] + width_list.append(text_size[0]) + if not self.build_data.y_index: + """没有指定y_index时,使用data自动生成""" + max_num = max(self.build_data.data) + s = int(max_num / 5) + _y_index = [max_num] + for _n in range(4): + max_num -= s + _y_index.append(max_num) + _y_index.sort() + self.build_data.y_index = _y_index + for item in self.build_data.y_index: + text_size = BuildImage.get_text_size(str(item), font) + if text_size[0] > padding_width: + padding_width = text_size[0] + height_list.append(text_size[1]) + width = ( + sum([w + self.build_data.space[0] for w in width_list]) + + height_list[0] + + self.build_data.space[0] * 2 + + 20 + ) + height = ( + sum([h + self.build_data.space[1] for h in height_list]) + + self.build_data.space[1] * 2 + + 30 + ) + if self.build_data.mat_type == MatType.BARH: + """横向柱状图时xy轴长度调换""" + _tmp = height + height = width + width = _tmp + A = BuildImage( + width, + (height + 10), + color=(255, 255, 255, 0), + ) + padding_height += 5 + await A.line( + ( + padding_width + 5, + padding_height, + padding_width + 5, + height - padding_height, + ), + width=2, + ) + await A.line( + ( + padding_width + 5, + height - padding_height, + width - padding_width + 5, + height - padding_height, + ), + width=2, + ) + _x_index = self.build_data.x_index + _y_index = self.build_data.y_index + if self.build_data.mat_type == MatType.BARH: + _tmp = _y_index + _y_index = _x_index + _x_index = _tmp + cur_width = padding_width + self.build_data.space[0] * 2 + cur_height = height - height_list[0] - 5 + x_point = [] + for i, _x in enumerate(_x_index): + """X轴数值""" + grid_height = cur_height + if self.build_data.is_grid: + grid_height = padding_height + await A.line((cur_width, cur_height - 1, cur_width, grid_height - 5)) + x_point.append(cur_width - 1) + mid_point = cur_width - int(width_list[i] / 2) + await A.text((mid_point, cur_height), str(_x), font=font) + cur_width += width_list[i] + self.build_data.space[0] + cur_width = padding_width + cur_height = height - self.build_data.padding[1] + for i, _y in enumerate(_y_index): + """Y轴数值""" + grid_width = cur_width + if self.build_data.is_grid: + grid_width = width - padding_width + 5 + await A.line((cur_width + 5, cur_height, grid_width + 11, cur_height)) + text_width = BuildImage.get_text_size(str(_y), font)[0] + await A.text( + (cur_width - text_width, cur_height - int(height_list[i] / 2) - 3), + str(_y), + font=font, + ) + cur_height -= height_list[i] + self.build_data.space[1] + graph_height = height - self.build_data.padding[1] - cur_height + 5 + return self.InitGraph( + mark_image=A, + x_height=height - height_list[0] - 5, + graph_height=graph_height, + x_point=x_point, + ) + + async def _build_line_graph( + self, init_graph: InitGraph, bar_color: list[str] + ) -> BuildImage: + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 折线图 + """ + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + mark_image = init_graph.mark_image + x_height = init_graph.x_height + graph_height = init_graph.graph_height + random_color = random.choice(bar_color) + _black_point = BuildImage(11, 11, color=random_color) + await _black_point.circle() + max_num = max(self.y_index) + point_list = [] + for x_p, y in zip(init_graph.x_point, self.build_data.data): + """折线图标点""" + y_height = int(y / max_num * init_graph.graph_height) + await mark_image.paste(_black_point, (x_p, x_height - y_height)) + point_list.append((x_p + 4, x_height - y_height + 4)) + for i in range(len(point_list) - 1): + """画线""" + a_x, a_y = point_list[i] + b_x, b_y = point_list[i + 1] + await mark_image.line((a_x, a_y, b_x, b_y), random_color) + if self.build_data.display_num: + """显示数值""" + value = self.build_data.data[i] + text_size = BuildImage.get_text_size(str(value), font) + await mark_image.text( + (a_x - int(text_size[0] / 2), a_y - text_size[1] - 5), + str(value), + font=font, + ) + """最后一个数值显示""" + value = self.build_data.data[-1] + text_size = BuildImage.get_text_size(str(value), font) + await mark_image.text( + ( + point_list[-1][0] - int(text_size[0] / 2), + point_list[-1][1] - text_size[1] - 5, + ), + str(value), + font=font, + ) + return mark_image + + async def _build_bar_graph(self): + pass + + async def _build_barh_graph(self): + pass diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index b09e9d92..ddb08407 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -7,7 +7,7 @@ from typing import Awaitable, Callable from nonebot.utils import is_coroutine_callable from ._build_image import BuildImage, ColorAlias -from ._build_mat import BuildMat +from ._build_mat import BuildMat, MatType from ._image_template import ImageTemplate, RowStyle # TODO: text2image 长度错误 From c6afb8c1e93a5e411be1b339d0691fb23d251347 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 20 May 2024 22:03:11 +0800 Subject: [PATCH 029/132] =?UTF-8?q?feat=E2=9C=A8:=20PIX=E5=9B=BE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/pix_gallery/__init__.py | 62 +++ zhenxun/plugins/pix_gallery/_data_source.py | 426 ++++++++++++++++++ .../plugins/pix_gallery/_model/__init__.py | 1 + .../pix_gallery/_model/omega_pixiv_illusts.py | 89 ++++ zhenxun/plugins/pix_gallery/_model/pixiv.py | 91 ++++ .../pix_gallery/_model/pixiv_keyword_user.py | 52 +++ zhenxun/plugins/pix_gallery/pix.py | 251 +++++++++++ .../plugins/pix_gallery/pix_add_keyword.py | 129 ++++++ .../pix_gallery/pix_pass_del_keyword.py | 217 +++++++++ zhenxun/plugins/pix_gallery/pix_show_info.py | 81 ++++ zhenxun/plugins/pix_gallery/pix_update.py | 221 +++++++++ zhenxun/utils/exception.py | 8 + zhenxun/utils/platform.py | 45 +- zhenxun/utils/utils.py | 48 ++ zhenxun/utils/withdraw_manage.py | 26 +- 15 files changed, 1738 insertions(+), 9 deletions(-) create mode 100644 zhenxun/plugins/pix_gallery/__init__.py create mode 100644 zhenxun/plugins/pix_gallery/_data_source.py create mode 100644 zhenxun/plugins/pix_gallery/_model/__init__.py create mode 100644 zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py create mode 100644 zhenxun/plugins/pix_gallery/_model/pixiv.py create mode 100644 zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py create mode 100644 zhenxun/plugins/pix_gallery/pix.py create mode 100644 zhenxun/plugins/pix_gallery/pix_add_keyword.py create mode 100644 zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py create mode 100644 zhenxun/plugins/pix_gallery/pix_show_info.py create mode 100644 zhenxun/plugins/pix_gallery/pix_update.py diff --git a/zhenxun/plugins/pix_gallery/__init__.py b/zhenxun/plugins/pix_gallery/__init__.py new file mode 100644 index 00000000..d549a249 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/__init__.py @@ -0,0 +1,62 @@ +from pathlib import Path +from typing import Tuple + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "hibiapi", + "HIBIAPI", + "https://api.obfs.dev", + help="如果没有自建或其他hibiapi请不要修改", + default_value="https://api.obfs.dev", +) +Config.add_plugin_config("pixiv", "PIXIV_NGINX_URL", "i.pximg.cf", help="Pixiv反向代理") +Config.add_plugin_config( + "pix", + "PIX_IMAGE_SIZE", + "master", + help="PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度)", + default_value="master", +) +Config.add_plugin_config( + "pix", + "SEARCH_HIBIAPI_BOOKMARKS", + 5000, + help="最低收藏,PIX使用HIBIAPI搜索图片时达到最低收藏才会添加至图库", + default_value=5000, + type=int, +) +Config.add_plugin_config( + "pix", + "WITHDRAW_PIX_MESSAGE", + (0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], +) +Config.add_plugin_config( + "pix", + "PIX_OMEGA_PIXIV_RATIO", + (10, 0), + help="PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0)", + default_value=(10, 0), + type=Tuple[int, int], +) +Config.add_plugin_config( + "pix", "TIMEOUT", 10, help="下载图片超时限制(秒)", default_value=10, type=int +) + +Config.add_plugin_config( + "pix", + "SHOW_INFO", + True, + help="是否显示图片的基本信息,如PID等", + default_value=True, + type=bool, +) + +Config.set_name("pix", "PIX图库") + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/pix_gallery/_data_source.py b/zhenxun/plugins/pix_gallery/_data_source.py new file mode 100644 index 00000000..a15eec28 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/_data_source.py @@ -0,0 +1,426 @@ +import asyncio +import math +from asyncio.exceptions import TimeoutError +from asyncio.locks import Semaphore +from copy import deepcopy +from pathlib import Path + +import aiofiles +from httpx import ConnectError + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links + +from ._model.omega_pixiv_illusts import OmegaPixivIllusts +from ._model.pixiv import Pixiv + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net/", +} + +HIBIAPI = Config.get_config("hibiapi", "HIBIAPI") +if not HIBIAPI: + HIBIAPI = "https://api.obfs.dev" +HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI + + +async def start_update_image_url( + current_keyword: list[str], black_pid: list[str], is_pid: bool +) -> tuple[int, int]: + """开始更新图片url + + 参数: + current_keyword: 关键词 + black_pid: 黑名单pid + is_pid: pid强制更新不受限制 + + 返回: + tuple[int, int]: pid数量和图片数量 + """ + global HIBIAPI + pid_count = 0 + pic_count = 0 + tasks = [] + semaphore = asyncio.Semaphore(10) + for keyword in current_keyword: + for page in range(1, 110): + if keyword.startswith("uid:"): + url = f"{HIBIAPI}/api/pixiv/member_illust" + params = {"id": keyword[4:], "page": page} + if page == 30: + break + elif keyword.startswith("pid:"): + url = f"{HIBIAPI}/api/pixiv/illust" + params = {"id": keyword[4:]} + else: + url = f"{HIBIAPI}/api/pixiv/search" + params = {"word": keyword, "page": page} + tasks.append( + asyncio.ensure_future( + search_image( + url, keyword, params, semaphore, page, black_pid, is_pid + ) + ) + ) + if keyword.startswith("pid:"): + break + result = await asyncio.gather(*tasks) + for x in result: + pid_count += x[0] + pic_count += x[1] + return pid_count, pic_count + + +async def search_image( + url: str, + keyword: str, + params: dict, + semaphore: Semaphore, + page: int = 1, + black: list[str] = [], + is_pid: bool = False, +) -> tuple[int, int]: + """搜索图片 + + 参数: + url: 搜索url + keyword: 关键词 + params: params参数 + semaphore: semaphore + page: 页面 + black: pid黑名单 + is_pid: pid强制更新不受限制 + + 返回: + tuple[int, int]: pid数量和图片数量 + """ + tmp_pid = [] + pic_count = 0 + pid_count = 0 + async with semaphore: + # try: + data = (await AsyncHttpx.get(url, params=params)).json() + if ( + not data + or data.get("error") + or (not data.get("illusts") and not data.get("illust")) + ): + return 0, 0 + if url != f"{HIBIAPI}/api/pixiv/illust": + logger.info(f'{keyword}: 获取数据成功...数据总量:{len(data["illusts"])}') + data = data["illusts"] + else: + logger.info(f'获取数据成功...PID:{params.get("id")}') + data = [data["illust"]] + img_data = {} + for x in data: + pid = x["id"] + title = x["title"] + width = x["width"] + height = x["height"] + view = x["total_view"] + bookmarks = x["total_bookmarks"] + uid = x["user"]["id"] + author = x["user"]["name"] + tags = [] + for tag in x["tags"]: + for i in tag: + if tag[i]: + tags.append(tag[i]) + img_urls = [] + if x["page_count"] == 1: + img_urls.append(x["meta_single_page"]["original_image_url"]) + else: + for urls in x["meta_pages"]: + img_urls.append(urls["image_urls"]["original"]) + if ( + ( + bookmarks >= Config.get_config("pix", "SEARCH_HIBIAPI_BOOKMARKS") + or ( + url == f"{HIBIAPI}/api/pixiv/member_illust" + and bookmarks >= 1500 + ) + or (url == f"{HIBIAPI}/api/pixiv/illust") + ) + and len(img_urls) < 10 + and _check_black(img_urls, black) + ) or is_pid: + img_data[pid] = { + "pid": pid, + "title": title, + "width": width, + "height": height, + "view": view, + "bookmarks": bookmarks, + "img_urls": img_urls, + "uid": uid, + "author": author, + "tags": tags, + } + else: + continue + for x in img_data.keys(): + data = img_data[x] + data_copy = deepcopy(data) + del data_copy["img_urls"] + for img_url in data["img_urls"]: + img_p = img_url[img_url.rfind("_") + 1 : img_url.rfind(".")] + data_copy["img_url"] = img_url + data_copy["img_p"] = img_p + data_copy["is_r18"] = "R-18" in data["tags"] + if not await Pixiv.exists( + pid=data["pid"], img_url=img_url, img_p=img_p + ): + data_copy["img_url"] = img_url + await Pixiv.create(**data_copy) + if data["pid"] not in tmp_pid: + pid_count += 1 + tmp_pid.append(data["pid"]) + pic_count += 1 + logger.info(f'存储图片PID:{data["pid"]} IMG_P:{img_p}') + else: + logger.warning(f'{data["pid"]} | {img_url} 已存在...') + # except Exception as e: + # logger.warning(f"PIX在线搜索图片错误,已再次调用 {type(e)}:{e}") + # await search_image(url, keyword, params, semaphore, page, black) + return pid_count, pic_count + + +async def get_image(img_url: str, user_id: str) -> str | Path | None: + """下载图片 + + 参数: + img_url: 图片url + user_id: 用户id + + 返回: + str | Path | None: 图片名称 + """ + if "https://www.pixiv.net/artworks" in img_url: + pid = img_url.rsplit("/", maxsplit=1)[-1] + params = {"id": pid} + for _ in range(3): + try: + response = await AsyncHttpx.get( + f"{HIBIAPI}/api/pixiv/illust", params=params + ) + if response.status_code == 200: + data = response.json() + if data.get("illust"): + if data["illust"]["page_count"] == 1: + img_url = data["illust"]["meta_single_page"][ + "original_image_url" + ] + else: + img_url = data["illust"]["meta_pages"][0]["image_urls"][ + "original" + ] + break + except TimeoutError: + pass + old_img_url = img_url + img_url = change_pixiv_image_links( + img_url, + Config.get_config("pix", "PIX_IMAGE_SIZE"), + Config.get_config("pixiv", "PIXIV_NGINX_URL"), + ) + old_img_url = change_pixiv_image_links( + old_img_url, None, Config.get_config("pixiv", "PIXIV_NGINX_URL") + ) + for _ in range(3): + try: + response = await AsyncHttpx.get( + img_url, + headers=headers, + timeout=Config.get_config("pix", "TIMEOUT"), + ) + if response.status_code == 404: + img_url = old_img_url + continue + async with aiofiles.open( + TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg", "wb" + ) as f: + await f.write(response.content) + change_img_md5( + TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg" + ) + return TEMP_PATH / f"pix_{user_id}_{img_url.split('/')[-1][:-4]}.jpg" + except TimeoutError: + logger.warning(f"PIX:{img_url} 图片下载超时...") + except ConnectError: + logger.warning(f"PIX:{img_url} 图片下载连接失败...") + return None + + +async def uid_pid_exists(id_: str) -> bool: + """检测 pid/uid 是否有效 + + 参数: + id_: pid/uid + + 返回: + bool: 是否有效 + """ + if id_.startswith("uid:"): + url = f"{HIBIAPI}/api/pixiv/member" + elif id_.startswith("pid:"): + url = f"{HIBIAPI}/api/pixiv/illust" + else: + return False + params = {"id": int(id_[4:])} + data = (await AsyncHttpx.get(url, params=params)).json() + if data.get("error"): + return False + return True + + +async def get_keyword_num(keyword: str) -> tuple[int, int, int, int, int]: + """查看图片相关 tag 数量 + + 参数: + keyword: 关键词tag + + 返回: + tuple[int, int, int, int, int]: 总数, r18数, Omg图库总数, Omg图库色图数, Omg图库r18数 + """ + count, r18_count = await Pixiv.get_keyword_num(keyword.split()) + count_, setu_count, r18_count_ = await OmegaPixivIllusts.get_keyword_num( + keyword.split() + ) + return count, r18_count, count_, setu_count, r18_count_ + + +async def remove_image(pid: int, img_p: str | None): + """删除置顶图片 + + 参数: + pid: pid + img_p: 图片 p 如 p0,p1 等 + """ + if img_p: + if "p" not in img_p: + img_p = f"p{img_p}" + if img_p: + await Pixiv.filter(pid=pid, img_p=img_p).delete() + else: + await Pixiv.filter(pid=pid).delete() + + +async def gen_keyword_pic( + _pass_keyword: list[str], not_pass_keyword: list[str], is_superuser: bool +) -> BuildImage: + """已通过或未通过的所有关键词/uid/pid + + 参数: + _pass_keyword: 通过列表 + not_pass_keyword: 未通过列表 + is_superuser: 是否超级用户 + + 返回: + BuildImage: 数据图片 + """ + _keyword = [ + x + for x in _pass_keyword + if not x.startswith("uid:") + and not x.startswith("pid:") + and not x.startswith("black:") + ] + _uid = [x for x in _pass_keyword if x.startswith("uid:")] + _pid = [x for x in _pass_keyword if x.startswith("pid:")] + _n_keyword = [ + x + for x in not_pass_keyword + if not x.startswith("uid:") + and not x.startswith("pid:") + and not x.startswith("black:") + ] + _n_uid = [ + x + for x in not_pass_keyword + if x.startswith("uid:") and not x.startswith("black:") + ] + _n_pid = [ + x + for x in not_pass_keyword + if x.startswith("pid:") and not x.startswith("black:") + ] + img_width = 0 + img_data = { + "_keyword": {"width": 0, "data": _keyword}, + "_uid": {"width": 0, "data": _uid}, + "_pid": {"width": 0, "data": _pid}, + "_n_keyword": {"width": 0, "data": _n_keyword}, + "_n_uid": {"width": 0, "data": _n_uid}, + "_n_pid": {"width": 0, "data": _n_pid}, + } + for x in list(img_data.keys()): + img_data[x]["width"] = math.ceil(len(img_data[x]["data"]) / 40) + img_width += img_data[x]["width"] * 200 + if not is_superuser: + img_width = ( + img_width + - ( + img_data["_n_keyword"]["width"] + + img_data["_n_uid"]["width"] + + img_data["_n_pid"]["width"] + ) + * 200 + ) + del img_data["_n_keyword"] + del img_data["_n_pid"] + del img_data["_n_uid"] + current_width = 0 + A = BuildImage(img_width, 1100) + for x in list(img_data.keys()): + if img_data[x]["data"]: + # img = BuildImage(img_data[x]["width"] * 200, 1100, 200, 1100, font_size=40) + img = BuildImage(img_data[x]["width"] * 200, 1100, font_size=40) + start_index = 0 + end_index = 40 + total_index = img_data[x]["width"] * 40 + for _ in range(img_data[x]["width"]): + tmp = BuildImage(198, 1100, font_size=20) + text_img = BuildImage(198, 100, font_size=50) + key_str = "\n".join( + [key for key in img_data[x]["data"][start_index:end_index]] + ) + await tmp.text((10, 100), key_str) + if x.find("_n") == -1: + await text_img.text((24, 24), "已收录") + else: + await text_img.text((24, 24), "待收录") + await tmp.paste(text_img, (0, 0)) + start_index += 40 + end_index = ( + end_index + 40 if end_index + 40 <= total_index else total_index + ) + background_img = BuildImage(200, 1100, color="#FFE4C4") + await background_img.paste(tmp, (1, 1)) + await img.paste(background_img) + await A.paste(img, (current_width, 0)) + current_width += img_data[x]["width"] * 200 + return A + + +def _check_black(img_urls: list[str], black: list[str]) -> bool: + """检测pid是否在黑名单中 + + 参数: + img_urls: 图片img列表 + black: 黑名单 + + 返回: + bool: 是否在黑名单中 + """ + for b in black: + for img_url in img_urls: + if b in img_url: + return False + return True diff --git a/zhenxun/plugins/pix_gallery/_model/__init__.py b/zhenxun/plugins/pix_gallery/_model/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/_model/__init__.py @@ -0,0 +1 @@ + diff --git a/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py new file mode 100644 index 00000000..17e2156c --- /dev/null +++ b/zhenxun/plugins/pix_gallery/_model/omega_pixiv_illusts.py @@ -0,0 +1,89 @@ + +from tortoise import fields +from tortoise.contrib.postgres.functions import Random + +from zhenxun.services.db_context import Model + + +class OmegaPixivIllusts(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + pid = fields.BigIntField() + """pid""" + uid = fields.BigIntField() + """uid""" + title = fields.CharField(255) + """标题""" + uname = fields.CharField(255) + """画师名称""" + classified = fields.IntField() + """标记标签, 0=未标记, 1=已人工标记或从可信已标记来源获取""" + nsfw_tag = fields.IntField() + """nsfw标签,-1=未标记, 0=safe, 1=setu. 2=r18""" + width = fields.IntField() + """宽度""" + height = fields.IntField() + """高度""" + tags = fields.TextField() + """tags""" + url = fields.CharField(255) + """pixiv url链接""" + + class Meta: + table = "omega_pixiv_illusts" + table_description = "omega图库数据表" + unique_together = ("pid", "url") + + @classmethod + async def query_images( + cls, + keywords: list[str] | None = None, + uid: int | None = None, + pid: int | None = None, + nsfw_tag: int | None = 0, + num: int = 100, + ) -> list["OmegaPixivIllusts"]: + """查找符合条件的图片 + + 参数: + keywords: 关键词 + uid: 画师uid + pid: 图片pid + nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 + num: 获取图片数量 + """ + if not num: + return [] + query = cls + if nsfw_tag is not None: + query = cls.filter(nsfw_tag=nsfw_tag) + if keywords: + for keyword in keywords: + query = query.filter(tags__contains=keyword) + elif uid: + query = query.filter(uid=uid) + elif pid: + query = query.filter(pid=pid) + query = query.annotate(rand=Random()).limit(num) + return await query.all() # type: ignore + + @classmethod + async def get_keyword_num( + cls, tags: list[str] | None = None + ) -> tuple[int, int, int]: + """获取相关关键词(keyword, tag)在图库中的数量 + + 参数: + tags: 关键词/Tag + """ + query = cls + if tags: + for tag in tags: + query = query.filter(tags__contains=tag) + else: + query = query.all() + count = await query.filter(nsfw_tag=0).count() + setu_count = await query.filter(nsfw_tag=1).count() + r18_count = await query.filter(nsfw_tag=2).count() + return count, setu_count, r18_count diff --git a/zhenxun/plugins/pix_gallery/_model/pixiv.py b/zhenxun/plugins/pix_gallery/_model/pixiv.py new file mode 100644 index 00000000..3451781d --- /dev/null +++ b/zhenxun/plugins/pix_gallery/_model/pixiv.py @@ -0,0 +1,91 @@ +from tortoise import fields +from tortoise.contrib.postgres.functions import Random + +from zhenxun.services.db_context import Model + + +class Pixiv(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + pid = fields.BigIntField() + """pid""" + uid = fields.BigIntField() + """uid""" + author = fields.CharField(255) + """作者""" + title = fields.CharField(255) + """标题""" + width = fields.IntField() + """宽度""" + height = fields.IntField() + """高度""" + view = fields.IntField() + """pixiv查看数""" + bookmarks = fields.IntField() + """收藏数""" + tags = fields.TextField() + """tags""" + img_url = fields.CharField(255) + """pixiv url链接""" + img_p = fields.CharField(255) + """图片pN""" + is_r18 = fields.BooleanField() + + class Meta: + table = "pixiv" + table_description = "pix图库数据表" + unique_together = ("pid", "img_url", "img_p") + + # 0:非r18 1:r18 2:混合 + @classmethod + async def query_images( + cls, + keywords: list[str] | None = None, + uid: int | None = None, + pid: int | None = None, + r18: int | None = 0, + num: int = 100, + ) -> list["Pixiv"]: + """查找符合条件的图片 + + 参数: + keywords: 关键词 + uid: 画师uid + pid: 图片pid + r18: 是否r18,0:非r18 1:r18 2:混合 + num: 查找图片的数量 + """ + if not num: + return [] + query = cls + if r18 == 0: + query = query.filter(is_r18=False) + elif r18 == 1: + query = query.filter(is_r18=True) + if keywords: + for keyword in keywords: + query = query.filter(tags__contains=keyword) + elif uid: + query = query.filter(uid=uid) + elif pid: + query = query.filter(pid=pid) + query = query.annotate(rand=Random()).limit(num) + return await query.all() # type: ignore + + @classmethod + async def get_keyword_num(cls, tags: list[str] | None = None) -> tuple[int, int]: + """获取相关关键词(keyword, tag)在图库中的数量 + + 参数: + tags: 关键词/Tag + """ + query = cls + if tags: + for tag in tags: + query = query.filter(tags__contains=tag) + else: + query = query.all() + count = await query.filter(is_r18=False).count() + r18_count = await query.filter(is_r18=True).count() + return count, r18_count diff --git a/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py new file mode 100644 index 00000000..5de544a5 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/_model/pixiv_keyword_user.py @@ -0,0 +1,52 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class PixivKeywordUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + keyword = fields.CharField(255, unique=True) + """关键词""" + is_pass = fields.BooleanField() + """是否通过""" + + class Meta: + table = "pixiv_keyword_users" + table_description = "pixiv关键词数据表" + + @classmethod + async def get_current_keyword(cls) -> tuple[list[str], list[str]]: + """获取当前通过与未通过的关键词""" + pass_keyword = [] + not_pass_keyword = [] + for data in await cls.all().values_list("keyword", "is_pass"): + if data[1]: + pass_keyword.append(data[0]) + else: + not_pass_keyword.append(data[0]) + return pass_keyword, not_pass_keyword + + @classmethod + async def get_black_pid(cls) -> list[str]: + """获取黑名单PID""" + black_pid = [] + keyword_list = await cls.filter(user_id="114514").values_list( + "keyword", flat=True + ) + for image in keyword_list: + black_pid.append(image[6:]) + return black_pid + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE pixiv_keyword_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE pixiv_keyword_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE pixiv_keyword_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py new file mode 100644 index 00000000..02154720 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix.py @@ -0,0 +1,251 @@ +import random + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.withdraw_manage import WithdrawManager + +from ._data_source import get_image +from ._model.omega_pixiv_illusts import OmegaPixivIllusts +from ._model.pixiv import Pixiv + +__plugin_meta__ = PluginMetadata( + name="PIX", + description="这里是PIX图库!", + usage=""" + 指令: + pix ?*[tags]: 通过 tag 获取相似图片,不含tag时随机抽取 + pid [uid]: 通过uid获取图片 + pix pid[pid]: 查看图库中指定pid图片 + 示例:pix 萝莉 白丝 + 示例:pix 萝莉 白丝 10 (10为数量) + 示例:pix #02 (当tag只有1个tag且为数字时,使用#标记,否则将被判定为数量) + 示例:pix 34582394 (查询指定uid图片) + 示例:pix pid:12323423 (查询指定pid图片) + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + superuser_help=""" + 指令: + pix -s ?*[tags]: 通过tag获取色图,不含tag时随机 + pix -r ?*[tags]: 通过tag获取r18图,不含tag时随机 + """, + menu_type="来点好康的", + limits=[BaseBlock(result="您有PIX图片正在处理,请稍等...")], + configs=[ + RegisterConfig( + key="MAX_ONCE_NUM2FORWARD", + value=None, + help="单次发送的图片数量达到指定值时转发为合并消息", + default_value=None, + type=int, + ), + RegisterConfig( + key="ALLOW_GROUP_SETU", + value=False, + help="允许非超级用户使用-s参数", + default_value=False, + type=bool, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="允许非超级用户使用-r参数", + default_value=False, + type=bool, + ), + ], + ).dict(), +) + +# pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True) + +_matcher = on_alconna( + Alconna( + "pix", + Args["tags?", list[str]], + Option("-s", action=store_true, help_text="色图"), + Option("-r", action=store_true, help_text="r18"), + ), + priority=5, + block=True, +) + +PIX_RATIO = None +OMEGA_RATIO = None + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[list[str]]): + global PIX_RATIO, OMEGA_RATIO + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if PIX_RATIO is None: + pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO") + PIX_RATIO = pix_omega_pixiv_ratio[0] / ( + pix_omega_pixiv_ratio[0] + pix_omega_pixiv_ratio[1] + ) + OMEGA_RATIO = 1 - PIX_RATIO + num = 1 + # keyword = arg.extract_plain_text().strip() + keyword = "" + spt = tags.result if tags.available else [] + if arparma.find("s"): + nsfw_tag = 1 + elif arparma.find("r"): + nsfw_tag = 2 + else: + nsfw_tag = 0 + if session.id1 not in bot.config.superusers: + if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or ( + nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18") + ): + await Text("你不能看这些噢,这些都是是留给管理员看的...").finish() + if (n := len(spt)) == 1: + if str(spt[0]).isdigit() and int(spt[0]) < 100: + num = int(spt[0]) + keyword = "" + elif spt[0].startswith("#"): + keyword = spt[0][1:] + elif n > 1: + if str(spt[-1]).isdigit(): + num = int(spt[-1]) + if num > 10: + if session.id1 not in bot.config.superusers or ( + session.id1 in bot.config.superusers and num > 30 + ): + num = random.randint(1, 10) + await Text(f"太贪心了,就给你发 {num}张 好了").send() + spt = spt[:-1] + keyword = " ".join(spt) + pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 + omega_num = num - pix_num + 15 + if str(keyword).isdigit(): + if num == 1: + pix_num = 15 + omega_num = 15 + all_image = await Pixiv.query_images( + uid=int(keyword), num=pix_num, r18=1 if nsfw_tag == 2 else 0 + ) + await OmegaPixivIllusts.query_images( + uid=int(keyword), num=omega_num, nsfw_tag=nsfw_tag + ) + elif keyword.lower().startswith("pid"): + pid = keyword.replace("pid", "").replace(":", "").replace(":", "") + if not str(pid).isdigit(): + await Text("PID必须是数字...").finish(reply=True) + all_image = await Pixiv.query_images( + pid=int(pid), r18=1 if nsfw_tag == 2 else 0 + ) + if not all_image: + all_image = await OmegaPixivIllusts.query_images( + pid=int(pid), nsfw_tag=nsfw_tag + ) + num = len(all_image) + else: + tmp = await Pixiv.query_images( + spt, r18=1 if nsfw_tag == 2 else 0, num=pix_num + ) + await OmegaPixivIllusts.query_images(spt, nsfw_tag=nsfw_tag, num=omega_num) + tmp_ = [] + all_image = [] + for x in tmp: + if x.pid not in tmp_: + all_image.append(x) + tmp_.append(x.pid) + if not all_image: + await Text(f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片...").finish( + reply=True + ) + msg_list = [] + for _ in range(num): + img_url = None + author = None + if not all_image: + await Text("坏了...发完了,没图了...").finish() + img = random.choice(all_image) + all_image.remove(img) # type: ignore + if isinstance(img, OmegaPixivIllusts): + img_url = img.url + author = img.uname + elif isinstance(img, Pixiv): + img_url = img.img_url + author = img.author + pid = img.pid + title = img.title + uid = img.uid + if img_url: + _img = await get_image(img_url, session.id1) + if _img: + if Config.get_config("pix", "SHOW_INFO"): + msg_list.append( + MessageFactory( + [ + Text( + f"title:{title}\n" + f"author:{author}\n" + f"PID:{pid}\nUID:{uid}\n" + ), + Image(_img), + ] + ) + ) + else: + msg_list.append(Image(_img)) + logger.info( + f" 查看PIX图库PID: {pid}", arparma.header_result, session=session + ) + else: + msg_list.append(Text("这张图似乎下载失败了")) + logger.info( + f" 查看PIX图库PID: {pid},下载图片出错", + arparma.header_result, + session=session, + ) + if ( + Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") + and num >= Config.get_config("pix", "MAX_ONCE_NUM2FORWARD") + and gid + ): + for msg in msg_list: + # receipt = await PlatformUtils.send_message( + # bot, None, group_id=gid, message=msg + # ) + receipt = await msg.send() + if receipt: + message_id = receipt.extract_message_id().message_id + await WithdrawManager.withdraw_message( + bot, + str(message_id), + Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"), + session, + ) + else: + for msg in msg_list: + receipt = await msg.send() + # receipt = await PlatformUtils.send_message( + # bot, session.id1, group_id=gid, message=msg + # ) + if receipt: + message_id = receipt.extract_message_id().message_id + await WithdrawManager.withdraw_message( + bot, + message_id, + Config.get_config("pix", "WITHDRAW_PIX_MESSAGE"), + session, + ) diff --git a/zhenxun/plugins/pix_gallery/pix_add_keyword.py b/zhenxun/plugins/pix_gallery/pix_add_keyword.py new file mode 100644 index 00000000..859f0de6 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_add_keyword.py @@ -0,0 +1,129 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import uid_pid_exists +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="PIX添加", + description="PIX关键词/UID/PID添加管理", + usage=""" + 指令: + 添加pix关键词 [Tag]: 添加一个pix搜索收录Tag + pix添加 uid [uid]: 添加一个pix搜索收录uid + pix添加 pid [pid]: 添加一个pix收录pid + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_add_matcher = on_alconna( + Alconna("添加pix关键词", Args["keyword", str]), priority=5, block=True +) + +_uid_matcher = on_alconna( + Alconna( + "pix添加", + Args["add_type", ["uid", "pid"]]["id", str], + Option("-f", action=store_true, help_text="强制收录不检查是否存在"), + ), + priority=5, + block=True, +) + +_black_matcher = on_alconna( + Alconna("添加pix黑名单", Args["pid", str]), priority=5, block=True +) + + +@_add_matcher.handle() +async def _(bot: Bot, session: EventSession, keyword: str, arparma: Arparma): + group_id = session.id3 or session.id2 or -1 + if not await PixivKeywordUser.exists(keyword=keyword): + await PixivKeywordUser.create( + user_id=str(session.id1), + group_id=str(group_id), + keyword=keyword, + is_pass=str(session.id1) in bot.config.superusers, + ) + text = f"已成功添加pixiv搜图关键词:{keyword}" + if session.id1 not in bot.config.superusers: + text += ",请等待管理员通过该关键词!" + await Text(text).send(reply=True) + logger.info( + f"添加了pixiv搜图关键词: {keyword}", arparma.header_result, session=session + ) + else: + await Text(f"该关键词 {keyword} 已存在...").send() + + +@_uid_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, add_type: str, id: str): + group_id = session.id3 or session.id2 or -1 + exists_flag = True + if arparma.find("f") and session.id1 in bot.config.superusers: + exists_flag = False + word = None + if add_type == "uid": + word = f"uid:{id}" + else: + word = f"pid:{id}" + if await Pixiv.get_or_none(pid=int(id), img_p="p0"): + await Text(f"该PID:{id}已存在...").finish(reply=True) + if not await uid_pid_exists(word) and exists_flag: + await Text("画师或作品不存在或搜索正在CD,请稍等...").finish(reply=True) + if not await PixivKeywordUser.exists(keyword=word): + await PixivKeywordUser.create( + user_id=session.id1, + group_id=str(group_id), + keyword=word, + is_pass=session.id1 in bot.config.superusers, + ) + text = f"已成功添加pixiv搜图UID/PID:{id}" + if session.id1 not in bot.config.superusers: + text += ",请等待管理员通过该关键词!" + await Text(text).send(reply=True) + else: + await Text(f"该UID/PID:{id} 已存在...").send() + + +@_black_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): + img_p = "" + if "p" in pid: + img_p = pid.split("p")[-1] + pid = pid.replace("_", "") + pid = pid[: pid.find("p")] + if not pid.isdigit: + await Text("PID必须全部是数字!").finish(reply=True) + if not await PixivKeywordUser.exists( + keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" + ): + await PixivKeywordUser.create( + user_id=114514, + group_id=114514, + keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", + is_pass=session.id1 in bot.config.superusers, + ) + await Text(f"已添加PID:{pid} 至黑名单中...").send() + logger.info( + f" 添加了pixiv搜图黑名单 PID:{pid}", arparma.header_result, session=session + ) + else: + await Text(f"PID:{pid} 已添加黑名单中,添加失败...").send() diff --git a/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py new file mode 100644 index 00000000..3cab7ca1 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py @@ -0,0 +1,217 @@ +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import remove_image +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="PIX删除", + description="PIX关键词/UID/PID添加管理", + usage=""" + 指令: + pix关键词 [y/n] [关键词/pid/uid] + 删除pix关键词 ['pid'/'uid'/'keyword'] [关键词/pid/uid] + 删除pix图片 *[pid] + 示例:pix关键词 y 萝莉 + 示例:pix关键词 y 12312312 uid + 示例:pix关键词 n 12312312 pid + 示例:删除pix关键词 keyword 萝莉 + 示例:删除pix关键词 uid 123123123 + 示例:删除pix关键词 pid 123123 + 示例:删除pix图片 4223442 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +_pass_matcher = on_alconna( + Alconna( + "pix关键词", Args["status", ["y", "n"]]["keyword", str]["type?", ["uid", "pid"]] + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_del_matcher = on_alconna( + Alconna("删除pix关键词", Args["type", ["pid", "uid", "keyword"]]["keyword", str]), + permission=SUPERUSER, + priority=1, + block=True, +) + +_del_pic_matcher = on_alconna( + Alconna( + "删除pix图片", + Args["pid", str], + Option("-b|--black", action=store_true, help_text=""), + ), + permission=SUPERUSER, + priority=1, + block=True, +) + + +@_pass_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + status: str, + keyword: str, + type: Match[str], +): + tmp = {"group": {}, "private": {}} + flag = status == "y" + if type.available: + if type.result == "uid": + keyword = f"uid:{keyword}" + else: + keyword = f"pid:{keyword}" + if not keyword[4:].isdigit(): + await Text(f"{keyword} 非全数字...").finish(reply=True) + data = await PixivKeywordUser.get_or_none(keyword=keyword) + user_id = 0 + group_id = 0 + if data: + data.is_pass = flag + await data.save(update_fields=["is_pass"]) + user_id, group_id = data.user_id, data.group_id + if not user_id: + await Text(f"未找到关键词/UID:{keyword},请检查关键词/UID是否存在...").finish( + reply=True + ) + if flag: + if group_id == -1: + if not tmp["private"].get(user_id): + tmp["private"][user_id] = {"keyword": [keyword]} + else: + tmp["private"][user_id]["keyword"].append(keyword) + else: + if not tmp["group"].get(group_id): + tmp["group"][group_id] = {} + if not tmp["group"][group_id].get(user_id): + tmp["group"][group_id][user_id] = {"keyword": [keyword]} + else: + tmp["group"][group_id][user_id]["keyword"].append(keyword) + await Text(f"已成功{'通过' if flag else '拒绝'}搜图关键词:{keyword}...").send() + for user in tmp["private"]: + text = ",".join(tmp["private"][user]["keyword"]) + await PlatformUtils.send_message( + bot, + user, + None, + f"你的关键词/UID/PID {text} 已被管理员通过,将在下一次进行更新...", + ) + # await bot.send_private_msg( + # user_id=user, + # message=f"你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...", + # ) + for group in tmp["group"]: + for user in tmp["group"][group]: + text = ",".join(tmp["group"][group][user]["keyword"]) + await PlatformUtils.send_message( + bot, + None, + group_id=group, + message=MessageFactory( + [ + Mention(user_id=user), + Text( + "你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." + ), + ] + ), + ) + # await bot.send_group_msg( + # group_id=group, + # message=Message( + # f"{at(user)}你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." + # ), + # ) + logger.info( + f" 通过了pixiv搜图关键词/UID: {keyword}", arparma.header_result, session=session + ) + + +@_del_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, type: str, keyword: str): + if type != "keyword": + keyword = f"{type}:{keyword}" + if data := await PixivKeywordUser.get_or_none(keyword=keyword): + await data.delete() + await Text(f"删除搜图关键词/UID:{keyword} 成功...").send() + logger.info( + f" 删除了pixiv搜图关键词: {keyword}", arparma.header_result, session=session + ) + else: + await Text(f"未查询到搜索关键词/UID/PID:{keyword},删除失败!").send() + + +@_del_pic_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, keyword: str): + msg = "" + black_pid = "" + flag = arparma.find("black") + img_p = None + if "p" in keyword: + img_p = keyword.split("p")[-1] + keyword = keyword.replace("_", "") + keyword = keyword[: keyword.find("p")] + elif "ugoira" in keyword: + img_p = keyword.split("ugoira")[-1] + keyword = keyword.replace("_", "") + keyword = keyword[: keyword.find("ugoira")] + if keyword.isdigit(): + if await Pixiv.query_images(pid=int(keyword), r18=2): + if await remove_image(int(keyword), img_p): + msg += f'{keyword}{f"_p{img_p}" if img_p else ""},' + if flag: + if await PixivKeywordUser.exists( + keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}" + ): + await PixivKeywordUser.create( + user_id="114514", + group_id="114514", + keyword=f"black:{keyword}{f'_p{img_p}' if img_p else ''}", + is_pass=False, + ) + black_pid += f'{keyword}{f"_p{img_p}" if img_p else ""},' + logger.info( + f" 删除了PIX图片 PID:{keyword}{f'_p{img_p}' if img_p else ''}", + arparma.header_result, + session=session, + ) + # else: + # await del_pic.send( + # f"PIX:删除pid:{pid}{f'_p{img_p}' if img_p else ''} 失败.." + # ) + else: + await Text( + f"PIX:图片pix:{keyword}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.." + ).send() + else: + await Text(f"PID必须为数字!pid:{keyword}").send(reply=True) + await Text(f"PIX:成功删除图片:{msg[:-1]}").send() + if flag: + await Text(f"成功图片PID加入黑名单:{black_pid[:-1]}").send() diff --git a/zhenxun/plugins/pix_gallery/pix_show_info.py b/zhenxun/plugins/pix_gallery/pix_show_info.py new file mode 100644 index 00000000..9dcf1af9 --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_show_info.py @@ -0,0 +1,81 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._data_source import gen_keyword_pic, get_keyword_num +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="查看pix图库", + description="让我看看管理员私藏了多少货", + usage=""" + 指令: + 我的pix关键词 + 显示pix关键词 + 查看pix图库 ?[tag]: 查看指定tag图片数量,为空时查看整个图库 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + ).dict(), +) + +_my_matcher = on_alconna(Alconna("我的pix关键词"), priority=5, block=True) + +_show_matcher = on_alconna(Alconna("显示pix关键词"), priority=5, block=True) + +_pix_matcher = on_alconna( + Alconna("查看pix图库", Args["keyword?", str]), priority=5, block=True +) + + +@_my_matcher.handle() +async def _(arparma: Arparma, session: EventSession): + data = await PixivKeywordUser.filter(user_id=session.id1).values_list( + "keyword", flat=True + ) + if not data: + await Text("您目前没有提供任何Pixiv搜图关键字...").finish(reply=True) + await Text(f"您目前提供的如下关键字:\n\t" + ",".join(data)).send() # type: ignore + logger.info("查看我的pix关键词", arparma.header_result, session=session) + + +@_show_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession): + _pass_keyword, not_pass_keyword = await PixivKeywordUser.get_current_keyword() + if _pass_keyword or not_pass_keyword: + image = await gen_keyword_pic( + _pass_keyword, not_pass_keyword, session.id1 in bot.config.superusers + ) + await Image(image.pic2bytes()).send() # type: ignore + else: + if session.id1 in bot.config.superusers: + await Text(f"目前没有已收录或待收录的搜索关键词...").send() + else: + await Text(f"目前没有已收录的搜索关键词...").send() + + +@_pix_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession, keyword: Match[str]): + _keyword = "" + if keyword.available: + _keyword = keyword.result + count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(_keyword) + await Text( + f"PIX图库:{_keyword}\n" + f"总数:{count + r18_count}\n" + f"美图:{count}\n" + f"R18:{r18_count}\n" + f"---------------\n" + f"Omega图库:{_keyword}\n" + f"总数:{count_ + setu_count + r18_count_}\n" + f"美图:{count_}\n" + f"色图:{setu_count}\n" + f"R18:{r18_count_}" + ).send() + logger.info("查看pix图库", arparma.header_result, session=session) diff --git a/zhenxun/plugins/pix_gallery/pix_update.py b/zhenxun/plugins/pix_gallery/pix_update.py new file mode 100644 index 00000000..de2880ef --- /dev/null +++ b/zhenxun/plugins/pix_gallery/pix_update.py @@ -0,0 +1,221 @@ +import asyncio +import os +import re +import time +from pathlib import Path + +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from ._data_source import start_update_image_url +from ._model.omega_pixiv_illusts import OmegaPixivIllusts +from ._model.pixiv import Pixiv +from ._model.pixiv_keyword_user import PixivKeywordUser + +__plugin_meta__ = PluginMetadata( + name="pix检查更新", + description="pix图库收录数据检查更新", + usage=""" + 指令: + 更新pix关键词 *[keyword/uid/pid] [num=max]: 更新仅keyword/uid/pid或全部 + pix检测更新:检测从未更新过的uid和pid + 示例:更新pix关键词keyword + 示例:更新pix关键词uid 10 + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.SUPERUSER + ).dict(), +) + + +_update_matcher = on_alconna( + Alconna("更新pix关键词", Args["type", ["uid", "pid", "keyword"]]["num?", int]), + permission=SUPERUSER, + priority=1, + block=True, +) + +_check_matcher = on_alconna( + Alconna( + "pix检测更新", Option("-u|--update", action=store_true, help_text="是否更新") + ), + permission=SUPERUSER, + priority=1, + block=True, +) + +_omega_matcher = on_alconna( + Alconna("检测omega图库"), permission=SUPERUSER, priority=1, block=True +) + + +@_update_matcher.handle() +async def _(arparma: Arparma, session: EventSession, type: str, num: Match[int]): + _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() + _pass_keyword.reverse() + black_pid = await PixivKeywordUser.get_black_pid() + _keyword = [ + x + for x in _pass_keyword + if not x.startswith("uid:") + and not x.startswith("pid:") + and not x.startswith("black:") + ] + _uid = [x for x in _pass_keyword if x.startswith("uid:")] + _pid = [x for x in _pass_keyword if x.startswith("pid:")] + _num = num.result if num.available else 9999 + if _num < 10000: + keyword_str = ",".join( + _keyword[: _num if _num < len(_keyword) else len(_keyword)] + ) + uid_str = ",".join(_uid[: _num if _num < len(_uid) else len(_uid)]) + pid_str = ",".join(_pid[: _num if _num < len(_pid) else len(_pid)]) + if type == "pid": + update_lst = _pid + info = f"开始更新Pixiv搜图PID:\n{pid_str}" + elif type == "uid": + update_lst = _uid + info = f"开始更新Pixiv搜图UID:\n{uid_str}" + elif type == "keyword": + update_lst = _keyword + info = f"开始更新Pixiv搜图关键词:\n{keyword_str}" + else: + update_lst = _pass_keyword + info = f"开始更新Pixiv搜图关键词:\n{keyword_str}\n更新UID:{uid_str}\n更新PID:{pid_str}" + _num = _num if _num < len(update_lst) else len(update_lst) + else: + if type == "pid": + update_lst = [f"pid:{_num}"] + info = f"开始更新Pixiv搜图UID:\npid:{_num}" + else: + update_lst = [f"uid:{_num}"] + info = f"开始更新Pixiv搜图UID:\nuid:{_num}" + await Text(info).send() + start_time = time.time() + pid_count, pic_count = await start_update_image_url(update_lst[:_num], black_pid, type == 'pid') + await Text( + f"Pixiv搜图关键词搜图更新完成...\n" + f"累计更新PID {pid_count} 个\n" + f"累计更新图片 {pic_count} 张" + + "\n耗时:{:.2f}秒".format((time.time() - start_time)) + ).send() + logger.info("更新pix关键词", arparma.header_result, session=session) + + +@_check_matcher.handle() +async def _(bot: Bot, arparma: Arparma, session: EventSession): + _pass_keyword, _ = await PixivKeywordUser.get_current_keyword() + x_uid = [] + x_pid = [] + _uid = [int(x[4:]) for x in _pass_keyword if x.startswith("uid:")] + _pid = [int(x[4:]) for x in _pass_keyword if x.startswith("pid:")] + all_images = await Pixiv.query_images(r18=2) + for img in all_images: + if img.pid not in x_pid: + x_pid.append(img.pid) + if img.uid not in x_uid: + x_uid.append(img.uid) + await Text( + "从未更新过的UID:" + + ",".join([f"uid:{x}" for x in _uid if x not in x_uid]) + + "\n" + + "从未更新过的PID:" + + ",".join([f"pid:{x}" for x in _pid if x not in x_pid]) + ).send() + if arparma.find("update"): + await Text("开始自动自动更新PID....").send() + update_lst = [f"pid:{x}" for x in _uid if x not in x_uid] + black_pid = await PixivKeywordUser.get_black_pid() + start_time = time.time() + pid_count, pic_count = await start_update_image_url(update_lst, black_pid, False) + await Text( + f"Pixiv搜图关键词搜图更新完成...\n" + f"累计更新PID {pid_count} 个\n" + f"累计更新图片 {pic_count} 张" + + "\n耗时:{:.2f}秒".format((time.time() - start_time)) + ).send() + logger.info( + f"pix检测更新, 是否更新: {arparma.find('update')}", + arparma.header_result, + session=session, + ) + + +@_omega_matcher.handle() +async def _(): + async def _tasks(line: str, all_pid: list[int], length: int, index: int): + data = line.split("VALUES", maxsplit=1)[-1].strip()[1:-2] + num_list = re.findall(r"(\d+)", data) + pid = int(num_list[1]) + uid = int(num_list[2]) + id_ = 3 + while num_list[id_] not in ["0", "1"]: + id_ += 1 + classified = int(num_list[id_]) + nsfw_tag = int(num_list[id_ + 1]) + width = int(num_list[id_ + 2]) + height = int(num_list[id_ + 3]) + str_list = re.findall(r"'(.*?)',", data) + title = str_list[0] + uname = str_list[1] + tags = str_list[2] + url = str_list[3] + if pid in all_pid: + logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") + return + _, is_create = await OmegaPixivIllusts.get_or_create( + pid=pid, + title=title, + width=width, + height=height, + url=url, + uid=uid, + nsfw_tag=nsfw_tag, + tags=tags, + uname=uname, + classified=classified, + ) + if is_create: + logger.info( + f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张" + ) + else: + logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") + + omega_pixiv_illusts = None + for file in os.listdir("."): + if "omega_pixiv_artwork" in file and ".sql" in file: + omega_pixiv_illusts = Path() / file + if omega_pixiv_illusts: + with open(omega_pixiv_illusts, "r", encoding="utf8") as f: + lines = f.readlines() + tasks = [] + length = len([x for x in lines if "INSERT INTO" in x.upper()]) + all_pid = await OmegaPixivIllusts.all().values_list("pid", flat=True) + index = 0 + logger.info("检测到OmegaPixivIllusts数据库,准备开始更新....") + for line in lines: + if "INSERT INTO" in line.upper(): + index += 1 + logger.info(f"line: {line} 加入更新计划") + tasks.append( + asyncio.create_task(_tasks(line, all_pid, length, index)) # type: ignore + ) + await asyncio.gather(*tasks) + omega_pixiv_illusts.unlink() diff --git a/zhenxun/utils/exception.py b/zhenxun/utils/exception.py index 4b1cbe17..dbade4e4 100644 --- a/zhenxun/utils/exception.py +++ b/zhenxun/utils/exception.py @@ -36,3 +36,11 @@ class InsufficientGold(Exception): """ pass + + +class NotFindSuperuser(Exception): + """ + 未找到超级用户 + """ + + pass diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index 12502fd8..bb5470bf 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -1,3 +1,4 @@ +import random from typing import Awaitable, Callable, Literal, Set import httpx @@ -20,11 +21,13 @@ from nonebot_plugin_saa import ( TargetQQPrivate, Text, ) +from nonebot_plugin_saa.abstract_factories import Receipt from pydantic import BaseModel from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger +from zhenxun.utils.exception import NotFindSuperuser class UserData(BaseModel): @@ -47,6 +50,34 @@ class UserData(BaseModel): class PlatformUtils: + @classmethod + async def send_superuser( + cls, + bot: Bot, + message: str | MessageFactory | Text | Image, + superuser_id: str | None = None, + ) -> Receipt | None: + """发送消息给超级用户 + + 参数: + bot: Bot + message: 消息 + superuser_id: 指定超级用户id. + + 异常: + NotFindSuperuser: 未找到超级用户id + + 返回: + Receipt | None: Receipt + """ + if not superuser_id: + platform = cls.get_platform(bot) + platform_superusers = bot.config.PLATFORM_SUPERUSERS.get(platform) or [] + if not platform_superusers: + raise NotFindSuperuser() + superuser_id = random.choice(platform_superusers) + return await cls.send_message(bot, superuser_id, None, message) + @classmethod async def get_group_member_list(cls, bot: Bot, group_id: str) -> list[UserData]: """获取群组/频道成员列表 @@ -277,7 +308,7 @@ class PlatformUtils: user_id: str | None, group_id: str | None, message: str | Text | MessageFactory | Image, - ) -> bool: + ) -> Receipt | None: """发送消息 参数: @@ -287,13 +318,12 @@ class PlatformUtils: message: 消息文本 返回: - bool: 是否发送成功 + Receipt | None: 是否发送成功 """ if target := cls.get_target(bot, user_id, group_id): send_message = Text(message) if isinstance(message, str) else message - await send_message.send_to(target, bot) - return True - return False + return await send_message.send_to(target, bot) + return None @classmethod async def update_group(cls, bot: Bot) -> int: @@ -581,7 +611,10 @@ async def broadcast_group( if is_continue: continue target = PlatformUtils.get_target( - _bot, None, group.group_id, group.channel_id + _bot, + None, + group.group_id, + # , group.channel_id ) if target: _used_group.append(key) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 9ef46d7d..ac27a994 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -9,6 +9,7 @@ import httpx import pypinyin import pytz +from zhenxun.configs.config import Config from zhenxun.services.log import logger @@ -165,3 +166,50 @@ async def get_group_avatar(gid: int | str) -> bytes | None: except Exception as e: logger.error("获取群头像错误", "Util", target=gid) return None + + +def change_pixiv_image_links( + url: str, size: str | None = None, nginx_url: str | None = None +) -> str: + """根据配置改变图片大小和反代链接 + + 参数: + url: 图片原图链接 + size: 模式 + nginx_url: 反代 + + 返回: + str: url + """ + if size == "master": + img_sp = url.rsplit(".", maxsplit=1) + url = img_sp[0] + img_type = img_sp[1] + url = url.replace("original", "master") + f"_master1200.{img_type}" + if not nginx_url: + nginx_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") + if nginx_url: + url = ( + url.replace("i.pximg.net", nginx_url) + .replace("i.pixiv.cat", nginx_url) + .replace("_webp", "") + ) + return url + + +def change_img_md5(path_file: str | Path) -> bool: + """改变图片MD5 + + 参数: + path_file: 图片路径 + + 返还: + bool: 是否修改成功 + """ + try: + with open(path_file, "a") as f: + f.write(str(int(time.time() * 1000))) + return True + except Exception as e: + logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e) + return False diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py index f2310d09..33c7b2b4 100644 --- a/zhenxun/utils/withdraw_manage.py +++ b/zhenxun/utils/withdraw_manage.py @@ -64,7 +64,11 @@ class WithdrawManager: @classmethod async def withdraw_message( - cls, bot: Bot, message_id: str | int, time: int | None = None + cls, + bot: Bot, + message_id: str | int, + time: int | tuple[int, int] | None = None, + session: EventSession | None = None, ): """消息撤回 @@ -74,8 +78,24 @@ class WithdrawManager: time: 延迟时间 """ if time: - logger.debug(f"将在 {time}秒 内撤回消息ID: {message_id}", "WithdrawManager") - await asyncio.sleep(time) + gid = None + _time = 1 + if isinstance(time, tuple): + if time[0] == 0: + return + if session: + gid = session.id3 or session.id2 + if not gid and int(time[1]) not in [0, 2]: + return + if gid and int(time[1]) not in [1, 2]: + return + _time = time[0] + else: + _time = time + logger.debug( + f"将在 {_time}秒 内撤回消息ID: {message_id}", "WithdrawManager" + ) + await asyncio.sleep(_time) if isinstance(bot, v11Bot): logger.debug(f"v11Bot 撤回消息ID: {message_id}", "WithdrawManager") await bot.delete_msg(message_id=int(message_id)) From 90ef1c843a162b7b4461d7c326da94814f932c97 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 23 May 2024 13:58:53 +0800 Subject: [PATCH 030/132] =?UTF-8?q?feat=E2=9C=A8:=20p=E7=AB=99=E6=8E=92?= =?UTF-8?q?=E8=A1=8C/=E6=90=9C=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 + zhenxun/configs/utils/__init__.py | 4 +- zhenxun/plugins/pixiv_rank_search/__init__.py | 215 ++++++++++++++++++ .../plugins/pixiv_rank_search/data_source.py | 173 ++++++++++++++ zhenxun/utils/utils.py | 17 ++ 5 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/pixiv_rank_search/__init__.py create mode 100644 zhenxun/plugins/pixiv_rank_search/data_source.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 0d5c4663..1f095bdd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,10 +11,12 @@ "displayname", "flmt", "getbbox", + "hibiapi", "httpx", "kaiheila", "nonebot", "onebot", + "pixiv", "tobytes", "unban", "userinfo", diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index d6a28180..c535d46d 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -1,6 +1,6 @@ import copy from pathlib import Path -from typing import Any, Callable, Dict, Type +from typing import Any, Callable, Dict, Set, Type import cattrs from pydantic import BaseModel @@ -163,6 +163,8 @@ class PluginExtraData(BaseModel): """技能被动""" superuser_help: str | None = None """超级用户帮助""" + aliases: Set[str] = set() + """额外名称""" class NoSuchConfig(Exception): diff --git a/zhenxun/plugins/pixiv_rank_search/__init__.py b/zhenxun/plugins/pixiv_rank_search/__init__.py new file mode 100644 index 00000000..a82a269e --- /dev/null +++ b/zhenxun/plugins/pixiv_rank_search/__init__.py @@ -0,0 +1,215 @@ +from asyncio.exceptions import TimeoutError + +from httpx import NetworkError +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.utils import is_valid_date + +from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls + +__plugin_meta__ = PluginMetadata( + name="P站排行/搜图", + description="P站排行榜直接冲,P站搜图跟着冲", + usage=""" + P站排行: + 可选参数: + 类型: + 1. 日排行 + 2. 周排行 + 3. 月排行 + 4. 原创排行 + 5. 新人排行 + 6. R18日排行 + 7. R18周排行 + 8. R18受男性欢迎排行 + 9. R18重口排行【慎重!】 + 【使用时选择参数序号即可,R18仅可私聊】 + p站排行 ?[参数] ?[数量] ?[日期] + 示例: + p站排行 [无参数默认为日榜] + p站排行 1 + p站排行 1 5 + p站排行 1 5 2018-4-25 + 【注意空格!!】【在线搜索会较慢】 + --------------------------------- + P站搜图: + 搜图 [关键词] ?[数量] ?[页数=1] ?[r18](不屏蔽R-18) + 示例: + 搜图 樱岛麻衣 + 搜图 樱岛麻衣 5 + 搜图 樱岛麻衣 5 r18 + 搜图 樱岛麻衣#1000users 5 + 【多个关键词用#分割】 + 【默认为 热度排序】 + 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + aliases={"P站排行", "搜图"}, + menu_type="来点好康的", + limits=[BaseBlock(result="P站排行榜或搜图正在搜索,请不要重复触发命令...")], + configs=[ + RegisterConfig( + key="TIMEOUT", + value=10, + help="图片下载超时限制", + default_value=10, + type=int, + ), + RegisterConfig( + key="MAX_PAGE_LIMIT", + value=20, + help="作品最大页数限制,超过的作品会被略过", + default_value=20, + type=int, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="图允许群聊中使用 r18 参数", + default_value=False, + type=bool, + ), + RegisterConfig( + module="hibiapi", + key="HIBIAPI", + value="https://api.obfs.dev", + help="如果没有自建或其他hibiapi请不要修改", + default_value="https://api.obfs.dev", + ), + RegisterConfig( + module="pixiv", + key="PIXIV_NGINX_URL", + value="i.pixiv.re", + help="Pixiv反向代理", + ), + ], + ).dict(), +) + + +rank_dict = { + "1": "day", + "2": "week", + "3": "month", + "4": "week_original", + "5": "week_rookie", + "6": "day_r18", + "7": "week_r18", + "8": "day_male_r18", + "9": "week_r18g", +} + +_rank_matcher = on_alconna( + Alconna("p站排行", Args["rank_type", int, 1]["num", int, 10]["datetime?", str]), + aliases={"p站排行榜"}, + priority=5, + block=True, + rule=to_me(), +) + +_keyword_matcher = on_alconna( + Alconna( + "搜图", + Args["keyword", str]["num", int, 10]["page", int, 1], + Option("-r", action=store_true, help_text="是否屏蔽r18"), + ), + priority=5, + block=True, + rule=to_me(), +) + + +@_rank_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + rank_type: int, + num: int, + datetime: Match[str], +): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + code = 0 + info_list = [] + _datetime = None + if datetime.available: + _datetime = datetime.result + if not is_valid_date(_datetime): + await Text("日期不合法,示例: 2018-4-25").finish(reply=True) + if rank_type in [6, 7, 8, 9]: + if gid: + await Text("羞羞脸!私聊里自己看!").finish(at_sender=True) + info_list, code = await get_pixiv_urls( + rank_dict[str(rank_type)], num, date=_datetime + ) + if code != 200 and info_list: + if isinstance(info_list[0], str): + await Text(info_list[0]).finish() + if not info_list: + await Text("没有找到啊,等等再试试吧~V").send(at_sender=True) + for title, author, urls in info_list: + try: + images = await download_pixiv_imgs(urls, session.id1) # type: ignore + await MessageFactory( + [Text(f"title: {title}\n"), Text(f"author: {author}\n")] + images + ).send() + + except (NetworkError, TimeoutError): + await Text("这张图网络直接炸掉了!").send() + logger.info( + f" 查看了P站排行榜 rank_type{rank_type}", arparma.header_result, session=session + ) + + +@_keyword_matcher.handle() +async def _( + bot: Bot, session: EventSession, arparma: Arparma, keyword: str, num: int, page: int +): + gid = session.id3 or session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if gid: + if arparma.find("r") and not Config.get_config( + "pixiv_rank_search", "ALLOW_GROUP_R18" + ): + await Text("(脸红#) 你不会害羞的 八嘎!").finish(at_sender=True) + r18 = 0 if arparma.find("r") else 1 + info_list = None + keyword = keyword.replace("#", " ") + info_list, code = await search_pixiv_urls(keyword, num, page, r18) + if code != 200 and isinstance(info_list[0], str): + await Text(info_list[0]).finish() + if not info_list: + await Text("没有找到啊,等等再试试吧~V").finish(at_sender=True) + for title, author, urls in info_list: + try: + images = await download_pixiv_imgs(urls, session.id1) # type: ignore + await MessageFactory( + [Text(f"title: {title}\n"), Text(f"author: {author}\n")] + images + ).send() + + except (NetworkError, TimeoutError): + await Text("这张图网络直接炸掉了!").send() + logger.info( + f" 查看了搜索 {keyword} R18:{r18}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/pixiv_rank_search/data_source.py b/zhenxun/plugins/pixiv_rank_search/data_source.py new file mode 100644 index 00000000..28b31b53 --- /dev/null +++ b/zhenxun/plugins/pixiv_rank_search/data_source.py @@ -0,0 +1,173 @@ +from asyncio.exceptions import TimeoutError +from pathlib import Path + +from nonebot_plugin_saa import Image, MessageFactory + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.utils import change_img_md5 + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net/", +} + + +async def get_pixiv_urls( + mode: str, num: int = 10, page: int = 1, date: str | None = None +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """获取排行榜图片url + + 参数: + mode: 模式类型 + num: 数量. + page: 页数. + date: 日期. + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + + params = {"mode": mode, "page": page} + if date: + params["date"] = date + hibiapi = Config.get_config("hibiapi", "HIBIAPI") + hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi + rank_url = f"{hibiapi}/api/pixiv/rank" + return await parser_data(rank_url, num, params, "rank") + + +async def search_pixiv_urls( + keyword: str, num: int, page: int, r18: int +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """搜图图片url + + 参数: + keyword: 关键词 + num: 数量 + page: 页数 + r18: 是否r18 + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + params = {"word": keyword, "page": page} + hibiapi = Config.get_config("hibiapi", "HIBIAPI") + hibiapi = hibiapi[:-1] if hibiapi[-1] == "/" else hibiapi + search_url = f"{hibiapi}/api/pixiv/search" + return await parser_data(search_url, num, params, "search", r18) + + +async def parser_data( + url: str, num: int, params: dict, type_: str, r18: int = 0 +) -> tuple[list[tuple[str, str, list[str]] | str], int]: + """解析数据搜索 + + 参数: + url: 访问URL + num: 数量 + params: 请求参数 + type_: 类型,rank或search + r18: 是否r18. + + 返回: + tuple[list[tuple[str, str, list[str]] | str], int]: 图片标题作者url数据,请求状态 + """ + info_list = [] + for _ in range(3): + try: + response = await AsyncHttpx.get( + url, + params=params, + timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), + ) + if response.status_code == 200: + data = response.json() + if data.get("illusts"): + data = data["illusts"] + break + except TimeoutError: + pass + except Exception as e: + logger.error(f"P站排行/搜图解析数据发生错误", e=e) + return ["发生了一些些错误..."], 995 + else: + return ["网络不太好?没有该页数?也许过一会就好了..."], 998 + num = num if num < 30 else 30 + _data = [] + for x in data: + if x["page_count"] < Config.get_config("pixiv_rank_search", "MAX_PAGE_LIMIT"): + if type_ == "search" and r18 == 1: + if "R-18" in str(x["tags"]): + continue + _data.append(x) + if len(_data) == num: + break + for x in _data: + title = x["title"] + author = x["user"]["name"] + urls = [] + if x["page_count"] == 1: + urls.append(x["image_urls"]["large"]) + else: + for j in x["meta_pages"]: + urls.append(j["image_urls"]["large"]) + info_list.append((title, author, urls)) + return info_list, 200 + + +async def download_pixiv_imgs( + urls: list[str], user_id: str, forward_msg_index: int | None = None +) -> list[Image]: + """下载图片 + + 参数: + urls: 图片链接 + user_id: 用户id + forward_msg_index: 转发消息中的图片排序. + + 返回: + MessageFactory: 图片 + """ + result_list = [] + index = 0 + for url in urls: + ws_url = Config.get_config("pixiv", "PIXIV_NGINX_URL") + url = url.replace("_webp", "") + if ws_url: + url = url.replace("i.pximg.net", ws_url).replace("i.pixiv.cat", ws_url) + try: + file = ( + TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" + if forward_msg_index is not None + else TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" + ) + file = Path(file) + try: + if await AsyncHttpx.download_file( + url, + file, + timeout=Config.get_config("pixiv_rank_search", "TIMEOUT"), + headers=headers, + ): + change_img_md5(file) + image = None + if forward_msg_index is not None: + image = Image( + TEMP_PATH + / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" + ) + else: + image = Image(TEMP_PATH / f"{user_id}_{index}_pixiv.jpg") + if image: + result_list.append(image) + index += 1 + except OSError: + if file.exists(): + file.unlink() + except Exception as e: + logger.error(f"P站排行/搜图下载图片错误", e=e) + return result_list diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index ac27a994..7ea698a9 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -213,3 +213,20 @@ def change_img_md5(path_file: str | Path) -> bool: except Exception as e: logger.warning(f"改变图片MD5错误 Path:{path_file}", e=e) return False + + +def is_valid_date(date_text: str, separator: str = "-") -> bool: + """日期是否合法 + + 参数: + date_text: 日期 + separator: 分隔符 + + 返回: + bool: 日期是否合法 + """ + try: + datetime.strptime(date_text, f"%Y{separator}%m{separator}%d") + return True + except ValueError: + return False From 1aed19035eddd65ad75bd45facdf95a8703f5199 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 23 May 2024 14:28:48 +0800 Subject: [PATCH 031/132] =?UTF-8?q?feat=E2=9C=A8:=20search=5Fanime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 36 ++++++++- pyproject.toml | 1 + zhenxun/plugins/poke/__init__.py | 85 +++++++++++++++++++++ zhenxun/plugins/search_anime/__init__.py | 67 ++++++++++++++++ zhenxun/plugins/search_anime/data_source.py | 53 +++++++++++++ 5 files changed, 241 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/poke/__init__.py create mode 100644 zhenxun/plugins/search_anime/__init__.py create mode 100644 zhenxun/plugins/search_anime/data_source.py diff --git a/poetry.lock b/poetry.lock index a0e7fedb..35afa584 100644 --- a/poetry.lock +++ b/poetry.lock @@ -654,6 +654,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "feedparser" +version = "6.0.11" +description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" +optional = false +python-versions = ">=3.6" +files = [ + {file = "feedparser-6.0.11-py3-none-any.whl", hash = "sha256:0be7ee7b395572b19ebeb1d6aafb0028dee11169f1c934e0ed67d54992f4ad45"}, + {file = "feedparser-6.0.11.tar.gz", hash = "sha256:c9d0407b64c6f2a065d0ebb292c2b35c01050cc0dc33757461aaabdc4c4184d5"}, +] + +[package.dependencies] +sgmllib3k = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "filelock" version = "3.13.1" @@ -2347,6 +2366,21 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "sgmllib3k" +version = "1.0.0" +description = "Py3k port of sgmllib." +optional = false +python-versions = "*" +files = [ + {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "six" version = "1.16.0" @@ -3171,4 +3205,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "1d5f9655208fe3bde4ebf3e4f39979dcc961d338d3735d001eb234acfe58f8e3" +content-hash = "11beb90d388207c12255f2de15ad66f40ede82677ceb966a93bc31ebd97977f3" diff --git a/pyproject.toml b/pyproject.toml index 678d2d0b..af263298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ pypinyin = "^0.51.0" beautifulsoup4 = "^4.12.3" lxml = "^5.1.0" psutil = "^5.9.8" +feedparser = "^6.0.11" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/poke/__init__.py b/zhenxun/plugins/poke/__init__.py new file mode 100644 index 00000000..555ebf15 --- /dev/null +++ b/zhenxun/plugins/poke/__init__.py @@ -0,0 +1,85 @@ +import os +import random + +from nonebot import on_notice +from nonebot.adapters.onebot.v11 import PokeNotifyEvent +from nonebot.adapters.onebot.v11.message import MessageSegment +from nonebot.plugin import PluginMetadata +from nonebot_plugin_saa import Image, MessageFactory, Text + +from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.services.log import logger +from zhenxun.utils.utils import CountLimiter + +__plugin_meta__ = PluginMetadata( + name="戳一戳", + description="戳一戳发送语音美图萝莉图不美哉?", + usage=""" + 戳一戳随机掉落语音或美图萝莉图 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").dict(), +) + +REPLY_MESSAGE = [ + "lsp你再戳?", + "连个可爱美少女都要戳的肥宅真恶心啊。", + "你再戳!", + "?再戳试试?", + "别戳了别戳了再戳就坏了555", + "我爪巴爪巴,球球别再戳了", + "你戳你🐎呢?!", + "那...那里...那里不能戳...绝对...", + "(。´・ω・)ん?", + "有事恁叫我,别天天一个劲戳戳戳!", + "欸很烦欸!你戳🔨呢", + "?", + "再戳一下试试?", + "???", + "正在关闭对您的所有服务...关闭成功", + "啊呜,太舒服刚刚竟然睡着了。什么事?", + "正在定位您的真实地址...定位成功。轰炸机已起飞", +] + + +_clmt = CountLimiter(3) + +poke_ = on_notice(priority=5, block=False) + + +@poke_.handle() +async def _(event: PokeNotifyEvent): + uid = str(event.user_id) if event.user_id else None + gid = str(event.group_id) if event.group_id else None + if event.self_id == event.target_id: + _clmt.increase(event.user_id) + if _clmt.check(event.user_id) or random.random() < 0.3: + rst = "" + if random.random() < 0.15: + await BanConsole.ban(uid, gid, 1, 60) + rst = "气死我了!" + await poke_.finish(rst + random.choice(REPLY_MESSAGE), at_sender=True) + rand = random.random() + path = random.choice(["luoli", "meitu"]) + if rand <= 0.3 and len(os.listdir(IMAGE_PATH / "image_management" / path)) > 0: + index = random.randint( + 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1 + ) + await MessageFactory( + [ + Text(f"id: {index}"), + Image(IMAGE_PATH / "image_management" / path / f"{index}.jpg"), + ] + ).send() + logger.info(f"USER {event.user_id} 戳了戳我") + elif 0.3 < rand < 0.6: + voice = random.choice(os.listdir(RECORD_PATH / "dinggong")) + result = MessageSegment.record(RECORD_PATH / "dinggong" / voice) + await poke_.send(result) + await poke_.send(voice.split("_")[1]) + logger.info( + f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}' + ) + else: + await poke_.send(MessageSegment("poke", {"qq": event.user_id})) diff --git a/zhenxun/plugins/search_anime/__init__.py b/zhenxun/plugins/search_anime/__init__.py new file mode 100644 index 00000000..b8aa1465 --- /dev/null +++ b/zhenxun/plugins/search_anime/__init__.py @@ -0,0 +1,67 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger + +from .data_source import from_anime_get_info + +__plugin_meta__ = PluginMetadata( + name="搜番", + description="找不到想看的动漫吗?", + usage=""" + 搜索动漫资源 + 指令: + 搜番 [番剧名称或者关键词] + 示例:搜番 刀剑神域 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + limits=[BaseBlock(result="搜索还未完成,不要重复触发!")], + configs=[ + RegisterConfig( + key="SEARCH_ANIME_MAX_INFO", + value=20, + help="搜索动漫返回的最大数量", + default_value=20, + type=10, + ) + ], + ).dict(), +) + +_matcher = on_alconna(Alconna("搜番", Args["name?", str]), priority=5, block=True) + + +@_matcher.handle() +async def _(name: Match[str]): + if name.available: + _matcher.set_path_arg("name", name.result) + + +@_matcher.got_path("name", prompt="是不是少了番名?") +async def _(session: EventSession, arparma: Arparma, name: str): + gid = session.id3 or session.id2 + await Text(f"开始搜番 {name}...").send() + anime_report = await from_anime_get_info( + name, + Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"), + ) + if anime_report: + if isinstance(anime_report, str): + await Text(anime_report).finish() + await Text("\n\n".join(anime_report)).send() + logger.info( + f"搜索番剧 {name} 成功: {anime_report}", + arparma.header_result, + session=session, + ) + else: + logger.info(f"未找到番剧 {name}...") + await Text(f"未找到番剧 {name}(也有可能是超时,再尝试一下?)").send() diff --git a/zhenxun/plugins/search_anime/data_source.py b/zhenxun/plugins/search_anime/data_source.py new file mode 100644 index 00000000..59d0ac61 --- /dev/null +++ b/zhenxun/plugins/search_anime/data_source.py @@ -0,0 +1,53 @@ +import time +from urllib import parse + +import feedparser +from lxml import etree + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + + +async def from_anime_get_info(key_word: str, max_: int) -> str | list[str]: + s_time = time.time() + url = "https://share.dmhy.org/topics/rss/rss.xml?keyword=" + parse.quote(key_word) + try: + repass = await get_repass(url, max_) + except Exception as e: + logger.error(f"发生了一些错误 {type(e)}", e=e) + return "发生了一些错误!" + repass.insert(0, f"搜索 {key_word} 结果(耗时 {int(time.time() - s_time)} 秒):\n") + return repass + + +async def get_repass(url: str, max_: int) -> list[str]: + put_line = [] + text = (await AsyncHttpx.get(url)).text + d = feedparser.parse(text) + max_ = ( + max_ + if max_ < len([e.link for e in d.entries]) + else len([e.link for e in d.entries]) + ) + url_list = [e.link for e in d.entries][:max_] + for u in url_list: + try: + text = (await AsyncHttpx.get(u)).text + html = etree.HTML(text) # type: ignore + magent = html.xpath('.//a[@id="a_magnet"]/text()')[0] + title = html.xpath(".//h3/text()")[0] + item = html.xpath('//div[@class="info resource-info right"]/ul/li') + class_a = ( + item[0] + .xpath("string(.)")[5:] + .strip() + .replace("\xa0", "") + .replace("\t", "") + ) + size = item[3].xpath("string(.)")[5:].strip() + put_line.append( + "【{}】| {}\n【{}】| {}".format(class_a, title, size, magent) + ) + except Exception as e: + logger.error(f"搜番发生错误", e=e) + return put_line From bd4106190edc955f1a3776db07abaf7fdb482924 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 23 May 2024 16:26:06 +0800 Subject: [PATCH 032/132] =?UTF-8?q?feat=E2=9C=A8:=20=E8=AF=86=E5=9B=BE=20?= =?UTF-8?q?=E5=92=8C=20buff=E7=9A=AE=E8=82=A4=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/search_anime/__init__.py | 3 +- .../search_buff_skin_price/__init__.py | 101 ++++++++++++++++++ .../search_buff_skin_price/data_source.py | 58 ++++++++++ zhenxun/plugins/search_image/__init__.py | 89 +++++++++++++++ zhenxun/plugins/search_image/saucenao.py | 63 +++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 zhenxun/plugins/search_buff_skin_price/__init__.py create mode 100644 zhenxun/plugins/search_buff_skin_price/data_source.py create mode 100644 zhenxun/plugins/search_image/__init__.py create mode 100644 zhenxun/plugins/search_image/saucenao.py diff --git a/zhenxun/plugins/search_anime/__init__.py b/zhenxun/plugins/search_anime/__init__.py index b8aa1465..87f4f2e1 100644 --- a/zhenxun/plugins/search_anime/__init__.py +++ b/zhenxun/plugins/search_anime/__init__.py @@ -1,4 +1,3 @@ -from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna from nonebot_plugin_saa import Text @@ -30,7 +29,7 @@ __plugin_meta__ = PluginMetadata( value=20, help="搜索动漫返回的最大数量", default_value=20, - type=10, + type=int, ) ], ).dict(), diff --git a/zhenxun/plugins/search_buff_skin_price/__init__.py b/zhenxun/plugins/search_buff_skin_price/__init__.py new file mode 100644 index 00000000..f29e963f --- /dev/null +++ b/zhenxun/plugins/search_buff_skin_price/__init__.py @@ -0,0 +1,101 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig +from zhenxun.services.log import logger + +from .data_source import get_price, update_buff_cookie + +__plugin_meta__ = PluginMetadata( + name="BUFF查询皮肤", + description="BUFF皮肤底价查询", + usage=""" + 在线实时获取BUFF指定皮肤所有磨损底价 + 指令: + 查询皮肤 [枪械名] [皮肤名称] + 示例:查询皮肤 ak47 二西莫夫 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + limits=[BaseBlock(result="您有皮肤正在搜索,请稍等...")], + configs=[ + RegisterConfig( + key="BUFF_PROXY", + value=None, + help="BUFF代理,有些厂ip可能被屏蔽", + ), + RegisterConfig( + key="COOKIE", + value=None, + help="BUFF的账号cookie", + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("查询皮肤", Args["name", str]["skin", str]), + aliases={"皮肤查询"}, + priority=5, + block=True, +) + +_cookie_matcher = on_alconna( + Alconna("设置cookie", Args["cookie", str]), + rule=to_me(), + permission=SUPERUSER, + priority=1, +) + + +@_matcher.handle() +async def _(name: Match[str], skin: Match[str]): + if name.available: + _matcher.set_path_arg("name", name.result) + if skin.available: + _matcher.set_path_arg("skin", skin.result) + + +@_matcher.got_path("name", prompt="要查询什么武器呢?") +@_matcher.got_path("skin", prompt="要查询该武器的什么皮肤呢?") +async def arg_handle( + session: EventSession, + arparma: Arparma, + name: str, + skin: str, +): + if name in ["算了", "取消"] or skin in ["算了", "取消"]: + await Text("已取消操作...").finish() + result = "" + if name in ["ak", "ak47"]: + name = "ak-47" + name = name + " | " + skin + try: + result, status_code = await get_price(name) + except FileNotFoundError: + await Text(f'请先对{NICKNAME}说"设置cookie"来设置cookie!').send(at_sender=True) + if status_code in [996, 997, 998]: + await Text(result).finish() + if result: + logger.info(f"查询皮肤: {name}", arparma.header_result, session=session) + await Text(result).finish() + else: + logger.info( + f" 查询皮肤:{name} 没有查询到", arparma.header_result, session=session + ) + await Text("没有查询到哦,请检查格式吧").send() + + +@_cookie_matcher.handle() +async def _(session: EventSession, arparma: Arparma, cookie: str): + result = update_buff_cookie(cookie) + await Text(result).send(at_sender=True) + logger.info("更新BUFF COOKIE", arparma.header_result, session=session) diff --git a/zhenxun/plugins/search_buff_skin_price/data_source.py b/zhenxun/plugins/search_buff_skin_price/data_source.py new file mode 100644 index 00000000..a66805bc --- /dev/null +++ b/zhenxun/plugins/search_buff_skin_price/data_source.py @@ -0,0 +1,58 @@ +from asyncio.exceptions import TimeoutError +from configs.config import Config +from utils.http_utils import AsyncHttpx +from services.log import logger + + +url = "https://buff.163.com/api/market/goods" + + +async def get_price(d_name: str) -> "str, int": + """ + 查看皮肤价格 + :param d_name: 武器皮肤,如:awp 二西莫夫 + """ + cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")} + name_list = [] + price_list = [] + parameter = {"game": "csgo", "page_num": "1", "search": d_name} + try: + response = await AsyncHttpx.get( + url, + proxy=Config.get_config("search_buff_skin_price", "BUFF_PROXY"), + params=parameter, + cookies=cookie, + ) + if response.status_code == 200: + try: + if response.text.find("Login Required") != -1: + return "BUFF登录被重置,请联系管理员重新登入", 996 + data = response.json()["data"] + total_page = data["total_page"] + data = data["items"] + for _ in range(total_page): + for i in range(len(data)): + name = data[i]["name"] + price = data[i]["sell_reference_price"] + name_list.append(name) + price_list.append(price) + except Exception as e: + logger.error(f"BUFF查询皮肤发生错误 {type(e)}:{e}") + return "没有查询到...", 998 + else: + return "访问失败!", response.status_code + except TimeoutError: + return "访问超时! 请重试或稍后再试!", 997 + result = f"皮肤: {d_name}({len(name_list)})\n" + for i in range(len(name_list)): + result += name_list[i] + ": " + price_list[i] + "\n" + return result[:-1], 999 + + +def update_buff_cookie(cookie: str) -> str: + Config.set_config("search_buff_skin_price", "COOKIE", cookie) + return "更新cookie成功" + + +if __name__ == "__main__": + print(get_price("awp 二西莫夫")) diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py new file mode 100644 index 00000000..3d0ab91f --- /dev/null +++ b/zhenxun/plugins/search_image/__init__.py @@ -0,0 +1,89 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger + +from .saucenao import get_saucenao_image + +__plugin_meta__ = PluginMetadata( + name="识图", + description="以图搜图,看破本源", + usage=""" + 识别图片 [二次元图片] + 指令: + 识图 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + configs=[ + RegisterConfig( + key="MAX_FIND_IMAGE_COUNT", + value=3, + help="搜索动漫返回的最大数量", + default_value=3, + type=int, + ), + RegisterConfig( + key="API_KEY", + value=None, + help="Saucenao的API_KEY,通过 https://saucenao.com/user.php?page=search-api 注册获取", + ), + ], + ).dict(), +) + + +_matcher = on_alconna( + Alconna("识图", Args["mode?", str]["image?", alcImg]), block=True, priority=5 +) + + +async def get_image_info(mod: str, url: str) -> str | list[Image | Text] | None: + if mod == "saucenao": + return await get_saucenao_image(url) + + +# def parse_image(key: str): +# async def _key_parser(state: T_State, img: Message = Arg(key)): +# if not get_message_img(img): +# await search_image.reject_arg(key, "请发送要识别的图片!") +# state[key] = img + +# return _key_parser + + +@_matcher.handle() +async def _(mode: Match[str], img: Match[alcImg]): + if mode.available: + _matcher.set_path_arg("mode", mode.result) + else: + _matcher.set_path_arg("mode", "saucenao") + if img.available: + _matcher.set_path_arg("image", img.result) + + +@_matcher.got_path("image", prompt="图来!") +async def _( + session: EventSession, + arparma: Arparma, + mode: str, + image: alcImg, +): + if not image.url: + await Text("图片url为空...").finish() + await Text("开始处理图片...").send() + info_list = await get_image_info(mode, image.url) + if isinstance(info_list, str): + await Text(info_list).finish(at_sender=True) + if not info_list: + await Text("未查询到...").finish() + for info in info_list[1:]: + await info.send() + logger.info(f" 识图: {image.url}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/search_image/saucenao.py b/zhenxun/plugins/search_image/saucenao.py new file mode 100644 index 00000000..c76c96b2 --- /dev/null +++ b/zhenxun/plugins/search_image/saucenao.py @@ -0,0 +1,63 @@ +import random + +from nonebot_plugin_saa import Image, Text + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services import logger +from zhenxun.utils.http_utils import AsyncHttpx + +API_URL_SAUCENAO = "https://saucenao.com/search.php" +API_URL_ASCII2D = "https://ascii2d.net/search/url/" +API_URL_IQDB = "https://iqdb.org/" + + +async def get_saucenao_image(url: str) -> str | list[Image | Text]: + """获取图片源 + + 参数: + url: 图片url + + 返回: + str | list[Image | Text]: 识图数据 + """ + api_key = Config.get_config("search_image", "API_KEY") + if not api_key: + return "Saucenao 缺失API_KEY!" + params = { + "output_type": 2, + "api_key": api_key, + "testmode": 1, + "numres": 6, + "db": 999, + "url": url, + } + data = (await AsyncHttpx.post(API_URL_SAUCENAO, params=params)).json() + if data["header"]["status"] != 0: + return f"Saucenao识图失败..status:{data['header']['status']}" + data = data["results"] + data = ( + data + if len(data) < Config.get_config("search_image", "MAX_FIND_IMAGE_COUNT") + else data[: Config.get_config("search_image", "MAX_FIND_IMAGE_COUNT")] + ) + msg_list = [] + index = random.randint(0, 10000) + if await AsyncHttpx.download_file(url, TEMP_PATH / f"saucenao_search_{index}.jpg"): + msg_list.append(Image(TEMP_PATH / f"saucenao_search_{index}.jpg")) + for info in data: + try: + similarity = info["header"]["similarity"] + tmp = f"相似度:{similarity}%\n" + for x in info["data"].keys(): + if x != "ext_urls": + tmp += f"{x}:{info['data'][x]}\n" + try: + if "source" not in info["data"].keys(): + tmp += f'source:{info["data"]["ext_urls"][0]}\n' + except KeyError: + tmp += f'source:{info["header"]["thumbnail"]}\n' + msg_list.append(Text(tmp[:-1])) + except Exception as e: + logger.warning(f"识图获取图片信息发生错误", e=e) + return msg_list From 570a8dd7f24567fb796b09639891a35ec421e34c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 24 May 2024 21:25:39 +0800 Subject: [PATCH 033/132] =?UTF-8?q?fix=F0=9F=90=9B:=20=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=8C=85=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search_buff_skin_price/data_source.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/zhenxun/plugins/search_buff_skin_price/data_source.py b/zhenxun/plugins/search_buff_skin_price/data_source.py index a66805bc..8dbe6a59 100644 --- a/zhenxun/plugins/search_buff_skin_price/data_source.py +++ b/zhenxun/plugins/search_buff_skin_price/data_source.py @@ -1,16 +1,20 @@ from asyncio.exceptions import TimeoutError -from configs.config import Config -from utils.http_utils import AsyncHttpx -from services.log import logger +from zhenxun.configs.config import Config +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx url = "https://buff.163.com/api/market/goods" -async def get_price(d_name: str) -> "str, int": - """ - 查看皮肤价格 - :param d_name: 武器皮肤,如:awp 二西莫夫 +async def get_price(d_name: str) -> tuple[str, int]: + """查看皮肤价格 + + 参数: + d_name: 武器皮肤,如:awp 二西莫夫 + + 返回: + tuple[str, int]: 查询数据和状态 """ cookie = {"session": Config.get_config("search_buff_skin_price", "COOKIE")} name_list = [] From 27c9394b0da5432b6ab0bc95098d4f44069cd61e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 26 May 2024 15:22:55 +0800 Subject: [PATCH 034/132] =?UTF-8?q?feat=E2=9C=A8:=20=E8=89=B2=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 + poetry.lock | 81 +++- pyproject.toml | 1 + zhenxun/plugins/send_setu_/__init__.py | 5 + zhenxun/plugins/send_setu_/_model.py | 87 +++++ .../plugins/send_setu_/send_setu/__init__.py | 227 +++++++++++ .../send_setu_/send_setu/_data_source.py | 362 ++++++++++++++++++ .../send_setu_/update_setu/__init__.py | 59 +++ .../send_setu_/update_setu/data_source.py | 187 +++++++++ zhenxun/services/db_context.py | 64 ++-- zhenxun/utils/image_utils.py | 27 ++ zhenxun/utils/withdraw_manage.py | 3 +- 12 files changed, 1071 insertions(+), 34 deletions(-) create mode 100644 zhenxun/plugins/send_setu_/__init__.py create mode 100644 zhenxun/plugins/send_setu_/_model.py create mode 100644 zhenxun/plugins/send_setu_/send_setu/__init__.py create mode 100644 zhenxun/plugins/send_setu_/send_setu/_data_source.py create mode 100644 zhenxun/plugins/send_setu_/update_setu/__init__.py create mode 100644 zhenxun/plugins/send_setu_/update_setu/data_source.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 1f095bdd..ac493193 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,9 +14,11 @@ "hibiapi", "httpx", "kaiheila", + "lolicon", "nonebot", "onebot", "pixiv", + "Setu", "tobytes", "unban", "userinfo", diff --git a/poetry.lock b/poetry.lock index 35afa584..f1770cb4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1684,6 +1684,85 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "opencv-python" +version = "4.9.0.80" +description = "Wrapper package for OpenCV python bindings." +optional = false +python-versions = ">=3.6" +files = [ + {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, + {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pillow" version = "9.5.0" @@ -3205,4 +3284,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "11beb90d388207c12255f2de15ad66f40ede82677ceb966a93bc31ebd97977f3" +content-hash = "76aa9b04323c716cda8d3e79a552d35c3f2d96eac39682c6c9c6b59291cbd398" diff --git a/pyproject.toml b/pyproject.toml index af263298..d218ba31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ beautifulsoup4 = "^4.12.3" lxml = "^5.1.0" psutil = "^5.9.8" feedparser = "^6.0.11" +opencv-python = "^4.9.0.80" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/send_setu_/__init__.py b/zhenxun/plugins/send_setu_/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/send_setu_/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/send_setu_/_model.py b/zhenxun/plugins/send_setu_/_model.py new file mode 100644 index 00000000..865af7d1 --- /dev/null +++ b/zhenxun/plugins/send_setu_/_model.py @@ -0,0 +1,87 @@ +from tortoise import fields +from tortoise.contrib.postgres.functions import Random +from tortoise.expressions import Q +from typing_extensions import Self + +from zhenxun.services.db_context import Model + + +class Setu(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + local_id = fields.IntField() + """本地存储下标""" + title = fields.CharField(255) + """标题""" + author = fields.CharField(255) + """作者""" + pid = fields.BigIntField() + """pid""" + img_hash = fields.TextField() + """图片hash""" + img_url = fields.CharField(255) + """pixiv url链接""" + is_r18 = fields.BooleanField() + """是否r18""" + tags = fields.TextField() + """tags""" + + class Meta: + table = "setu" + table_description = "色图数据表" + unique_together = ("pid", "img_url") + + @classmethod + async def query_image( + cls, + local_id: int | None = None, + tags: list[str] | None = None, + r18: bool = False, + limit: int = 50, + ) -> list[Self] | Self | None: + """通过tag查找色图 + + 参数: + local_id: 本地色图 id + tags: tags + r18: 是否 r18,0:非r18 1:r18 2:混合 + limit: 获取数量 + + 返回: + list[Self] | Self | None: 色图数据 + """ + if local_id: + return await cls.filter(is_r18=r18, local_id=local_id).first() + query = cls.filter(is_r18=r18) + if tags: + for tag in tags: + query = query.filter( + Q(tags__contains=tag) + | Q(title__contains=tag) + | Q(author__contains=tag) + ) + query = query.annotate(rand=Random()).limit(limit) + return await query.all() + + @classmethod + async def delete_image(cls, pid: int, img_url: str) -> int: + """删除图片并替换 + + 参数: + pid: 图片pid + + 返回: + int: 删除返回的本地id + """ + print(pid) + return_id = -1 + if query := await cls.get_or_none(pid=pid, img_url=img_url): + num = await cls.filter(is_r18=query.is_r18).count() + last_image = await cls.get_or_none(is_r18=query.is_r18, local_id=num - 1) + if last_image: + return_id = last_image.local_id + last_image.local_id = query.local_id + await last_image.save(update_fields=["local_id"]) + await query.delete() + return return_id diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py new file mode 100644 index 00000000..94a82cb5 --- /dev/null +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -0,0 +1,227 @@ +import random +from typing import Tuple + +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig +from zhenxun.models.sign_user import SignUser +from zhenxun.models.user_console import UserConsole +from zhenxun.services.log import logger +from zhenxun.utils.withdraw_manage import WithdrawManager + +from ._data_source import SetuManage, base_config + +__plugin_meta__ = PluginMetadata( + name="色图", + description="不要小看涩图啊混蛋!", + usage=""" + 搜索 lolicon 图库,每日色图time... + 多个tag使用#连接 + 指令: + 色图: 随机色图 + 色图 -r: 随机在线r18涩图 + 色图 -id [id]: 本地指定id色图 + 色图 *[tags]: 在线搜索指定tag色图 + 色图 *[tags] -r: 同上, r18色图 + [1-9]张涩图: 本地随机色图连发 + [1-9]张[tags]的涩图: 在线搜索指定tag色图连发 + 示例:色图 萝莉|少女#白丝|黑丝 + 示例:色图 萝莉#猫娘 + 注: + tag至多取前20项,| 为或,萝莉|少女=萝莉或者少女 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="来点好康的", + limits=[PluginCdBlock(result="您冲的太快了,请稍后再冲.")], + configs=[ + RegisterConfig( + key="WITHDRAW_SETU_MESSAGE", + value=(0, 1), + help="自动撤回,参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊)", + default_value=(0, 1), + type=Tuple[int, int], + ), + RegisterConfig( + key="ONLY_USE_LOCAL_SETU", + value=False, + help="仅仅使用本地色图,不在线搜索", + default_value=False, + type=bool, + ), + RegisterConfig( + key="INITIAL_SETU_PROBABILITY", + value=0.7, + help="初始色图概率,总概率 = 初始色图概率 + 好感度", + default_value=0.7, + type=float, + ), + RegisterConfig( + key="DOWNLOAD_SETU", + value=True, + help="是否存储下载的色图,使用本地色图可以加快图片发送速度", + default_value=True, + type=float, + ), + RegisterConfig( + key="TIMEOUT", + value=10, + help="色图下载超时限制(秒)", + default_value=10, + type=int, + ), + RegisterConfig( + key="SHOW_INFO", + value=True, + help="是否显示色图的基本信息,如PID等", + default_value=True, + type=bool, + ), + RegisterConfig( + key="ALLOW_GROUP_R18", + value=False, + help="在群聊中启用R18权限", + default_value=False, + type=bool, + ), + RegisterConfig( + key="MAX_ONCE_NUM2FORWARD", + value=None, + help="单次发送的图片数量达到指定值时转发为合并消息", + default_value=None, + type=int, + ), + RegisterConfig( + key="MAX_ONCE_NUM", + value=10, + help="单次发送图片数量限制", + default_value=10, + type=int, + ), + RegisterConfig( + module="pixiv", + key="PIXIV_NGINX_URL", + value="i.pixiv.re", + help="Pixiv反向代理", + default_value="i.pixiv.re", + ), + ], + ).dict(), +) + + +@run_postprocessor +async def _( + matcher: Matcher, + exception: Exception | None, + session: EventSession, +): + if matcher.plugin_name == "send_setu": + # 添加数据至数据库 + try: + await SetuManage.save_to_database() + logger.info("色图数据自动存储数据库成功...") + except Exception: + pass + + +_matcher = on_alconna( + Alconna( + "色图", + Args["tags?", str], + Option("-n", Args["num", int, 1], help_text="数量"), + Option("-id", Args["local_id", int], help_text="本地id"), + Option("-r", action=store_true, help_text="r18"), + ), + aliases={"涩图", "不够色", "来一发", "再来点"}, + priority=5, + block=True, +) + +_matcher.shortcut( + r".*?(?P\d*)[份|发|张|个|次|点](?P.*)[瑟|色|涩]图.*?", + command="色图", + arguments=["{tags}", "-n", "{num}"], + prefix=True, +) + + +@_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + num: Match[int], + tags: Match[str], + local_id: Match[int], +): + _tags = tags.result.split("#") if tags.available else None + if _tags and NICKNAME in _tags: + await Text( + "咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀" + ).finish() + if not session.id1: + await Text("用户id为空...").finish() + gid = session.id3 or session.id2 + user_console = await UserConsole.get_user(session.id1, session.platform) + user, _ = await SignUser.get_or_create( + user_id=session.id1, + defaults={"user_console": user_console, "platform": session.platform}, + ) + if session.id1 not in bot.config.superusers: + """超级用户跳过罗翔""" + if result := SetuManage.get_luo(float(user.impression)): + await result.finish() + is_r18 = arparma.find("r") + _num = num.result + if is_r18 and gid: + """群聊中禁止查看r18""" + if not base_config.get("ALLOW_GROUP_R18"): + await Text( + random.choice( + [ + "这种不好意思的东西怎么可能给这么多人看啦", + "羞羞脸!给我滚出克私聊!", + "变态变态变态变态大变态!", + ] + ) + ).finish() + if local_id.available: + """指定id""" + result = await SetuManage.get_setu(local_id=local_id.result) + if isinstance(result, str): + await Text(result).finish(reply=True) + await result[0].finish() + result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18) + if isinstance(result_list, str): + await Text(result_list).finish(reply=True) + for result in result_list: + logger.info(f"发送色图 {result}", arparma.header_result, session=session) + receipt = await result.send() + if receipt: + message_id = receipt.extract_message_id().message_id # type: ignore + await WithdrawManager.withdraw_message( + bot, + message_id, + base_config.get("WITHDRAW_SETU_MESSAGE"), + session, + ) + logger.info( + f"调用发送 {num}张 色图 tags: {_tags}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/send_setu_/send_setu/_data_source.py b/zhenxun/plugins/send_setu_/send_setu/_data_source.py new file mode 100644 index 00000000..796578b9 --- /dev/null +++ b/zhenxun/plugins/send_setu_/send_setu/_data_source.py @@ -0,0 +1,362 @@ +import os +import random +from pathlib import Path + +from asyncpg import UniqueViolationError +from nonebot_plugin_saa import Image, MessageFactory, Text +from pydantic import BaseModel + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import compressed_image +from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links + +from .._model import Setu + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + +base_config = Config.get("send_setu") + + +class SetuManage: + + URL = "https://api.lolicon.app/setu/v2" + save_data = [] + + @classmethod + async def get_setu( + cls, + *, + local_id: int | None = None, + num: int = 10, + tags: list[str] | None = None, + is_r18: bool = False, + ) -> list[MessageFactory] | str: + """获取色图 + + 参数: + local_id: 指定图片id + num: 数量 + tags: 标签 + is_r18: 是否r18 + + 返回: + list[MessageFactory] | str: 色图数据列表或消息 + + """ + result_list = [] + if local_id: + """本地id""" + data_list = await cls.get_setu_list(local_id=local_id) + if isinstance(data_list, str): + return data_list + file = await cls.get_image(data_list[0]) + if isinstance(file, str): + return file + return [cls.init_image_message(file, data_list[0])] + if base_config.get("ONLY_USE_LOCAL_SETU"): + """仅使用本地色图""" + flag = False + data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18) + if isinstance(data_list, str): + return data_list + cls.save_data = data_list + if num > len(data_list): + num = len(data_list) + flag = True + setu_list = random.sample(data_list, num) + for setu in setu_list: + base_path = None + if setu.is_r18: + base_path = IMAGE_PATH / "_r18" + else: + base_path = IMAGE_PATH / "_setu" + file_path = base_path / f"{setu.local_id}.jpg" + if not file_path.exists(): + return f"本地色图Id: {setu.local_id} 不存在..." + result_list.append(cls.init_image_message(file_path, setu)) + if flag: + result_list.append( + MessageFactory([Text("坏了,已经没图了,被榨干了!")]) + ) + return result_list + data_list = await cls.search_lolicon(tags, num, is_r18) + if isinstance(data_list, str): + """搜索失败, 从本地数据库中搜索""" + data_list = await cls.get_setu_list(tags=tags, is_r18=is_r18) + if isinstance(data_list, str): + return data_list + if not data_list: + return "没找到符合条件的色图..." + cls.save_data = data_list + flag = False + if num > len(data_list): + num = len(data_list) + flag = True + for setu in data_list: + file = await cls.get_image(setu) + if isinstance(file, str): + result_list.append(MessageFactory([Text(file)])) + continue + result_list.append(cls.init_image_message(file, setu)) + if not result_list: + return "没找到符合条件的色图..." + if flag: + result_list.append(MessageFactory([Text("坏了,已经没图了,被榨干了!")])) + return result_list + + @classmethod + def init_image_message(cls, file: Path, setu: Setu) -> MessageFactory: + """初始化图片发送消息 + + 参数: + file: 图片路径 + setu: Setu + + 返回: + MessageFactory: 发送消息内容 + """ + data_list = [] + if base_config.get("SHOW_INFO"): + data_list.append( + Text( + f"id:{setu.local_id or ''}\n" + f"title:{setu.title}\n" + f"author:{setu.author}\n" + f"PID:{setu.pid}\n" + ) + ) + data_list.append(Image(file)) + return MessageFactory(data_list) + + @classmethod + async def get_setu_list( + cls, + *, + local_id: int | None = None, + tags: list[str] | None = None, + is_r18: bool = False, + ) -> list[Setu] | str: + """获取数据库中的色图数据 + + 参数: + local_id: 色图本地id. + tags: 标签. + is_r18: 是否r18. + + 返回: + list[Setu] | str: 色图数据列表或消息 + """ + image_list: list[Setu] = [] + if local_id: + image_count = await Setu.filter(is_r18=is_r18).count() - 1 + if local_id < 0 or local_id > image_count: + return f"超过当前上下限!({image_count})" + image_list = [await Setu.query_image(local_id, r18=is_r18)] # type: ignore + elif tags: + image_list = await Setu.query_image(tags=tags, r18=is_r18) # type: ignore + else: + image_list = await Setu.query_image(r18=is_r18) # type: ignore + if not image_list: + return "没找到符合条件的色图..." + return image_list + + @classmethod + def get_luo(cls, impression: float) -> MessageFactory | None: + """罗翔 + + 参数: + impression: 好感度 + + 返回: + MessageFactory | None: 返回数据 + """ + if initial_setu_probability := base_config.get("INITIAL_SETU_PROBABILITY"): + probability = float(impression) + initial_setu_probability * 100 + if probability < random.randint(1, 101): + return MessageFactory( + [ + Text("我为什么要给你发这个?"), + Image( + IMAGE_PATH + / "luoxiang" + / random.choice(os.listdir(IMAGE_PATH / "luoxiang")) + ), + Text(f"\n(快向{NICKNAME}签到提升好感度吧!)"), + ] + ) + return None + + @classmethod + async def get_image(cls, setu: Setu) -> str | Path: + """下载图片 + + 参数: + setu: Setu + + 返回: + str | Path: 图片路径或返回消息 + """ + url = change_pixiv_image_links(setu.img_url) + index = setu.local_id if setu.local_id else random.randint(1, 100000) + file_name = f"{index}_temp_setu.jpg" + base_path = TEMP_PATH + if setu.local_id: + """本地图片存在直接返回""" + file_name = f"{index}.jpg" + if setu.is_r18: + base_path = IMAGE_PATH / "_r18" + else: + base_path = IMAGE_PATH / "_setu" + local_file = base_path / file_name + if local_file.exists(): + return local_file + file = base_path / file_name + download_success = False + for i in range(3): + logger.debug(f"尝试在线下载第 {i+1} 次", "色图") + try: + if await AsyncHttpx.download_file( + url, + file, + timeout=base_config.get("TIMEOUT"), + ): + download_success = True + if setu.local_id is not None: + if ( + os.path.getsize(base_path / f"{index}.jpg") + > 1024 * 1024 * 1.5 + ): + compressed_image( + base_path / f"{index}.jpg", + ) + change_img_md5(file) + logger.info(f"下载 lolicon 图片 {url} 成功, id:{index}") + break + except TimeoutError as e: + logger.error(f"下载图片超时", "色图", e=e) + except Exception as e: + logger.error(f"下载图片错误", "色图", e=e) + return file if download_success else "图片被小怪兽恰掉啦..!QAQ" + + @classmethod + async def search_lolicon( + cls, tags: list[str] | None, num: int, is_r18: bool + ) -> list[Setu] | str: + """搜索lolicon色图 + + 参数: + tags: 标签 + num: 数量 + is_r18: 是否r18 + + 返回: + list[Setu] | str: 色图数据或返回消息 + """ + params = { + "r18": 1 if is_r18 else 0, # 添加r18参数 0为否,1为是,2为混合 + "tag": tags, # 若指定tag + "num": 20, # 一次返回的结果数量 + "size": ["original"], + } + for count in range(3): + logger.debug(f"尝试获取图片URL第 {count+1} 次", "色图") + try: + response = await AsyncHttpx.get( + cls.URL, timeout=base_config.get("TIMEOUT"), params=params + ) + if response.status_code == 200: + data = response.json() + if not data["error"]: + data = data["data"] + result_list = cls.__handle_data(data) + num = num if num < len(data) else len(data) + random_list = random.sample(result_list, num) + if not random_list: + return "没找到符合条件的色图..." + return random_list + else: + return "没找到符合条件的色图..." + except TimeoutError as e: + logger.error(f"获取图片URL超时", "色图", e=e) + except Exception as e: + logger.error(f"访问页面错误", "色图", e=e) + return "我网线被人拔了..QAQ" + + @classmethod + def __handle_data(cls, data: dict) -> list[Setu]: + """lolicon数据处理 + + 参数: + data: lolicon数据 + + 返回: + list[Setu]: 整理的数据 + """ + result_list = [] + for i in range(len(data)): + img_url = data[i]["urls"]["original"] + img_url = change_pixiv_image_links(img_url) + title = data[i]["title"] + author = data[i]["author"] + pid = data[i]["pid"] + tags = [] + for j in range(len(data[i]["tags"])): + tags.append(data[i]["tags"][j]) + # if command != "色图r": + # if "R-18" in tags: + # tags.remove("R-18") + setu = Setu( + title=title, + author=author, + pid=pid, + img_url=img_url, + tags=",".join(tags), + is_r18="R-18" in tags, + ) + result_list.append(setu) + return result_list + + @classmethod + async def save_to_database(cls): + """存储色图数据到数据库 + + 参数: + data_list: 色图数据列表 + """ + set_list = [] + exists_list = [] + for data in cls.save_data: + if f"{data.pid}:{data.img_url}" not in exists_list: + exists_list.append(f"{data.pid}:{data.img_url}") + set_list.append(data) + if set_list: + create_list = [] + _cnt = 0 + _r18_cnt = 0 + for setu in set_list: + try: + if not await Setu.exists(pid=setu.pid, img_url=setu.img_url): + idx = await Setu.filter(is_r18=setu.is_r18).count() + setu.local_id = idx + (_r18_cnt if setu.is_r18 else _cnt) + setu.img_hash = "" + if setu.is_r18: + _r18_cnt += 1 + else: + _cnt += 1 + create_list.append(setu) + except UniqueViolationError: + pass + cls.save_data = [] + if create_list: + try: + await Setu.bulk_create(create_list, 10) + logger.debug(f"成功保存 {len(create_list)} 条色图数据") + except Exception as e: + logger.error("存储色图数据错误...", e=e) diff --git a/zhenxun/plugins/send_setu_/update_setu/__init__.py b/zhenxun/plugins/send_setu_/update_setu/__init__.py new file mode 100644 index 00000000..fc3aa3ac --- /dev/null +++ b/zhenxun/plugins/send_setu_/update_setu/__init__.py @@ -0,0 +1,59 @@ +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import BaseBlock, PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from .data_source import update_setu_img + +__plugin_meta__ = PluginMetadata( + name="更新色图", + description="更新数据库内存在的色图", + usage=""" + 更新数据库内存在的色图 + 指令: + 更新色图 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + limits=[BaseBlock(result="色图正在更新...")], + ).dict(), +) + +_matcher = on_alconna( + Alconna("更新色图"), rule=to_me(), permission=SUPERUSER, priority=1, block=True +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + if Config.get_config("send_setu", "DOWNLOAD_SETU"): + await Text("开始更新色图...").send(reply=True) + result = await update_setu_img(True) + if result: + await Text(result).send() + logger.info("更新色图", arparma.header_result, session=session) + else: + await Text("更新色图配置未开启...").send() + + +# 更新色图 +@scheduler.scheduled_job( + "cron", + hour=4, + minute=30, +) +async def _(): + if Config.get_config("send_setu", "DOWNLOAD_SETU"): + result = await update_setu_img() + if result: + logger.info(result, "自动更新色图") diff --git a/zhenxun/plugins/send_setu_/update_setu/data_source.py b/zhenxun/plugins/send_setu_/update_setu/data_source.py new file mode 100644 index 00000000..07d217d6 --- /dev/null +++ b/zhenxun/plugins/send_setu_/update_setu/data_source.py @@ -0,0 +1,187 @@ +import os +import shutil +from datetime import datetime + +import nonebot +import ujson as json +from asyncpg.exceptions import UniqueViolationError +from nonebot.drivers import Driver +from PIL import UnidentifiedImageError + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH, TEXT_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import compressed_image +from zhenxun.utils.utils import change_pixiv_image_links + +from .._model import Setu + +driver: Driver = nonebot.get_driver() + +_path = IMAGE_PATH + + +# 替换旧色图数据,修复local_id一直是50的问题 +@driver.on_startup +async def update_old_setu_data(): + path = TEXT_PATH + setu_data_file = path / "setu_data.json" + r18_data_file = path / "r18_setu_data.json" + if setu_data_file.exists() or r18_data_file.exists(): + index = 0 + r18_index = 0 + count = 0 + fail_count = 0 + for file in [setu_data_file, r18_data_file]: + if file.exists(): + data = json.load(open(file, "r", encoding="utf8")) + for x in data: + if file == setu_data_file: + idx = index + if "R-18" in data[x]["tags"]: + data[x]["tags"].remove("R-18") + else: + idx = r18_index + img_url = ( + data[x]["img_url"].replace("i.pixiv.cat", "i.pximg.net") + if "i.pixiv.cat" in data[x]["img_url"] + else data[x]["img_url"] + ) + # idx = r18_index if 'R-18' in data[x]["tags"] else index + try: + if not await Setu.exists(pid=data[x]["pid"], url=img_url): + await Setu.create( + local_id=idx, + title=data[x]["title"], + author=data[x]["author"], + pid=data[x]["pid"], + img_hash=data[x]["img_hash"], + img_url=img_url, + is_r18="R-18" in data[x]["tags"], + tags=",".join(data[x]["tags"]), + ) + count += 1 + if "R-18" in data[x]["tags"]: + r18_index += 1 + else: + index += 1 + logger.info( + f'添加旧色图数据成功 PID:{data[x]["pid"]} index:{idx}....' + ) + except UniqueViolationError: + fail_count += 1 + logger.info( + f'添加旧色图数据失败,色图重复 PID:{data[x]["pid"]} index:{idx}....' + ) + file.unlink() + setu_url_path = path / "setu_url.json" + setu_r18_url_path = path / "setu_r18_url.json" + if setu_url_path.exists(): + setu_url_path.unlink() + if setu_r18_url_path.exists(): + setu_r18_url_path.unlink() + logger.info( + f"更新旧色图数据完成,成功更新数据:{count} 条,累计失败:{fail_count} 条" + ) + + +# 删除色图rar文件夹 +shutil.rmtree(IMAGE_PATH / "setu_rar", ignore_errors=True) +shutil.rmtree(IMAGE_PATH / "r18_rar", ignore_errors=True) +shutil.rmtree(IMAGE_PATH / "rar", ignore_errors=True) + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + + +async def update_setu_img(flag: bool = False) -> str | None: + """更新色图 + + 参数: + flag: 是否手动更新. + + 返回: + str | None: 更新信息 + """ + image_list = await Setu.all().order_by("local_id") + image_list.reverse() + _success = 0 + error_info = [] + error_type = [] + count = 0 + for image in image_list: + count += 1 + path = _path / "_r18" if image.is_r18 else _path / "_setu" + local_image = path / f"{image.local_id}.jpg" + path.mkdir(exist_ok=True, parents=True) + TEMP_PATH.mkdir(exist_ok=True, parents=True) + if not local_image.exists() or not image.img_hash: + temp_file = TEMP_PATH / f"{image.local_id}.jpg" + if temp_file.exists(): + temp_file.unlink() + url_ = change_pixiv_image_links(image.img_url) + try: + if not await AsyncHttpx.download_file( + url_, TEMP_PATH / f"{image.local_id}.jpg" + ): + continue + _success += 1 + try: + if ( + os.path.getsize( + TEMP_PATH / f"{image.local_id}.jpg", + ) + > 1024 * 1024 * 1.5 + ): + compressed_image( + TEMP_PATH / f"{image.local_id}.jpg", + path / f"{image.local_id}.jpg", + ) + else: + logger.info( + f"不需要压缩,移动图片{TEMP_PATH}/{image.local_id}.jpg " + f"--> /{path}/{image.local_id}.jpg" + ) + os.rename( + TEMP_PATH / f"{image.local_id}.jpg", + path / f"{image.local_id}.jpg", + ) + except FileNotFoundError: + logger.warning(f"文件 {image.local_id}.jpg 不存在,跳过...") + continue + # img_hash = str(get_img_hash(f"{path}/{image.local_id}.jpg")) + image.img_hash = "" + await image.save(update_fields=["img_hash"]) + # await Setu.update_setu_data(image.pid, img_hash=img_hash) + except UnidentifiedImageError: + # 图片已删除 + unlink = False + with open(local_image, "r") as f: + if "404 Not Found" in f.read(): + unlink = True + if unlink: + local_image.unlink() + max_num = await Setu.delete_image(image.pid, image.img_url) + if (path / f"{max_num}.jpg").exists(): + os.rename(path / f"{max_num}.jpg", local_image) + logger.warning(f"更新色图 PID:{image.pid} 404,已删除并替换") + except Exception as e: + _success -= 1 + logger.error(f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}") + if type(e) not in error_type: + error_type.append(type(e)) + error_info.append( + f"更新色图 {image.local_id}.jpg 错误 {type(e)}: {e}" + ) + else: + logger.info(f"更新色图 {image.local_id}.jpg 已存在") + if _success or error_info or flag: + return ( + f'{str(datetime.now()).split(".")[0]} 更新 色图 完成,本地存在 {count} 张,实际更新 {_success} 张,以下为更新时未知错误:\n' + + "\n".join(error_info), + ) + return None diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 34e0264c..dfd02925 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -51,39 +51,39 @@ async def init(): i_bind = bind if not i_bind: i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" - # try: - await Tortoise.init( - db_url=i_bind, - modules={"models": MODELS}, - # timezone="Asia/Shanghai" - ) - logger.info(f"Database loaded successfully!") - # except Exception as e: - # raise Exception(f"数据库连接错误... {type(e)}: {e}") - if SCRIPT_METHOD: - db = Tortoise.get_connection("default") - logger.debug( - f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..." + try: + await Tortoise.init( + db_url=i_bind, + modules={"models": MODELS}, + # timezone="Asia/Shanghai" ) - sql_list = [] - for module, func in SCRIPT_METHOD: - try: - if is_coroutine_callable(func): - sql = await func() - else: - sql = func() - if sql: - sql_list += sql - except Exception as e: - logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) - for sql in sql_list: - logger.debug(f"执行SQL: {sql}") - try: - await db.execute_query_dict(sql) - # await TestSQL.raw(sql) - except Exception as e: - logger.debug(f"执行SQL: {sql} 错误...", e=e) - await Tortoise.generate_schemas() + if SCRIPT_METHOD: + db = Tortoise.get_connection("default") + logger.debug( + f"即将运行SCRIPT_METHOD方法, 合计 {len(SCRIPT_METHOD)} 个..." + ) + sql_list = [] + for module, func in SCRIPT_METHOD: + try: + if is_coroutine_callable(func): + sql = await func() + else: + sql = func() + if sql: + sql_list += sql + except Exception as e: + logger.debug(f"{module} 执行SCRIPT_METHOD方法出错...", e=e) + for sql in sql_list: + logger.debug(f"执行SQL: {sql}") + try: + await db.execute_query_dict(sql) + # await TestSQL.raw(sql) + except Exception as e: + logger.debug(f"执行SQL: {sql} 错误...", e=e) + await Tortoise.generate_schemas() + logger.info(f"Database loaded successfully!") + except Exception as e: + raise Exception(f"数据库连接错误...", e=e) async def disconnect(): diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index ddb08407..4dadb7ff 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -4,8 +4,11 @@ import re from pathlib import Path from typing import Awaitable, Callable +import cv2 from nonebot.utils import is_coroutine_callable +from zhenxun.configs.path_config import IMAGE_PATH + from ._build_image import BuildImage, ColorAlias from ._build_mat import BuildMat, MatType from ._image_template import ImageTemplate, RowStyle @@ -337,3 +340,27 @@ async def build_sort_image( curr_h += img.height + 10 curr_w += max([x.width for x in ig]) + 30 return A + + +def compressed_image( + in_file: str | Path, + out_file: str | Path | None = None, + ratio: float = 0.9, +): + """压缩图片 + + 参数: + in_file: 被压缩的文件路径 + out_file: 压缩后输出的文件路径 + ratio: 压缩率,宽高 * 压缩率 + """ + in_file = IMAGE_PATH / in_file if isinstance(in_file, str) else in_file + if out_file: + out_file = IMAGE_PATH / out_file if isinstance(out_file, str) else out_file + else: + out_file = in_file + h, w, d = cv2.imread(str(in_file.absolute())).shape + img = cv2.resize( + cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) + ) + cv2.imwrite(str(out_file.absolute()), img) diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py index 33c7b2b4..b5b8d176 100644 --- a/zhenxun/utils/withdraw_manage.py +++ b/zhenxun/utils/withdraw_manage.py @@ -7,6 +7,7 @@ from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot_plugin_session import EventSession +from ruamel.yaml.comments import CommentedSeq from zhenxun.services.log import logger @@ -80,7 +81,7 @@ class WithdrawManager: if time: gid = None _time = 1 - if isinstance(time, tuple): + if isinstance(time, (tuple, CommentedSeq)): if time[0] == 0: return if session: From 2e17f56f1e4cacb837c45e1020599ea783d9c007 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 26 May 2024 15:49:44 +0800 Subject: [PATCH 035/132] =?UTF-8?q?fix=F0=9F=90=9B:=20=E7=AD=BE=E5=88=B0ui?= =?UTF-8?q?d=E6=98=BE=E7=A4=BA=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 950a7aab..73dd926a 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -135,7 +135,7 @@ async def _generate_card( ) if next_impression == 0: ratio = 0 - await bar.resize(width=int(bar.width * ratio) or bar.width, height=bar.height) + await bar.resize(width=int(bar.width * ratio) or 1, height=bar.height) await bar_bk.paste(bar) font_size = 30 if "好感度双倍加持卡" in gift: @@ -163,7 +163,7 @@ async def _generate_card( nickname_img = await BuildImage.build_text_image( nickname, size=50, font_color=(255, 255, 255) ) - user_console = await user.user_console.first() + user_console = await user.user_console if user_console and user_console.uid: uid = f"{user_console.uid}".rjust(12, "0") uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] @@ -188,7 +188,7 @@ async def _generate_card( today_sign_text_img = await BuildImage.build_text_image("", size=30) value_list = ( await SignUser.annotate() - .order_by("impression") + .order_by("-impression") .values_list("user_id", flat=True) ) index = value_list.index(user.user_id) + 1 # type: ignore From 2021a2cc1c493ac263d64feb4c8a77d4c92ceeb5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 27 May 2024 16:09:24 +0800 Subject: [PATCH 036/132] =?UTF-8?q?feat=E2=9C=A8:=20=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 2 +- zhenxun/models/statistics.py | 29 +++ zhenxun/plugins/search_image/__init__.py | 9 - .../plugins/send_setu_/send_setu/__init__.py | 2 +- zhenxun/plugins/statistics/__init__.py | 131 +++++++++++ zhenxun/plugins/statistics/_data_source.py | 131 +++++++++++ .../plugins/statistics/statistics_handle.py | 170 ++++++++++++++ zhenxun/plugins/statistics/statistics_hook.py | 40 ++++ zhenxun/utils/_build_mat.py | 210 +++++++++++++----- 9 files changed, 657 insertions(+), 67 deletions(-) create mode 100644 zhenxun/models/statistics.py create mode 100644 zhenxun/plugins/statistics/__init__.py create mode 100644 zhenxun/plugins/statistics/_data_source.py create mode 100644 zhenxun/plugins/statistics/statistics_handle.py create mode 100644 zhenxun/plugins/statistics/statistics_hook.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index b0ae260a..6bfe34e3 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -54,7 +54,7 @@ from public.bag_users t1 """ -@driver.on_startup +# @driver.on_startup async def _(): global flag await shop_register.load_register() diff --git a/zhenxun/models/statistics.py b/zhenxun/models/statistics.py new file mode 100644 index 00000000..43576fcb --- /dev/null +++ b/zhenxun/models/statistics.py @@ -0,0 +1,29 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class Statistics(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255, null=True) + """群聊id""" + plugin_name = fields.CharField(255) + """插件名称""" + create_time = fields.DatetimeField(auto_now=True) + """添加日期""" + + class Meta: + table = "statistics" + table_description = "插件调用统计数据库" + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE statistics RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE statistics ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE statistics ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py index 3d0ab91f..fce64046 100644 --- a/zhenxun/plugins/search_image/__init__.py +++ b/zhenxun/plugins/search_image/__init__.py @@ -50,15 +50,6 @@ async def get_image_info(mod: str, url: str) -> str | list[Image | Text] | None: return await get_saucenao_image(url) -# def parse_image(key: str): -# async def _key_parser(state: T_State, img: Message = Arg(key)): -# if not get_message_img(img): -# await search_image.reject_arg(key, "请发送要识别的图片!") -# state[key] = img - -# return _key_parser - - @_matcher.handle() async def _(mode: Match[str], img: Match[alcImg]): if mode.available: diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py index 94a82cb5..c38cf222 100644 --- a/zhenxun/plugins/send_setu_/send_setu/__init__.py +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -189,7 +189,7 @@ async def _( if result := SetuManage.get_luo(float(user.impression)): await result.finish() is_r18 = arparma.find("r") - _num = num.result + _num = num.result if num.available else 1 if is_r18 and gid: """群聊中禁止查看r18""" if not base_config.get("ALLOW_GROUP_R18"): diff --git a/zhenxun/plugins/statistics/__init__.py b/zhenxun/plugins/statistics/__init__.py new file mode 100644 index 00000000..bd99a86f --- /dev/null +++ b/zhenxun/plugins/statistics/__init__.py @@ -0,0 +1,131 @@ +import os +from pathlib import Path + +import nonebot +import ujson as json + +from zhenxun.configs.path_config import DATA_PATH + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) + +old_file1 = DATA_PATH / "_prefix_count.json" +old_file2 = DATA_PATH / "_prefix_user_count.json" +new_path = DATA_PATH / "statistics" +new_path.mkdir(parents=True, exist_ok=True) +if old_file1.exists(): + os.rename(old_file1, new_path / "_prefix_count.json") +if old_file2.exists(): + os.rename(old_file2, new_path / "_prefix_user_count.json") + + +# 修改旧数据 + +statistics_group_file = DATA_PATH / "statistics" / "_prefix_count.json" +statistics_user_file = DATA_PATH / "statistics" / "_prefix_user_count.json" + +for file in [statistics_group_file, statistics_user_file]: + if file.exists(): + with open(file, "r", encoding="utf8") as f: + data = json.load(f) + if not (statistics_group_file.parent / f"{file}.bak").exists(): + with open(f"{file}.bak", "w", encoding="utf8") as wf: + json.dump(data, wf, ensure_ascii=False, indent=4) + for x in ["total_statistics", "day_statistics"]: + for key in data[x].keys(): + num = 0 + if data[x][key].get("ai") is not None: + if data[x][key].get("Ai") is not None: + data[x][key]["Ai"] += data[x][key]["ai"] + else: + data[x][key]["Ai"] = data[x][key]["ai"] + del data[x][key]["ai"] + if data[x][key].get("抽卡") is not None: + if data[x][key].get("游戏抽卡") is not None: + data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] + else: + data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] + del data[x][key]["抽卡"] + if data[x][key].get("我的道具") is not None: + num += data[x][key]["我的道具"] + del data[x][key]["我的道具"] + if data[x][key].get("使用道具") is not None: + num += data[x][key]["使用道具"] + del data[x][key]["使用道具"] + if data[x][key].get("我的金币") is not None: + num += data[x][key]["我的金币"] + del data[x][key]["我的金币"] + if data[x][key].get("购买") is not None: + num += data[x][key]["购买"] + del data[x][key]["购买"] + if data[x][key].get("商店") is not None: + data[x][key]["商店"] += num + else: + data[x][key]["商店"] = num + for x in ["week_statistics", "month_statistics"]: + for key in data[x].keys(): + if key == "total": + if data[x][key].get("ai") is not None: + if data[x][key].get("Ai") is not None: + data[x][key]["Ai"] += data[x][key]["ai"] + else: + data[x][key]["Ai"] = data[x][key]["ai"] + del data[x][key]["ai"] + if data[x][key].get("抽卡") is not None: + if data[x][key].get("游戏抽卡") is not None: + data[x][key]["游戏抽卡"] += data[x][key]["抽卡"] + else: + data[x][key]["游戏抽卡"] = data[x][key]["抽卡"] + del data[x][key]["抽卡"] + if data[x][key].get("我的道具") is not None: + num += data[x][key]["我的道具"] + del data[x][key]["我的道具"] + if data[x][key].get("使用道具") is not None: + num += data[x][key]["使用道具"] + del data[x][key]["使用道具"] + if data[x][key].get("我的金币") is not None: + num += data[x][key]["我的金币"] + del data[x][key]["我的金币"] + if data[x][key].get("购买") is not None: + num += data[x][key]["购买"] + del data[x][key]["购买"] + if data[x][key].get("商店") is not None: + data[x][key]["商店"] += num + else: + data[x][key]["商店"] = num + else: + for day in data[x][key].keys(): + num = 0 + if data[x][key][day].get("ai") is not None: + if data[x][key][day].get("Ai") is not None: + data[x][key][day]["Ai"] += data[x][key][day]["ai"] + else: + data[x][key][day]["Ai"] = data[x][key][day]["ai"] + del data[x][key][day]["ai"] + if data[x][key][day].get("抽卡") is not None: + if data[x][key][day].get("游戏抽卡") is not None: + data[x][key][day]["游戏抽卡"] += data[x][key][day][ + "抽卡" + ] + else: + data[x][key][day]["游戏抽卡"] = data[x][key][day][ + "抽卡" + ] + del data[x][key][day]["抽卡"] + if data[x][key][day].get("我的道具") is not None: + num += data[x][key][day]["我的道具"] + del data[x][key][day]["我的道具"] + if data[x][key][day].get("使用道具") is not None: + num += data[x][key][day]["使用道具"] + del data[x][key][day]["使用道具"] + if data[x][key][day].get("我的金币") is not None: + num += data[x][key][day]["我的金币"] + del data[x][key][day]["我的金币"] + if data[x][key][day].get("购买") is not None: + num += data[x][key][day]["购买"] + del data[x][key][day]["购买"] + if data[x][key][day].get("商店") is not None: + data[x][key][day]["商店"] += num + else: + data[x][key][day]["商店"] = num + with open(file, "w", encoding="utf8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) diff --git a/zhenxun/plugins/statistics/_data_source.py b/zhenxun/plugins/statistics/_data_source.py new file mode 100644 index 00000000..526d4713 --- /dev/null +++ b/zhenxun/plugins/statistics/_data_source.py @@ -0,0 +1,131 @@ +from datetime import datetime, timedelta + +from tortoise.functions import Count + +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType + + +class StatisticsManage: + + @classmethod + async def get_statistics( + cls, + plugin_name: str | None, + is_global: bool, + search_type: str | None, + user_id: str | None = None, + group_id: str | None = None, + ): + day = None + day_type = "" + if search_type == "day": + day = 1 + day_type = "日" + if search_type == "week": + day = 7 + day_type = "周" + if search_type == "month": + day = 30 + day_type = "月" + if day_type: + day_type += f"({day}天)" + title = "" + if user_id: + """查用户""" + query = GroupInfoUser.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + user = await query.first() + title = f"{user.user_name if user else user_id} {day_type}功能调用统计" + elif group_id: + """查群组""" + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) + title = f"{group.group_name if group else group_id} {day_type}功能调用统计" + else: + title = "功能调用统计" + if is_global and not user_id: + title = "全局 " + title + return await cls.get_global_statistics(plugin_name, day, title) + if user_id: + return await cls.get_my_statistics(user_id, group_id, day, title) + if group_id: + return await cls.get_group_statistics(group_id, day, title) + return None + + @classmethod + async def get_global_statistics( + cls, plugin_name: str | None, day: int | None, title: str + ) -> BuildImage | str: + query = Statistics + if plugin_name: + query = query.filter(plugin_name=plugin_name) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def get_my_statistics( + cls, user_id: str, group_id: str | None, day: int | None, title: str + ): + query = Statistics.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def get_group_statistics(cls, group_id: str, day: int | None, title: str): + query = Statistics.filter(group_id=group_id) + if day: + time = datetime.now() - timedelta(days=day) + query = query.filter(create_time__gte=time) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .values_list("plugin_name", "count") + ) + if not data_list: + return "统计数据为空..." + return await cls.__build_image(data_list, title) + + @classmethod + async def __build_image(cls, data_list: list[tuple[str, int]], title: str): + mat = BuildMat(MatType.BARH) + module2count = {x[0]: x[1] for x in data_list} + plugin_info = await PluginInfo.filter( + module__in=module2count.keys(), plugin_type=PluginType.NORMAL + ).all() + x_index = [] + data = [] + for plugin in plugin_info: + x_index.append(plugin.name) + data.append(module2count.get(plugin.module, 0)) + mat.x_index = x_index + mat.data = data + mat.title = title + return await mat.build() diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py new file mode 100644 index 00000000..e95e3b44 --- /dev/null +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -0,0 +1,170 @@ +import asyncio +import os + +import ujson as json +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import ( + Alconna, + Args, + Arparma, + Match, + Option, + on_alconna, + store_true, +) +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.group_info import GroupInfo +from zhenxun.utils.depends import OneCommand +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildMat + +from ._data_source import StatisticsManage + +__plugin_meta__ = PluginMetadata( + name="功能调用统计可视化", + description="功能调用统计可视化", + usage=""" + usage: + 功能调用统计可视化 + 指令: + 功能调用统计 + 日功能调用统计 + 周功能调用统计 + 月功能调用统计 + 我的功能调用统计 : 当前群我的统计 + 我的功能调用统计 -g: 我的全局统计 + 我的日功能调用统计 + 我的周功能调用统计 + 我的月功能调用统计 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.ADMIN, + menu_type="数据统计", + aliases={"功能调用统计"}, + superuser_help=""" + "全局功能调用统计", + "全局日功能调用统计", + "全局周功能调用统计", + "全局月功能调用统计", + """.strip(), + ).dict(), +) + + +_matcher = on_alconna( + Alconna( + "功能调用统计", + Args["name?", str], + Option("-g|--global", action=store_true, help_text="全局统计"), + Option("-my", action=store_true, help_text="我的"), + Option("-t|--type", Args["search_type", ["day", "week", "month"]]), + ), + priority=5, + block=True, +) + +_matcher.shortcut( + "日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day"], + prefix=True, +) + +_matcher.shortcut( + "周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week"], + prefix=True, +) + +_matcher.shortcut( + "月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month"], + prefix=True, +) + +_matcher.shortcut( + "全局功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week", "-g"], + prefix=True, +) + +_matcher.shortcut( + "全局月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month", "-g"], + prefix=True, +) + +_matcher.shortcut( + "我的功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的日功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "day", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的周功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "week", "-my"], + prefix=True, +) + +_matcher.shortcut( + "我的月功能调用统计(?P.*)", + command="功能调用统计", + arguments=["{name}", "-t", "month", "-my"], + prefix=True, +) + + +@_matcher.handle() +async def _( + session: EventSession, arparma: Arparma, name: Match[str], search_type: Match[str] +): + plugin_name = name.result if name.available else None + st = search_type.result if search_type.available else None + uid = session.id1 if arparma.find("my") else None + gid = session.id3 or session.id2 + is_global = arparma.find("global") + if uid and is_global: + """个人全局""" + gid = None + if result := await StatisticsManage.get_statistics( + plugin_name, arparma.find("global"), st, uid, gid + ): + if isinstance(result, str): + await Text(result).finish(reply=True) + else: + await Image(result.pic2bytes()).send() + else: + await Text("获取数据失败...").send() diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/plugins/statistics/statistics_hook.py new file mode 100644 index 00000000..27062a4a --- /dev/null +++ b/zhenxun/plugins/statistics/statistics_hook.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.matcher import Matcher +from nonebot.message import run_postprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.utils.enum import PluginType + +__plugin_meta__ = PluginMetadata( + name="功能调用统计", + description="功能调用统计", + usage="""""".strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + ).dict(), +) + + +@run_postprocessor +async def _( + matcher: Matcher, exception: Exception | None, bot: Bot, session: EventSession +): + plugin = await PluginInfo.get_or_none(module=matcher.plugin_name) + plugin_type = plugin.plugin_type if plugin else None + if ( + plugin_type == PluginType.NORMAL + and matcher.priority not in [1, 999] + and matcher.plugin_name not in ["update_info", "statistics_handle"] + ): + await Statistics.create( + user_id=session.id1, + group_id=session.id3 or session.id2, + plugin_name=matcher.plugin_name, + create_time=datetime.now(), + ) diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py index 1506e655..4f22d923 100644 --- a/zhenxun/utils/_build_mat.py +++ b/zhenxun/utils/_build_mat.py @@ -33,7 +33,7 @@ class BuildMatData(BaseModel): """显示轴坐标值""" y_index: list[int | float] = [] """数据轴坐标值""" - space: tuple[int, int] = (15, 15) + space: tuple[int, int] = (20, 20) """坐标值间隔(X, Y)""" rotate: tuple[int, int] = (0, 0) """坐标值旋转(X, Y)""" @@ -68,9 +68,13 @@ class BuildMat: mark_image: BuildImage """BuildImage""" x_height: int - """横坐标高度""" + """横坐标开始高度""" + y_width: int + """纵坐标开始宽度""" x_point: list[int] """横坐标坐标""" + y_point: list[int] + """纵坐标坐标""" graph_height: int """坐标轴高度""" @@ -233,9 +237,9 @@ class BuildMat: else: raise ValueError("y轴坐标值必须有序...") - async def build(self): + async def build(self) -> BuildImage: """构造图片""" - A = None + A = BuildImage(1, 1) bar_color = self.build_data.bar_color if "*" in bar_color: bar_color = [ @@ -252,12 +256,12 @@ class BuildMat: if self.build_data.mat_type == MatType.LINE: mark_image = await self._build_line_graph(init_graph, bar_color) if self.build_data.mat_type == MatType.BAR: - pass + mark_image = await self._build_bar_graph(init_graph, bar_color) if self.build_data.mat_type == MatType.BARH: - pass + mark_image = await self._build_barh_graph(init_graph, bar_color) if mark_image: padding_width, padding_height = self.build_data.padding - width = mark_image.width + padding_width * 2 + width = mark_image.width + padding_width height = mark_image.height + padding_height * 2 if self.build_data.background: if isinstance(self.build_data.background, bytes): @@ -269,7 +273,7 @@ class BuildMat: else: A = BuildImage(width, height, self.build_data.background_color) if A: - await A.paste(mark_image, (padding_width, padding_height)) + await A.paste(mark_image, (10, padding_height)) if self.build_data.title: font = BuildImage.load_font( self.build_data.font, self.build_data.font_size + 7 @@ -305,108 +309,169 @@ class BuildMat: padding_width = 0 padding_height = 0 font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) - width_list = [] - height_list = [] + x_width_list = [] + y_height_list = [] for x in self.build_data.x_index: text_size = BuildImage.get_text_size(x, font) if text_size[1] > padding_height: padding_height = text_size[1] - width_list.append(text_size[0]) + x_width_list.append(text_size) if not self.build_data.y_index: """没有指定y_index时,使用data自动生成""" max_num = max(self.build_data.data) + if max_num < 5: + max_num = 5 s = int(max_num / 5) _y_index = [max_num] for _n in range(4): max_num -= s _y_index.append(max_num) _y_index.sort() - self.build_data.y_index = _y_index + # if len(_y_index) > 1: + # if _y_index[0] == _y_index[-1]: + # _tmp = ["_" for _ in range(len(_y_index) - 1)] + # _tmp.append(str(_y_index[0])) + # _y_index = _tmp + self.build_data.y_index = _y_index # type: ignore for item in self.build_data.y_index: text_size = BuildImage.get_text_size(str(item), font) if text_size[0] > padding_width: padding_width = text_size[0] - height_list.append(text_size[1]) - width = ( - sum([w + self.build_data.space[0] for w in width_list]) - + height_list[0] - + self.build_data.space[0] * 2 - + 20 - ) + y_height_list.append(text_size) + if self.build_data.mat_type == MatType.BARH: + _tmp = x_width_list + x_width_list = y_height_list + y_height_list = _tmp + old_space = self.build_data.space + width = padding_width * 2 + self.build_data.space[0] * 2 + 20 height = ( - sum([h + self.build_data.space[1] for h in height_list]) + sum([h[1] + self.build_data.space[1] for h in y_height_list]) + self.build_data.space[1] * 2 + 30 ) + _x_index = self.build_data.x_index + _y_index = self.build_data.y_index + _barh_max_text_width = 0 if self.build_data.mat_type == MatType.BARH: - """横向柱状图时xy轴长度调换""" - _tmp = height - height = width - width = _tmp + """XY轴下标互换""" + _tmp = _y_index + _y_index = _x_index + _x_index = _tmp + """额外增加字体宽度""" + for s in self.build_data.x_index: + s_w, s_h = BuildImage.get_text_size(s, font) + if s_w > _barh_max_text_width: + _barh_max_text_width = s_w + width += _barh_max_text_width + width += self.build_data.space[0] * 2 - old_space[0] * 2 + """X轴重新等均分配""" + x_length = width - padding_width * 2 - _barh_max_text_width + x_space = int((x_length - 20) / (len(_x_index) + 1)) + if x_space < 50: + """加大间距更加美观""" + x_space = 50 + self.build_data.space = (x_space, self.build_data.space[1]) + width += self.build_data.space[0] * (len(_x_index) - 1) + else: + """非横向柱状图时加字体宽度""" + width += sum([w[0] + self.build_data.space[0] for w in x_width_list]) + A = BuildImage( - width, + width + 5, (height + 10), + # color=(255, 255, 255), color=(255, 255, 255, 0), ) padding_height += 5 + """高""" await A.line( ( - padding_width + 5, + padding_width + 5 + _barh_max_text_width, padding_height, - padding_width + 5, + padding_width + 5 + _barh_max_text_width, height - padding_height, ), width=2, ) + """长""" await A.line( ( - padding_width + 5, + padding_width + 5 + _barh_max_text_width, height - padding_height, width - padding_width + 5, height - padding_height, ), width=2, ) - _x_index = self.build_data.x_index - _y_index = self.build_data.y_index - if self.build_data.mat_type == MatType.BARH: - _tmp = _y_index - _y_index = _x_index - _x_index = _tmp - cur_width = padding_width + self.build_data.space[0] * 2 - cur_height = height - height_list[0] - 5 + x_cur_width = ( + padding_width + _barh_max_text_width + self.build_data.space[0] + 5 + ) + if self.build_data.mat_type != MatType.BARH: + """添加字体宽度""" + x_cur_width += x_width_list[0][0] + x_cur_height = height - y_height_list[0][1] - 5 + # await A.point((x_cur_width, x_cur_height), (0, 0, 0)) x_point = [] for i, _x in enumerate(_x_index): """X轴数值""" - grid_height = cur_height + grid_height = x_cur_height if self.build_data.is_grid: grid_height = padding_height - await A.line((cur_width, cur_height - 1, cur_width, grid_height - 5)) - x_point.append(cur_width - 1) - mid_point = cur_width - int(width_list[i] / 2) - await A.text((mid_point, cur_height), str(_x), font=font) - cur_width += width_list[i] + self.build_data.space[0] - cur_width = padding_width - cur_height = height - self.build_data.padding[1] + await A.line( + ( + x_cur_width, + x_cur_height - 1, + x_cur_width, + grid_height - 5, + ) + ) + x_point.append(x_cur_width - 1) + mid_point = x_cur_width - int(x_width_list[i][0] / 2) + await A.text((mid_point, x_cur_height), str(_x), font=font) + x_cur_width += self.build_data.space[0] + if self.build_data.mat_type != MatType.BARH: + """添加字体宽度""" + x_cur_width += x_width_list[i][0] + y_cur_width = padding_width + _barh_max_text_width + y_cur_height = height - self.build_data.padding[1] - 9 + start_height = y_cur_height + # await A.point((y_cur_width, y_cur_height), (0, 0, 0)) + y_point = [] for i, _y in enumerate(_y_index): """Y轴数值""" - grid_width = cur_width + grid_width = y_cur_width if self.build_data.is_grid: grid_width = width - padding_width + 5 - await A.line((cur_width + 5, cur_height, grid_width + 11, cur_height)) + y_point.append(y_cur_height) + await A.line((y_cur_width + 5, y_cur_height, grid_width + 11, y_cur_height)) text_width = BuildImage.get_text_size(str(_y), font)[0] await A.text( - (cur_width - text_width, cur_height - int(height_list[i] / 2) - 3), + ( + y_cur_width - text_width, + y_cur_height - int(y_height_list[i][1] / 2) - 3, + ), str(_y), font=font, ) - cur_height -= height_list[i] + self.build_data.space[1] - graph_height = height - self.build_data.padding[1] - cur_height + 5 + y_cur_height -= y_height_list[i][1] + self.build_data.space[1] + graph_height = 0 + if self.build_data.mat_type == MatType.BARH: + graph_height = ( + x_cur_width + - self.build_data.space[0] + - _barh_max_text_width + - padding_width + - 5 + ) + else: + graph_height = start_height - y_cur_height + 7 return self.InitGraph( mark_image=A, - x_height=height - height_list[0] - 5, + x_height=height - y_height_list[0][1] - 5, + y_width=padding_width + 5 + _barh_max_text_width, graph_height=graph_height, x_point=x_point, + y_point=y_point, ) async def _build_line_graph( @@ -432,9 +497,9 @@ class BuildMat: point_list = [] for x_p, y in zip(init_graph.x_point, self.build_data.data): """折线图标点""" - y_height = int(y / max_num * init_graph.graph_height) - await mark_image.paste(_black_point, (x_p, x_height - y_height)) - point_list.append((x_p + 4, x_height - y_height + 4)) + y_height = int(y / max_num * graph_height) + await mark_image.paste(_black_point, (x_p - 3, x_height - y_height)) + point_list.append((x_p + 1, x_height - y_height + 1)) for i in range(len(point_list) - 1): """画线""" a_x, a_y = point_list[i] @@ -462,8 +527,41 @@ class BuildMat: ) return mark_image - async def _build_bar_graph(self): + async def _build_bar_graph(self, init_graph: InitGraph, bar_color: list[str]): + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 折线图 + """ pass - async def _build_barh_graph(self): - pass + async def _build_barh_graph(self, init_graph: InitGraph, bar_color: list[str]): + """构建折线图 + + 参数: + init_graph: InitGraph + bar_color: 颜色列表 + + 返回: + BuildImage: 横向柱状图 + """ + font = BuildImage.load_font(self.build_data.font, self.build_data.font_size) + mark_image = init_graph.mark_image + y_width = init_graph.y_width + graph_height = init_graph.graph_height + random_color = random.choice(bar_color) + max_num = max(self.y_index) + for y_p, y in zip(init_graph.y_point, self.build_data.data): + bar_width = int(y / max_num * graph_height) + bar = BuildImage(bar_width, 18, random_color) + await mark_image.paste(bar, (y_width + 1, y_p - 9)) + if self.build_data.display_num: + """显示数值""" + await mark_image.text( + (y_width + bar_width + 5, y_p - 12), str(y), font=font + ) + return mark_image From 876bba479cadae0ad152856bb8cbb05d7cdb3451 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 27 May 2024 16:09:52 +0800 Subject: [PATCH 037/132] =?UTF-8?q?feat=E2=9C=A8:=20=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/statistics/statistics_handle.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py index e95e3b44..862b6595 100644 --- a/zhenxun/plugins/statistics/statistics_handle.py +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -1,7 +1,3 @@ -import asyncio -import os - -import ujson as json from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import ( Alconna, @@ -15,12 +11,8 @@ from nonebot_plugin_alconna import ( from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH from zhenxun.configs.utils import PluginExtraData -from zhenxun.models.group_info import GroupInfo -from zhenxun.utils.depends import OneCommand from zhenxun.utils.enum import PluginType -from zhenxun.utils.image_utils import BuildMat from ._data_source import StatisticsManage From 126dbc39b450b41e5c69b13e662d42ff82b24e29 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 27 May 2024 23:26:24 +0800 Subject: [PATCH 038/132] =?UTF-8?q?feat=E2=9C=A8:=20=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/translate/__init__.py | 91 ++++++++++++++++++++++++ zhenxun/plugins/translate/data_source.py | 83 +++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 zhenxun/plugins/translate/__init__.py create mode 100644 zhenxun/plugins/translate/data_source.py diff --git a/zhenxun/plugins/translate/__init__.py b/zhenxun/plugins/translate/__init__.py new file mode 100644 index 00000000..62dc15c9 --- /dev/null +++ b/zhenxun/plugins/translate/__init__.py @@ -0,0 +1,91 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import CheckConfig +from zhenxun.utils.image_utils import ImageTemplate + +from .data_source import language, translate_message + +__plugin_meta__ = PluginMetadata( + name="翻译", + description="出国旅游好助手", + usage=""" + 指令: + 翻译语种: (查看soruce与to可用值,代码与中文都可) + 示例: + 翻译 你好: 将中文翻译为英文 + 翻译 Hello: 将英文翻译为中文 + + 翻译 你好 -to 希腊语: 将"你好"翻译为希腊语 + 翻译 你好: 允许form和to使用中文 + 翻译 你好 -form:中文 to:日语 你好: 指定原语种并将"你好"翻译为日文 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="一些工具", + configs=[ + RegisterConfig(key="APPID", value=None, help="百度翻译APPID"), + RegisterConfig(key="SECRET_KEY", value=None, help="百度翻译秘钥"), + ], + ).dict(), +) + +_matcher = on_alconna( + Alconna( + "翻译", + Args["text", str], + Option("-s|--source", Args["source_text", str, "auto"]), + Option("-t|--to", Args["to_text", str, "auto"]), + ), + priority=5, + block=True, +) + +_language_matcher = on_alconna(Alconna("翻译语种"), priority=5, block=True) + + +@_language_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + s = "" + column_list = ["语种", "代码"] + data_list = [] + for key, value in language.items(): + data_list.append([key, value]) + image = await ImageTemplate.table_page("翻译语种", "", column_list, data_list) + await Image(image.pic2bytes()).send() + logger.info(f"查看翻译语种", arparma.header_result, session=session) + + +@_matcher.handle( + parameterless=[ + CheckConfig(config="APPID"), + CheckConfig(config="SECRET_KEY"), + ] +) +async def _( + session: EventSession, + arparma: Arparma, + text: str, + source_text: Match[str], + to_text: Match[str], +): + source = source_text.result if source_text.available else "auto" + to = to_text.result if to_text.available else "auto" + values = language.values() + keys = language.keys() + if source not in values and source not in keys: + await Text("源语种不支持...").finish() + if to not in values and to not in keys: + await Text("目标语种不支持...").finish() + result = await translate_message(text, source, to) + await Text(result).send(reply=True) + logger.info( + f"source: {source}, to: {to}, 翻译: {text}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/translate/data_source.py b/zhenxun/plugins/translate/data_source.py new file mode 100644 index 00000000..a7a3018d --- /dev/null +++ b/zhenxun/plugins/translate/data_source.py @@ -0,0 +1,83 @@ +import time +from hashlib import md5 + +from zhenxun.configs.config import Config +from zhenxun.utils.http_utils import AsyncHttpx + +URL = "http://api.fanyi.baidu.com/api/trans/vip/translate" + + +language = { + "自动": "auto", + "粤语": "yue", + "韩语": "kor", + "泰语": "th", + "葡萄牙语": "pt", + "希腊语": "el", + "保加利亚语": "bul", + "芬兰语": "fin", + "斯洛文尼亚语": "slo", + "繁体中文": "cht", + "中文": "zh", + "文言文": "wyw", + "法语": "fra", + "阿拉伯语": "ara", + "德语": "de", + "荷兰语": "nl", + "爱沙尼亚语": "est", + "捷克语": "cs", + "瑞典语": "swe", + "越南语": "vie", + "英语": "en", + "日语": "jp", + "西班牙语": "spa", + "俄语": "ru", + "意大利语": "it", + "波兰语": "pl", + "丹麦语": "dan", + "罗马尼亚语": "rom", + "匈牙利语": "hu", +} + + +async def translate_message(word: str, form: str, to: str) -> str: + """翻译 + + 参数: + word (str): 翻译文字 + form (str): 源语言 + to (str): 目标语言 + + 返回: + str: 翻译后的文字 + """ + if form in language: + form = language[form] + if to in language: + to = language[to] + salt = str(time.time()) + app_id = Config.get_config("translate", "APPID") + secret_key = Config.get_config("translate", "SECRET_KEY") + sign = app_id + word + salt + secret_key # type: ignore + md5_ = md5() + md5_.update(sign.encode("utf-8")) + sign = md5_.hexdigest() + params = { + "q": word, + "from": form, + "to": to, + "appid": app_id, + "salt": salt, + "sign": sign, + } + url = URL + "?" + for key, value in params.items(): + url += f"{key}={value}&" + url = url[:-1] + resp = await AsyncHttpx.get(url) + data = resp.json() + if data.get("error_code"): + return data.get("error_msg") + if trans_result := data.get("trans_result"): + return trans_result[0]["dst"] + return "没有找到翻译捏..." From 55db970cded4a779ce0c9eddaa3b5e4a199933d5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 28 May 2024 02:18:50 +0800 Subject: [PATCH 039/132] =?UTF-8?q?feat=E2=9C=A8:=20=E5=BE=AE=E5=8D=9A?= =?UTF-8?q?=E7=83=AD=E6=90=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/coser.py | 2 +- zhenxun/plugins/wbtop/__init__.py | 56 +++++++++++++++++++++++++ zhenxun/plugins/wbtop/data_source.py | 63 ++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/wbtop/__init__.py create mode 100644 zhenxun/plugins/wbtop/data_source.py diff --git a/zhenxun/plugins/coser.py b/zhenxun/plugins/coser.py index 01a2aba7..a3194c11 100644 --- a/zhenxun/plugins/coser.py +++ b/zhenxun/plugins/coser.py @@ -4,7 +4,7 @@ from typing import Tuple from nonebot.adapters import Bot from nonebot.params import RegexGroup from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession diff --git a/zhenxun/plugins/wbtop/__init__.py b/zhenxun/plugins/wbtop/__init__.py new file mode 100644 index 00000000..60ab1e37 --- /dev/null +++ b/zhenxun/plugins/wbtop/__init__.py @@ -0,0 +1,56 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncPlaywright + +from .data_source import get_hot_image + +__plugin_meta__ = PluginMetadata( + name="微博热搜", + description="刚买完瓜,在吃瓜现场", + usage=""" + 指令: + 微博热搜:发送实时热搜 + 微博热搜 [id]:截图该热搜页面 + 示例:微博热搜 5 + """.strip(), + extra=PluginExtraData( + author="HibiKier & yajiwa", + version="0.1", + ).dict(), +) + + +_matcher = on_alconna(Alconna("微博热搜", Args["idx?", int]), priority=5, block=True) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, idx: Match[int]): + result, data_list = await get_hot_image() + if isinstance(result, str): + await Text(result).finish(reply=True) + if idx.available: + _idx = idx.result + url = data_list[_idx - 1]["url"] + file = IMAGE_PATH / "temp" / f"wbtop_{session.id1}.png" + img = await AsyncPlaywright.screenshot( + url, + file, + "#pl_feed_main", + wait_time=12, + ) + if img: + await Image(file).send() + logger.info( + f"查询微博热搜 Id: {_idx}", arparma.header_result, session=session + ) + else: + await Text("获取图片失败...").send() + else: + await Image(result.pic2bytes()).send() + logger.info(f"查询微博热搜", arparma.header_result, session=session) diff --git a/zhenxun/plugins/wbtop/data_source.py b/zhenxun/plugins/wbtop/data_source.py new file mode 100644 index 00000000..c734b1ec --- /dev/null +++ b/zhenxun/plugins/wbtop/data_source.py @@ -0,0 +1,63 @@ +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage + +URL = "https://weibo.com/ajax/side/hotSearch" + + +async def get_data() -> list | str: + """获取数据 + + 返回: + list | str: 数据或消息 + """ + data_list = [] + for _ in range(3): + try: + response = await AsyncHttpx.get(URL, timeout=20) + if response.status_code == 200: + data_json = response.json()["data"]["realtime"] + for item in data_json: + if "is_ad" in item: + """广告跳过""" + continue + data = { + "hot_word": item["note"], + "hot_word_num": str(item["num"]), + "url": "https://s.weibo.com/weibo?q=%23" + item["word"] + "%23", + } + data_list.append(data) + if not data: + return "没有搜索到..." + return data_list + except Exception as e: + logger.error("获取微博热搜错误", e=e) + return "获取失败,请十分钟后再试..." + + +async def get_hot_image() -> tuple[BuildImage | str, list]: + """构造图片 + + 返回: + BuildImage | str: 热搜图片 + """ + data = await get_data() + if isinstance(data, str): + return data, [] + bk = BuildImage(700, 32 * 50 + 280, color="#797979") + wbtop_bk = BuildImage(700, 280, background=f"{IMAGE_PATH}/other/webtop.png") + await bk.paste(wbtop_bk) + text_bk = BuildImage(700, 32 * 50, color="#797979") + image_list = [] + for i, data in enumerate(data): + title = f"{i + 1}. {data['hot_word']}" + hot = str(data["hot_word_num"]) + img = BuildImage(700, 30, font_size=20) + w, h = img.getsize(title) + await img.text((10, int((30 - h) / 2)), title) + await img.text((580, int((30 - h) / 2)), hot) + image_list.append(img) + text_bk = await text_bk.auto_paste(image_list, 1, 2, 0) + await bk.paste(text_bk, (0, 280)) + return bk, data From 215624ad3393f7b24bdbc68494a9f0533a29bb97 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 28 May 2024 03:24:32 +0800 Subject: [PATCH 040/132] =?UTF-8?q?feat=E2=9C=A8:=20=E8=AF=86=E7=95=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/search_image/__init__.py | 6 +-- zhenxun/plugins/what_anime/__init__.py | 58 +++++++++++++++++++++++ zhenxun/plugins/what_anime/data_source.py | 47 ++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 zhenxun/plugins/what_anime/__init__.py create mode 100644 zhenxun/plugins/what_anime/data_source.py diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py index fce64046..f3eb1102 100644 --- a/zhenxun/plugins/search_image/__init__.py +++ b/zhenxun/plugins/search_image/__init__.py @@ -51,13 +51,13 @@ async def get_image_info(mod: str, url: str) -> str | list[Image | Text] | None: @_matcher.handle() -async def _(mode: Match[str], img: Match[alcImg]): +async def _(mode: Match[str], image: Match[alcImg]): if mode.available: _matcher.set_path_arg("mode", mode.result) else: _matcher.set_path_arg("mode", "saucenao") - if img.available: - _matcher.set_path_arg("image", img.result) + if image.available: + _matcher.set_path_arg("image", image.result) @_matcher.got_path("image", prompt="图来!") diff --git a/zhenxun/plugins/what_anime/__init__.py b/zhenxun/plugins/what_anime/__init__.py new file mode 100644 index 00000000..4881f6dc --- /dev/null +++ b/zhenxun/plugins/what_anime/__init__.py @@ -0,0 +1,58 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import Match, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from .data_source import get_anime + +__plugin_meta__ = PluginMetadata( + name="识番", + description="以图识番", + usage=""" + usage: + api.trace.moe 以图识番 + 指令: + 识番 [图片] + """.strip(), + extra=PluginExtraData( + author="HibiKier", version="0.1", menu_type="一些工具" + ).dict(), +) + + +_matcher = on_alconna(Alconna("识番", Args["image?", alcImg]), block=True, priority=5) + + +@_matcher.handle() +async def _(image: Match[alcImg]): + if image.available: + _matcher.set_path_arg("image", image.result) + + +@_matcher.got_path("image", prompt="图来!") +async def _( + session: EventSession, + arparma: Arparma, + image: alcImg, +): + if not image.url: + await Text("图片url为空...").finish() + await Text("开始识别...").send() + anime_data_report = await get_anime(image.url) + if anime_data_report: + await Text(anime_data_report).send(reply=True) + logger.info( + f" 识番 {image.url} --> {anime_data_report}", + arparma.header_result, + session=session, + ) + else: + logger.info( + f"识番 {image.url} 未找到...", arparma.header_result, session=session + ) + await Text(f"没有寻找到该番剧,果咩..").send(reply=True) diff --git a/zhenxun/plugins/what_anime/data_source.py b/zhenxun/plugins/what_anime/data_source.py new file mode 100644 index 00000000..15801f62 --- /dev/null +++ b/zhenxun/plugins/what_anime/data_source.py @@ -0,0 +1,47 @@ +import time + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + + +async def get_anime(anime: str) -> str: + s_time = time.time() + url = "https://api.trace.moe/search?anilistInfo&url={}".format(anime) + logger.debug("[info]Now starting get the {}".format(url)) + try: + anime_json = (await AsyncHttpx.get(url)).json() + if not anime_json["error"]: + if anime_json == "Error reading imagenull": + return "图像源错误,注意必须是静态图片哦" + repass = "" + # 拿到动漫 中文名 + for anime in anime_json["result"][:5]: + synonyms = anime["anilist"]["synonyms"] + for x in synonyms: + _count_ch = 0 + for word in x: + if "\u4e00" <= word <= "\u9fff": + _count_ch += 1 + if _count_ch > 3: + anime_name = x + break + else: + anime_name = anime["anilist"]["title"]["native"] + episode = anime["episode"] + from_ = int(anime["from"]) + m, s = divmod(from_, 60) + similarity = anime["similarity"] + putline = "[ {} ][{}][{}:{}] 相似度:{:.2%}".format( + anime_name, + episode if episode else "?", + m, + s, + similarity, + ) + repass += putline + "\n" + return f"耗时 {int(time.time() - s_time)} 秒\n" + repass[:-1] + else: + return f'访问错误 error:{anime_json["error"]}' + except Exception as e: + logger.error(f"识番发生错误", e=e) + return "发生了奇怪的错误,那就没办法了,再试一次?" From c3b2a3b623ce3d752d9dc54e9fdda3e67911c258 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 28 May 2024 03:41:32 +0800 Subject: [PATCH 041/132] =?UTF-8?q?feat=E2=9C=A8:=20=E5=85=B3=E4=BA=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/about.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 zhenxun/plugins/about.py diff --git a/zhenxun/plugins/about.py b/zhenxun/plugins/about.py new file mode 100644 index 00000000..35a0d237 --- /dev/null +++ b/zhenxun/plugins/about.py @@ -0,0 +1,42 @@ +from pathlib import Path + +from nonebot import on_regex +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +__plugin_meta__ = PluginMetadata( + name="识番", + description="想要更加了解真寻吗", + usage=""" + 指令: + 关于 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").dict(), +) + + +_matcher = on_alconna(Alconna("关于"), priority=5, block=True, rule=to_me()) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + ver_file = Path() / "__version__" + version = None + if ver_file.exists(): + with open(ver_file, "r", encoding="utf8") as f: + version = f.read().split(":")[-1].strip() + info = f""" +『绪山真寻Bot』 +版本:{version} +简介:基于Nonebot2开发,支持多平台,是一个非常可爱的Bot呀,希望与大家要好好相处 +项目地址:https://github.com/HibiKier/zhenxun_bot +文档地址:https://hibikier.github.io/zhenxun_bot/ + """.strip() + await Text(info).send() + logger.info("查看关于", arparma.header_result, session=session) From fabeb4711fdeef6a8eeb41b111de0d9c9abb027c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 28 May 2024 14:22:24 +0800 Subject: [PATCH 042/132] =?UTF-8?q?feat=E2=9C=A8:=20=E5=A4=8D=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 121 +++++++++++++++++++++++- pyproject.toml | 1 + zhenxun/plugins/fudu.py | 172 +++++++++++++++++++++++++++++++++++ zhenxun/utils/image_utils.py | 17 ++++ 4 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/fudu.py diff --git a/poetry.lock b/poetry.lock index f1770cb4..2c7c7abd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -910,6 +910,28 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "imagehash" +version = "4.3.1" +description = "Image Hashing library" +optional = false +python-versions = "*" +files = [ + {file = "ImageHash-4.3.1-py2.py3-none-any.whl", hash = "sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5"}, + {file = "ImageHash-4.3.1.tar.gz", hash = "sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70"}, +] + +[package.dependencies] +numpy = "*" +pillow = "*" +PyWavelets = "*" +scipy = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "iso8601" version = "1.1.0" @@ -2225,6 +2247,56 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pywavelets" +version = "1.6.0" +description = "PyWavelets, wavelet transform module" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pywavelets-1.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ddc1ff5ad706313d930f857f9656f565dfb81b85bbe58a9db16ad8fa7d1537c5"}, + {file = "pywavelets-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78feab4e0c25fa32034b6b64cb854c6ce15663b4f0ffb25d8f0ee58915300f9b"}, + {file = "pywavelets-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be36f08efe9bc3abf40cf40cd2ee0aa0db26e4894e13ce5ac178442864161e8c"}, + {file = "pywavelets-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0595c51472c9c5724fe087cb73e2797053fd25c788d6553fdad6ff61abc60e91"}, + {file = "pywavelets-1.6.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:058a750477dde633ac53b8806f835af3559d52db6532fb2b93c1f4b5441365b8"}, + {file = "pywavelets-1.6.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:538795d9c4181152b414285b5a7f72ac52581ecdcdce74b6cca3fa0b8a5ab0aa"}, + {file = "pywavelets-1.6.0-cp310-cp310-win32.whl", hash = "sha256:47de024ba4f9df97e98b5f540340e1a9edd82d2c477450bef8c9b5381487128e"}, + {file = "pywavelets-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e2c44760c0906ddf2176920a2613287f6eea947f166ce7eee9546081b06a6835"}, + {file = "pywavelets-1.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d91aaaf6de53b758bcdc96c81cdb5a8607758602be49f691188c0e108cf1e738"}, + {file = "pywavelets-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b5302edb6d1d1ff6636d37c9ff29c4892f2a3648d736cc1df01f3f36e25c8cf"}, + {file = "pywavelets-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5e655446e37a3c87213d5c6386b86f65c4d61736b4432d720171e7dd6523d6a"}, + {file = "pywavelets-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ec7d69b746a0eaa327b829a3252a63619f2345e263177be5dd9bf30d7933c8d"}, + {file = "pywavelets-1.6.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97ea9613bd6b7108ebb44b709060adc7e2d5fac73be7152342bdd5513d75f84e"}, + {file = "pywavelets-1.6.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:48b3813c6d1a7a8194f37dbb5dbbdf2fe1112152c91445ea2e54f64ff6350c36"}, + {file = "pywavelets-1.6.0-cp311-cp311-win32.whl", hash = "sha256:4ffb484d096a5eb10af7121e0203546a03e1369328df321a33ef91f67bac40cf"}, + {file = "pywavelets-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:274bc47b289585383aa65519b3fcae5b4dee5e31db3d4198d4fad701a70e59f7"}, + {file = "pywavelets-1.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d6ec113386a432e04103f95e351d2657b42145bd1e1ed26513423391bcb5f011"}, + {file = "pywavelets-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab652112d3932d21f020e281e06926a751354c2b5629fb716f5eb9d0104b84e5"}, + {file = "pywavelets-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47b0314a22616c5f3f08760f0e00b4a15b7c7dadca5e39bb701cf7869a4207c5"}, + {file = "pywavelets-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138471513bc0a4cd2ddc4e50c7ec04e3468c268e101a0d02f698f6aedd1d5e79"}, + {file = "pywavelets-1.6.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67936491ae3e5f957c428e34fdaed21f131535b8d60c7c729a1b539ce8864837"}, + {file = "pywavelets-1.6.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd798cee3d28fb3d32a26a00d9831a20bf316c36d685e4ced01b4e4a8f36f5ce"}, + {file = "pywavelets-1.6.0-cp312-cp312-win32.whl", hash = "sha256:e772f7f0c16bfc3be8ac3cd10d29a9920bb7a39781358856223c491b899e6e79"}, + {file = "pywavelets-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:4ef15a63a72afa67ae9f4f3b06c95c5382730fb3075e668d49a880e65f2f089c"}, + {file = "pywavelets-1.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:627df378e63e9c789b6f2e7060cb4264ebae6f6b0efc1da287a2c060de454a1f"}, + {file = "pywavelets-1.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a413b51dc19e05243fe0b0864a8e8a16b5ca9bf2e4713da00a95b1b5747a5367"}, + {file = "pywavelets-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be615c6c1873e189c265d4a76d1751ec49b17e29725e6dd2e9c74f1868f590b7"}, + {file = "pywavelets-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4021ef69ec9f3862f66580fc4417be728bd78722914394594b48212fd1fcaf21"}, + {file = "pywavelets-1.6.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8fbf7b61b28b5457693c034e58a01622756d1fd60a80ae13ac5888b1d3e57e80"}, + {file = "pywavelets-1.6.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f58ddbb0a6cd243928876edfc463b990763a24fb94498607d6fea690e32cca4c"}, + {file = "pywavelets-1.6.0-cp39-cp39-win32.whl", hash = "sha256:42a22e68e345b6de7d387ef752111ab4530c98048d2b4bdac8ceefb078b4ead6"}, + {file = "pywavelets-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:32198de321892743c1a3d1957fe1cd8a8ecc078bfbba6b8f3982518e897271d7"}, + {file = "pywavelets-1.6.0.tar.gz", hash = "sha256:ea027c70977122c5fc27b2510f0a0d9528f9c3df6ea3e4c577ca55fd00325a5b"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<3" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pyyaml" version = "6.0.1" @@ -2445,6 +2517,53 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "scipy" +version = "1.13.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, + {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, + {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, + {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, + {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, + {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, + {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, + {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, + {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, + {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "sgmllib3k" version = "1.0.0" @@ -3284,4 +3403,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "76aa9b04323c716cda8d3e79a552d35c3f2d96eac39682c6c9c6b59291cbd398" +content-hash = "c1da4a148819ff244d291be9ca67466a07d994f29d2c1821a2e640997bfe617c" diff --git a/pyproject.toml b/pyproject.toml index d218ba31..9181a780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ lxml = "^5.1.0" psutil = "^5.9.8" feedparser = "^6.0.11" opencv-python = "^4.9.0.80" +imagehash = "^4.3.1" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py new file mode 100644 index 00000000..bb00565d --- /dev/null +++ b/zhenxun/plugins/fudu.py @@ -0,0 +1,172 @@ +import random + +from nonebot import on_message +from nonebot.adapters import Event +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Image as alcImg +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import get_img_hash +from zhenxun.utils.rules import ensure_group + +__plugin_meta__ = PluginMetadata( + name="复读", + description="群友的本质是什么?是复读机哒!", + usage=""" + usage: + 重复3次相同的消息时会复读 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.HIDDEN, + tasks=[Task(module="fudu", name="复读")], + configs=[ + RegisterConfig( + key="FUDU_PROBABILITY", + value=0.7, + help="复读概率", + default_value=0.7, + type=float, + ), + RegisterConfig( + module="_task", + key="DEFAULT_FUDU", + value=True, + help="被动 复读 进群默认开关状态", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +class Fudu: + def __init__(self): + self.data = {} + + def append(self, key, content): + self._create(key) + self.data[key]["data"].append(content) + + def clear(self, key): + self._create(key) + self.data[key]["data"] = [] + self.data[key]["is_repeater"] = False + + def size(self, key) -> int: + self._create(key) + return len(self.data[key]["data"]) + + def check(self, key, content) -> bool: + self._create(key) + return self.data[key]["data"][0] == content + + def get(self, key): + self._create(key) + return self.data[key]["data"][0] + + def is_repeater(self, key): + self._create(key) + return self.data[key]["is_repeater"] + + def set_repeater(self, key): + self._create(key) + self.data[key]["is_repeater"] = True + + def _create(self, key): + if self.data.get(key) is None: + self.data[key] = {"is_repeater": False, "data": []} + + +_manage = Fudu() + + +_matcher = on_message(rule=ensure_group, priority=999) + + +@_matcher.handle() +async def _(message: UniMsg, event: Event, session: EventSession): + task = await TaskInfo.get_or_none(module="fudu") + if task and not task.status: + return + if event.is_tome(): + return + group_id = session.id2 or "" + plain_text = message.extract_plain_text() + image_list = [] + for m in message: + if isinstance(m, alcImg): + if m.url: + image_list.append(m.url) + if not plain_text and not image_list: + return + if plain_text and plain_text.startswith(f"@可爱的{NICKNAME}"): + await Text("复制粘贴的虚空艾特?").send(reply=True) + if image_list: + img_hash = await get_fudu_img_hash(image_list[0], group_id) + else: + img_hash = "" + add_msg = plain_text + "|-|" + img_hash + if _manage.size(group_id) == 0: + _manage.append(group_id, add_msg) + elif _manage.check(group_id, add_msg): + _manage.append(group_id, add_msg) + else: + _manage.clear(group_id) + _manage.append(group_id, add_msg) + if _manage.size(group_id) > 2: + if random.random() < Config.get_config( + "fudu", "FUDU_PROBABILITY" + ) and not _manage.is_repeater(group_id): + if random.random() < 0.2: + if plain_text.endswith("打断施法!"): + await Text("打断" + plain_text).finish() + else: + await Text("打断施法!").finish() + _manage.set_repeater(group_id) + rst = None + if image_list and plain_text: + rst = MessageFactory( + [Text(plain_text), Image(TEMP_PATH / f"compare_{group_id}_img.jpg")] + ) + elif image_list: + rst = Image(TEMP_PATH / f"compare_{group_id}_img.jpg") + elif plain_text: + rst = Text(plain_text) + if rst: + await rst.finish() + + +async def get_fudu_img_hash(url: str, group_id: str) -> str: + """下载图片获取哈希值 + + 参数: + url: 图片url + group_id: 群组id + + 返回: + str: 哈希值 + """ + try: + if await AsyncHttpx.download_file( + url, TEMP_PATH / f"compare_{group_id}_img.jpg" + ): + img_hash = get_img_hash(TEMP_PATH / f"compare_{group_id}_img.jpg") + return str(img_hash) + else: + logger.warning(f"复读下载图片失败...") + except Exception as e: + logger.warning(f"复读读取图片Hash出错 {type(e)}:{e}") + return "" diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index 4dadb7ff..dfd03d29 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -5,7 +5,10 @@ from pathlib import Path from typing import Awaitable, Callable import cv2 +import imagehash +from imagehash import ImageHash from nonebot.utils import is_coroutine_callable +from PIL import Image from zhenxun.configs.path_config import IMAGE_PATH @@ -364,3 +367,17 @@ def compressed_image( cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) ) cv2.imwrite(str(out_file.absolute()), img) + + +def get_img_hash(image_file: str | Path) -> str: + """获取图片的hash值 + + 参数: + image_file: 图片文件路径 + + 返回: + str: 哈希值 + """ + with open(image_file, "rb") as fp: + hash_value = imagehash.average_hash(Image.open(fp)) + return str(hash_value) From f73b40ebdb2a8a133113b8a8778d6f8be1cc63c8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 28 May 2024 15:35:54 +0800 Subject: [PATCH 043/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E7=BE=A4=E6=AC=A2=E8=BF=8E=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/group_welcome_msg.py | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 zhenxun/plugins/group_welcome_msg.py diff --git a/zhenxun/plugins/group_welcome_msg.py b/zhenxun/plugins/group_welcome_msg.py new file mode 100644 index 00000000..2784f458 --- /dev/null +++ b/zhenxun/plugins/group_welcome_msg.py @@ -0,0 +1,62 @@ +import re + +import ujson as json +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.rules import ensure_group + +__plugin_meta__ = PluginMetadata( + name="查看群欢迎消息", + description="查看群欢迎消息", + usage=""" + usage: + 查看群欢迎消息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + ).dict(), +) + +_matcher = on_alconna(Alconna("群欢迎消息"), rule=ensure_group, priority=5, block=True) + + +BASE_PATH = DATA_PATH / "welcome_message" + + +@_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, +): + path = BASE_PATH / f"{session.platform or session.bot_type}" / f"{session.id2}" + if session.id3: + path = ( + BASE_PATH + / f"{session.platform or session.bot_type}" + / f"{session.id3}" + / f"{session.id2}" + ) + file = path / "text.json" + if not file.exists(): + await Text("未设置群欢迎消息...").finish(reply=True) + message = json.load(open(file))["message"] + message_split = re.split(r"\[image:\d+\]", message) + if len(message_split) == 1: + await Text(message_split[0]).finish(reply=True) + idx = 0 + data_list = [] + for msg in message_split[:-1]: + data_list.append(Text(msg)) + data_list.append(Image(path / f"{idx}.png")) + idx += 1 + data_list.append(message_split[-1]) + await MessageFactory(data_list).send(reply=True) + logger.info("查看群欢迎消息", arparma.header_result, session=session) From 091ae93731d08d9362c578723bb11f39b450b5f0 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 29 May 2024 02:01:26 +0800 Subject: [PATCH 044/132] =?UTF-8?q?feat=E2=9C=A8:=20=E7=A6=81=E8=A8=80?= =?UTF-8?q?=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/fudu.py | 29 +------ zhenxun/plugins/mute/__init__.py | 5 ++ zhenxun/plugins/mute/_data_source.py | 124 +++++++++++++++++++++++++++ zhenxun/plugins/mute/mute_message.py | 38 ++++++++ zhenxun/plugins/mute/mute_setting.py | 117 +++++++++++++++++++++++++ zhenxun/utils/image_utils.py | 25 +++++- zhenxun/utils/platform.py | 17 ++++ 7 files changed, 328 insertions(+), 27 deletions(-) create mode 100644 zhenxun/plugins/mute/__init__.py create mode 100644 zhenxun/plugins/mute/_data_source.py create mode 100644 zhenxun/plugins/mute/mute_message.py create mode 100644 zhenxun/plugins/mute/mute_setting.py diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py index bb00565d..db1c0766 100644 --- a/zhenxun/plugins/fudu.py +++ b/zhenxun/plugins/fudu.py @@ -15,7 +15,7 @@ from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.http_utils import AsyncHttpx -from zhenxun.utils.image_utils import get_img_hash +from zhenxun.utils.image_utils import get_download_image_hash, get_img_hash from zhenxun.utils.rules import ensure_group __plugin_meta__ = PluginMetadata( @@ -91,7 +91,7 @@ class Fudu: _manage = Fudu() - + _matcher = on_message(rule=ensure_group, priority=999) @@ -115,7 +115,7 @@ async def _(message: UniMsg, event: Event, session: EventSession): if plain_text and plain_text.startswith(f"@可爱的{NICKNAME}"): await Text("复制粘贴的虚空艾特?").send(reply=True) if image_list: - img_hash = await get_fudu_img_hash(image_list[0], group_id) + img_hash = await get_download_image_hash(image_list[0], group_id) else: img_hash = "" add_msg = plain_text + "|-|" + img_hash @@ -147,26 +147,3 @@ async def _(message: UniMsg, event: Event, session: EventSession): rst = Text(plain_text) if rst: await rst.finish() - - -async def get_fudu_img_hash(url: str, group_id: str) -> str: - """下载图片获取哈希值 - - 参数: - url: 图片url - group_id: 群组id - - 返回: - str: 哈希值 - """ - try: - if await AsyncHttpx.download_file( - url, TEMP_PATH / f"compare_{group_id}_img.jpg" - ): - img_hash = get_img_hash(TEMP_PATH / f"compare_{group_id}_img.jpg") - return str(img_hash) - else: - logger.warning(f"复读下载图片失败...") - except Exception as e: - logger.warning(f"复读读取图片Hash出错 {type(e)}:{e}") - return "" diff --git a/zhenxun/plugins/mute/__init__.py b/zhenxun/plugins/mute/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/mute/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/mute/_data_source.py b/zhenxun/plugins/mute/_data_source.py new file mode 100644 index 00000000..7c03123e --- /dev/null +++ b/zhenxun/plugins/mute/_data_source.py @@ -0,0 +1,124 @@ +import time + +import ujson as json +from pydantic import BaseModel + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH + +base_config = Config.get("mute") + + +class GroupData(BaseModel): + + count: int + """次数""" + time: int + """检测时长""" + duration: int + """禁言时长""" + message_data: dict = {} + """消息存储""" + + +class MuteManage: + + file = DATA_PATH / "group_mute_data.json" + + def __init__(self) -> None: + self._group_data: dict[str, GroupData] = {} + if self.file.exists(): + _data = json.load(open(self.file)) + for gid in _data: + self._group_data[gid] = GroupData( + count=_data[gid]["count"], + time=_data[gid]["time"], + duration=_data[gid]["duration"], + ) + + def get_group_data(self, group_id: str) -> GroupData: + """获取群组数据 + + 参数: + group_id: 群组id + + 返回: + GroupData: GroupData + """ + if group_id not in self._group_data: + self._group_data[group_id] = GroupData( + count=base_config.get("MUTE_DEFAULT_COUNT"), + time=base_config.get("MUTE_DEFAULT_TIME"), + duration=base_config.get("MUTE_DEFAULT_DURATION"), + ) + return self._group_data[group_id] + + def reset(self, user_id: str, group_id: str): + """重置用户检查次数 + + 参数: + user_id: 用户id + group_id: 群组id + """ + if group_data := self._group_data.get(group_id): + if user_id in group_data.message_data: + group_data.message_data[user_id]["count"] = 0 + + def save_data(self): + """保存数据""" + data = {} + for gid in self._group_data: + data[gid] = { + "count": self._group_data[gid].count, + "time": self._group_data[gid].time, + "duration": self._group_data[gid].duration, + } + with open(self.file, "w") as f: + json.dump(data, f, indent=4, ensure_ascii=False) + + def add_message(self, user_id: str, group_id: str, message: str) -> int: + """添加消息 + + 参数: + user_id: 用户id + group_id: 群组id + message: 消息内容 + + 返回: + int: 禁言时长 + """ + if group_id not in self._group_data: + self._group_data[group_id] = GroupData( + count=base_config.get("MUTE_DEFAULT_COUNT"), + time=base_config.get("MUTE_DEFAULT_TIME"), + duration=base_config.get("MUTE_DEFAULT_DURATION"), + ) + group_data = self._group_data[group_id] + if group_data.duration == 0: + return 0 + message_data = group_data.message_data + if not message_data.get(user_id): + message_data[user_id] = { + "time": time.time(), + "count": 1, + "message": message, + } + else: + if message.find(message_data[user_id]["message"]) != -1: + message_data[user_id]["count"] += 1 + else: + message_data[user_id]["time"] = time.time() + message_data[user_id]["count"] = 1 + message_data[user_id]["message"] = message + if time.time() - message_data[user_id]["time"] > group_data.time: + message_data[user_id]["time"] = time.time() + message_data[user_id]["count"] = 1 + if ( + message_data[user_id]["count"] > group_data.count + and time.time() - message_data[user_id]["time"] < group_data.time + ): + return group_data.duration + return 0 + + +mute_manage = MuteManage() diff --git a/zhenxun/plugins/mute/mute_message.py b/zhenxun/plugins/mute/mute_message.py new file mode 100644 index 00000000..401b2fc5 --- /dev/null +++ b/zhenxun/plugins/mute/mute_message.py @@ -0,0 +1,38 @@ +from nonebot import on_message +from nonebot.adapters import Bot +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import get_download_image_hash +from zhenxun.utils.platform import PlatformUtils + +from ._data_source import mute_manage + +_matcher = on_message(priority=1, block=False) + + +@_matcher.handle() +async def _(bot: Bot, session: EventSession, message: UniMsg): + group_id = session.id2 + if not session.id1 or not group_id: + return + plain_text = message.extract_plain_text() + image_list = [m.url for m in message if isinstance(m, alcImage) and m.url] + img_hash = "" + for url in image_list: + img_hash += await get_download_image_hash(url, "_mute_") + _message = plain_text + img_hash + if duration := mute_manage.add_message(session.id1, group_id, _message): + try: + await PlatformUtils.ban_user(bot, session.id1, group_id, duration) + await Text(f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!").send( + at_sender=True + ) + mute_manage.reset(session.id1, group_id) + logger.info(f"检测刷屏 被禁言 {duration} 分钟", "禁言检查", session=session) + except Exception as e: + logger.error("禁言发送错误", "禁言检测", session=session, e=e) diff --git a/zhenxun/plugins/mute/mute_setting.py b/zhenxun/plugins/mute/mute_setting.py new file mode 100644 index 00000000..96237f4a --- /dev/null +++ b/zhenxun/plugins/mute/mute_setting.py @@ -0,0 +1,117 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.rules import ensure_group + +from ._data_source import base_config, mute_manage + +__plugin_meta__ = PluginMetadata( + name="刷屏禁言", + description="刷屏禁言相关操作", + usage=""" + 刷屏禁言相关操作,需要 {NICKNAME} 有群管理员权限 + 指令: + 设置刷屏: 查看当前设置 + -c [count]: 检测最大次数 + -t [time]: 规定时间内 + -d [duration]: 禁言时长 + 示例: + 设置刷屏 -c 10: 设置最大次数为10 + 设置刷屏 -t 100 -d 20: 设置规定时间和禁言时长 + 设置刷屏 -d 10: 设置禁言时长为10 + * 即 X 秒内发送同样消息 N 次,禁言 M 分钟 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.ADMIN, + admin_level=base_config.get("MUTE_LEVEL", 5), + configs=[ + RegisterConfig( + key="MUTE_LEVEL", + value=5, + help="更改禁言设置的管理权限", + default_value=5, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_COUNT", + value=10, + help="刷屏禁言默认检测次数", + default_value=10, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_TIME", + value=7, + help="刷屏检测默认规定时间", + default_value=7, + type=int, + ), + RegisterConfig( + key="MUTE_DEFAULT_DURATION", + value=10, + help="刷屏检测默禁言时长(分钟)", + default_value=10, + type=int, + ), + ], + ).dict(), +) + + +_setting_matcher = on_alconna( + Alconna( + "刷屏设置", + Option("-t|--time", Args["time", int], help_text="检测时长"), + Option("-c|--count", Args["count", int], help_text="检测次数"), + Option("-d|--duration", Args["duration", int], help_text="禁言时长"), + ), + rule=ensure_group, + block=True, + priority=5, +) + + +@_setting_matcher.handle() +async def _( + session: EventSession, + arparma: Arparma, + time: Match[int], + count: Match[int], + duration: Match[int], +): + group_id = session.id2 + if not session.id1 or not group_id: + return + _time = time.result if time.available else None + _count = count.result if count.available else None + _duration = duration.result if duration.available else None + group_data = mute_manage.get_group_data(group_id) + if _time is None and _count is None and _duration is None: + await Text( + f"最大次数:{group_data.count} 次\n" + f"规定时间:{group_data.time} 秒\n" + f"禁言时长:{group_data.duration:.2f} 分钟\n" + f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】" + ).finish(reply=True) + if _time is not None: + group_data.time = _time + if _count is not None: + group_data.count = _count + if _duration is not None: + group_data.duration = _duration + await Text("设置成功!").send(reply=True) + logger.info( + f"设置禁言配置 time: {_time}, count: {_count}, duration: {_duration}", + arparma.header_result, + session=session, + ) + mute_manage.save_data() diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index dfd03d29..b44abcde 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -10,7 +10,9 @@ from imagehash import ImageHash from nonebot.utils import is_coroutine_callable from PIL import Image -from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx from ._build_image import BuildImage, ColorAlias from ._build_mat import BuildMat, MatType @@ -381,3 +383,24 @@ def get_img_hash(image_file: str | Path) -> str: with open(image_file, "rb") as fp: hash_value = imagehash.average_hash(Image.open(fp)) return str(hash_value) + + +async def get_download_image_hash(url: str, mark: str) -> str: + """下载图片获取哈希值 + + 参数: + url: 图片url + mark: 随机标志符 + + 返回: + str: 哈希值 + """ + try: + if await AsyncHttpx.download_file( + url, TEMP_PATH / f"compare_download_{mark}_img.jpg" + ): + img_hash = get_img_hash(TEMP_PATH / f"compare_download_{mark}_img.jpg") + return str(img_hash) + except Exception as e: + logger.warning(f"下载读取图片Hash出错", e=e) + return "" diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index bb5470bf..ac4844e5 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -50,6 +50,23 @@ class UserData(BaseModel): class PlatformUtils: + @classmethod + async def ban_user(cls, bot: Bot, user_id: str, group_id: str, duration: int): + """禁言 + + 参数: + bot: Bot + user_id: 用户id + group_id: 群组id + duration: 禁言时长(分钟) + """ + if isinstance(bot, v11Bot): + await bot.set_group_ban( + group_id=int(group_id), + user_id=int(user_id), + duration=duration * 60, + ) + @classmethod async def send_superuser( cls, From bfa6896ef9bd5d4ec1576024447b9236ea29129f Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 29 May 2024 02:10:33 +0800 Subject: [PATCH 045/132] =?UTF-8?q?feat=E2=9C=A8:=20=E8=83=BD=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E5=A5=BD=E5=A5=BD=E8=AF=B4=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/nbnhhsh.py | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 zhenxun/plugins/nbnhhsh.py diff --git a/zhenxun/plugins/nbnhhsh.py b/zhenxun/plugins/nbnhhsh.py new file mode 100644 index 00000000..7c46d51e --- /dev/null +++ b/zhenxun/plugins/nbnhhsh.py @@ -0,0 +1,60 @@ +import ujson as json +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +__plugin_meta__ = PluginMetadata( + name="能不能好好说话", + description="能不能好好说话,说人话", + usage=""" + 说人话 + 指令: + nbnhhsh [文本] + 能不能好好说话 [文本] + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1", aliases={"nbnhhsh"}).dict(), +) + +URL = "https://lab.magiconch.com/api/nbnhhsh/guess" + +_matcher = on_alconna( + Alconna("nbnhhsh", Args["text", str]), + aliases={"能不能好好说话"}, + priority=5, + block=True, +) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma, text: str): + response = await AsyncHttpx.post( + URL, + data=json.dumps({"text": text}), # type: ignore + timeout=5, + headers={"content-type": "application/json"}, + ) + try: + data = response.json() + tmp = "" + result = "" + for x in data: + trans = "" + if x.get("trans"): + trans = x["trans"][0] + elif x.get("inputting"): + trans = ",".join(x["inputting"]) + tmp += f'{x["name"]} -> {trans}\n' + result += trans + logger.info( + f" 发送能不能好好说话: {text} -> {result}", + arparma.header_result, + session=session, + ) + await Text(f"{tmp}={result}").send(reply=True) + except (IndexError, KeyError): + await Text("没有找到对应的翻译....").send() From d1fda8714d0756b050caff0df0c98e1a656910d2 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 29 May 2024 02:14:24 +0800 Subject: [PATCH 046/132] =?UTF-8?q?feat=E2=9C=A8:=20=E8=AF=AD=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/quotations.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 zhenxun/plugins/quotations.py diff --git a/zhenxun/plugins/quotations.py b/zhenxun/plugins/quotations.py new file mode 100644 index 00000000..e67ec048 --- /dev/null +++ b/zhenxun/plugins/quotations.py @@ -0,0 +1,32 @@ +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Arparma, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +__plugin_meta__ = PluginMetadata( + name="一言二次元语录", + description="二次元语录给你力量", + usage=""" + usage: + 一言二次元语录 + 指令: + 语录/二次元 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + +URL = "https://international.v1.hitokoto.cn/?c=a" + +_matcher = on_alconna(Alconna("语录"), aliases={"二次元"}, priority=5, block=True) + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + data = (await AsyncHttpx.get(URL, timeout=5)).json() + result = f'{data["hitokoto"]}\t——{data["from"]}' + await Text(result).send() + logger.info(f" 发送语录:" + result, arparma.header_result, session=session) From 12b8c3e04ccbd38eedc2467d922b75199bd911a2 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 29 May 2024 19:17:03 +0800 Subject: [PATCH 047/132] =?UTF-8?q?feat=E2=9C=A8:=20p=E6=90=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/pid_search.py | 113 +++++++++++++++++++++++++++++ zhenxun/plugins/pix_gallery/pix.py | 6 -- 2 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 zhenxun/plugins/pid_search.py diff --git a/zhenxun/plugins/pid_search.py b/zhenxun/plugins/pid_search.py new file mode 100644 index 00000000..a8916585 --- /dev/null +++ b/zhenxun/plugins/pid_search.py @@ -0,0 +1,113 @@ +from asyncio.exceptions import TimeoutError + +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.utils import change_pixiv_image_links +from zhenxun.utils.withdraw_manage import WithdrawManager + +__plugin_meta__ = PluginMetadata( + name="pid搜索", + description="通过 pid 搜索图片", + usage=""" + usage: + 通过 pid 搜索图片 + 指令: + p搜 [pid] + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + +_matcher = on_alconna( + Alconna("p搜", Args["pid", int]), aliases={"P搜"}, priority=5, block=True +) + + +@_matcher.handle() +async def _(pid: Match[int]): + if pid.available: + _matcher.set_path_arg("pid", pid.result) + + +@_matcher.got_path("pid", prompt="需要查询的图片PID是?") +async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: int): + url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust" + if pid in ["取消", "算了"]: + await Text("已取消操作...").finish() + for _ in range(3): + try: + data = ( + await AsyncHttpx.get( + url, + params={"id": pid}, + timeout=5, + ) + ).json() + except TimeoutError: + pass + except Exception as e: + await Text(f"发生了一些错误..{type(e)}:{e}").finish() + else: + if data.get("error"): + await Text(data["error"]["user_message"]).finish(reply=True) + data = data["illust"] + if not data["width"] and not data["height"]: + await Text(f"没有搜索到 PID:{pid} 的图片").finish(reply=True) + pid = data["id"] + title = data["title"] + author = data["user"]["name"] + author_id = data["user"]["id"] + image_list = [] + try: + image_list.append(data["meta_single_page"]["original_image_url"]) + except KeyError: + for image_url in data["meta_pages"]: + image_list.append(image_url["image_urls"]["original"]) + for i, img_url in enumerate(image_list): + img_url = change_pixiv_image_links(img_url) + if not await AsyncHttpx.download_file( + img_url, + TEMP_PATH / f"pid_search_{session.id1}_{i}.png", + headers=headers, + ): + await Text("图片下载失败了...").finish(reply=True) + tmp = "" + if session.id3 or session.id2: + tmp = "\n【注】将在30后撤回......" + receipt = await MessageFactory( + [ + Text( + f"title:{title}\n" + f"pid:{pid}\n" + f"author:{author}\n" + f"author_id:{author_id}\n" + ), + Image(TEMP_PATH / f"pid_search_{session.id1}_{i}.png"), + Text(f"{tmp}"), + ] + ).send() + logger.info( + f" 查询图片 PID:{pid}", arparma.header_result, session=session + ) + if session.id3 or session.id2: + await WithdrawManager.withdraw_message( + bot, receipt.extract_message_id().message_id, 30 # type: ignore + ) + break + else: + await Text("图片下载失败了...").send(reply=True) diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py index 02154720..0502f8c2 100644 --- a/zhenxun/plugins/pix_gallery/pix.py +++ b/zhenxun/plugins/pix_gallery/pix.py @@ -223,9 +223,6 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[list[ and gid ): for msg in msg_list: - # receipt = await PlatformUtils.send_message( - # bot, None, group_id=gid, message=msg - # ) receipt = await msg.send() if receipt: message_id = receipt.extract_message_id().message_id @@ -238,9 +235,6 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[list[ else: for msg in msg_list: receipt = await msg.send() - # receipt = await PlatformUtils.send_message( - # bot, session.id1, group_id=gid, message=msg - # ) if receipt: message_id = receipt.extract_message_id().message_id await WithdrawManager.withdraw_message( From 899acc248d6947c406987619cd7363d701c38aef Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 29 May 2024 19:27:58 +0800 Subject: [PATCH 048/132] =?UTF-8?q?feat=E2=9C=A8:=20roll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/roll.py | 67 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 zhenxun/plugins/roll.py diff --git a/zhenxun/plugins/roll.py b/zhenxun/plugins/roll.py new file mode 100644 index 00000000..98092d08 --- /dev/null +++ b/zhenxun/plugins/roll.py @@ -0,0 +1,67 @@ +import asyncio +import random + +from nonebot import on_command +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession +from nonebot_plugin_userinfo import EventUserInfo, UserInfo + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +__plugin_meta__ = PluginMetadata( + name="roll", + description="犹豫不决吗?那就让我帮你决定吧", + usage=""" + usage: + 随机数字 或 随机选择事件 + 指令: + roll: 随机 0-100 的数字 + roll *[文本]: 随机事件 + 示例:roll 吃饭 睡觉 打游戏 + """.strip(), + extra=PluginExtraData(author="HibiKier", version="0.1").dict(), +) + + +_matcher = on_command("roll", priority=5, block=True) + + +@_matcher.handle() +async def _( + session: EventSession, + message: UniMsg, + user_info: UserInfo = EventUserInfo(), +): + text = message.extract_plain_text().strip().replace("roll", "", 1).split() + if not text: + await Text(f"roll: {random.randint(0, 100)}").finish(reply=True) + user_name = ( + user_info.user_displayname or user_info.user_remark or user_info.user_name + ) + await Text( + random.choice( + [ + "转动命运的齿轮,拨开眼前迷雾...", + f"启动吧,命运的水晶球,为{user_name}指引方向!", + "嗯哼,在此刻转动吧!命运!", + f"在此祈愿,请为{user_name}降下指引...", + ] + ) + ).send() + await asyncio.sleep(1) + random_text = random.choice(text) + await Text( + random.choice( + [ + f"让{NICKNAME}看看是什么结果!答案是:‘{random_text}’", + f"根据命运的指引,接下来{user_name} ‘{random_text}’ 会比较好", + f"祈愿被回应了!是 ‘{random_text}’!", + f"结束了,{user_name},命运之轮停在了 ‘{random_text}’!", + ] + ) + ).send(reply=True) + logger.info(f"发送roll:{text}", "roll", session=session) From 15aba0bea9ae9d38e235db2c700c569c18c2738e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 10 Jun 2024 21:10:04 +0800 Subject: [PATCH 049/132] =?UTF-8?q?feat=E2=9C=A8:=20=E4=BF=84=E7=BD=97?= =?UTF-8?q?=E6=96=AF=E8=BD=AE=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/russian/__init__.py | 186 ++++++++++ zhenxun/plugins/russian/command.py | 108 ++++++ zhenxun/plugins/russian/data_source.py | 479 +++++++++++++++++++++++++ zhenxun/plugins/russian/model.py | 107 ++++++ zhenxun/utils/_build_mat.py | 2 +- 5 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/russian/__init__.py create mode 100644 zhenxun/plugins/russian/command.py create mode 100644 zhenxun/plugins/russian/data_source.py create mode 100644 zhenxun/plugins/russian/model.py diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py new file mode 100644 index 00000000..b5396843 --- /dev/null +++ b/zhenxun/plugins/russian/__init__.py @@ -0,0 +1,186 @@ +from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Arparma +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Match +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName + +from .command import ( + _accept_matcher, + _rank_matcher, + _record_matcher, + _refuse_matcher, + _russian_matcher, + _settlement_matcher, + _shoot_matcher, +) +from .data_source import Russian, russian_manage +from .model import RussianUser + +__plugin_meta__ = PluginMetadata( + name="俄罗斯轮盘", + description="虽然是运气游戏,但这可是战场啊少年", + usage=""" + 又到了决斗时刻 + 指令: + 装弹 [金额] [子弹数] ?[at]: 开启游戏,装填子弹,可选自定义金额,或邀请决斗对象 + 接受对决: 接受当前存在的对决 + 拒绝对决: 拒绝邀请的对决 + 开枪: 开出未知的一枪 + 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用) + 我的战绩: 对,你的战绩 + 轮盘胜场排行/轮盘败场排行/轮盘欧洲人排行/轮盘慈善家排行/轮盘最高连胜排行/轮盘最高连败排行: 各种排行榜 + 示例:装弹 3 100 @sdd + * 注:同一时间群内只能有一场对决 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="群内小游戏", + configs=[ + RegisterConfig( + key="MAX_RUSSIAN_BET_GOLD", + value=1000, + help="俄罗斯轮盘最大赌注金额", + default_value=1000, + type=int, + ) + ], + ).dict(), +) + + +@_russian_matcher.handle() +async def _(money: int, num: Match[int], at_user: Match[alcAt]): + _russian_matcher.set_path_arg("money", money) + if num.available: + _russian_matcher.set_path_arg("num", num.result) + if at_user.available: + _russian_matcher.set_path_arg("at_user", at_user.result.target) + + +@_russian_matcher.got_path("num", prompt="请输入装填子弹的数量!(最多6颗)") +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + money: int, + num: int, + at_user: Match[alcAt], + uname: str = UserName(), +): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + _at_user = at_user.result.target if at_user.available else None + rus = Russian( + at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=num + ) + result = await russian_manage.add_russian(bot, gid, rus) + await result.send() + logger.info( + f"添加俄罗斯轮盘 装弹: {num}, 金额: {money}", + arparma.header_result, + session=session, + ) + + +@_accept_matcher.handle() +async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): + global a + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + result = russian_manage.accept(gid, session.id1, uname) + await result.send() + logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session) + + +@_refuse_matcher.handle() +async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + result = russian_manage.refuse(gid, session.id1, uname) + await result.send() + logger.info(f"俄罗斯轮盘拒绝对决", arparma.header_result, session=session) + + +@_settlement_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + result = await russian_manage.settlement(gid, session.id1, session.platform) + await result.send() + logger.info(f"俄罗斯轮盘结算", arparma.header_result, session=session) + + +@_shoot_matcher.handle() +async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + result, settle = await russian_manage.shoot( + bot, gid, session.id1, uname, session.platform + ) + await result.send() + if settle: + await settle.send() + logger.info(f"俄罗斯轮盘开枪", arparma.header_result, session=session) + + +@_record_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + user, _ = await RussianUser.get_or_create(user_id=session.id1, group_id=gid) + await Text( + f"俄罗斯轮盘\n" + f"总胜利场次:{user.win_count}\n" + f"当前连胜:{user.winning_streak}\n" + f"最高连胜:{user.max_winning_streak}\n" + f"总失败场次:{user.fail_count}\n" + f"当前连败:{user.losing_streak}\n" + f"最高连败:{user.max_losing_streak}\n" + f"赚取金币:{user.make_money}\n" + f"输掉金币:{user.lose_money}", + ).send(reply=True) + logger.info(f"俄罗斯轮盘查看战绩", arparma.header_result, session=session) + + +@_rank_matcher.handle() +async def _(session: EventSession, arparma: Arparma, rank_type: str, num: int): + gid = session.id2 + if not session.id1: + await Text("用户id为空...").finish() + if not gid: + await Text("群组id为空...").finish() + if 51 < num or num < 10: + num = 10 + result = await russian_manage.rank(session.id1, gid, rank_type, num) + if isinstance(result, str): + await Text(result).finish(reply=True) + result.show() + await Image(result.pic2bytes()).send(reply=True) + logger.info( + f"查看轮盘排行: {rank_type} 数量: {num}", arparma.header_result, session=session + ) diff --git a/zhenxun/plugins/russian/command.py b/zhenxun/plugins/russian/command.py new file mode 100644 index 00000000..40491da7 --- /dev/null +++ b/zhenxun/plugins/russian/command.py @@ -0,0 +1,108 @@ +from nonebot_plugin_alconna import Alconna, Args +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import on_alconna + +from zhenxun.utils.rules import ensure_group + +_russian_matcher = on_alconna( + Alconna( + "俄罗斯轮盘", + Args["money", int]["num?", int]["at_user?", alcAt], + ), + aliases={"装弹"}, + rule=ensure_group, + priority=5, + block=True, +) + +_accept_matcher = on_alconna( + Alconna("接受对决"), + aliases={"接受决斗", "接受挑战"}, + rule=ensure_group, + priority=5, + block=True, +) + +_refuse_matcher = on_alconna( + Alconna("拒绝对决"), + aliases={"拒绝决斗", "拒绝挑战"}, + rule=ensure_group, + priority=5, + block=True, +) + +_shoot_matcher = on_alconna( + Alconna("开枪"), + aliases={"咔", "嘭", "嘣"}, + rule=ensure_group, + priority=5, + block=True, +) + +_settlement_matcher = on_alconna( + Alconna("结算"), + rule=ensure_group, + priority=5, + block=True, +) + +_record_matcher = on_alconna( + Alconna("我的战绩"), + rule=ensure_group, + priority=5, + block=True, +) + +_rank_matcher = on_alconna( + Alconna( + "russian-rank", + Args["rank_type", ["win", "lose", "a", "b", "max_win", "max_lose"]][ + "num?", int, 10 + ], + ), + rule=ensure_group, + priority=5, + block=True, +) + +_rank_matcher.shortcut( + r"轮盘胜场排行(?P\d*)", + command="russian-rank", + arguments=["win", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘败场排行(?P\d*)", + command="russian-rank", + arguments=["lose", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘欧洲人排行(?P\d*)", + command="russian-rank", + arguments=["a", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘慈善家排行(?P\d*)", + command="russian-rank", + arguments=["b", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘最高连胜排行(?P\d*)", + command="russian-rank", + arguments=["max_win", "{num}"], + prefix=True, +) + +_rank_matcher.shortcut( + r"轮盘最高连败排行(?P\d*)", + command="russian-rank", + arguments=["max_lose", "{num}"], + prefix=True, +) diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py new file mode 100644 index 00000000..4d1f4568 --- /dev/null +++ b/zhenxun/plugins/russian/data_source.py @@ -0,0 +1,479 @@ +import random +import time +from datetime import datetime, timedelta + +from apscheduler.jobstores.base import JobLookupError +from nonebot.adapters import Bot +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from pydantic import BaseModel + +from zhenxun.configs.config import NICKNAME, Config +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.user_console import UserConsole +from zhenxun.utils.enum import GoldHandle +from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType, text2image +from zhenxun.utils.platform import PlatformUtils + +from .model import RussianUser + +base_config = Config.get("russian") + + +class Russian(BaseModel): + + at_user: str | None + """指定决斗对象""" + player1: tuple[str, str] + """玩家1id, 昵称""" + player2: tuple[str, str] | None = None + """玩家2id, 昵称""" + money: int + """金额""" + bullet_num: int + """子弹数""" + bullet_arr: list[int] = [] + """子弹排列""" + bullet_index: int = 0 + """当前子弹下标""" + next_user: str = "" + """下一个开枪用户""" + time: float = time.time() + """创建时间""" + win_user: str | None = None + """胜利者""" + + +class RussianManage: + + def __init__(self) -> None: + self._data: dict[str, Russian] = {} + + def __check_is_timeout(self, group_id: str) -> bool: + """检查决斗是否超时 + + 参数: + group_id: 群组id + + 返回: + bool: 是否超时 + """ + if russian := self._data.get(group_id): + if russian.time + 30 < time.time(): + return True + return False + + def __random_bullet(self, num: int) -> list[int]: + """随机排列子弹 + + 参数: + num: 子弹数量 + + 返回: + list[int]: 子弹排列数组 + """ + bullet_list = [0, 0, 0, 0, 0, 0, 0] + for i in random.sample([0, 1, 2, 3, 4, 5, 6], num): + bullet_list[i] = 1 + return bullet_list + + def __build_job( + self, bot: Bot, group_id: str, is_add: bool = False, platform: str | None = None + ): + """移除定时任务和构建新定时任务 + + 参数: + bot: Bot + group_id: 群组id + is_add: 是否添加新定时任务. + platform: 平台 + """ + try: + scheduler.remove_job(f"russian_job_{group_id}") + except JobLookupError: + pass + if is_add: + date = datetime.now() + timedelta(seconds=31) + scheduler.add_job( + self.__auto_end_game, + "date", + run_date=date.replace(microsecond=0), + id=f"russian_job_{group_id}", + args=[bot, group_id, platform], + ) + + async def __auto_end_game(self, bot: Bot, group_id: str, platform: str): + """自动结束对决 + + 参数: + bot: Bot + group_id: 群组id + platform: 平台 + """ + result = await self.settlement(group_id, None, platform) + if result: + await PlatformUtils.send_message(bot, None, group_id, result) + + async def add_russian( + self, bot: Bot, group_id: str, rus: Russian + ) -> Text | MessageFactory: + """添加决斗 + + 参数: + bot: Bot + group_id: 群组id + rus: Russian + + 返回: + Text | MessageFactory: 返回消息 + """ + russian = self._data.get(group_id) + if russian: + if russian.time + 30 < time.time(): + if not russian.player2: + return Text( + f"现在是 {russian.player1[1]} 发起的对决, 请接受对决或等待决斗超时..." + ) + else: + return Text( + f"{russian.player1[1]} 和 {russian.player2[1]}的对决还未结束!" + ) + return Text( + f"现在是 {russian.player1[1]} 发起的对决\n请等待比赛结束后再开始下一轮..." + ) + max_money = base_config.get("MAX_RUSSIAN_BET_GOLD") + if rus.money > max_money: + return Text(f"太多了!单次金额不能超过{max_money}!") + user = await UserConsole.get_user(rus.player1[0]) + if user.gold < rus.money: + return Text("你没有足够的钱支撑起这场挑战") + rus.bullet_arr = self.__random_bullet(rus.bullet_num) + self._data[group_id] = rus + message_list = [] + if rus.at_user: + user = await GroupInfoUser.get_or_none( + user_id=rus.at_user, group_id=group_id + ) + message_list = [ + Text(f"{rus.player1[1]} 向"), + Mention(rus.at_user), + Text( + f"发起了决斗!请 {user.user_name if user else rus.at_user} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!" + ), + ] + else: + message_list = [ + Text( + "若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】" + ) + ] + result = Text( + "咔 " * rus.bullet_num + + f"装填完毕\n挑战金额:{rus.money}\n第一枪的概率为:{float(rus.bullet_num) / 7.0 * 100:.2f}%\n" + ) + message_list.insert(0, result) + self.__build_job(bot, group_id, True) + return MessageFactory(message_list) + + def accept(self, group_id: str, user_id: str, uname: str) -> Text | MessageFactory: + """接受对决 + + 参数: + group_id: 群组id + user_id: 用户id + uname: 用户名称 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if russian.at_user and russian.at_user != user_id: + return Text("又不是找你决斗,你接受什么啊!气!") + if russian.player2: + return Text("当前决斗已被其他玩家接受!请等待下局对决!") + russian.player2 = (user_id, uname) + russian.next_user = russian.player1[0] + return MessageFactory( + [ + Text("决斗已经开始!请"), + Mention(russian.player1[0]), + Text("先开枪!"), + ] + ) + return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!") + + def refuse(self, group_id: str, user_id: str, uname: str) -> Text | MessageFactory: + """拒绝决斗 + + 参数: + group_id: 群组id + user_id: 用户id + uname: 用户名称 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if russian.at_user: + if russian.at_user != user_id: + return Text("又不是找你决斗,你拒绝什么啊!气!") + del self._data[group_id] + return MessageFactory( + [Mention(russian.player1[0]), Text(f"{uname}拒绝了你的对决!")] + ) + return Text("当前决斗并没有指定对手,无法拒绝哦!") + return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!") + + async def shoot( + self, bot: Bot, group_id: str, user_id: str, uname: str, platform: str + ) -> tuple[Text | MessageFactory, Text | MessageFactory | None]: + """开枪 + + 参数: + bot: Bot + group_id: 群组id + user_id: 用户id + uname: 用户名称 + platform: 平台 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if not russian.player2: + return Text("当前还没有玩家接受对决,无法开枪..."), None + if user_id not in [russian.player1[0], russian.player2[0]]: + """非玩家1和玩家2发送开枪""" + return ( + Text( + random.choice( + [ + f"不要打扰 {russian.player1[1]} 和 {russian.player2[1]} 的决斗啊!", + f"给我好好做好一个观众!不然{NICKNAME}就要生气了", + f"不要捣乱啊baka{uname}!", + ] + ) + ), + None, + ) + if user_id != russian.next_user: + """相同玩家连续开枪""" + return ( + Text(f"你的左轮不是连发的!该 {russian.player2[1]} 开枪了!"), + None, + ) + if russian.bullet_arr[russian.bullet_index] == 1: + """去世""" + result = Text( + random.choice( + [ + '"嘭!",你直接去世了', + "眼前一黑,你直接穿越到了异世界...(死亡)", + "终究还是你先走一步...", + ] + ) + ) + settle = await self.settlement(group_id, user_id, platform) + return result, settle + else: + """存活""" + p = (russian.bullet_index + 1) / len(russian.bullet_arr) * 100 + result = ( + random.choice( + [ + "呼呼,没有爆裂的声响,你活了下来", + "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了", + '"咔",你没死,看来运气不错', + ] + ) + + f"\n下一枪中弹的概率: {p:.2f}%, 轮到 " + ) + next_user = ( + russian.player2[0] + if russian.next_user == russian.player1[0] + else russian.player1[0] + ) + russian.next_user = next_user + russian.bullet_index += 1 + self.__build_job(bot, group_id, True) + return ( + MessageFactory([Text(result), Mention(next_user), Text(" 了!")]), + None, + ) + return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!"), None + + async def settlement( + self, group_id: str, user_id: str | None, platform: str | None = None + ) -> Text | MessageFactory: + """结算 + + 参数: + group_id: 群组id + user_id: 用户id + platform: 平台 + + 返回: + Text | MessageFactory: 返回消息 + """ + if russian := self._data.get(group_id): + if not russian.player2: + if self.__check_is_timeout(group_id): + del self._data[group_id] + return Text("规定时间内还未有人接受决斗,当前决斗过期...") + return Text("决斗还未开始,,无法结算哦...") + if user_id and user_id not in [russian.player1[0], russian.player1[0]]: + return Text("吃瓜群众不要捣乱!黄牌警告!") + if not self.__check_is_timeout(group_id): + return Text( + f"{russian.player1[1]} 和 {russian.player1[1]} 比赛并未超时,请继续比赛..." + ) + win_user = None + lose_user = None + if win_user: + russian.next_user = ( + russian.player1[0] + if win_user == russian.player2[0] + else russian.player2[0] + ) + if russian.next_user != russian.player1[0]: + win_user = russian.player1 + lose_user = russian.player2 + else: + win_user = russian.player2 + lose_user = russian.player1 + if win_user and lose_user: + rand = 0 + if russian.money > 10: + rand = random.randint(0, 5) + fee = int(russian.money * float(rand) / 100) + fee = 1 if fee < 1 and rand != 0 else fee + else: + fee = 0 + winner = await RussianUser.add_count(win_user[0], group_id, "win") + loser = await RussianUser.add_count(lose_user[0], group_id, "lose") + await RussianUser.money( + win_user[0], group_id, "win", russian.money - fee + ) + await RussianUser.money(lose_user[0], group_id, "lose", russian.money) + await UserConsole.add_gold( + win_user[0], russian.money - fee, "russian", platform + ) + await UserConsole.reduce_gold( + lose_user[0], russian.money, GoldHandle.PLUGIN, "russian", platform + ) + result = [Text("这场决斗是 "), Mention(win_user[0]), Text(" 胜利了!")] + image = await text2image( + f"结算:\n" + f"\t胜者:{win_user[1]}\n" + f"\t赢取金币:{russian.money - fee}\n" + f"\t累计胜场:{winner.win_count}\n" + f"\t累计赚取金币:{winner.make_money}\n" + f"-------------------\n" + f"\t败者:{lose_user[1]}\n" + f"\t输掉金币:{russian.money}\n" + f"\t累计败场:{loser.fail_count}\n" + f"\t累计输掉金币:{loser.lose_money}\n" + f"-------------------\n" + f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n" + f"子弹排列:{russian.bullet_arr}", + padding=10, + color="#f9f6f2", + ) + result.append(Image(image.pic2bytes())) + del self._data[group_id] + return MessageFactory(result) + return Text("赢家和输家获取错误...") + return Text("比赛并没有开始...无法结算...") + + async def __get_x_index(self, users: list[RussianUser], group_id: str): + uid_list = [u.user_id for u in users] + group_user_list = await GroupInfoUser.filter( + user_id__in=uid_list, group_id=group_id + ).all() + group_user = {gu.user_id: gu.user_name for gu in group_user_list} + data = [] + for uid in uid_list: + if uid in group_user: + data.append(group_user[uid]) + else: + data.append(uid) + return data + + async def rank( + self, user_id: str, group_id: str, rank_type: str, num: int + ) -> BuildImage | str: + x_index = [] + data = [] + title = "" + x_name = "" + if rank_type == "win": + users = ( + await RussianUser.filter(group_id=group_id, win_count__not=0) + .order_by("win_count") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.win_count for u in users] + title = "胜场排行" + x_name = "场次" + if rank_type == "lose": + users = ( + await RussianUser.filter(group_id=group_id, fail_count__not=0) + .order_by("fail_count") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.fail_count for u in users] + title = "败场排行" + x_name = "场次" + if rank_type == "a": + users = ( + await RussianUser.filter(group_id=group_id, make_money__not=0) + .order_by("make_money") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.make_money for u in users] + title = "欧洲人排行" + x_name = "金币" + if rank_type == "b": + users = ( + await RussianUser.filter(group_id=group_id, lose_money__not=0) + .order_by("lose_money") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.lose_money for u in users] + title = "慈善家排行" + x_name = "金币" + if rank_type == "max_win": + users = ( + await RussianUser.filter(group_id=group_id, max_winning_streak__not=0) + .order_by("max_winning_streak") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.max_winning_streak for u in users] + title = "最高连胜排行" + x_name = "场次" + if rank_type == "max_lose": + users = ( + await RussianUser.filter(group_id=group_id, max_losing_streak__not=0) + .order_by("max_losing_streak") + .limit(num) + ) + x_index = await self.__get_x_index(users, group_id) + data = [u.max_losing_streak for u in users] + title = "最高连败排行" + x_name = "场次" + if not data: + return "当前数据为空..." + mat = BuildMat(MatType.BARH) + mat.x_index = x_index + mat.data = data # type: ignore + mat.title = title + mat.x_name = x_name + return await mat.build() + + +russian_manage = RussianManage() diff --git a/zhenxun/plugins/russian/model.py b/zhenxun/plugins/russian/model.py new file mode 100644 index 00000000..0fab9298 --- /dev/null +++ b/zhenxun/plugins/russian/model.py @@ -0,0 +1,107 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class RussianUser(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255) + """群聊id""" + win_count = fields.IntField(default=0) + """胜利次数""" + fail_count = fields.IntField(default=0) + """失败次数""" + make_money = fields.IntField(default=0) + """赢得金币""" + lose_money = fields.IntField(default=0) + """输得金币""" + winning_streak = fields.IntField(default=0) + """当前连胜""" + losing_streak = fields.IntField(default=0) + """当前连败""" + max_winning_streak = fields.IntField(default=0) + """最大连胜""" + max_losing_streak = fields.IntField(default=0) + """最大连败""" + + class Meta: + table = "russian_users" + table_description = "俄罗斯轮盘数据表" + unique_together = ("user_id", "group_id") + + @classmethod + async def add_count(cls, user_id: str, group_id: str, itype: str): + """添加用户输赢次数 + + 说明: + user_id: 用户id + group_id: 群号 + itype: 输或赢 'win' or 'lose' + """ + user, _ = await cls.get_or_create(user_id=user_id, group_id=group_id) + if itype == "win": + _max = ( + user.max_winning_streak + if user.max_winning_streak > user.winning_streak + 1 + else user.winning_streak + 1 + ) + user.win_count = user.win_count + 1 + user.winning_streak = user.winning_streak + 1 + user.losing_streak = 0 + user.max_winning_streak = _max + await user.save( + update_fields=[ + "win_count", + "winning_streak", + "losing_streak", + "max_winning_streak", + ] + ) + elif itype == "lose": + _max = ( + user.max_losing_streak + if user.max_losing_streak > user.losing_streak + 1 + else user.losing_streak + 1 + ) + user.fail_count = user.fail_count + 1 + user.losing_streak = user.losing_streak + 1 + user.winning_streak = 0 + user.max_losing_streak = _max + await user.save( + update_fields=[ + "fail_count", + "winning_streak", + "losing_streak", + "max_losing_streak", + ] + ) + return user + + @classmethod + async def money(cls, user_id: str, group_id: str, itype: str, count: int): + """添加用户输赢金钱 + + 参数: + user_id: 用户id + group_id: 群号 + itype: 输或赢 'win' or 'lose' + count: 金钱数量 + """ + user, _ = await cls.get_or_create(user_id=str(user_id), group_id=group_id) + if itype == "win": + user.make_money = user.make_money + count + elif itype == "lose": + user.lose_money = user.lose_money + count + await user.save(update_fields=["make_money", "lose_money"]) + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE russian_users RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE russian_users ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE russian_users ALTER COLUMN group_id TYPE character varying(255);", + ] diff --git a/zhenxun/utils/_build_mat.py b/zhenxun/utils/_build_mat.py index 4f22d923..6f30d301 100644 --- a/zhenxun/utils/_build_mat.py +++ b/zhenxun/utils/_build_mat.py @@ -556,7 +556,7 @@ class BuildMat: random_color = random.choice(bar_color) max_num = max(self.y_index) for y_p, y in zip(init_graph.y_point, self.build_data.data): - bar_width = int(y / max_num * graph_height) + bar_width = int(y / max_num * graph_height) or 1 bar = BuildImage(bar_width, 18, random_color) await mark_image.paste(bar, (y_width + 1, y_p - 9)) if self.build_data.display_num: From 4b48fc2557101394c90eff75431472330749d282 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 18 Jul 2024 23:16:29 +0800 Subject: [PATCH 050/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=B7=BB=E5=8A=A0=E7=BB=9F=E4=B8=80=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 103 ++++++++++++++- pyproject.toml | 1 + zhenxun/builtin_plugins/admin/ban/__init__.py | 1 - .../admin/plugin_switch/__init__.py | 119 ++++++++++++++++-- .../admin/plugin_switch/_data_source.py | 87 +++++++++++++ .../admin/plugin_switch/command.py | 51 +++++++- zhenxun/builtin_plugins/nickname.py | 5 +- zhenxun/builtin_plugins/sign_in/__init__.py | 20 +-- zhenxun/plugins/roll.py | 6 +- 9 files changed, 359 insertions(+), 34 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2c7c7abd..4c1c1694 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiofiles" @@ -309,6 +309,57 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "black" +version = "24.4.2" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "cachetools" version = "5.3.2" @@ -1388,6 +1439,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nb-cli" version = "1.3.0" @@ -1785,6 +1852,38 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pillow" version = "9.5.0" @@ -3403,4 +3502,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c1da4a148819ff244d291be9ca67466a07d994f29d2c1821a2e640997bfe617c" +content-hash = "bb01964309a665f0348ca69fecf771a9c6f7d99147c47010c0e64d2d13fe25ad" diff --git a/pyproject.toml b/pyproject.toml index 9181a780..b8cd14b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ psutil = "^5.9.8" feedparser = "^6.0.11" opencv-python = "^4.9.0.80" imagehash = "^4.3.1" +black = "^24.4.2" [tool.poetry.dev-dependencies] diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 822bcf98..f57b7b07 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -8,7 +8,6 @@ from nonebot_plugin_alconna import ( At, Match, Option, - Subcommand, on_alconna, store_true, ) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index bbaab13b..5d84f2b0 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -43,6 +43,12 @@ __plugin_meta__ = PluginMetadata( 插件列表 开启/关闭[功能名称] ?[-t ["private", "p", "group", "g"](关闭类型)] ?[-g 群组Id] + 开启/关闭插件df[功能名称]: 开启/关闭指定插件进群默认状态 + 开启/关闭所有插件df: 开启/关闭所有插件进群默认状态 + 开启/关闭所有插件: + 私聊中: 开启/关闭所有插件全局状态 + 群组中: 开启/关闭当前群组所有插件状态 + 私聊下: 示例: 开启签到 : 全局开启签到 @@ -90,19 +96,69 @@ async def _( bot: Bot, session: EventSession, arparma: Arparma, - name: str, + plugin_name: Match[str], group: Match[str], task: Query[bool] = AlconnaQuery("task.value", False), + default_status: Query[bool] = AlconnaQuery("default.value", False), + all: Query[bool] = AlconnaQuery("all.value", False), ): - if gid := session.id3 or session.id2: + if not all.result and not plugin_name.available: + await Text("请输入功能名称").finish(reply=True) + name = plugin_name.result + gid = session.id3 or session.id2 + if gid: + """群组中使用命令""" if task.result: result = await PluginManage.unblock_group_task(name, gid) + logger.info(f"开启群组被动 {name}", arparma.header_result, session=session) else: - result = await PluginManage.block_group_plugin(name, gid) + if session.id1 in bot.config.superusers and ( + all.result or default_status.result + ): + if all.result: + """所有插件""" + result = await PluginManage.set_all_plugin_status( + True, default_status.result, gid + ) + logger.info( + f"超级用户开启群组中全部功能", + arparma.header_result, + session=session, + ) + else: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, True) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: + result = await PluginManage.block_group_plugin(name, gid) + logger.info(f"开启功能 {name}", arparma.header_result, session=session) await Text(result).send(reply=True) - logger.info(f"开启功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: + """私聊""" group_id = group.result if group.available else None + if all.result: + result = await PluginManage.set_all_plugin_status( + True, default_status.result, group_id + ) + logger.info( + f"超级用户开启全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) + await Text(result).finish(reply=True) + if default_status.result: + result = await PluginManage.set_default_status(name, True) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + target=group_id, + ) + await Text(result).finish(reply=True) if task.result: result = await PluginManage.superuser_task_handle(name, group_id, True) await Text(result).send(reply=True) @@ -128,20 +184,67 @@ async def _( bot: Bot, session: EventSession, arparma: Arparma, - name: str, + plugin_name: Match[str], block_type: Match[str], group: Match[str], task: Query[bool] = AlconnaQuery("task.value", False), + default_status: Query[bool] = AlconnaQuery("default.value", False), + all: Query[bool] = AlconnaQuery("all.value", False), ): - if gid := session.id3 or session.id2: + if not all.result and not plugin_name.available: + await Text("请输入功能名称").finish(reply=True) + name = plugin_name.result + gid = session.id3 or session.id2 + if gid: if task.result: result = await PluginManage.block_group_task(name, gid) else: - result = await PluginManage.unblock_group_plugin(name, gid) + if session.id1 in bot.config.superusers and ( + all.result or default_status.result + ): + if all.result: + """所有插件""" + result = await PluginManage.set_all_plugin_status( + False, default_status.result, gid + ) + logger.info( + f"超级用户开启群组中全部功能", + arparma.header_result, + session=session, + ) + else: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, False) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: + result = await PluginManage.unblock_group_plugin(name, gid) + logger.info(f"关闭功能 {name}", arparma.header_result, session=session) await Text(result).send(reply=True) - logger.info(f"关闭功能 {name}", arparma.header_result, session=session) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None + if all.result: + result = await PluginManage.set_all_plugin_status( + False, default_status.result, group_id + ) + logger.info( + f"超级用户关闭全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) + await Text(result).finish(reply=True) + if default_status.result: + result = await PluginManage.set_default_status(name, False) + logger.info( + f"超级用户关闭 {name} 功能进群默认开关", + arparma.header_result, + session=session, + target=group_id, + ) + await Text(result).finish(reply=True) if task.result: result = await PluginManage.superuser_task_handle(name, group_id, False) await Text(result).send(reply=True) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index 5894cfbc..1c96eed0 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -145,8 +145,75 @@ async def build_task(group_id: str | None) -> BuildImage: class PluginManage: + @classmethod + async def set_default_status(cls, plugin_name: str, status: bool) -> str: + """设置插件进群默认状态 + + 参数: + plugin_name: 插件名称 + status: 状态 + + 返回: + str: 返回信息 + """ + if plugin_name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(plugin_name)) + else: + plugin = await PluginInfo.get_or_none(name=plugin_name) + if plugin: + plugin.default_status = status + await plugin.save(update_fields=["default_status"]) + return f'成功将 {plugin.name} 进群默认状态修改为: {"开启" if status else "关闭"}' + return f"没有找到这个功能喔..." + + @classmethod + async def set_all_plugin_status( + cls, status: bool, is_default: bool = False, group_id: str | None = None + ) -> str: + """修改所有插件状态 + + 参数: + status: 状态 + is_default: 是否进群默认. + group_id: 指定群组id. + + 返回: + str: 返回信息 + """ + if is_default: + await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( + default_status=status + ) + return f'成功将所有功能进群默认状态修改为: {"开启" if status else "关闭"}' + if group_id: + if group := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + if status: + group.block_plugin = "" + else: + module_list = await PluginInfo.filter( + plugin_type=PluginType.NORMAL + ).values_list("module", flat=True) + group.block_plugin = ",".join(module_list) + "," # type: ignore + await group.save(update_fields=["block_plugin"]) + return f'成功将此群组所有功能状态修改为: {"开启" if status else "关闭"}' + return "获取群组失败..." + await PluginInfo.filter(plugin_type=PluginType.NORMAL).update( + status=status, block_type=BlockType.ALL if not status else None + ) + return f'成功将所有功能全局状态修改为: {"开启" if status else "关闭"}' + @classmethod async def is_wake(cls, group_id: str) -> bool: + """是否醒来 + + 参数: + group_id: 群组id + + 返回: + bool: 是否醒来 + """ if c := await GroupConsole.get_or_none( group_id=group_id, channel_id__isnull=True ): @@ -155,22 +222,42 @@ class PluginManage: @classmethod async def sleep(cls, group_id: str): + """休眠 + + 参数: + group_id: 群组id + """ await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( status=False ) @classmethod async def wake(cls, group_id: str): + """醒来 + + 参数: + group_id: 群组id + """ await GroupConsole.filter(group_id=group_id, channel_id__isnull=True).update( status=True ) @classmethod async def block(cls, module: str): + """禁用 + + 参数: + module: 模块名 + """ await PluginInfo.filter(module=module).update(status=False) @classmethod async def unblock(cls, module: str): + """启用 + + 参数: + module: 模块名 + """ await PluginInfo.filter(module=module).update(status=True) @classmethod diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index f1323227..13f13999 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -14,9 +14,11 @@ _status_matcher = on_alconna( Alconna( "switch", Option("-t|--task", action=store_true, help_text="被动技能"), + Option("-df|--default", action=store_true, help_text="进群默认开关"), + Option("--all", action=store_true, help_text="全部插件"), Subcommand( "open", - Args["name", [str, int]], + Args["plugin_name?", [str, int]], Option( "-g|--group", Args["group", str], @@ -24,7 +26,7 @@ _status_matcher = on_alconna( ), Subcommand( "close", - Args["name", [str, int]], + Args["plugin_name?", [str, int]], Option( "-t|--type", Args["block_type", ["all", "a", "private", "p", "group", "g"]], @@ -72,6 +74,27 @@ _status_matcher.shortcut( ) +_status_matcher.shortcut( + r"开启所有插件", + command="switch", + arguments=["open", "s", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启所有插件df", + command="switch", + arguments=["open", "s", "-df", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"开启插件df(?P.+)", + command="switch", + arguments=["open", "{name}", "-df"], + prefix=True, +) + _status_matcher.shortcut( r"开启(?P.+)", command="switch", @@ -79,6 +102,7 @@ _status_matcher.shortcut( prefix=True, ) + _status_matcher.shortcut( r"关闭群被动(?P.+)", command="switch", @@ -86,6 +110,28 @@ _status_matcher.shortcut( prefix=True, ) + +_status_matcher.shortcut( + r"关闭所有插件", + command="switch", + arguments=["close", "s", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭所有插件df", + command="switch", + arguments=["close", "s", "-df", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭插件df(?P.+)", + command="switch", + arguments=["close", "{name}", "-df"], + prefix=True, +) + _status_matcher.shortcut( r"关闭(?P.+)", command="switch", @@ -93,7 +139,6 @@ _status_matcher.shortcut( prefix=True, ) - _group_status_matcher.shortcut( r"醒来", command="group-status", diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py index 7a67c38c..b02b4ac1 100644 --- a/zhenxun/builtin_plugins/nickname.py +++ b/zhenxun/builtin_plugins/nickname.py @@ -18,6 +18,7 @@ from zhenxun.models.ban_console import BanConsole from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName from zhenxun.utils.enum import PluginType __plugin_meta__ = PluginMetadata( @@ -177,7 +178,7 @@ async def _( @_global_nickname_matcher.handle(parameterless=[CheckNickname()]) async def _( session: EventSession, - user_info: UserInfo = EventUserInfo(), + nickname: str = UserName(), reg_group: tuple[Any, ...] = RegexGroup(), ): if session.id1: @@ -185,7 +186,7 @@ async def _( await FriendUser.set_user_nickname( session.id1, name, - user_info.user_displayname or user_info.user_remark or user_info.user_name, + nickname, session.platform, ) await GroupInfoUser.filter(user_id=session.id1).update(nickname=name) diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index d6c1e40a..964c1d5e 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -14,6 +14,7 @@ from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName from ._data_source import SignManage from .goods_register import driver @@ -106,29 +107,23 @@ _sign_matcher.shortcut( @_sign_matcher.assign("$main") async def _( - session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() + session: EventSession, arparma: Arparma, nickname: str = UserName() ): - nickname = ( - user_info.user_displayname or user_info.user_remark or user_info.user_name - ) if session.id1: if path := await SignManage.sign(session, nickname): logger.info("签到成功", arparma.header_result, session=session) - await Image(path).finish(reply=True) + await Image(path).finish() return Text("用户id为空...").send() @_sign_matcher.assign("my") async def _( - session: EventSession, arparma: Arparma, user_info: UserInfo = EventUserInfo() + session: EventSession, arparma: Arparma, nickname: str = UserName() ): - nickname = ( - user_info.user_displayname or user_info.user_remark or user_info.user_name - ) if session.id1: if image := await SignManage.sign(session, nickname, True): logger.info("查看我的签到", arparma.header_result, session=session) - await Image(image).finish(reply=True) + await Image(image).finish() return Text("用户id为空...").send() @@ -137,11 +132,8 @@ async def _( session: EventSession, arparma: Arparma, num: int, - user_info: UserInfo = EventUserInfo(), + nickname: str = UserName() ): - nickname = ( - user_info.user_displayname or user_info.user_remark or user_info.user_name - ) if session.id1: if image := await SignManage.rank(session.id1, num): logger.info("查看签到排行", arparma.header_result, session=session) diff --git a/zhenxun/plugins/roll.py b/zhenxun/plugins/roll.py index 98092d08..1427080a 100644 --- a/zhenxun/plugins/roll.py +++ b/zhenxun/plugins/roll.py @@ -11,6 +11,7 @@ from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.depends import UserName __plugin_meta__ = PluginMetadata( name="roll", @@ -34,14 +35,11 @@ _matcher = on_command("roll", priority=5, block=True) async def _( session: EventSession, message: UniMsg, - user_info: UserInfo = EventUserInfo(), + user_name: str = UserName(), ): text = message.extract_plain_text().strip().replace("roll", "", 1).split() if not text: await Text(f"roll: {random.randint(0, 100)}").finish(reply=True) - user_name = ( - user_info.user_displayname or user_info.user_remark or user_info.user_name - ) await Text( random.choice( [ From d45baaddbc911956f8352830ba80cff95c8fdb9a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 20 Jul 2024 00:45:26 +0800 Subject: [PATCH 051/132] =?UTF-8?q?=F0=9F=8E=A8=20:=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/init/init_config.py | 3 +-- zhenxun/services/db_context.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/zhenxun/builtin_plugins/init/init_config.py b/zhenxun/builtin_plugins/init/init_config.py index a837d4f9..26534d4d 100644 --- a/zhenxun/builtin_plugins/init/init_config.py +++ b/zhenxun/builtin_plugins/init/init_config.py @@ -4,8 +4,7 @@ import nonebot from nonebot import get_loaded_plugins from nonebot.drivers import Driver from nonebot.plugin import Plugin -from ruamel import yaml -from ruamel.yaml import YAML, round_trip_dump, round_trip_load +from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap from zhenxun.configs.config import Config diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index dfd02925..4bd8db7b 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -80,10 +80,12 @@ async def init(): # await TestSQL.raw(sql) except Exception as e: logger.debug(f"执行SQL: {sql} 错误...", e=e) + if sql_list: + logger.debug("SCRIPT_METHOD方法执行完毕!") await Tortoise.generate_schemas() logger.info(f"Database loaded successfully!") except Exception as e: - raise Exception(f"数据库连接错误...", e=e) + raise Exception(f"数据库连接错误...") async def disconnect(): From 167555016ab4c8ea990b4df09b9e035ae1906d70 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 20 Jul 2024 00:46:21 +0800 Subject: [PATCH 052/132] =?UTF-8?q?=E2=9C=A8=20:=20=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=AE=A1=E7=90=86=E5=85=81=E8=AE=B8=E7=BE=A4?= =?UTF-8?q?=E7=BB=84=E7=AE=A1=E7=90=86=E5=91=98=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 51 ++++++++++--------- .../admin/plugin_switch/command.py | 16 +++--- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 5d84f2b0..36e57c41 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -12,7 +12,7 @@ from zhenxun.utils.enum import BlockType, PluginType from ._data_source import PluginManage, build_plugin, build_task from .command import _group_status_matcher, _status_matcher -base_config = Config.get("admin_bot_manage") +base_config = Config.get("plugin_switch") __plugin_meta__ = PluginMetadata( @@ -23,6 +23,7 @@ __plugin_meta__ = PluginMetadata( 格式: 开启/关闭[功能名称] : 开关功能 开启/关闭群被动[被动名称] : 群被动开关 + 开启/关闭所有插件 : 开启/关闭当前群组所有插件状态 群被动状态 : 查看被动技能开关状态 醒来 : 结束休眠 休息吧 : 群组休眠, 不会再响应命令 @@ -112,30 +113,30 @@ async def _( result = await PluginManage.unblock_group_task(name, gid) logger.info(f"开启群组被动 {name}", arparma.header_result, session=session) else: - if session.id1 in bot.config.superusers and ( - all.result or default_status.result - ): + if session.id1 in bot.config.superusers and default_status.result: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, True) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: if all.result: """所有插件""" result = await PluginManage.set_all_plugin_status( True, default_status.result, gid ) logger.info( - f"超级用户开启群组中全部功能", + f"开启群组中全部功能", arparma.header_result, session=session, ) else: - """单个插件的进群默认修改""" - result = await PluginManage.set_default_status(name, True) + result = await PluginManage.block_group_plugin(name, gid) logger.info( - f"超级用户开启 {name} 功能进群默认开关", - arparma.header_result, - session=session, + f"开启功能 {name}", arparma.header_result, session=session ) - else: - result = await PluginManage.block_group_plugin(name, gid) - logger.info(f"开启功能 {name}", arparma.header_result, session=session) await Text(result).send(reply=True) elif session.id1 in bot.config.superusers: """私聊""" @@ -199,30 +200,30 @@ async def _( if task.result: result = await PluginManage.block_group_task(name, gid) else: - if session.id1 in bot.config.superusers and ( - all.result or default_status.result - ): + if session.id1 in bot.config.superusers and default_status.result: + """单个插件的进群默认修改""" + result = await PluginManage.set_default_status(name, False) + logger.info( + f"超级用户开启 {name} 功能进群默认开关", + arparma.header_result, + session=session, + ) + else: if all.result: """所有插件""" result = await PluginManage.set_all_plugin_status( False, default_status.result, gid ) logger.info( - f"超级用户开启群组中全部功能", + f"关闭群组中全部功能", arparma.header_result, session=session, ) else: - """单个插件的进群默认修改""" - result = await PluginManage.set_default_status(name, False) + result = await PluginManage.unblock_group_plugin(name, gid) logger.info( - f"超级用户开启 {name} 功能进群默认开关", - arparma.header_result, - session=session, + f"关闭功能 {name}", arparma.header_result, session=session ) - else: - result = await PluginManage.unblock_group_plugin(name, gid) - logger.info(f"关闭功能 {name}", arparma.header_result, session=session) await Text(result).send(reply=True) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index 13f13999..ddbdb535 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -37,14 +37,14 @@ _status_matcher = on_alconna( ), ), ), - rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL"), + rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL"), priority=5, block=True, ) _group_status_matcher = on_alconna( Alconna("group-status", Args["status", ["sleep", "wake"]]), - rule=admin_check("admin_bot_manage", "CHANGE_GROUP_SWITCH_LEVEL") + rule=admin_check("plugin_switch", "CHANGE_GROUP_SWITCH_LEVEL") & ensure_group & to_me(), priority=5, @@ -75,21 +75,21 @@ _status_matcher.shortcut( _status_matcher.shortcut( - r"开启所有插件", + r"开启所有(插件|功能)", command="switch", arguments=["open", "s", "--all"], prefix=True, ) _status_matcher.shortcut( - r"开启所有插件df", + r"开启所有(插件|功能)df", command="switch", arguments=["open", "s", "-df", "--all"], prefix=True, ) _status_matcher.shortcut( - r"开启插件df(?P.+)", + r"开启(插件|功能)df(?P.+)", command="switch", arguments=["open", "{name}", "-df"], prefix=True, @@ -112,21 +112,21 @@ _status_matcher.shortcut( _status_matcher.shortcut( - r"关闭所有插件", + r"关闭所有(插件|功能)", command="switch", arguments=["close", "s", "--all"], prefix=True, ) _status_matcher.shortcut( - r"关闭所有插件df", + r"关闭所有(插件|功能)df", command="switch", arguments=["close", "s", "-df", "--all"], prefix=True, ) _status_matcher.shortcut( - r"关闭插件df(?P.+)", + r"关闭(插件|功能)df(?P.+)", command="switch", arguments=["close", "{name}", "-df"], prefix=True, From 487f019c890f5b7c2aa9232a8401c502c56a1e80 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 21 Jul 2024 19:06:50 +0800 Subject: [PATCH 053/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=AD=BE=E5=88=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 2 +- .../builtin_plugins/sign_in/_data_source.py | 27 ++++++++----------- zhenxun/builtin_plugins/sign_in/utils.py | 3 +-- zhenxun/configs/utils/__init__.py | 5 +++- zhenxun/models/sign_log.py | 2 +- zhenxun/plugins/ai/data_source.py | 4 +-- zhenxun/plugins/mute/_data_source.py | 2 +- 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 6bfe34e3..b0ae260a 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -54,7 +54,7 @@ from public.bag_users t1 """ -# @driver.on_startup +@driver.on_startup async def _(): global flag await shop_register.load_register() diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 2affe19d..c5d41de8 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -80,14 +80,14 @@ class SignManage: @classmethod async def sign( - cls, session: EventSession, nickname: str, is_view_card: bool = False + cls, session: EventSession, nickname: str, is_card_view: bool = False ) -> Path | None: """签到 参数: session: Session nickname: 用户昵称 - is_view_card: 是否展示卡片 + is_card_view: 是否展示卡片 返回: Path: 卡片路径 @@ -100,17 +100,15 @@ class SignManage: user_id=session.id1, defaults={"user_console": user_console, "platform": session.platform}, ) - new_log = await SignLog.filter(user_id=session.id1).first() - file_name = f"{user}_sign_{datetime.now().date()}.png" - if ( - user.sign_count != 0 - or (new_log and now > new_log.create_time) - or file_name in os.listdir(SIGN_TODAY_CARD_PATH) - ): - path = await get_card(user, nickname, -1, user_console.gold, "") - else: - path = await cls._handle_sign_in(user, nickname, session, is_view_card) - return path + new_log = ( + await SignLog.filter(user_id=session.id1).order_by("-create_time").first() + ) + if not is_card_view: + if not new_log or (new_log and new_log.create_time.date() != now.date()): + return await cls._handle_sign_in(user, nickname, session) + return await get_card( + user, nickname, -1, user_console.gold, "", is_card_view=is_card_view + ) @classmethod async def _handle_sign_in( @@ -118,7 +116,6 @@ class SignManage: user: SignUser, nickname: str, session: EventSession, - is_view_card: bool, ) -> Path: """签到处理 @@ -126,7 +123,6 @@ class SignManage: user: SignUser nickname: 用户昵称 session: Session - is_view_card: 是否展示卡片 返回: Path: 卡片路径 @@ -165,5 +161,4 @@ class SignManage: gold, gift, rand + add_probability > 0.97 or rand < specify_probability, - is_view_card, ) diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 73dd926a..75ee333f 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -172,9 +172,8 @@ async def _generate_card( uid_img = await BuildImage.build_text_image( f"UID: {uid}", size=30, font_color=(255, 255, 255) ) - sign_count = await SignLog.filter(user_id=user.user_id).count() sign_day_img = await BuildImage.build_text_image( - f"{sign_count}", size=40, font_color=(211, 64, 33) + f"{user.sign_count}", size=40, font_color=(211, 64, 33) ) lik_text1_img = await BuildImage.build_text_image("当前", size=20) lik_text2_img = await BuildImage.build_text_image( diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index c535d46d..4a192b3d 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -69,7 +69,10 @@ class ConfigGroup(BaseModel): def get(self, c: str, default: Any = None) -> Any: cfg = self.configs.get(c) if cfg is not None: - return cfg.value + if cfg.value is not None: + return cfg.value + if cfg.default_value is not None: + return cfg.default_value return default diff --git a/zhenxun/models/sign_log.py b/zhenxun/models/sign_log.py index fffdee2c..cbfca947 100644 --- a/zhenxun/models/sign_log.py +++ b/zhenxun/models/sign_log.py @@ -10,7 +10,7 @@ class SignLog(Model): id = fields.IntField(pk=True, generated=True, auto_increment=True) """自增id""" - user_id = fields.CharField(255, unique=True, description="用户id") + user_id = fields.CharField(255, description="用户id") """用户id""" impression = fields.DecimalField(10, 3, default=0, description="好感度") """好感度""" diff --git a/zhenxun/plugins/ai/data_source.py b/zhenxun/plugins/ai/data_source.py index 4b9136c4..dbef7731 100644 --- a/zhenxun/plugins/ai/data_source.py +++ b/zhenxun/plugins/ai/data_source.py @@ -24,7 +24,7 @@ anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) async def get_chat_result( message: UniMsg, user_id: str, nickname: str -) -> Text | MessageFactory: +) -> Text | MessageFactory | None: """获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 参数: @@ -54,7 +54,7 @@ async def get_chat_result( if not rst: rst = await xie_ai(text) if not rst: - return no_result() + return None if nickname: if len(nickname) < 5: if random.random() < 0.5: diff --git a/zhenxun/plugins/mute/_data_source.py b/zhenxun/plugins/mute/_data_source.py index 7c03123e..206de400 100644 --- a/zhenxun/plugins/mute/_data_source.py +++ b/zhenxun/plugins/mute/_data_source.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from zhenxun.configs.config import Config from zhenxun.configs.path_config import DATA_PATH -base_config = Config.get("mute") +base_config = Config.get("mute_setting") class GroupData(BaseModel): From c75b0950bdd2b6951832bc28e884ee4f67a065e6 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 21 Jul 2024 23:26:56 +0800 Subject: [PATCH 054/132] =?UTF-8?q?=F0=9F=90=9B=20:=20fix=20uuid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 5 +++++ zhenxun/models/goods_info.py | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index b0ae260a..91e350b8 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -1,4 +1,5 @@ import os +import uuid from nonebot import require from nonebot.drivers import Driver @@ -57,6 +58,10 @@ from public.bag_users t1 @driver.on_startup async def _(): global flag + if goods_list := await GoodsInfo.filter(uuid__isnull=True).all(): + for goods in goods_list: + goods.uuid = uuid.uuid1() # type: ignore + await GoodsInfo.bulk_update(goods_list, ["uuid"], 10) await shop_register.load_register() if ( flag diff --git a/zhenxun/models/goods_info.py b/zhenxun/models/goods_info.py index 53aefe61..f776500b 100644 --- a/zhenxun/models/goods_info.py +++ b/zhenxun/models/goods_info.py @@ -153,10 +153,6 @@ class GoodsInfo(Model): @classmethod async def _run_script(cls): - if goods_list := await cls.filter(uuid__isnull=True).all(): - for goods in goods_list: - goods.uuid = uuid.uuid1() - await cls.bulk_update(goods_list, ["uuid"], 10) return [ "ALTER TABLE goods_info ADD uuid VARCHAR(255);", "ALTER TABLE goods_info ADD daily_limit Integer DEFAULT 0;", From 475a188a80c70c7dc1ea40d09a118ec93f9eb76b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 23 Jul 2024 19:00:38 +0800 Subject: [PATCH 055/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BE=A4=E8=A2=AB=E5=8A=A8=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 28 ++++++++++++++----- zhenxun/plugins/group_welcome_msg.py | 2 +- zhenxun/utils/image_utils.py | 8 ++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 36e57c41..aa08ae78 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -76,20 +76,34 @@ async def _( bot: Bot, session: EventSession, arparma: Arparma, - task: Query[bool] = AlconnaQuery("task.value", False), ): - image = None - if task.result: - image = await build_task(session.id3 or session.id2) - elif session.id1 in bot.config.superusers: + if session.id1 in bot.config.superusers: image = await build_plugin() - if image: await Image(image.pic2bytes()).send(reply=True) logger.info( - f"查看{'功能' if arparma.find('task') else '被动'}列表", + f"查看功能列表", arparma.header_result, session=session, ) + else: + await Text("权限不足捏...").send(reply=True) + + +@_status_matcher.assign("task") +async def _( + session: EventSession, + arparma: Arparma, +): + image = await build_task(session.id3 or session.id2) + if image: + await Image(image.pic2bytes()).send(reply=True) + logger.info( + f"查看群被动列表", + arparma.header_result, + session=session, + ) + else: + await Text("获取群被动任务失败...").send(reply=True) @_status_matcher.assign("open") diff --git a/zhenxun/plugins/group_welcome_msg.py b/zhenxun/plugins/group_welcome_msg.py index 2784f458..064987e6 100644 --- a/zhenxun/plugins/group_welcome_msg.py +++ b/zhenxun/plugins/group_welcome_msg.py @@ -47,7 +47,7 @@ async def _( file = path / "text.json" if not file.exists(): await Text("未设置群欢迎消息...").finish(reply=True) - message = json.load(open(file))["message"] + message = json.load(open(file, encoding="utf8"))["message"] message_split = re.split(r"\[image:\d+\]", message) if len(message_split) == 1: await Text(message_split[0]).finish(reply=True) diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index b44abcde..0c6a2b27 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -380,8 +380,12 @@ def get_img_hash(image_file: str | Path) -> str: 返回: str: 哈希值 """ - with open(image_file, "rb") as fp: - hash_value = imagehash.average_hash(Image.open(fp)) + hash_value = "" + try: + with open(image_file, "rb") as fp: + hash_value = imagehash.average_hash(Image.open(fp)) + except Exception as e: + logger.warning(f"获取图片Hash出错", "禁言检测", e=e) return str(hash_value) From cf208e2f6496472f5bab1029498dec7304f4e53f Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 23 Jul 2024 19:53:42 +0800 Subject: [PATCH 056/132] =?UTF-8?q?=F0=9F=90=9B=20:=E7=BE=A4=E8=A2=AB?= =?UTF-8?q?=E5=8A=A8=E7=8A=B6=E6=80=81=E4=BF=AE=E6=94=B9=E6=97=B6=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index aa08ae78..ddc0e910 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -79,31 +79,14 @@ async def _( ): if session.id1 in bot.config.superusers: image = await build_plugin() - await Image(image.pic2bytes()).send(reply=True) logger.info( f"查看功能列表", arparma.header_result, session=session, ) + await Image(image.pic2bytes()).finish(reply=True) else: - await Text("权限不足捏...").send(reply=True) - - -@_status_matcher.assign("task") -async def _( - session: EventSession, - arparma: Arparma, -): - image = await build_task(session.id3 or session.id2) - if image: - await Image(image.pic2bytes()).send(reply=True) - logger.info( - f"查看群被动列表", - arparma.header_result, - session=session, - ) - else: - await Text("获取群被动任务失败...").send(reply=True) + await Text("权限不足捏...").finish(reply=True) @_status_matcher.assign("open") @@ -151,7 +134,7 @@ async def _( logger.info( f"开启功能 {name}", arparma.header_result, session=session ) - await Text(result).send(reply=True) + await Text(result).finish(reply=True) elif session.id1 in bot.config.superusers: """私聊""" group_id = group.result if group.available else None @@ -176,22 +159,22 @@ async def _( await Text(result).finish(reply=True) if task.result: result = await PluginManage.superuser_task_handle(name, group_id, True) - await Text(result).send(reply=True) logger.info( f"超级用户开启被动技能 {name}", arparma.header_result, session=session, target=group_id, ) + await Text(result).finish(reply=True) else: result = await PluginManage.superuser_block(name, None, group_id) - await Text(result).send(reply=True) logger.info( f"超级用户开启功能 {name}", arparma.header_result, session=session, target=group_id, ) + await Text(result).finish(reply=True) @_status_matcher.assign("close") @@ -238,7 +221,7 @@ async def _( logger.info( f"关闭功能 {name}", arparma.header_result, session=session ) - await Text(result).send(reply=True) + await Text(result).finish(reply=True) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None if all.result: @@ -262,13 +245,13 @@ async def _( await Text(result).finish(reply=True) if task.result: result = await PluginManage.superuser_task_handle(name, group_id, False) - await Text(result).send(reply=True) logger.info( f"超级用户关闭被动技能 {name}", arparma.header_result, session=session, target=group_id, ) + await Text(result).finish(reply=True) else: _type = BlockType.ALL if block_type.available: @@ -277,13 +260,13 @@ async def _( elif block_type.result in ["g", "group"]: _type = BlockType.GROUP result = await PluginManage.superuser_block(name, _type, group_id) - await Text(result).send(reply=True) logger.info( f"超级用户关闭功能 {name}, 禁用类型: {_type}", arparma.header_result, session=session, target=group_id, ) + await Text(result).finish(reply=True) @_group_status_matcher.handle() @@ -305,3 +288,20 @@ async def _( logger.info("醒来", arparma.header_result, session=session) await Text("呜..醒来了...").finish() return Text("群组id为空...").send() + + +@_status_matcher.assign("task") +async def _( + session: EventSession, + arparma: Arparma, +): + image = await build_task(session.id3 or session.id2) + if image: + logger.info( + f"查看群被动列表", + arparma.header_result, + session=session, + ) + await Image(image.pic2bytes()).finish(reply=True) + else: + await Text("获取群被动任务失败...").finish(reply=True) From 137870b698e45e585b4394d7f87052ce8bd5da3b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 27 Jul 2024 04:30:03 +0800 Subject: [PATCH 057/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AF=8D=E6=9D=A1word=5Fbank?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/admin_help.py | 2 +- .../builtin_plugins/superuser/super_help.py | 2 +- zhenxun/plugins/word_bank/__init__.py | 18 + zhenxun/plugins/word_bank/_config.py | 24 + zhenxun/plugins/word_bank/_data_source.py | 288 +++++++++ zhenxun/plugins/word_bank/_model.py | 566 ++++++++++++++++++ zhenxun/plugins/word_bank/_rule.py | 59 ++ zhenxun/plugins/word_bank/command.py | 54 ++ zhenxun/plugins/word_bank/message_handle.py | 31 + zhenxun/plugins/word_bank/word_handle.py | 314 ++++++++++ zhenxun/utils/_image_template.py | 22 +- 11 files changed, 1371 insertions(+), 9 deletions(-) create mode 100644 zhenxun/plugins/word_bank/__init__.py create mode 100644 zhenxun/plugins/word_bank/_config.py create mode 100644 zhenxun/plugins/word_bank/_data_source.py create mode 100644 zhenxun/plugins/word_bank/_model.py create mode 100644 zhenxun/plugins/word_bank/_rule.py create mode 100644 zhenxun/plugins/word_bank/command.py create mode 100644 zhenxun/plugins/word_bank/message_handle.py create mode 100644 zhenxun/plugins/word_bank/word_handle.py diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index 6cad3cc0..b151ed09 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -125,7 +125,7 @@ async def build_help() -> BuildImage: ) if task_list := await TaskInfo.all(): task_str = "\n".join([task.name for task in task_list]) - task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str + task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) await task_image.circle_corner(10) A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") diff --git a/zhenxun/builtin_plugins/superuser/super_help.py b/zhenxun/builtin_plugins/superuser/super_help.py index a47943d7..5136bb5c 100644 --- a/zhenxun/builtin_plugins/superuser/super_help.py +++ b/zhenxun/builtin_plugins/superuser/super_help.py @@ -121,7 +121,7 @@ async def build_help() -> BuildImage: ) if task_list := await TaskInfo.all(): task_str = "\n".join([task.name for task in task_list]) - task_str = "通过 开启/关闭 来控制群被动\n----------\n" + task_str + task_str = "通过 开启/关闭群被动 来控制群被动\n----------\n" + task_str task_image = await text2image(task_str, padding=5, color=(255, 255, 255)) await task_image.circle_corner(10) A = BuildImage(task_image.width + 50, task_image.height + 85, "#EAEDF2") diff --git a/zhenxun/plugins/word_bank/__init__.py b/zhenxun/plugins/word_bank/__init__.py new file mode 100644 index 00000000..c3ef6546 --- /dev/null +++ b/zhenxun/plugins/word_bank/__init__.py @@ -0,0 +1,18 @@ +from pathlib import Path + +import nonebot + +from zhenxun.configs.config import Config + +Config.add_plugin_config( + "word_bank", + "WORD_BANK_LEVEL", + 5, + help="设置增删词库的权限等级", + default_value=5, + type=int, +) +Config.set_name("word_bank", "词库问答") + + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/word_bank/_config.py b/zhenxun/plugins/word_bank/_config.py new file mode 100644 index 00000000..3d074c18 --- /dev/null +++ b/zhenxun/plugins/word_bank/_config.py @@ -0,0 +1,24 @@ +from zhenxun.configs.path_config import DATA_PATH + +data_dir = DATA_PATH / "word_bank" +data_dir.mkdir(parents=True, exist_ok=True) + +scope2int = { + "全局": 0, + "群聊": 1, + "私聊": 2, +} + +type2int = { + "精准": 0, + "模糊": 1, + "正则": 2, + "图片": 3, +} + +int2type = { + 0: "精准", + 1: "模糊", + 2: "正则", + 3: "图片", +} diff --git a/zhenxun/plugins/word_bank/_data_source.py b/zhenxun/plugins/word_bank/_data_source.py new file mode 100644 index 00000000..efd7052d --- /dev/null +++ b/zhenxun/plugins/word_bank/_data_source.py @@ -0,0 +1,288 @@ +import re + +from nonebot.adapters.onebot.v11 import unescape +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMessage, UniMsg +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text + +from zhenxun.utils.image_utils import ImageTemplate + +from ._model import WordBank + + +def get_img_and_at_list(message: UniMsg) -> tuple[list[str], list[str]]: + """获取图片和at数据 + + 参数: + message: UniMsg + + 返回: + tuple[list[str], list[str]]: 图片列表,at列表 + """ + img_list, at_list = [], [] + for msg in message: + if isinstance(msg, alcImage): + img_list.append(msg.url) + elif isinstance(msg, alcAt): + at_list.append(msg.target) + return img_list, at_list + + +def get_problem(message: UniMsg) -> str: + """获取问题内容 + + 参数: + message: UniMsg + + 返回: + str: 问题文本 + """ + problem = "" + a, b = True, True + for msg in message: + if isinstance(msg, alcText) or isinstance(msg, str): + msg = str(msg) + if "问" in str(msg) and a: + a = False + split_text = msg.split("问") + if len(split_text) > 1: + problem += "问".join(split_text[1:]) + if b: + if "答" in problem: + b = False + problem = problem.split("答")[0] + elif "答" in msg and b: + b = False + # problem += "答".join(msg.split("答")[:-1]) + problem += msg.split("答")[0] + if not a and not b: + break + if isinstance(msg, alcAt): + problem += f"[at:{msg.target}]" + return problem + + +def get_answer(message: UniMsg) -> UniMessage | None: + """获取at时回答 + + 参数: + message: UniMsg + + 返回: + str: 回答内容 + """ + temp_message = None + answer = "" + index = 0 + for msg in message: + index += 1 + if isinstance(msg, alcText) or isinstance(msg, str): + msg = str(msg) + if "答" in msg: + answer += "答".join(msg.split("答")[1:]) + break + if answer: + temp_message = message[index:] + temp_message.insert(0, alcText(answer)) + return temp_message + + +class WordBankManage: + + @classmethod + async def update_word( + cls, + replace: str, + problem: str = "", + index: int | None = None, + group_id: str | None = None, + word_scope: int = 1, + ) -> tuple[str, str]: + """修改群词条 + + 参数: + params: 参数 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, str]: 处理消息,替换的旧词条 + """ + return await cls.__word_handle( + problem, group_id, "update", index, None, word_scope, replace + ) + + @classmethod + async def delete_word( + cls, + problem: str, + index: int | None = None, + aid: int | None = None, + group_id: str | None = None, + word_scope: int = 1, + ) -> tuple[str, str]: + """删除群词条 + + 参数: + params: 参数 + index: 指定下标 + aid: 指定回答下标 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, str]: 处理消息,空 + """ + return await cls.__word_handle( + problem, group_id, "delete", index, aid, word_scope + ) + + @classmethod + async def __word_handle( + cls, + problem: str, + group_id: str | None, + handle_type: str, + index: int | None = None, + aid: int | None = None, + word_scope: int = 0, + replace_problem: str = "", + ) -> tuple[str, str]: + """词条操作 + + 参数: + problem: 参数 + group_id: 群号 + handle_type: 类型 + index: 指定回答下标 + aid: 指定回答下标 + word_scope: 词条范围 + replace_problem: 替换问题内容 + + 返回: + tuple[str, str]: 处理消息,替换的旧词条 + """ + if index is not None: + problem, code = await cls.__get_problem_str(index, group_id, word_scope) + if code != 200: + return problem, "" + if handle_type == "delete": + if index: + problem, _problem_list = await WordBank.get_problem_all_answer( + problem, None, group_id, word_scope + ) + if not _problem_list: + return problem, "" + if await WordBank.delete_group_problem(problem, group_id, aid, word_scope): # type: ignore + return "删除词条成功!", "" + return "词条不存在", "" + if handle_type == "update": + old_problem = await WordBank.update_group_problem( + problem, replace_problem, group_id, word_scope=word_scope + ) + return f"修改词条成功!\n{old_problem} -> {replace_problem}", old_problem + return "类型错误", "" + + @classmethod + async def __get_problem_str( + cls, idx: int, group_id: str | None = None, word_scope: int = 1 + ) -> tuple[str, int]: + """通过id获取问题字符串 + + 参数: + idx: 下标 + group_id: 群号 + word_scope: 获取类型 + """ + if word_scope in [0, 2]: + all_problem = await WordBank.get_problem_by_scope(word_scope) + elif group_id: + all_problem = await WordBank.get_group_all_problem(group_id) + else: + raise Exception("词条类型与群组id不能为空") + if idx < 0 or idx >= len(all_problem): + return "问题下标id必须在范围内", 999 + return all_problem[idx][0], 200 + + @classmethod + async def show_word( + cls, + problem: str | None, + index: int | None = None, + group_id: str | None = None, + word_scope: int | None = 1, + ) -> Text | MessageFactory | Image: + """获取群词条 + + 参数: + problem: 问题 + group_id: 群组id + word_scope: 词条范围 + index: 指定回答下标 + """ + if problem or index != None: + msg_list = [] + problem, _problem_list = await WordBank.get_problem_all_answer( + problem, # type: ignore + index, + group_id if group_id is None else None, + word_scope, + ) + if not _problem_list: + return Text(problem) + for msg in _problem_list: + _text = str(msg) + if isinstance(msg, Mention): + _text = f"[at:{msg.data}]" + elif isinstance(msg, Image): + _text = msg.data + elif isinstance(msg, list): + _text = [] + for m in msg: + __text = str(m) + if isinstance(m, Mention): + __text = f"[at:{m.data['user_id']}]" + elif isinstance(m, Image): + # TODO: 显示词条回答图片 + # __text = (m.data["image"], 30, 30) + __text = "[图片]" + _text.append(__text) + msg_list.append("".join(_text)) + column_name = ["序号", "回答内容"] + data_list = [] + for index, msg in enumerate(msg_list): + data_list.append([index, msg]) + template_image = await ImageTemplate.table_page( + f"词条 {problem} 的回答", None, column_name, data_list + ) + return Image(template_image.pic2bytes()) + else: + result = [] + if group_id: + _problem_list = await WordBank.get_group_all_problem(group_id) + elif word_scope is not None: + _problem_list = await WordBank.get_problem_by_scope(word_scope) + else: + raise Exception("群组id和词条范围不能都为空") + global_problem_list = await WordBank.get_problem_by_scope(0) + if not _problem_list and not global_problem_list: + return Text("未收录任何词条...") + column_name = ["序号", "关键词", "匹配类型", "收录用户"] + data_list = [list(s) for s in _problem_list] + for i in range(len(data_list)): + data_list[i].insert(0, i) + group_image = await ImageTemplate.table_page( + "群组内词条" if group_id else "私聊词条", None, column_name, data_list + ) + result.append(Image(group_image.pic2bytes())) + if global_problem_list: + data_list = [list(s) for s in global_problem_list] + for i in range(len(data_list)): + data_list[i].insert(0, i) + global_image = await ImageTemplate.table_page( + "全局词条", None, column_name, data_list + ) + result.append(Image(global_image.pic2bytes())) + return MessageFactory(result) diff --git a/zhenxun/plugins/word_bank/_model.py b/zhenxun/plugins/word_bank/_model.py new file mode 100644 index 00000000..eef86941 --- /dev/null +++ b/zhenxun/plugins/word_bank/_model.py @@ -0,0 +1,566 @@ +import random +import re +import time +import uuid +from datetime import datetime +from typing import Any + +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from tortoise import Tortoise, fields +from tortoise.expressions import Q +from typing_extensions import Self + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.db_context import Model +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import get_img_hash + +from ._config import int2type + +path = DATA_PATH / "word_bank" + + +class WordBank(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + user_id = fields.CharField(255) + """用户id""" + group_id = fields.CharField(255, null=True) + """群聊id""" + word_scope = fields.IntField(default=0) + """生效范围 0: 全局 1: 群聊 2: 私聊""" + word_type = fields.IntField(default=0) + """词条类型 0: 完全匹配 1: 模糊 2: 正则 3: 图片""" + status = fields.BooleanField() + """词条状态""" + problem = fields.TextField() + """问题,为图片时使用图片hash""" + answer = fields.TextField() + """回答""" + placeholder = fields.TextField(null=True) + """占位符""" + image_path = fields.TextField(null=True) + """使用图片作为问题时图片存储的路径""" + to_me = fields.CharField(255, null=True) + """昵称开头时存储的昵称""" + create_time = fields.DatetimeField(auto_now=True) + """创建时间""" + update_time = fields.DatetimeField(auto_now_add=True) + """更新时间""" + platform = fields.CharField(255, default="qq") + """平台""" + author = fields.CharField(255, null=True, default="") + """收录人""" + + class Meta: + table = "word_bank2" + table_description = "词条数据库" + + @classmethod + async def exists( + cls, + user_id: str | None, + group_id: str | None, + problem: str, + answer: str | None, + word_scope: int | None = None, + word_type: int | None = None, + ) -> bool: + """检测问题是否存在 + + 参数: + user_id: 用户id + group_id: 群号 + problem: 问题 + answer: 回答 + word_scope: 词条范围 + word_type: 词条类型 + """ + query = cls.filter(problem=problem) + if user_id: + query = query.filter(user_id=user_id) + if group_id: + query = query.filter(group_id=group_id) + if answer: + query = query.filter(answer=answer) + if word_type is not None: + query = query.filter(word_type=word_type) + if word_scope is not None: + query = query.filter(word_scope=word_scope) + return await query.exists() + + @classmethod + async def add_problem_answer( + cls, + user_id: str, + group_id: str | None, + word_scope: int, + word_type: int, + problem: str, + answer: list[str | alcText | alcAt | alcImage], + to_me_nickname: str | None = None, + platform: str = "", + author: str = "", + ): + """添加或新增一个问答 + + 参数: + user_id: 用户id + group_id: 群号 + word_scope: 词条范围, + word_type: 词条类型, + problem: 问题, 为图片时是URl + answer: 回答 + to_me_nickname: at真寻名称 + platform: 所属平台 + author: 收录人id + """ + # 对图片做额外处理 + image_path = None + if word_type == 3: + _file = ( + path / "problem" / f"{group_id}" / f"{user_id}_{int(time.time())}.jpg" + ) + _file.parent.mkdir(exist_ok=True, parents=True) + await AsyncHttpx.download_file(problem, _file) + problem = get_img_hash(_file) + image_path = f"problem/{group_id}/{user_id}_{int(time.time())}.jpg" + new_answer, placeholder_list = await cls._answer2format( + answer, user_id, group_id + ) + if not await cls.exists( + user_id, group_id, problem, new_answer, word_scope, word_type + ): + await cls.create( + user_id=user_id, + group_id=group_id, + word_scope=word_scope, + word_type=word_type, + status=True, + problem=str(problem).strip(), + answer=new_answer, + image_path=image_path, + placeholder=",".join(placeholder_list), + create_time=datetime.now().replace(microsecond=0), + update_time=datetime.now().replace(microsecond=0), + to_me=to_me_nickname, + platform=platform, + author=author, + ) + + @classmethod + async def _answer2format( + cls, + answer: list[str | alcText | alcAt | alcImage], + user_id: str, + group_id: str | None, + ) -> tuple[str, list[Any]]: + """将特殊字段转化为占位符,图片,at等 + + 参数: + answer: 回答内容 + user_id: 用户id + group_id: 群号 + + 返回: + tuple[str, list[Any]]: 替换后的文本回答内容,占位符 + """ + placeholder_list = [] + text = "" + index = 0 + for seg in answer: + placeholder = uuid.uuid1() + if isinstance(seg, str): + text += seg + elif isinstance(seg, alcText): + text += seg.text + elif seg.type == "face": # TODO: face貌似无用... + text += f"[face:placeholder_{placeholder}]" + placeholder_list.append(seg.data["id"]) + elif isinstance(seg, alcAt): + text += f"[at:placeholder_{placeholder}]" + placeholder_list.append(seg.target) + elif isinstance(seg, alcImage) and seg.url: + text += f"[image:placeholder_{placeholder}]" + index += 1 + _file = ( + path + / "answer" + / f"{group_id or user_id}" + / f"{user_id}_{placeholder}.jpg" + ) + _file.parent.mkdir(exist_ok=True, parents=True) + await AsyncHttpx.download_file(seg.url, _file) + placeholder_list.append( + f"answer/{group_id or user_id}/{user_id}_{placeholder}.jpg" + ) + return text, placeholder_list + + @classmethod + async def _format2answer( + cls, + problem: str, + answer: str, + user_id: int, + group_id: int, + query: Self | None = None, + ) -> MessageFactory | Text: + """将占位符转换为实际内容 + + 参数: + problem: 问题内容 + answer: 回答内容 + user_id: 用户id + group_id: 群组id + """ + result_list = [] + if not query: + query = await cls.get_or_none( + problem=problem, + user_id=user_id, + group_id=group_id, + answer=answer, + ) + if not answer: + answer = str(query.answer) # type: ignore + if query and query.placeholder: + type_list = re.findall(rf"\[(.*?):placeholder_.*?]", answer) + answer_split = re.split(rf"\[.*:placeholder_.*?]", answer) + placeholder_split = query.placeholder.split(",") + for index, ans in enumerate(answer_split): + result_list.append(Text(ans)) + if index < len(type_list): + t = type_list[index] + p = placeholder_split[index] + if t == "image": + result_list.append(Image(path / p)) + elif t == "at": + result_list.append(Mention(p)) + return MessageFactory(result_list) + return Text(answer) + + @classmethod + async def check_problem( + cls, + group_id: str | None, + problem: str, + word_scope: int | None = None, + word_type: int | None = None, + ) -> Any: + """检测是否包含该问题并获取所有回答 + + 参数: + group_id: 群组id + problem: 问题内容 + word_scope: 词条范围 + word_type: 词条类型 + """ + query = cls + if group_id: + if word_scope: + query = query.filter(word_scope=word_scope) + else: + query = query.filter(Q(group_id=group_id) | Q(word_scope=0)) + else: + query = query.filter(Q(word_scope=2) | Q(word_scope=0)) + if word_type: + query = query.filter(word_scope=word_type) + # 完全匹配 + if data_list := await query.filter( + Q(Q(word_type=0) | Q(word_type=3)), Q(problem=problem) + ).all(): + return data_list + db = Tortoise.get_connection("default") + # 模糊匹配 + sql = query.filter(word_type=1).sql() + " and POSITION(problem in $1) > 0" + data_list = await db.execute_query_dict(sql, [problem]) + if data_list: + return [cls(**data) for data in data_list] + # 正则 + sql = ( + query.filter(word_type=2, word_scope__not=999).sql() + " and $1 ~ problem;" + ) + data_list = await db.execute_query_dict(sql, [problem]) + if data_list: + return [cls(**data) for data in data_list] + return None + + @classmethod + async def get_answer( + cls, + group_id: str | None, + problem: str, + word_scope: int | None = None, + word_type: int | None = None, + ) -> Text | MessageFactory | None: + """根据问题内容获取随机回答 + + 参数: + user_id: 用户id + group_id: 群组id + problem: 问题内容 + word_scope: 词条范围 + word_type: 词条类型 + """ + data_list = await cls.check_problem(group_id, problem, word_scope, word_type) + if data_list: + random_answer = random.choice(data_list) + if random_answer.word_type == 2: + r = re.search(random_answer.problem, problem) + has_placeholder = re.search(rf"\$(\d)", random_answer.answer) + if r and r.groups() and has_placeholder: + pats = re.sub(r"\$(\d)", r"\\\1", random_answer.answer) + random_answer.answer = re.sub(random_answer.problem, pats, problem) + return ( + await cls._format2answer( + random_answer.problem, + random_answer.answer, + random_answer.user_id, + random_answer.group_id, + random_answer, + ) + if random_answer.placeholder + else Text(random_answer.answer) + ) + + @classmethod + async def get_problem_all_answer( + cls, + problem: str, + index: int | None = None, + group_id: str | None = None, + word_scope: int | None = 0, + ) -> tuple[str, list[Text | MessageFactory]]: + """获取指定问题所有回答 + + 参数: + problem: 问题 + index: 下标 + group_id: 群号 + word_scope: 词条范围 + + 返回: + tuple[str, list[Text | MessageFactory]]: 问题和所有回答 + """ + if index is not None: + # TODO: group_by和order_by不能同时使用 + if group_id: + _problem = ( + await cls.filter(group_id=group_id).order_by("create_time") + # .group_by("problem") + .values_list("problem", flat=True) + ) + else: + _problem = ( + await cls.filter(word_scope=(word_scope or 0)).order_by( + "create_time" + ) + # .group_by("problem") + .values_list("problem", flat=True) + ) + # if index is None and problem not in _problem: + # return "词条不存在...", [] + sort_problem = [] + for p in _problem: + if p not in sort_problem: + sort_problem.append(p) + if index > len(sort_problem) - 1: + return "下标错误,必须小于问题数量...", [] + problem = sort_problem[index] # type: ignore + f = cls.filter(problem=problem, word_scope=(word_scope or 0)) + if group_id: + f = f.filter(group_id=group_id) + answer_list = await f.all() + if not answer_list: + return "词条不存在...", [] + return problem, [await cls._format2answer("", "", 0, 0, a) for a in answer_list] + + @classmethod + async def delete_group_problem( + cls, + problem: str, + group_id: str | None, + index: int | None = None, + word_scope: int = 1, + ): + """删除指定问题全部或指定回答 + + 参数: + problem: 问题文本 + group_id: 群号 + index: 回答下标 + word_scope: 词条范围 + """ + if await cls.exists(None, group_id, problem, None, word_scope): + if index is not None: + if group_id: + query = await cls.filter( + group_id=group_id, problem=problem, word_scope=word_scope + ).all() + else: + query = await cls.filter( + word_scope=word_scope, problem=problem + ).all() + await query[index].delete() + else: + if group_id: + await WordBank.filter( + group_id=group_id, problem=problem, word_scope=word_scope + ).delete() + else: + await WordBank.filter( + word_scope=word_scope, problem=problem + ).delete() + return True + return False + + @classmethod + async def update_group_problem( + cls, + problem: str, + replace_str: str, + group_id: str | None, + index: int | None = None, + word_scope: int = 1, + ) -> str: + """修改词条问题 + + 参数: + problem: 问题 + replace_str: 替换问题 + group_id: 群号 + index: 问题下标 + word_scope: 词条范围 + + 返回: + str: 修改前的问题 + """ + if index is not None: + if group_id: + query = await cls.filter(group_id=group_id, problem=problem).all() + else: + query = await cls.filter(word_scope=word_scope, problem=problem).all() + tmp = query[index].problem + query[index].problem = replace_str + await query[index].save(update_fields=["problem"]) + return tmp + else: + if group_id: + await cls.filter(group_id=group_id, problem=problem).update( + problem=replace_str + ) + else: + await cls.filter(word_scope=word_scope, problem=problem).update( + problem=replace_str + ) + return problem + + @classmethod + async def get_group_all_problem(cls, group_id: str) -> list[tuple[Any | str]]: + """获取群聊所有词条 + + 参数: + group_id: 群号 + """ + return cls._handle_problem( + await cls.filter(group_id=group_id).order_by("create_time").all() # type: ignore + ) + + @classmethod + async def get_problem_by_scope(cls, word_scope: int): + """通过词条范围获取词条 + + 参数: + word_scope: 词条范围 + """ + return cls._handle_problem( + await cls.filter(word_scope=word_scope).order_by("create_time").all() # type: ignore + ) + + @classmethod + async def get_problem_by_type(cls, word_type: int): + """通过词条类型获取词条 + + 参数: + word_type: 词条类型 + """ + return cls._handle_problem( + await cls.filter(word_type=word_type).order_by("create_time").all() # type: ignore + ) + + @classmethod + def _handle_problem(cls, problem_list: list["WordBank"]): + """格式化处理问题 + + 参数: + msg_list: 消息列表 + """ + _tmp = [] + result_list = [] + for q in problem_list: + if q.problem not in _tmp: + # TODO: 获取收录人名称 + problem = ( + (path / q.image_path, 30, 30) if q.image_path else q.problem, + int2type[q.word_type], + # q.author, + "-", + ) + result_list.append(problem) + _tmp.append(q.problem) + return result_list + + @classmethod + async def _move( + cls, + user_id: str, + group_id: str | None, + problem: str, + answer: str, + placeholder: str, + ): + """旧词条图片移动方法 + + 参数: + user_id: 用户id + group_id: 群号 + problem: 问题 + answer: 回答 + placeholder: 占位符 + """ + word_scope = 0 + word_type = 0 + # 对图片做额外处理 + if not await cls.exists( + user_id, group_id, problem, answer, word_scope, word_type + ): + await cls.create( + user_id=user_id, + group_id=group_id, + word_scope=word_scope, + word_type=word_type, + status=True, + problem=problem, + answer=answer, + image_path=None, + placeholder=placeholder, + create_time=datetime.now().replace(microsecond=0), + update_time=datetime.now().replace(microsecond=0), + ) + + @classmethod + async def _run_script(cls): + return [ + "ALTER TABLE word_bank2 ADD to_me varchar(255);", # 添加 to_me 字段 + "ALTER TABLE word_bank2 ALTER COLUMN create_time TYPE timestamp with time zone USING create_time::timestamp with time zone;", + "ALTER TABLE word_bank2 ALTER COLUMN update_time TYPE timestamp with time zone USING update_time::timestamp with time zone;", + "ALTER TABLE word_bank2 RENAME COLUMN user_qq TO user_id;", # 将user_qq改为user_id + "ALTER TABLE word_bank2 ALTER COLUMN user_id TYPE character varying(255);", + "ALTER TABLE word_bank2 ALTER COLUMN group_id TYPE character varying(255);", + "ALTER TABLE word_bank2 ADD platform varchar(255) DEFAULT 'qq';", + "ALTER TABLE word_bank2 ADD author varchar(255) DEFAULT '';", + ] diff --git a/zhenxun/plugins/word_bank/_rule.py b/zhenxun/plugins/word_bank/_rule.py new file mode 100644 index 00000000..c61d7a1e --- /dev/null +++ b/zhenxun/plugins/word_bank/_rule.py @@ -0,0 +1,59 @@ +from io import BytesIO + +import imagehash +from nonebot.adapters import Bot, Event +from nonebot.typing import T_State +from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession +from PIL import Image +from requests import session + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx + +from ._data_source import get_img_and_at_list +from ._model import WordBank + + +async def check( + bot: Bot, + event: Event, + message: UniMsg, + session: EventSession, + state: T_State, +) -> bool: + text = message.extract_plain_text().strip() + img_list, at_list = get_img_and_at_list(message) + problem = text + if not text and len(img_list) == 1: + try: + r = await AsyncHttpx.get(img_list[0]) + problem = str(imagehash.average_hash(Image.open(BytesIO(r.content)))) + except Exception as e: + logger.warning(f"获取图片失败", "词条检测", session=session, e=e) + if at_list: + temp = "" + # TODO: 支持更多消息类型 + for msg in message: + if isinstance(msg, alcAt): + temp += f"[at:{msg.target}]" + elif isinstance(msg, alcText): + temp += msg.text + problem = temp + if event.is_tome() and bot.config.nickname: + if isinstance(message[0], alcAt) and message[0].target == bot.self_id: + problem = f"[at:{bot.self_id}]" + problem + else: + if problem and bot.config.nickname: + nickname = [ + nk for nk in bot.config.nickname if str(message).startswith(nk) + ] + problem = nickname[0] + problem if nickname else problem + if problem and ( + await WordBank.check_problem(session.id3 or session.id2, problem) is not None + ): + state["problem"] = problem # type: ignore + return True + return False diff --git a/zhenxun/plugins/word_bank/command.py b/zhenxun/plugins/word_bank/command.py new file mode 100644 index 00000000..880de48d --- /dev/null +++ b/zhenxun/plugins/word_bank/command.py @@ -0,0 +1,54 @@ +from nonebot import on_regex +from nonebot_plugin_alconna import ( + Alconna, + Args, + Option, + Subcommand, + on_alconna, + store_true, +) + +from zhenxun.utils.rules import admin_check, ensure_group + +_add_matcher = on_regex( + r"^(全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*)", + priority=5, + block=True, + rule=admin_check("word_bank", "WORD_BANK_LEVEL"), +) + + +_del_matcher = on_alconna( + Alconna( + "删除词条", + Args["problem?", str], + Option("--all", action=store_true, help_text="所有词条"), + Option("--id", Args["index", int], help_text="下标id"), + Option("--aid", Args["answer_id", int], help_text="回答下标id"), + ), + priority=5, + block=True, +) + + +_update_matcher = on_alconna( + Alconna( + "修改词条", + Args["replace", str]["problem?", str], + Option("--id", Args["index", int], help_text="词条id"), + Option("--all", action=store_true, help_text="全局词条"), + ) +) + +_show_matcher = on_alconna( + Alconna( + "显示词条", + Args["problem?", str], + Option("-g|--group", Args["gid", str], help_text="群组id"), + Option("--id", Args["index", int], help_text="词条id"), + Option("--all", action=store_true, help_text="全局词条"), + ), + aliases={"查看词条"}, + priority=5, + block=True, +) diff --git a/zhenxun/plugins/word_bank/message_handle.py b/zhenxun/plugins/word_bank/message_handle.py new file mode 100644 index 00000000..a9bf624a --- /dev/null +++ b/zhenxun/plugins/word_bank/message_handle.py @@ -0,0 +1,31 @@ +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services import logger +from zhenxun.utils.enum import PluginType + +from ._model import WordBank +from ._rule import check + +__plugin_meta__ = PluginMetadata( + name="词库问答回复操作", + description="", + usage="""""", + extra=PluginExtraData( + author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + ).dict(), +) + +_matcher = on_message(priority=6, block=True, rule=check) + + +@_matcher.handle() +async def _(session: EventSession, state: T_State): + if problem := state.get("problem"): + gid = session.id3 or session.id2 + if result := await WordBank.get_answer(gid, problem): + await result.send() + logger.info(f" 触发词条 {problem}", "词条检测", session=session) diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py new file mode 100644 index 00000000..3bea68ad --- /dev/null +++ b/zhenxun/plugins/word_bank/word_handle.py @@ -0,0 +1,314 @@ +import re +from typing import Any + +from nonebot.adapters import Bot, Message +from nonebot.adapters.onebot.v11 import unescape +from nonebot.exception import FinishedException +from nonebot.internal.params import Arg, ArgStr +from nonebot.params import RegexGroup +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_State +from nonebot_plugin_alconna import AlconnaQuery, Arparma +from nonebot_plugin_alconna import Image as alcImage +from nonebot_plugin_alconna import Match, Query +from nonebot_plugin_alconna import Text as alcText +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.services.log import logger + +from ._config import scope2int, type2int +from ._data_source import WordBankManage, get_answer, get_img_and_at_list, get_problem +from ._model import WordBank +from .command import _add_matcher, _del_matcher, _show_matcher, _update_matcher + +base_config = Config.get("word_bank") + + +__plugin_meta__ = PluginMetadata( + name="词库问答", + description="自定义词条内容随机回复", + usage=""" + usage: + 对指定问题的随机回答,对相同问题可以设置多个不同回答 + 删除词条后每个词条的id可能会变化,请查看后再删除 + 更推荐使用id方式删除 + 问题回答支持的类型:at, image + 查看词条命令:群聊时为 群词条+全局词条,私聊时为 私聊词条+全局词条 + 添加词条正则:添加词条(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*) + 正则问可以通过$1类推()捕获的组 + 指令: + 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答 + 删除词条 [问题/下标] ?[下标]:删除指定词条指定或全部回答 + 修改词条 [问题/下标] [新问题]:修改词条问题 + 查看词条 ?[问题/下标]:查看全部词条或对应词条回答 + 示例:添加图片词条问答嗨嗨嗨 + [图片]... + 示例:添加词条@萝莉 我来啦 + 示例:添加词条问谁是萝莉答是我 + 示例:添加词条正则问那个(.+)是萝莉答没错$1是萝莉 + 示例:删除词条 谁是萝莉 + 示例:删除词条 谁是萝莉 0 + 示例:删除词条 id:0 1 + 示例:修改词条 谁是萝莉 是你 + 示例:修改词条 id:0 是你 + 示例:查看词条 + 示例:查看词条 谁是萝莉 + 示例:查看词条 id:0 (群/私聊词条) + 示例:查看词条 gid:0 (全局词条) + """.strip(), + extra=PluginExtraData( + author="HibiKier & yajiwa", + version="0.1", + superuser_help=""" + 在私聊中超级用户额外设置 + 指令: + (全局|私聊)?添加词条\s*?(模糊|正则|图片)?问\s*?(\S*\s?\S*)\s*?答\s?(\S*):添加问答词条,可重复添加相同问题的不同回答 + 全局添加词条 + 私聊添加词条 + (私聊情况下)删除词条: 删除私聊词条 + (私聊情况下)删除全局词条 + (私聊情况下)修改词条: 修改私聊词条 + (私聊情况下)修改全局词条 + 用法与普通用法相同 + """, + admin_level=base_config.get("WORD_BANK_LEVEL"), + ).dict(), +) + + +@_add_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + state: T_State, + message: UniMsg, + reg_group: tuple[Any, ...] = RegexGroup(), +): + img_list, at_list = get_img_and_at_list(message) + user_id = session.id1 + group_id = session.id3 or session.id2 + if not group_id and user_id not in bot.config.superusers: + await Text("权限不足捏...").finish(reply=True) + word_scope, word_type, problem, answer = reg_group + if not word_scope and not group_id: + word_scope = "私聊" + if ( + word_scope + and word_scope in ["全局", "私聊"] + and user_id not in bot.config.superusers + ): + await Text("权限不足,无法添加该范围词条...").finish(reply=True) + if (not problem or not problem.strip()) and word_type != "图片": + await Text("词条问题不能为空!").finish(reply=True) + if (not answer or not answer.strip()) and not len(img_list) and not len(at_list): + await Text("词条回答不能为空!").finish(reply=True) + if word_type != "图片": + state["problem_image"] = "YES" + temp_problem = message.copy() + # answer = message.copy() + # 对at问题对额外处理 + # if at_list: + answer = get_answer(message.copy()) + # text = str(message.pop(0)).split("答", maxsplit=1)[-1].strip() + # temp_problem.insert(0, alcText(text)) + state["word_scope"] = word_scope + state["word_type"] = word_type + state["problem"] = get_problem(temp_problem) + state["answer"] = answer + logger.info( + f"添加词条 范围: {word_scope} 类型: {word_type} 问题: {problem} 回答: {answer}", + "添加词条", + session=session, + ) + + +@_add_matcher.got("problem_image", prompt="请发送该回答设置的问题图片") +async def _( + bot: Bot, + session: EventSession, + message: UniMsg, + word_scope: str | None = ArgStr("word_scope"), + word_type: str | None = ArgStr("word_type"), + problem: str | None = ArgStr("problem"), + answer: Any = Arg("answer"), +): + if not session.id1: + await Text("用户id不存在...").finish() + user_id = session.id1 + group_id = session.id3 or session.id2 + try: + if word_type == "图片": + problem = [m for m in message if isinstance(m, alcImage)][0].url + elif word_type == "正则" and problem: + problem = unescape(problem) + try: + re.compile(problem) + except re.error: + await Text(f"添加词条失败,正则表达式 {problem} 非法!").finish( + reply=True + ) + # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent): + # word_scope = "私聊" + nickname = None + if problem and bot.config.nickname: + nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)] + if not problem: + await Text("获取问题失败...").finish(reply=True) + await WordBank.add_problem_answer( + user_id, + ( + group_id + if group_id and (not word_scope or word_scope == "私聊") + else "0" + ), + scope2int[word_scope] if word_scope else 1, + type2int[word_type] if word_type else 0, + problem, + answer, + nickname[0] if nickname else None, + session.platform, + session.id1, + ) + except Exception as e: + if isinstance(e, FinishedException): + await _add_matcher.finish() + logger.error( + f"添加词条 {problem} 错误...", + "添加词条", + session=session, + e=e, + ) + await Text( + f"添加词条 {problem if word_type != '图片' else '图片'} 发生错误!" + ).finish(reply=True) + if word_type == "图片": + result = MessageFactory([Text("添加词条 "), Image(problem), Text(" 成功!")]) + else: + result = Text(f"添加词条 {problem} 成功!") + await result.send() + logger.info( + f"添加词条 {problem} 成功!", + "添加词条", + session=session, + ) + + +@_del_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + problem: Match[str], + index: Match[int], + answer_id: Match[int], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not problem.available and not index.available: + await Text("此命令之后需要跟随指定词条或id,通过“显示词条“查看").finish( + reply=True + ) + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + if gid := session.id3 or session.id2: + result, _ = await WordBankManage.delete_word( + problem.result, + index.result if index.available else None, + answer_id.result if answer_id.available else None, + gid, + word_scope, + ) + else: + if session.id1 not in bot.config.superusers: + await Text("权限不足捏...").finish(reply=True) + result, _ = await WordBankManage.delete_word( + problem.result, + index.result if index.available else None, + answer_id.result if answer_id.available else None, + None, + word_scope, + ) + await Text(result).send(reply=True) + logger.info(f"删除词条: {problem.result}", arparma.header_result, session=session) + + +@_update_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + replace: str, + problem: Match[str], + index: Match[int], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + if not problem.available and not index.available: + await Text("此命令之后需要跟随指定词条或id,通过“显示词条“查看").finish( + reply=True + ) + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + if gid := session.id3 or session.id2: + result, old_problem = await WordBankManage.update_word( + replace, + problem.result if problem.available else "", + index.result if index.available else None, + gid, + word_scope, + ) + else: + if session.id1 not in bot.config.superusers: + await Text("权限不足捏...").finish(reply=True) + result, old_problem = await WordBankManage.update_word( + replace, + problem.result if problem.available else "", + index.result if index.available else None, + session.id3 or session.id2, + word_scope, + ) + await Text(result).send(reply=True) + logger.info( + f"更新词条词条: {old_problem} -> {replace}", + arparma.header_result, + session=session, + ) + + +@_show_matcher.handle() +async def _( + session: EventSession, + problem: Match[str], + index: Match[int], + gid: Match[str], + arparma: Arparma, + all: Query[bool] = AlconnaQuery("all.value", False), +): + word_scope = 1 if session.id3 or session.id2 else 2 + if all.result: + word_scope = 0 + group_id = session.id3 or session.id2 + if gid.available: + group_id = gid.result + if problem.available: + if index.available: + if index.result < 0 or index.result > len( + await WordBank.get_problem_by_scope(2) + ): + await Text("id必须在范围内...").finish(reply=True) + result = await WordBankManage.show_word( + problem.result, + index.result if index.available else None, + group_id, + word_scope, + ) + else: + result = await WordBankManage.show_word( + None, index.result if index.available else None, group_id, word_scope + ) + await result.send() + logger.info(f"查看词条回答: {problem}", arparma.header_result, session=session) diff --git a/zhenxun/utils/_image_template.py b/zhenxun/utils/_image_template.py index 6f160090..399c054c 100644 --- a/zhenxun/utils/_image_template.py +++ b/zhenxun/utils/_image_template.py @@ -110,11 +110,20 @@ class ImageTemplate: 返回: BuildImage: 表格图片 """ + font = BuildImage.load_font(font_size=50) + min_width, _ = BuildImage.get_text_size(head_text, font) table = await cls.table( - column_name, data_list, row_space, column_space, padding, text_style + column_name, + data_list, + row_space, + column_space, + padding, + text_style, ) await table.circle_corner() - table_bk = BuildImage(table.width + 100, table.height + 50, "#EAEDF2") + table_bk = BuildImage( + max(table.width, min_width) + 100, table.height + 50, "#EAEDF2" + ) await table_bk.paste(table, center_type="center") height = table_bk.height + 200 background = BuildImage(table_bk.width, height, (255, 255, 255), font_size=50) @@ -144,13 +153,12 @@ class ImageTemplate: column_space: 列间距. padding: 文本内间距. text_style: 文本样式. + min_width: 最低宽度 返回: BuildImage: 表格图片 """ font = BuildImage.load_font("HYWenHei-85W.ttf", 20) - column_num = max([len(l) for l in data_list]) - list_data = [] column_data = [] for i in range(len(column_name)): c = [] @@ -163,7 +171,7 @@ class ImageTemplate: build_data_list = [] _, base_h = BuildImage.get_text_size("A", font) for i, column_list in enumerate(column_data): - name_width, name_height = BuildImage.get_text_size(column_name[i], font) + name_width, _ = BuildImage.get_text_size(column_name[i], font) _temp = {"width": name_width, "data": column_list} for s in column_list: if isinstance(s, tuple): @@ -207,8 +215,8 @@ class ImageTemplate: ) cur_h += base_h + row_space column_image_list.append(background) - height = max([bk.height for bk in column_image_list]) - width = sum([bk.width for bk in column_image_list]) + # height = max([bk.height for bk in column_image_list]) + # width = sum([bk.width for bk in column_image_list]) return await BuildImage.auto_paste( column_image_list, len(column_image_list), column_space ) From c4712224aac31fe5c631e9398328a57540c0db10 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 27 Jul 2024 04:48:08 +0800 Subject: [PATCH 058/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=AF=8D=E6=9D=A1=E7=AE=A1=E7=90=86=E5=91=BD=E4=BB=A4=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/word_bank/_rule.py | 1 - zhenxun/plugins/word_bank/word_handle.py | 44 ++++++++++++++---------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/zhenxun/plugins/word_bank/_rule.py b/zhenxun/plugins/word_bank/_rule.py index c61d7a1e..3ce032ca 100644 --- a/zhenxun/plugins/word_bank/_rule.py +++ b/zhenxun/plugins/word_bank/_rule.py @@ -8,7 +8,6 @@ from nonebot_plugin_alconna import Text as alcText from nonebot_plugin_alconna import UniMsg from nonebot_plugin_session import EventSession from PIL import Image -from requests import session from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py index 3bea68ad..29f980b0 100644 --- a/zhenxun/plugins/word_bank/word_handle.py +++ b/zhenxun/plugins/word_bank/word_handle.py @@ -42,23 +42,28 @@ __plugin_meta__ = PluginMetadata( 正则问可以通过$1类推()捕获的组 指令: 添加词条 ?[模糊|正则|图片]问...答...:添加问答词条,可重复添加相同问题的不同回答 - 删除词条 [问题/下标] ?[下标]:删除指定词条指定或全部回答 - 修改词条 [问题/下标] [新问题]:修改词条问题 - 查看词条 ?[问题/下标]:查看全部词条或对应词条回答 - 示例:添加图片词条问答嗨嗨嗨 - [图片]... - 示例:添加词条@萝莉 我来啦 - 示例:添加词条问谁是萝莉答是我 - 示例:添加词条正则问那个(.+)是萝莉答没错$1是萝莉 - 示例:删除词条 谁是萝莉 - 示例:删除词条 谁是萝莉 0 - 示例:删除词条 id:0 1 - 示例:修改词条 谁是萝莉 是你 - 示例:修改词条 id:0 是你 - 示例:查看词条 - 示例:查看词条 谁是萝莉 - 示例:查看词条 id:0 (群/私聊词条) - 示例:查看词条 gid:0 (全局词条) + 示例: + 添加词条问你好答你也好 + 添加图片词条问答看看涩图 + 删除词条 ?[问题] ?[序号] ?[回答序号]:删除指定词条指定或全部回答 + 示例: + 删除词条 谁是萝莉 : 删除文字是 谁是萝莉 的词条 + 删除词条 --id 2 : 删除序号为2的词条 + 删除词条 谁是萝莉 --aid 2 : 删除 谁是萝莉 词条的第2个回答 + 删除词条 --id 2 --aid 2 : 删除序号为2词条的第2个回答 + 修改词条 [替换文字] ?[旧词条文字] ?[序号]:修改词条问题 + 示例: + 修改词条 谁是萝莉 谁是萝莉啊? : 将词条 谁是萝莉 修改为 谁是萝莉啊? + 修改词条 谁是萝莉 --id 2 : 将序号为2的词条修改为 谁是萝莉 + 查看词条 ?[问题] ?[序号]:查看全部词条或对应词条回答 + 示例: + 查看词条: + (在群组中使用时): 查看当前群组词条和全局词条 + (在私聊中使用时): 查看当前私聊词条和全局词条 + 查看词条 谁是萝莉 : 查看词条 谁是萝莉 的全部回答 + 查看词条 --id 2 : 查看词条序号为2的全部回答 + 查看词条 谁是萝莉 --all: 查看全局词条 谁是萝莉 的全部回答 + 查看词条 --id 2 --all: 查看全局词条序号为2的全部回答 """.strip(), extra=PluginExtraData( author="HibiKier & yajiwa", @@ -70,9 +75,10 @@ __plugin_meta__ = PluginMetadata( 全局添加词条 私聊添加词条 (私聊情况下)删除词条: 删除私聊词条 - (私聊情况下)删除全局词条 (私聊情况下)修改词条: 修改私聊词条 - (私聊情况下)修改全局词条 + 通过添加参数 --all才指定全局词条 + 示例: + 删除词条 --id 2 --all: 删除全局词条中序号为2的词条 用法与普通用法相同 """, admin_level=base_config.get("WORD_BANK_LEVEL"), From 86048e041c8c98e46b97fd0e9fed2b8886da7202 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 27 Jul 2024 04:49:16 +0800 Subject: [PATCH 059/132] =?UTF-8?q?=F0=9F=8E=A8:=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AF=8D=E6=9D=A1=E7=AE=A1=E7=90=86=E5=91=BD=E4=BB=A4=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/word_bank/_data_source.py | 3 --- zhenxun/plugins/word_bank/command.py | 9 +-------- zhenxun/plugins/word_bank/word_handle.py | 6 ++---- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/zhenxun/plugins/word_bank/_data_source.py b/zhenxun/plugins/word_bank/_data_source.py index efd7052d..467490f3 100644 --- a/zhenxun/plugins/word_bank/_data_source.py +++ b/zhenxun/plugins/word_bank/_data_source.py @@ -1,6 +1,3 @@ -import re - -from nonebot.adapters.onebot.v11 import unescape from nonebot_plugin_alconna import At as alcAt from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import Text as alcText diff --git a/zhenxun/plugins/word_bank/command.py b/zhenxun/plugins/word_bank/command.py index 880de48d..64049d30 100644 --- a/zhenxun/plugins/word_bank/command.py +++ b/zhenxun/plugins/word_bank/command.py @@ -1,12 +1,5 @@ from nonebot import on_regex -from nonebot_plugin_alconna import ( - Alconna, - Args, - Option, - Subcommand, - on_alconna, - store_true, -) +from nonebot_plugin_alconna import Alconna, Args, Option, on_alconna, store_true from zhenxun.utils.rules import admin_check, ensure_group diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py index 29f980b0..820ae244 100644 --- a/zhenxun/plugins/word_bank/word_handle.py +++ b/zhenxun/plugins/word_bank/word_handle.py @@ -1,7 +1,7 @@ import re from typing import Any -from nonebot.adapters import Bot, Message +from nonebot.adapters import Bot from nonebot.adapters.onebot.v11 import unescape from nonebot.exception import FinishedException from nonebot.internal.params import Arg, ArgStr @@ -10,9 +10,7 @@ from nonebot.plugin import PluginMetadata from nonebot.typing import T_State from nonebot_plugin_alconna import AlconnaQuery, Arparma from nonebot_plugin_alconna import Image as alcImage -from nonebot_plugin_alconna import Match, Query -from nonebot_plugin_alconna import Text as alcText -from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_alconna import Match, Query, UniMsg from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession From aa7a8271f398354ba887576507c25d3c29b0f34a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 28 Jul 2024 03:37:37 +0800 Subject: [PATCH 060/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=B8=B8=E6=88=8F=E6=8A=BD=E5=8D=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 390 ++++++++++++++- pyproject.toml | 3 + zhenxun/plugins/draw_card/__init__.py | 289 +++++++++++ zhenxun/plugins/draw_card/config.py | 203 ++++++++ zhenxun/plugins/draw_card/count_manager.py | 149 ++++++ .../plugins/draw_card/handles/azur_handle.py | 307 ++++++++++++ .../plugins/draw_card/handles/ba_handle.py | 153 ++++++ .../plugins/draw_card/handles/base_handle.py | 296 +++++++++++ .../plugins/draw_card/handles/fgo_handle.py | 223 +++++++++ .../draw_card/handles/genshin_handle.py | 465 ++++++++++++++++++ .../draw_card/handles/guardian_handle.py | 399 +++++++++++++++ .../draw_card/handles/onmyoji_handle.py | 178 +++++++ .../plugins/draw_card/handles/pcr_handle.py | 149 ++++++ .../draw_card/handles/pretty_handle.py | 422 ++++++++++++++++ .../plugins/draw_card/handles/prts_handle.py | 343 +++++++++++++ zhenxun/plugins/draw_card/rule.py | 10 + zhenxun/plugins/draw_card/util.py | 61 +++ 17 files changed, 4039 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/draw_card/__init__.py create mode 100644 zhenxun/plugins/draw_card/config.py create mode 100644 zhenxun/plugins/draw_card/count_manager.py create mode 100644 zhenxun/plugins/draw_card/handles/azur_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/ba_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/base_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/fgo_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/genshin_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/guardian_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/onmyoji_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/pcr_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/pretty_handle.py create mode 100644 zhenxun/plugins/draw_card/handles/prts_handle.py create mode 100644 zhenxun/plugins/draw_card/rule.py create mode 100644 zhenxun/plugins/draw_card/util.py diff --git a/poetry.lock b/poetry.lock index 4c1c1694..c526dd27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,6 +16,126 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "aiohttp" +version = "3.9.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, + {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, + {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, + {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, + {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, + {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, + {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, + {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, + {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, + {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, + {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, + {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, + {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, + {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, + {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, + {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, + {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, + {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, + {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, + {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, + {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, + {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, + {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, + {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, + {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, + {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, + {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "aiosqlite" version = "0.17.0" @@ -585,6 +705,26 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cn2an" +version = "0.5.22" +description = "Convert Chinese numerals and Arabic numerals." +optional = false +python-versions = ">=3.6" +files = [ + {file = "cn2an-0.5.22-py3-none-any.whl", hash = "sha256:cba4c8f305b43da01f50696047cca3116c727424ac62338da6a3426e01454f3e"}, + {file = "cn2an-0.5.22.tar.gz", hash = "sha256:27ae5b56441d7329ed2ececffa026bfa8fc353dcf1fb0d9146b303b9cce3ac37"}, +] + +[package.dependencies] +proces = ">=0.1.3" +setuptools = ">=47.3.1" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "colorama" version = "0.4.6" @@ -627,6 +767,33 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "dateparser" +version = "1.2.0" +description = "Date parsing library designed to parse dates from HTML pages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"}, + {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"}, +] + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter"] +fasttext = ["fasttext"] +langdetect = ["langdetect"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "distlib" version = "0.3.8" @@ -745,6 +912,97 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "greenlet" version = "3.0.3" @@ -2013,6 +2271,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "proces" +version = "0.1.7" +description = "text preprocess." +optional = false +python-versions = ">=3.6" +files = [ + {file = "proces-0.1.7-py3-none-any.whl", hash = "sha256:308325bbc96877263f06e57e5e9c760c4b42cc722887ad60be6b18fc37d68762"}, + {file = "proces-0.1.7.tar.gz", hash = "sha256:70a05d9e973dd685f7a9092c58be695a8181a411d63796c213232fd3fdc43775"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -2461,6 +2735,99 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "regex" +version = "2024.7.24" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, + {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, + {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, + {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, + {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, + {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, + {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, + {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, + {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, + {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, + {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, + {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, + {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, + {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, + {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, + {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, + {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, + {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, + {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, + {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, + {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, + {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, + {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, + {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, + {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, + {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, + {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "requests" version = "2.31.0" @@ -2663,6 +3030,27 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "setuptools" +version = "71.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, + {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, +] + +[package.extras] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "sgmllib3k" version = "1.0.0" @@ -3502,4 +3890,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "bb01964309a665f0348ca69fecf771a9c6f7d99147c47010c0e64d2d13fe25ad" +content-hash = "8259b57e9de269479dfb70be17e3060fedc0426ae22abf3317448e50edba9b23" diff --git a/pyproject.toml b/pyproject.toml index b8cd14b8..a1b297e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ feedparser = "^6.0.11" opencv-python = "^4.9.0.80" imagehash = "^4.3.1" black = "^24.4.2" +cn2an = "^0.5.22" +aiohttp = "^3.9.5" +dateparser = "^1.2.0" [tool.poetry.dev-dependencies] diff --git a/zhenxun/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py new file mode 100644 index 00000000..36e56ad7 --- /dev/null +++ b/zhenxun/plugins/draw_card/__init__.py @@ -0,0 +1,289 @@ +import asyncio +import traceback +from dataclasses import dataclass +from typing import Any + +import nonebot +from cn2an import cn2an +from nonebot import on_keyword, on_message, on_regex +from nonebot.log import logger +from nonebot.matcher import Matcher +from nonebot.params import RegexGroup +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot.typing import T_Handler +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData + +from .handles.azur_handle import AzurHandle +from .handles.ba_handle import BaHandle +from .handles.base_handle import BaseHandle +from .handles.fgo_handle import FgoHandle +from .handles.genshin_handle import GenshinHandle +from .handles.guardian_handle import GuardianHandle +from .handles.onmyoji_handle import OnmyojiHandle +from .handles.pcr_handle import PcrHandle +from .handles.pretty_handle import PrettyHandle +from .handles.prts_handle import PrtsHandle +from .rule import rule + +__plugin_meta__ = PluginMetadata( + name="游戏抽卡", + description="就算是模拟抽卡也不能改变自己是个非酋", + usage=""" + usage: + 模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡 + 指令: + 原神[1-180]抽: 原神常驻池 + 原神角色[1-180]抽: 原神角色UP池子 + 原神角色2池[1-180]抽: 原神角色UP池子 + 原神武器[1-180]抽: 原神武器UP池子 + 重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率] + 方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池 + 赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池 + 坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池 + pcr/公主连接[1-300]抽: 公主连接卡池 + 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池 + fgo[1-300]抽: fgo卡池 (已失效) + 阴阳师[1-300]抽: 阴阳师卡池 + ba/碧蓝档案[1-200]抽:碧蓝档案卡池 (已失效) + * 以上指令可以通过 XX一井 来指定最大抽取数量 * + * 示例:原神一井 * + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="抽卡相关", + superuser_help=""" + 更新方舟信息 + 重载方舟卡池 + 更新原神信息 + 重载原神卡池 + 更新赛马娘信息 + 重载赛马娘卡池 + 更新坎公骑冠剑信息 + 更新碧蓝航线信息 + 更新fgo信息 + 更新阴阳师信息 + """, + ).dict(), +) + +_hidden = on_message(rule=lambda: False) + + +@dataclass +class Game: + keywords: set[str] + handle: BaseHandle + flag: bool + config_name: str + max_count: int = 300 # 一次最大抽卡数 + reload_time: int | None = None # 重载UP池时间(小时) + has_other_pool: bool = False + + +games = ( + Game( + {"azur", "碧蓝航线"}, + AzurHandle(), + Config.get_config("draw_card", "AZUR_FLAG", True), + "AZUR_FLAG", + ), + Game( + {"fgo", "命运冠位指定"}, + FgoHandle(), + Config.get_config("draw_card", "FGO_FLAG", True), + "FGO_FLAG", + ), + Game( + {"genshin", "原神"}, + GenshinHandle(), + Config.get_config("draw_card", "GENSHIN_FLAG", True), + "GENSHIN_FLAG", + max_count=180, + reload_time=18, + has_other_pool=True, + ), + Game( + {"guardian", "坎公骑冠剑"}, + GuardianHandle(), + Config.get_config("draw_card", "GUARDIAN_FLAG", True), + "GUARDIAN_FLAG", + reload_time=4, + ), + Game( + {"onmyoji", "阴阳师"}, + OnmyojiHandle(), + Config.get_config("draw_card", "ONMYOJI_FLAG", True), + "ONMYOJI_FLAG", + ), + Game( + {"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"}, + PcrHandle(), + Config.get_config("draw_card", "PCR_FLAG", True), + "PCR_FLAG", + ), + Game( + {"pretty", "马娘", "赛马娘"}, + PrettyHandle(), + Config.get_config("draw_card", "PRETTY_FLAG", True), + "PRETTY_FLAG", + max_count=200, + reload_time=4, + ), + Game( + {"prts", "方舟", "明日方舟"}, + PrtsHandle(), + Config.get_config("draw_card", "PRTS_FLAG", True), + "PRTS_FLAG", + reload_time=4, + ), + Game( + {"ba", "碧蓝档案"}, + BaHandle(), + Config.get_config("draw_card", "BA_FLAG", True), + "BA_FLAG", + ), +) + + +def create_matchers(): + def draw_handler(game: Game) -> T_Handler: + async def handler( + session: EventSession, + args: tuple[Any, ...] = RegexGroup(), + ): + pool_name, pool_type_, num, unit = args + if num == "单": + num = 1 + else: + try: + num = int(cn2an(num, mode="smart")) + except ValueError: + await Text("必!须!是!数!字!").finish(reply=True) + if unit == "井": + num *= game.max_count + if num < 1: + await Text("虚空抽卡???").finish(reply=True) + elif num > game.max_count: + await Text("一井都满不足不了你嘛!快爬开!").finish(reply=True) + pool_name = ( + pool_name.replace("池", "") + .replace("武器", "arms") + .replace("角色", "char") + .replace("卡牌", "card") + .replace("卡", "card") + ) + try: + if pool_type_ in ["2池", "二池"]: + pool_name = pool_name + "1" + res = await game.handle.draw( + num, pool_name=pool_name, user_id=session.id1 + ) + logger.info( + f"游戏抽卡 类型: {list(game.keywords)[1]} 卡池: {pool_name} 数量: {num}", + "游戏抽卡", + session=session, + ) + except: + logger.warning(traceback.format_exc()) + await Text("出错了...").finish(reply=True) + await res.send() + + return handler + + def update_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher): + await game.handle.update_info() + await matcher.finish("更新完成!") + + return handler + + def reload_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher): + res = await game.handle.reload_pool() + if res: + await res.send() + + return handler + + def reset_handler(game: Game) -> T_Handler: + async def handler(matcher: Matcher, session: EventSession): + if not session.id1: + await Text("获取用户id失败...").finish() + if game.handle.reset_count(session.id1): + await Text("重置成功!").send() + + return handler + + def scheduled_job(game: Game) -> T_Handler: + async def handler(): + await game.handle.reload_pool() + + return handler + + for game in games: + pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})" + num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})" + unit_pattern = r"([抽|井|连])" + pool_type = "()" + if game.has_other_pool: + pool_type = r"([2二]池)?" + draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format( + "|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern + ) + update_keywords = {f"更新{keyword}信息" for keyword in game.keywords} + reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords} + reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords} + on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler( + draw_handler(game) + ) + on_keyword( + update_keywords, priority=1, block=True, permission=SUPERUSER + ).append_handler(update_handler(game)) + on_keyword( + reload_keywords, priority=1, block=True, permission=SUPERUSER + ).append_handler(reload_handler(game)) + on_keyword(reset_keywords, priority=5, block=True).append_handler( + reset_handler(game) + ) + if game.reload_time: + scheduler.add_job( + scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1 + ) + + +create_matchers() + + +# 更新资源 +@scheduler.scheduled_job( + "cron", + hour=4, + minute=1, +) +async def _(): + tasks = [] + for game in games: + if game.flag: + tasks.append(asyncio.ensure_future(game.handle.update_info())) + await asyncio.gather(*tasks) + + +driver = nonebot.get_driver() + + +@driver.on_startup +async def _(): + tasks = [] + for game in games: + if game.flag: + game.handle.init_data() + if not game.handle.data_exists(): + tasks.append(asyncio.ensure_future(game.handle.update_info())) + await asyncio.gather(*tasks) diff --git a/zhenxun/plugins/draw_card/config.py b/zhenxun/plugins/draw_card/config.py new file mode 100644 index 00000000..0aff3ef8 --- /dev/null +++ b/zhenxun/plugins/draw_card/config.py @@ -0,0 +1,203 @@ +import nonebot +import ujson as json +from pydantic import BaseModel, Extra, ValidationError + +from zhenxun.configs.config import Config as AConfig +from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.services.log import logger + + +# 原神 +class GenshinConfig(BaseModel, extra=Extra.ignore): + GENSHIN_FIVE_P: float = 0.006 + GENSHIN_FOUR_P: float = 0.051 + GENSHIN_THREE_P: float = 0.43 + GENSHIN_G_FIVE_P: float = 0.016 + GENSHIN_G_FOUR_P: float = 0.13 + I72_ADD: float = 0.0585 + + +# 明日方舟 +class PrtsConfig(BaseModel, extra=Extra.ignore): + PRTS_SIX_P: float = 0.02 + PRTS_FIVE_P: float = 0.08 + PRTS_FOUR_P: float = 0.48 + PRTS_THREE_P: float = 0.42 + + +# 赛马娘 +class PrettyConfig(BaseModel, extra=Extra.ignore): + PRETTY_THREE_P: float = 0.03 + PRETTY_TWO_P: float = 0.18 + PRETTY_ONE_P: float = 0.79 + + +# 坎公骑冠剑 +class GuardianConfig(BaseModel, extra=Extra.ignore): + GUARDIAN_THREE_CHAR_P: float = 0.0275 + GUARDIAN_TWO_CHAR_P: float = 0.19 + GUARDIAN_ONE_CHAR_P: float = 0.7825 + GUARDIAN_THREE_CHAR_UP_P: float = 0.01375 + GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375 + GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03 + GUARDIAN_FIVE_ARMS_P: float = 0.03 + GUARDIAN_FOUR_ARMS_P: float = 0.09 + GUARDIAN_THREE_ARMS_P: float = 0.27 + GUARDIAN_TWO_ARMS_P: float = 0.58 + GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01 + GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02 + + +# 公主连结 +class PcrConfig(BaseModel, extra=Extra.ignore): + PCR_THREE_P: float = 0.025 + PCR_TWO_P: float = 0.18 + PCR_ONE_P: float = 0.795 + PCR_G_THREE_P: float = 0.025 + PCR_G_TWO_P: float = 0.975 + + +# 碧蓝航线 +class AzurConfig(BaseModel, extra=Extra.ignore): + AZUR_FIVE_P: float = 0.012 + AZUR_FOUR_P: float = 0.07 + AZUR_THREE_P: float = 0.12 + AZUR_TWO_P: float = 0.51 + AZUR_ONE_P: float = 0.3 + + +# 命运-冠位指定 +class FgoConfig(BaseModel, extra=Extra.ignore): + FGO_SERVANT_FIVE_P: float = 0.01 + FGO_SERVANT_FOUR_P: float = 0.03 + FGO_SERVANT_THREE_P: float = 0.4 + FGO_CARD_FIVE_P: float = 0.04 + FGO_CARD_FOUR_P: float = 0.12 + FGO_CARD_THREE_P: float = 0.4 + + +# 阴阳师 +class OnmyojiConfig(BaseModel, extra=Extra.ignore): + ONMYOJI_SP: float = 0.0025 + ONMYOJI_SSR: float = 0.01 + ONMYOJI_SR: float = 0.2 + ONMYOJI_R: float = 0.7875 + + +# 碧蓝档案 +class BaConfig(BaseModel, extra=Extra.ignore): + BA_THREE_P: float = 0.025 + BA_TWO_P: float = 0.185 + BA_ONE_P: float = 0.79 + BA_G_TWO_P: float = 0.975 + + +class Config(BaseModel, extra=Extra.ignore): + # 开关 + PRTS_FLAG: bool = True + GENSHIN_FLAG: bool = True + PRETTY_FLAG: bool = True + GUARDIAN_FLAG: bool = True + PCR_FLAG: bool = True + AZUR_FLAG: bool = True + FGO_FLAG: bool = True + ONMYOJI_FLAG: bool = True + BA_FLAG: bool = True + + # 其他配置 + PCR_TAI: bool = False + SEMAPHORE: int = 5 + + # 抽卡概率 + prts: PrtsConfig = PrtsConfig() + genshin: GenshinConfig = GenshinConfig() + pretty: PrettyConfig = PrettyConfig() + guardian: GuardianConfig = GuardianConfig() + pcr: PcrConfig = PcrConfig() + azur: AzurConfig = AzurConfig() + fgo: FgoConfig = FgoConfig() + onmyoji: OnmyojiConfig = OnmyojiConfig() + ba: BaConfig = BaConfig() + + +driver = nonebot.get_driver() + +# DRAW_PATH = Path("data/draw_card").absolute() +DRAW_PATH = IMAGE_PATH / "draw_card" +# try: +# DRAW_PATH = Path(global_config.draw_path).absolute() +# except: +# pass +config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json" + +draw_config: Config = Config() + +for game_flag, game_name in zip( + [ + "PRTS_FLAG", + "GENSHIN_FLAG", + "PRETTY_FLAG", + "GUARDIAN_FLAG", + "PCR_FLAG", + "AZUR_FLAG", + "FGO_FLAG", + "ONMYOJI_FLAG", + "PCR_TAI", + "BA_FLAG", + ], + [ + "明日方舟", + "原神", + "赛马娘", + "坎公骑冠剑", + "公主连结", + "碧蓝航线", + "命运-冠位指定(FGO)", + "阴阳师", + "pcr台服卡池", + "碧蓝档案", + ], +): + AConfig.add_plugin_config( + "draw_card", + game_flag, + True, + help=f"{game_name} 抽卡开关", + default_value=True, + type=bool, + ) +AConfig.add_plugin_config( + "draw_card", + "SEMAPHORE", + 5, + help=f"异步数据下载数量限制", + default_value=5, + type=int, +) +AConfig.set_name("draw_card", "游戏抽卡") + + +@driver.on_startup +def check_config(): + global draw_config + + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + draw_config = Config() + logger.warning("draw_card:配置文件不存在,已重新生成配置文件...") + else: + with config_path.open("r", encoding="utf8") as fp: + data = json.load(fp) + try: + draw_config = Config.parse_obj({**data}) + except ValidationError: + draw_config = Config() + logger.warning("draw_card:配置文件格式错误,已重新生成配置文件...") + + with config_path.open("w", encoding="utf8") as fp: + json.dump( + draw_config.dict(), + fp, + indent=4, + ensure_ascii=False, + ) diff --git a/zhenxun/plugins/draw_card/count_manager.py b/zhenxun/plugins/draw_card/count_manager.py new file mode 100644 index 00000000..7768b057 --- /dev/null +++ b/zhenxun/plugins/draw_card/count_manager.py @@ -0,0 +1,149 @@ +from typing import Optional, TypeVar, Generic +from pydantic import BaseModel +from cachetools import TTLCache + + +class BaseUserCount(BaseModel): + count: int = 0 # 当前抽卡次数 + + +TCount = TypeVar("TCount", bound="BaseUserCount") + + +class DrawCountManager(Generic[TCount]): + """ + 抽卡统计保底 + """ + + def __init__( + self, game_draw_count_rule: tuple, star2name: tuple, max_draw_count: int + ): + """ + 初始化保底统计 + + 例如:DrawCountManager((10, 90, 180), ("4", "5", "5")) + + 抽卡保底需要的次数和返回的对应名称,例如星级等 + + :param game_draw_count_rule:抽卡规则 + :param star2name:星级对应的名称 + :param max_draw_count:最大累计抽卡次数,当下次单次抽卡超过该次数时将会清空数据 + + """ + # 只有保底 + # 超过60秒重置抽卡次数 + self._data: TTLCache[int, TCount] = TTLCache(maxsize=1000, ttl=60) + self._guarantee_tuple = game_draw_count_rule + self._star2name = star2name + self._max_draw_count = max_draw_count + + @classmethod + def get_count_class(cls) -> TCount: + raise NotImplementedError + + def _get_count(self, key: int) -> TCount: + if self._data.get(key) is None: + self._data[key] = self.get_count_class() + else: + self._data[key] = self._data[key] + return self._data[key] + + def increase(self, key: int, value: int = 1): + """ + 用户抽卡次数加1 + """ + self._get_count(key).count += value + + def get_max_guarantee(self): + """ + 获取最大保底抽卡次数 + """ + return self._guarantee_tuple[-1] + + def get_user_count(self, key: int) -> int: + """ + 获取当前抽卡次数 + """ + return self._get_count(key).count + + def reset(self, key: int): + """ + 清空记录 + """ + self._data.pop(key, None) + + +class GenshinUserCount(BaseUserCount): + five_index: int = 0 # 获取五星时的抽卡次数 + four_index: int = 0 # 获取四星时的抽卡次数 + is_up: bool = False # 下次五星是否必定为up + + +class GenshinCountManager(DrawCountManager[GenshinUserCount]): + @classmethod + def get_count_class(cls) -> GenshinUserCount: + return GenshinUserCount() + + def set_is_up(self, key: int, value: bool): + """ + 设置下次是否必定up + """ + self._get_count(key).is_up = value + + def is_up(self, key: int) -> bool: + """ + 判断该次保底是否必定为up + """ + return self._get_count(key).is_up + + def get_user_five_index(self, key: int) -> int: + """ + 获取用户上次获取五星的次数 + """ + return self._get_count(key).five_index + + def get_user_four_index(self, key: int) -> int: + """ + 获取用户上次获取四星的次数 + """ + return self._get_count(key).four_index + + def mark_five_index(self, key: int): + """ + 标记用户该次次数为五星 + """ + self._get_count(key).five_index = self._get_count(key).count + + def mark_four_index(self, key: int): + """ + 标记用户该次次数为四星 + """ + self._get_count(key).four_index = self._get_count(key).count + + def check_count(self, key: int, count: int): + """ + 检查用户该次抽卡次数累计是否超过最大限制次数 + """ + if self._get_count(key).count + count > self._max_draw_count: + self._data.pop(key, None) + + def get_user_guarantee_count(self, key: int) -> int: + user = self._get_count(key) + return ( + self.get_max_guarantee() + - (user.count % self.get_max_guarantee() - user.five_index) + ) % self.get_max_guarantee() or self.get_max_guarantee() + + def check(self, key: int) -> Optional[int]: + """ + 是否保底 + """ + # print(self._data) + user = self._get_count(key) + if user.count - user.five_index == 90: + user.five_index = user.count + return 5 + if user.count - user.four_index == 10: + user.four_index = user.count + return 4 + return None diff --git a/zhenxun/plugins/draw_card/handles/azur_handle.py b/zhenxun/plugins/draw_card/handles/azur_handle.py new file mode 100644 index 00000000..06949085 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/azur_handle.py @@ -0,0 +1,307 @@ +import random +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_saa import Image, MessageFactory, Text +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle +from .base_handle import UpChar as _UpChar +from .base_handle import UpEvent as _UpEvent + + +class AzurChar(BaseData): + type_: str # 舰娘类型 + + @property + def star_str(self) -> str: + return ["白", "蓝", "紫", "金"][self.star - 1] + + +class UpChar(_UpChar): + type_: str # 舰娘类型 + + +class UpEvent(_UpEvent): + up_char: list[UpChar] # up对象 + + +class AzurHandle(BaseHandle[AzurChar]): + def __init__(self): + super().__init__("azur", "碧蓝航线") + self.max_star = 4 + self.config = draw_config.azur + self.ALL_CHAR: list[AzurChar] = [] + self.UP_EVENT: UpEvent | None = None + + def get_card(self, pool_name: str, **kwargs) -> AzurChar: + if pool_name == "轻型": + type_ = ["驱逐", "轻巡", "维修"] + elif pool_name == "重型": + type_ = ["重巡", "战列", "战巡", "重炮"] + else: + type_ = ["维修", "潜艇", "重巡", "轻航", "航母"] + up_pool_flag = pool_name == "活动" + # Up + up_ship = ( + [x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else [] + ) + # print(up_ship) + acquire_char = None + if up_ship and up_pool_flag: + up_zoom: list[tuple[float, float]] = [(0, up_ship[0].zoom / 100)] + # 初始化概率 + cur_ = up_ship[0].zoom / 100 + for i in range(len(up_ship)): + try: + up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100)) + cur_ += up_ship[i + 1].zoom / 100 + except IndexError: + pass + rand = random.random() + # 抽取up + for i, zoom in enumerate(up_zoom): + if zoom[0] <= rand <= zoom[1]: + try: + acquire_char = [ + x for x in self.ALL_CHAR if x.name == up_ship[i].name + ][0] + except IndexError: + pass + # 没有up或者未抽取到up + if not acquire_char: + star = self.get_star( + # [4, 3, 2, 1], + [4, 3, 2, 2], + [ + self.config.AZUR_FOUR_P, + self.config.AZUR_THREE_P, + self.config.AZUR_TWO_P, + self.config.AZUR_ONE_P, + ], + ) + acquire_char = random.choice( + [ + x + for x in self.ALL_CHAR + if x.star == star and x.type_ in type_ and not x.limited + ] + ) + return acquire_char + + async def draw(self, count: int, **kwargs) -> MessageFactory: + index2card = self.get_cards(count, **kwargs) + cards = [card[0] for card in index2card] + up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] + result = self.format_result(index2card, **{**kwargs, "up_list": up_list}) + gen_img = await self.generate_img(cards) + return MessageFactory([Image(gen_img.pic2bytes()), Text(result)]) + + async def generate_card_img(self, card: AzurChar) -> BuildImage: + sep_w = 5 + sep_t = 5 + sep_b = 20 + w = 100 + h = 100 + bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) + frame_path = str(self.img_path / f"{card.star}_star.png") + frame = BuildImage(w, h, background=frame_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(w, h, background=img_path) + # 加圆角 + await frame.circle_corner(6) + await img.circle_corner(6) + await bg.paste(img, (sep_w, sep_t)) + await bg.paste(frame, (sep_w, sep_t)) + # 加名字 + text = card.name[:6] + "..." if len(card.name) > 7 else card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), + text, + font=font, + fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1], + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + AzurChar( + name=value["名称"], + star=int(value["星级"]), + limited="可以建造" not in value["获取途径"], + type_=value["类型"], + ) + for value in self.load_data().values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_EVENT: + data = {"char": json.loads(self.UP_EVENT.json())} + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + info = {} + # 更新图鉴 + url = "https://wiki.biligame.com/blhx/舰娘图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + contents = dom.xpath( + "//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']" + ) + for index, content in enumerate(contents): + char_list = content.xpath("./div[@id='CardSelectTr']/div") + for char in char_list: + try: + name = char.xpath("./span/a/text()")[0] + frame = char.xpath("./div/div/a/img/@alt")[0] + avatar = char.xpath("./div/img/@srcset")[0] + except IndexError: + continue + member_dict = { + "名称": remove_prohibited_str(name), + "头像": unquote(str(avatar).split(" ")[-2]), + "星级": self.parse_star(frame), + "类型": char.xpath("./@data-param1")[0].split(",")[-1], + } + info[member_dict["名称"]] = member_dict + # 更新额外信息 + for key in info.keys(): + # TODO: 各种舰娘·改获取错误 + url = f"https://wiki.biligame.com/blhx/{key}" + result = await self.get_url(url) + if not result: + info[key]["获取途径"] = [] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + time = dom.xpath( + "//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()" + )[0] + sources = [] + if "无法建造" in time: + sources.append("无法建造") + elif "活动已关闭" in time: + sources.append("活动限定") + else: + sources.append("可以建造") + info[key]["获取途径"] = sources + except IndexError: + info[key]["获取途径"] = [] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载头像框 + idx = 1 + BLHX_URL = "https://patchwiki.biligame.com/images/blhx" + for url in [ + "/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png", + "/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png", + "/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png", + "/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png", + "/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png", + ]: + await self.download_img(BLHX_URL + url, f"{idx}_star") + idx += 1 + await self.update_up_char() + + @staticmethod + def parse_star(star: str) -> int: + if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]: + return 1 + elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]: + return 2 + elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]: + return 3 + elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]: + return 4 + elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]: + return 5 + elif star in [ + "舰娘头像外框最高方案.png", + "舰娘头像外框决战方案.png", + "舰娘头像外框超稀有META.png", + "舰娘头像外框精锐META.png", + ]: + return 6 + else: + return 6 + + async def update_up_char(self): + url = "https://wiki.biligame.com/blhx/游戏活动表" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取活动表出错") + return + try: + dom = etree.HTML(result, etree.HTMLParser()) + dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0] + url = "https://wiki.biligame.com" + dd.xpath("./@href")[0] + title = dd.xpath("string(.)") + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取活动页面出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + timer = dom.xpath("//span[@class='eventTimer']")[0] + start_time = dateparser.parse(timer.xpath("./@data-start")[0]) + end_time = dateparser.parse(timer.xpath("./@data-end")[0]) + ships = dom.xpath("//table[@class='shipinfo']") + up_chars = [] + for ship in ships: + name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0] + type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型 + try: + p = float(str(ship.xpath(".//sup/text()")[0]).strip("%")) + except (IndexError, ValueError): + p = 0 + star = self.parse_star( + ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0] + ) + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=p, type_=type_) + ) + self.UP_EVENT = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.dump_up_char() + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_EVENT: + return MessageFactory( + [Text(f"重载成功!\n当前活动:{self.UP_EVENT.title}")] + ) diff --git a/zhenxun/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py new file mode 100644 index 00000000..a5d50743 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/ba_handle.py @@ -0,0 +1,153 @@ +import random + +from PIL import ImageDraw + +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font +from .base_handle import BaseData, BaseHandle + + +class BaChar(BaseData): + pass + + +class BaHandle(BaseHandle[BaChar]): + def __init__(self): + super().__init__("ba", "碧蓝档案") + self.max_star = 3 + self.config = draw_config.ba + self.ALL_CHAR: list[BaChar] = [] + + def get_card(self, mode: int = 1) -> BaChar: + if mode == 2: + star = self.get_star( + [3, 2], [self.config.BA_THREE_P, self.config.BA_G_TWO_P] + ) + else: + star = self.get_star( + [3, 2, 1], + [self.config.BA_THREE_P, self.config.BA_TWO_P, self.config.BA_ONE_P], + ) + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + return random.choice(chars) + + def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(2) + card_count = 0 + else: + card = self.get_card(1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + async def generate_card_img(self, card: BaChar) -> BuildImage: + sep_w = 5 + sep_h = 5 + star_h = 15 + img_w = 90 + img_h = 100 + font_h = 20 + bar_h = 20 + bar_w = 90 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + bar = BuildImage(bar_w, bar_h, color="#6495ED") + await bg.paste(img, (sep_w, sep_h)) + await bg.paste(bar, (sep_w, img_h - bar_h + sep_h)) + if card.star == 1: + star_path = str(self.img_path / "star-1.png") + star_w = 15 + elif card.star == 2: + star_path = str(self.img_path / "star-2.png") + star_w = 30 + else: + star_path = str(self.img_path / "star-3.png") + star_w = 45 + star = BuildImage(star_w, star_h, background=star_path) + await bg.paste(star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h)) + text = card.name[:5] + "..." if len(card.name) > 6 else card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + BaChar( + name=value["名称"], + star=int(value["星级"]), + limited=True if "(" in key else False, + ) + for key, value in self.load_data().items() + ] + + def title2star(self, title: int): + if title == "Star-3.png": + return 3 + elif title == "Star-2.png": + return 2 + else: + return 1 + + async def _update_info(self): + # TODO: ba获取链接失效 + info = {} + url = "https://lonqie.github.io/SchaleDB/data/cn/students.min.json?v=49" + result = (await AsyncHttpx.get(url)).json() + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + else: + for char in result: + try: + name = char["Name"] + avatar = ( + "https://github.com/lonqie/SchaleDB/raw/main/images/student/icon/" + + char["CollectionTexture"] + + ".png" + ) + star = char["StarGrade"] + except IndexError: + continue + member_dict = { + "头像": avatar, + "名称": name, + "星级": star, + } + info[member_dict["名称"]] = member_dict + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + await self.download_img( + "https://patchwiki.biligame.com/images/bluearchive/thumb/e/e0/82nj2x9sxko473g7782r14fztd4zyky.png/15px-Star-1.png", + "star-1", + ) + await self.download_img( + "https://patchwiki.biligame.com/images/bluearchive/thumb/0/0b/msaff2g0zk6nlyl1rrn7n1ri4yobcqc.png/30px-Star-2.png", + "star-2", + ) + await self.download_img( + "https://patchwiki.biligame.com/images/bluearchive/thumb/8/8a/577yv79x1rwxk8efdccpblo0lozl158.png/46px-Star-3.png", + "star-3", + ) diff --git a/zhenxun/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py new file mode 100644 index 00000000..1e48482a --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/base_handle.py @@ -0,0 +1,296 @@ +import asyncio +import random +from asyncio.exceptions import TimeoutError +from datetime import datetime +from typing import Generic, TypeVar + +import aiohttp +import anyio +import ujson as json +from nonebot_plugin_saa import Image +from nonebot_plugin_saa import Image as SaaImage +from nonebot_plugin_saa import MessageFactory, Text +from PIL import Image +from pydantic import BaseModel, Extra + +from zhenxun.configs.path_config import DATA_PATH +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import DRAW_PATH, draw_config +from ..util import circled_number, cn2py + + +class BaseData(BaseModel, extra=Extra.ignore): + name: str # 名字 + star: int # 星级 + limited: bool # 限定 + + def __eq__(self, other: "BaseData"): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + + @property + def star_str(self) -> str: + return "".join(["★" for _ in range(self.star)]) + + +class UpChar(BaseData): + zoom: float # up提升倍率 + + +class UpEvent(BaseModel): + title: str # up池标题 + pool_img: str # up池封面 + start_time: datetime | None # 开始时间 + end_time: datetime | None # 结束时间 + up_char: list[UpChar] # up对象 + + +TC = TypeVar("TC", bound="BaseData") + + +class BaseHandle(Generic[TC]): + def __init__(self, game_name: str, game_name_cn: str): + self.game_name = game_name + self.game_name_cn = game_name_cn + self.max_star = 1 # 最大星级 + self.game_card_color: str = "#ffffff" + self.data_path = DATA_PATH / "draw_card" + self.img_path = DRAW_PATH / f"{self.game_name}" + self.up_path = DATA_PATH / "draw_card" / "draw_card_up" + self.img_path.mkdir(parents=True, exist_ok=True) + self.up_path.mkdir(parents=True, exist_ok=True) + self.data_files: list[str] = [f"{self.game_name}.json"] + + async def draw(self, count: int, **kwargs) -> MessageFactory: + index2card = self.get_cards(count, **kwargs) + cards = [card[0] for card in index2card] + result = self.format_result(index2card) + gen_img = await self.generate_img(cards) + return MessageFactory([SaaImage(gen_img.pic2bytes()), Text(result)]) + + # 抽取卡池 + def get_card(self, **kwargs) -> TC: + raise NotImplementedError + + def get_cards(self, count: int, **kwargs) -> list[tuple[TC, int]]: + return [(self.get_card(**kwargs), i) for i in range(count)] + + # 获取星级 + @staticmethod + def get_star(star_list: list[int], probability_list: list[float]) -> int: + return random.choices(star_list, weights=probability_list, k=1)[0] + + def format_result(self, index2card: list[tuple[TC, int]], **kwargs) -> str: + card_list = [card[0] for card in index2card] + results = [ + self.format_star_result(card_list, **kwargs), + self.format_max_star(index2card, **kwargs), + self.format_max_card(card_list, **kwargs), + ] + results = [rst for rst in results if rst] + return "\n".join(results) + + def format_star_result(self, card_list: list[TC], **kwargs) -> str: + star_dict: dict[str, int] = {} # 记录星级及其次数 + + card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True) + for card in card_list_sorted: + try: + star_dict[card.star_str] += 1 + except KeyError: + star_dict[card.star_str] = 1 + + rst = "" + for star_str, count in star_dict.items(): + rst += f"[{star_str}×{count}] " + return rst.strip() + + def format_max_star( + self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs + ) -> str: + up_list = up_list or kwargs.get("up_list", []) + rst = "" + for card, index in card_list: + if card.star == self.max_star: + if card.name in up_list: + rst += f"第 {index} 抽获取UP {card.name}\n" + else: + rst += f"第 {index} 抽获取 {card.name}\n" + return rst.strip() + + def format_max_card(self, card_list: list[TC], **kwargs) -> str: + card_dict: dict[TC, int] = {} # 记录卡牌抽取次数 + + for card in card_list: + try: + card_dict[card] += 1 + except KeyError: + card_dict[card] = 1 + + max_count = max(card_dict.values()) + max_card = list(card_dict.keys())[list(card_dict.values()).index(max_count)] + if max_count <= 1: + return "" + return f"抽取到最多的是{max_card.name},共抽取了{max_count}次" + + async def generate_img( + self, + cards: list[TC], + num_per_line: int = 5, + max_per_line: tuple[int, int] = (40, 10), + ) -> BuildImage: + """ + 生成统计图片 + cards: 卡牌列表 + num_per_line: 单行角色显示数量 + max_per_line: 当card_list超过一定数值时,更改单行数量 + """ + if len(cards) > max_per_line[0]: + num_per_line = max_per_line[1] + if len(cards) > 90: + card_dict: dict[TC, int] = {} # 记录卡牌抽取次数 + for card in cards: + try: + card_dict[card] += 1 + except KeyError: + card_dict[card] = 1 + card_list = list(card_dict) + num_list = list(card_dict.values()) + else: + card_list = cards + num_list = [1] * len(cards) + + card_imgs: list[BuildImage] = [] + for card, num in zip(card_list, num_list): + card_img = await self.generate_card_img(card) + # 数量 > 1 时加数字上标 + if num > 1: + label = circled_number(num) + label_w = int(min(card_img.width, card_img.height) / 7) + label = label.resize( + ( + int(label_w * label.width / label.height), + label_w, + ), + Image.ANTIALIAS, # type: ignore + ) + await card_img.paste(label) + + card_imgs.append(card_img) + + # img_w = card_imgs[0].width + # img_h = card_imgs[0].height + # if len(card_imgs) < num_per_line: + # w = img_w * len(card_imgs) + # else: + # w = img_w * num_per_line + # h = img_h * math.ceil(len(card_imgs) / num_per_line) + # img = BuildImage(w, h, img_w, img_h, color=self.game_card_color) + # for card_img in card_imgs: + # await img.paste(card_img) + return await BuildImage.auto_paste(card_imgs, 10, color=self.game_card_color) # type: ignore + + async def generate_card_img(self, card: TC) -> BuildImage: + img = str(self.img_path / f"{cn2py(card.name)}.png") + return BuildImage(100, 100, background=img) + + def load_data(self, filename: str = "") -> dict: + if not filename: + filename = f"{self.game_name}.json" + filepath = self.data_path / filename + if not filepath.exists(): + return {} + with filepath.open("r", encoding="utf8") as f: + return json.load(f) + + def dump_data(self, data: dict, filename: str = ""): + if not filename: + filename = f"{self.game_name}.json" + filepath = self.data_path / filename + with filepath.open("w", encoding="utf8") as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def data_exists(self) -> bool: + for file in self.data_files: + if not (self.data_path / file).exists(): + return False + return True + + def _init_data(self): + raise NotImplementedError + + def init_data(self): + try: + self._init_data() + except Exception as e: + logger.warning(f"{self.game_name_cn} 导入角色数据错误:{type(e)}:{e}") + + async def _update_info(self): + raise NotImplementedError + + def client(self) -> aiohttp.ClientSession: + headers = { + "User-Agent": '"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)"' + } + return aiohttp.ClientSession(headers=headers) + + async def update_info(self): + try: + async with asyncio.Semaphore(draw_config.SEMAPHORE): + async with self.client() as session: + self.session = session + await self._update_info() + except Exception as e: + logger.warning(f"{self.game_name_cn} 更新数据错误:{type(e)}:{e}") + self.init_data() + + async def get_url(self, url: str) -> str: + result = "" + retry = 5 + for i in range(retry): + try: + async with self.session.get(url, timeout=10) as response: + result = await response.text() + break + except TimeoutError: + logger.warning(f"访问 {url} 超时, 重试 {i + 1}/{retry}") + await asyncio.sleep(1) + return result + + async def download_img(self, url: str, name: str) -> bool: + img_path = self.img_path / f"{cn2py(name)}.png" + if img_path.exists(): + return True + try: + async with self.session.get(url, timeout=10) as response: + async with await anyio.open_file(img_path, "wb") as f: + await f.write(await response.read()) + return True + except TimeoutError: + logger.warning( + f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}" + ) + return False + except: + logger.warning( + f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}" + ) + return False + + async def _reload_pool(self) -> MessageFactory | None: + return None + + async def reload_pool(self) -> MessageFactory | None: + try: + async with self.client() as session: + self.session = session + return await self._reload_pool() + except Exception as e: + logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e) + + def reset_count(self, user_id: str) -> bool: + return False diff --git a/zhenxun/plugins/draw_card/handles/fgo_handle.py b/zhenxun/plugins/draw_card/handles/fgo_handle.py new file mode 100644 index 00000000..5acb8c5f --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/fgo_handle.py @@ -0,0 +1,223 @@ +import random + +import ujson as json +from lxml import etree +from PIL import ImageDraw + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class FgoData(BaseData): + pass + + +class FgoChar(FgoData): + pass + + +class FgoCard(FgoData): + pass + + +class FgoHandle(BaseHandle[FgoData]): + def __init__(self): + super().__init__("fgo", "命运-冠位指定") + self.data_files.append("fgo_card.json") + self.max_star = 5 + self.config = draw_config.fgo + self.ALL_CHAR: list[FgoChar] = [] + self.ALL_CARD: list[FgoCard] = [] + + def get_card(self, mode: int = 1) -> FgoData: + if mode == 1: + star = self.get_star( + [8, 7, 6, 5, 4, 3], + [ + self.config.FGO_SERVANT_FIVE_P, + self.config.FGO_SERVANT_FOUR_P, + self.config.FGO_SERVANT_THREE_P, + self.config.FGO_CARD_FIVE_P, + self.config.FGO_CARD_FOUR_P, + self.config.FGO_CARD_THREE_P, + ], + ) + elif mode == 2: + star = self.get_star( + [5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P] + ) + else: + star = self.get_star( + [8, 7, 6], + [ + self.config.FGO_SERVANT_FIVE_P, + self.config.FGO_SERVANT_FOUR_P, + self.config.FGO_SERVANT_THREE_P, + ], + ) + if star > 5: + star -= 3 + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + else: + chars = [x for x in self.ALL_CARD if x.star == star and not x.limited] + return random.choice(chars) + + def get_cards(self, count: int, **kwargs) -> list[tuple[FgoData, int]]: + card_list = [] # 获取所有角色 + servant_count = 0 # 保底计算 + card_count = 0 # 保底计算 + for i in range(count): + servant_count += 1 + card_count += 1 + if card_count == 9: # 四星卡片保底 + mode = 2 + elif servant_count == 10: # 三星从者保底 + mode = 3 + else: # 普通抽 + mode = 1 + card = self.get_card(mode) + if isinstance(card, FgoCard) and card.star > self.max_star - 2: + card_count = 0 + if isinstance(card, FgoChar): + servant_count = 0 + card_list.append((card, i + 1)) + return card_list + + async def generate_card_img(self, card: FgoData) -> BuildImage: + sep_w = 5 + sep_t = 5 + sep_b = 20 + w = 128 + h = 140 + bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(w, h, background=img_path) + await bg.paste(img, (sep_w, sep_t)) + # 加名字 + text = card.name[:6] + "..." if len(card.name) > 7 else card.name + font = load_font(fontsize=16) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + FgoChar( + name=value["名称"], + star=int(value["星级"]), + limited=( + True + if not ( + "圣晶石召唤" in value["入手方式"] + or "圣晶石召唤(Story卡池)" in value["入手方式"] + ) + else False + ), + ) + for value in self.load_data().values() + ] + self.ALL_CARD = [ + FgoCard(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data("fgo_card.json").values() + ] + + async def _update_info(self): + # TODO: fgo获取链接失效 + fgo_info = {} + for i in range(500): + url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} page {i} 出错") + continue + fgo_data = json.loads(result) + if int(fgo_data["nums"]) <= 0: + break + for x in fgo_data["data"]: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "card_id": x["charid"], + "头像": x["icon"], + "名称": remove_prohibited_str(x["name"]), + "职阶": x["classes"], + "星级": int(x["star"]), + "hp": x["lvmax4hp"], + "atk": x["lvmax4atk"], + "card_quick": x["cardquick"], + "card_arts": x["cardarts"], + "card_buster": x["cardbuster"], + "宝具": x["tprop"], + } + fgo_info[name] = member_dict + # 更新额外信息 + for key in fgo_info.keys(): + url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}' + result = await self.get_url(url) + if not result: + fgo_info[key]["入手方式"] = ["圣晶石召唤"] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + obtain = dom.xpath( + "//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()" + )[0] + obtain = str(obtain).strip() + if "限时活动免费获取 活动结束后无法获得" in obtain: + obtain = ["活动获取"] + elif "非限时UP无法获得" in obtain: + obtain = ["限时召唤"] + else: + if "&" in obtain: + obtain = obtain.split("&") + else: + obtain = obtain.split(" ") + obtain = [s.strip() for s in obtain if s.strip()] + fgo_info[key]["入手方式"] = obtain + except IndexError: + fgo_info[key]["入手方式"] = ["圣晶石召唤"] + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(fgo_info) + logger.info(f"{self.game_name_cn} 更新成功") + # fgo_card.json + fgo_card_info = {} + for i in range(500): + url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错") + continue + fgo_data = json.loads(result) + if int(fgo_data["nums"]) <= 0: + break + for x in fgo_data["data"]: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "card_id": x["equipid"], + "头像": x["icon"], + "名称": name, + "星级": int(x["star"]), + "hp": x["lvmax_hp"], + "atk": x["lvmax_atk"], + "skill_e": str(x["skill_e"]).split("
")[:-1], + } + fgo_card_info[name] = member_dict + self.dump_data(fgo_card_info, "fgo_card.json") + logger.info(f"{self.game_name_cn} 卡牌更新成功") + # 下载头像 + for value in fgo_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in fgo_card_info.values(): + await self.download_img(value["头像"], value["名称"]) diff --git a/zhenxun/plugins/draw_card/handles/genshin_handle.py b/zhenxun/plugins/draw_card/handles/genshin_handle.py new file mode 100644 index 00000000..3298e988 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/genshin_handle.py @@ -0,0 +1,465 @@ +import random +from datetime import datetime, timedelta +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_saa import Image as SaaImage +from nonebot_plugin_saa import MessageFactory, Text +from PIL import Image, ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..count_manager import GenshinCountManager +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class GenshinData(BaseData): + pass + + +class GenshinChar(GenshinData): + pass + + +class GenshinArms(GenshinData): + pass + + +class GenshinHandle(BaseHandle[GenshinData]): + def __init__(self): + super().__init__("genshin", "原神") + self.data_files.append("genshin_arms.json") + self.max_star = 5 + self.game_card_color = "#ebebeb" + self.config = draw_config.genshin + + self.ALL_CHAR: list[GenshinData] = [] + self.ALL_ARMS: list[GenshinData] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_CHAR_LIST: UpEvent | None = [] + self.UP_ARMS: UpEvent | None = None + + self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180) + + # 抽取卡池 + def get_card( + self, + pool_name: str, + mode: int = 1, + add: float = 0.0, + is_up: bool = False, + card_index: int = 0, + ): + """ + mode 1:普通抽 2:四星保底 3:五星保底 + """ + if mode == 1: + star = self.get_star( + [5, 4, 3], + [ + self.config.GENSHIN_FIVE_P + add, + self.config.GENSHIN_FOUR_P, + self.config.GENSHIN_THREE_P, + ], + ) + elif mode == 2: + star = self.get_star( + [5, 4], + [self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P], + ) + else: + star = 5 + + if pool_name == "char": + up_event = self.UP_CHAR_LIST[card_index] + all_list = self.ALL_CHAR + [ + x for x in self.ALL_ARMS if x.star == star and x.star < 5 + ] + elif pool_name == "arms": + up_event = self.UP_ARMS + all_list = self.ALL_ARMS + [ + x for x in self.ALL_CHAR if x.star == star and x.star < 5 + ] + else: + up_event = None + all_list = self.ALL_ARMS + self.ALL_CHAR + + acquire_char = None + # 是否UP + if up_event and star > 3: + # 获取up角色列表 + up_list = [x.name for x in up_event.up_char if x.star == star] + # 成功获取up角色 + if random.random() < 0.5 or is_up: + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_list if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + chars = [x for x in all_list if x.star == star and not x.limited] + acquire_char = random.choice(chars) + return acquire_char + + def get_cards( + self, count: int, user_id: int, pool_name: str, card_index: int = 0 + ) -> list[tuple[GenshinData, int]]: + card_list = [] # 获取角色列表 + add = 0.0 + count_manager = self.count_manager + count_manager.check_count(user_id, count) # 检查次数累计 + pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS + for i in range(count): + count_manager.increase(user_id) + star = count_manager.check(user_id) # 是否有四星或五星保底 + if ( + count_manager.get_user_count(user_id) + - count_manager.get_user_five_index(user_id) + ) % count_manager.get_max_guarantee() >= 72: + add += draw_config.genshin.I72_ADD + if star: + if star == 4: + card = self.get_card(pool_name, 2, add=add, card_index=card_index) + else: + card = self.get_card( + pool_name, + 3, + add, + count_manager.is_up(user_id), + card_index=card_index, + ) + else: + card = self.get_card( + pool_name, + 1, + add, + count_manager.is_up(user_id), + card_index=card_index, + ) + # print(f"{count_manager.get_user_count(user_id)}:", + # count_manager.get_user_five_index(user_id), star, card.star, add) + # 四星角色 + if card.star == 4: + count_manager.mark_four_index(user_id) + # 五星角色 + elif card.star == self.max_star: + add = 0 + count_manager.mark_five_index(user_id) # 记录五星保底 + count_manager.mark_four_index(user_id) # 记录四星保底 + if pool and card.name in [ + x.name for x in pool.up_char if x.star == self.max_star + ]: + count_manager.set_is_up(user_id, True) + else: + count_manager.set_is_up(user_id, False) + card_list.append((card, count_manager.get_user_count(user_id))) + return card_list + + async def generate_card_img(self, card: GenshinData) -> BuildImage: + sep_w = 10 + sep_h = 5 + frame_w = 112 + frame_h = 132 + img_w = 106 + img_h = 106 + bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB") + frame_path = str(self.img_path / "avatar_frame.png") + frame = Image.open(frame_path) + # 加名字 + text = card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(frame) + draw.text( + ((frame_w - text_w) / 2, frame_h - 15 - text_h / 2), + text, + font=font, + fill="gray", + ) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + if isinstance(card, GenshinArms): + # 武器卡背景不是透明的,切去上方两个圆弧 + r = 12 + circle = Image.new("L", (r * 2, r * 2), 0) + alpha = Image.new("L", img.size, 255) + alpha.paste(circle, (-r - 3, -r - 3)) # 左上角 + alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角 + img.markImg.putalpha(alpha) + star_path = str(self.img_path / f"{card.star}_star.png") + star = Image.open(star_path) + await bg.paste(frame, (sep_w, sep_h)) + await bg.paste(img, (sep_w + 3, sep_h + 3)) + await bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6)) + return bg + + def format_pool_info(self, pool_name: str, card_index: int = 0) -> str: + info = "" + up_event = None + if pool_name == "char": + up_event = self.UP_CHAR_LIST[card_index] + elif pool_name == "arms": + up_event = self.UP_ARMS + if up_event: + star5_list = [x.name for x in up_event.up_char if x.star == 5] + star4_list = [x.name for x in up_event.up_char if x.star == 4] + if star5_list: + info += f"五星UP:{' '.join(star5_list)}\n" + if star4_list: + info += f"四星UP:{' '.join(star4_list)}\n" + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw( + self, count: int, user_id: int, pool_name: str = "", **kwargs + ) -> Text | MessageFactory: + card_index = 0 + if "1" in pool_name: + card_index = 1 + pool_name = pool_name.replace("1", "") + index2cards = self.get_cards(count, user_id, pool_name, card_index) + cards = [card[0] for card in index2cards] + up_event = None + if pool_name == "char": + if card_index == 1 and len(self.UP_CHAR_LIST) == 1: + return Text("当前没有第二个角色UP池") + up_event = self.UP_CHAR_LIST[card_index] + elif pool_name == "arms": + up_event = self.UP_ARMS + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_star_result(cards) + result += ( + "\n" + max_star_str + if (max_star_str := self.format_max_star(index2cards, up_list=up_list)) + else "" + ) + result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽" + # result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】" + pool_info = self.format_pool_info(pool_name, card_index) + img = await self.generate_img(cards) + bk = BuildImage(img.width, img.height + 50, font_size=20, color="#ebebeb") + await bk.paste(img) + await bk.text( + (0, img.height + 10), + "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】", + ) + return MessageFactory([Text(pool_info), SaaImage(bk.pic2bytes()), Text(result)]) + + def _init_data(self): + self.ALL_CHAR = [ + GenshinChar( + name=value["名称"], + star=int(value["星级"]), + limited=value["常驻/限定"] == "限定UP", + ) + for key, value in self.load_data().items() + if "旅行者" not in key + ] + self.ALL_ARMS = [ + GenshinArms( + name=value["名称"], + star=int(value["星级"]), + limited="祈愿" not in value["获取途径"], + ) + for value in self.load_data("genshin_arms.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {}))) + self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {}))) + self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR_LIST and self.UP_ARMS: + data = { + "char": json.loads(self.UP_CHAR_LIST[0].json()), + "arms": json.loads(self.UP_ARMS.json()), + } + if len(self.UP_CHAR_LIST) > 1: + data["char1"] = json.loads(self.UP_CHAR_LIST[1].json()) + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # genshin.json + char_info = {} + url = "https://wiki.biligame.com/ys/角色筛选" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[3]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()[:1]), + } + char_info[member_dict["名称"]] = member_dict + # 更新额外信息 + for key in char_info.keys(): + result = await self.get_url(f"https://wiki.biligame.com/ys/{key}") + if not result: + char_info[key]["常驻/限定"] = "未知" + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + limit = dom.xpath( + "//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()" + )[0] + char_info[key]["常驻/限定"] = str(limit).strip() + except IndexError: + char_info[key]["常驻/限定"] = "未知" + logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}") + self.dump_data(char_info) + logger.info(f"{self.game_name_cn} 更新成功") + # genshin_arms.json + arms_info = {} + url = "https://wiki.biligame.com/ys/武器图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[4]/img/@alt")[0] + sources = str(char.xpath("./td[5]/text()")[0]).split(",") + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()[:1]), + "获取途径": [s.strip() for s in sources if s.strip()], + } + arms_info[member_dict["名称"]] = member_dict + self.dump_data(arms_info, "genshin_arms.json") + logger.info(f"{self.game_name_cn} 武器更新成功") + # 下载头像 + for value in char_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in arms_info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + idx = 1 + YS_URL = "https://patchwiki.biligame.com/images/ys" + for url in [ + "/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png", + "/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png", + "/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png", + "/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png", + "/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png", + ]: + await self.download_img(YS_URL + url, f"{idx}_star") + idx += 1 + # 下载头像框 + await self.download_img( + YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame" + ) + await self.update_up_char() + + async def update_up_char(self): + self.UP_CHAR_LIST = [] + url = "https://wiki.biligame.com/ys/祈愿" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取祈愿页面出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + tables = dom.xpath( + "//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody" + ) + if not tables or len(tables) < 2: + logger.warning(f"{self.game_name_cn}获取活动祈愿出错") + return + try: + for index, table in enumerate(tables): + title = table.xpath("./tr[1]/th/img/@title")[0] + title = str(title).split("」")[0] + "」" if "」" in title else title + pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2] + time = table.xpath("./tr[2]/td/text()")[0] + star5_list = table.xpath("./tr[3]/td/a/@title") + star4_list = table.xpath("./tr[4]/td/a/@title") + start, end = str(time).split("~") + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + if not start_time and end_time: + start_time = end_time - timedelta(days=20) + if start_time and end_time and start_time <= datetime.now() <= end_time: + up_event = UpEvent( + title=title, + pool_img=pool_img, + start_time=start_time, + end_time=end_time, + up_char=[ + UpChar(name=name, star=5, limited=False, zoom=50) + for name in star5_list + ] + + [ + UpChar(name=name, star=4, limited=False, zoom=50) + for name in star4_list + ], + ) + if "神铸赋形" not in title: + self.UP_CHAR_LIST.append(up_event) + else: + self.UP_ARMS = up_event + if self.UP_CHAR_LIST and self.UP_ARMS: + self.dump_up_char() + char_title = " & ".join([x.title for x in self.UP_CHAR_LIST]) + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}" + ) + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + def reset_count(self, user_id: str) -> bool: + self.count_manager.reset(user_id) + return True + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR_LIST and self.UP_ARMS: + if len(self.UP_CHAR_LIST) > 1: + return MessageFactory( + [ + Text( + f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}" + ), + Image(self.UP_CHAR_LIST[0].pool_img), + Image(self.UP_CHAR_LIST[1].pool_img), + Image(self.UP_ARMS.pool_img), + ] + ) + return MessageFactory( + [ + Text( + f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}" + ), + Image(self.UP_CHAR_LIST[0].pool_img), + Image(self.UP_ARMS.pool_img), + ] + ) diff --git a/zhenxun/plugins/draw_card/handles/guardian_handle.py b/zhenxun/plugins/draw_card/handles/guardian_handle.py new file mode 100644 index 00000000..c24caa0b --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/guardian_handle.py @@ -0,0 +1,399 @@ +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from nonebot_plugin_saa import Image, MessageFactory, Text +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class GuardianData(BaseData): + pass + + +class GuardianChar(GuardianData): + pass + + +class GuardianArms(GuardianData): + pass + + +class GuardianHandle(BaseHandle[GuardianData]): + def __init__(self): + super().__init__("guardian", "坎公骑冠剑") + self.data_files.append("guardian_arms.json") + self.config = draw_config.guardian + + self.ALL_CHAR: list[GuardianChar] = [] + self.ALL_ARMS: list[GuardianArms] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_ARMS: UpEvent | None = None + + def get_card(self, pool_name: str, mode: int = 1) -> GuardianData: + if pool_name == "char": + if mode == 1: + star = self.get_star( + [3, 2, 1], + [ + self.config.GUARDIAN_THREE_CHAR_P, + self.config.GUARDIAN_TWO_CHAR_P, + self.config.GUARDIAN_ONE_CHAR_P, + ], + ) + else: + star = self.get_star( + [3, 2], + [ + self.config.GUARDIAN_THREE_CHAR_P, + self.config.GUARDIAN_TWO_CHAR_P, + ], + ) + up_event = self.UP_CHAR + self.max_star = 3 + all_data = self.ALL_CHAR + else: + if mode == 1: + star = self.get_star( + [5, 4, 3, 2], + [ + self.config.GUARDIAN_FIVE_ARMS_P, + self.config.GUARDIAN_FOUR_ARMS_P, + self.config.GUARDIAN_THREE_ARMS_P, + self.config.GUARDIAN_TWO_ARMS_P, + ], + ) + else: + star = self.get_star( + [5, 4], + [ + self.config.GUARDIAN_FIVE_ARMS_P, + self.config.GUARDIAN_FOUR_ARMS_P, + ], + ) + up_event = self.UP_ARMS + self.max_star = 5 + all_data = self.ALL_ARMS + + acquire_char = None + # 是否UP + if up_event and star == self.max_star and pool_name: + # 获取up角色列表 + up_list = [x.name for x in up_event.up_char if x.star == star] + # 成功获取up角色 + if random.random() < 0.5: + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_data if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + chars = [x for x in all_data if x.star == star and not x.limited] + acquire_char = random.choice(chars) + return acquire_char + + def get_cards(self, count: int, pool_name: str) -> list[tuple[GuardianData, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(pool_name, 2) + card_count = 0 + else: + card = self.get_card(pool_name, 1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self, pool_name: str) -> str: + info = "" + up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS + if up_event: + if pool_name == "char": + up_list = [x.name for x in up_event.up_char if x.star == 3] + info += f'三星UP:{" ".join(up_list)}\n' + else: + up_list = [x.name for x in up_event.up_char if x.star == 5] + info += f'五星UP:{" ".join(up_list)}\n' + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory: + index2card = self.get_cards(count, pool_name) + cards = [card[0] for card in index2card] + up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info(pool_name) + img = await self.generate_img(cards) + return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + + async def generate_card_img(self, card: GuardianData) -> BuildImage: + sep_w = 1 + sep_h = 1 + block_w = 170 + block_h = 90 + img_w = 90 + img_h = 90 + if isinstance(card, GuardianChar): + block_color = "#2e2923" + font_color = "#e2ccad" + star_w = 90 + star_h = 30 + star_name = f"{card.star}_star.png" + frame_path = "" + else: + block_color = "#EEE4D5" + font_color = "#A65400" + star_w = 45 + star_h = 45 + star_name = f"{card.star}_star_rank.png" + frame_path = str(self.img_path / "avatar_frame.png") + bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED") + block = BuildImage(block_w, block_h, color=block_color) + star_path = str(self.img_path / star_name) + star = BuildImage(star_w, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await block.paste(img, (0, 0)) + if frame_path: + frame = BuildImage(img_w, img_h, background=frame_path) + await block.paste(frame, (0, 0)) + await block.paste( + star, + (int((block_w + img_w - star_w) / 2), block_h - star_h - 30), + ) + # 加名字 + text = card.name[:4] + "..." if len(card.name) > 5 else card.name + font = load_font(fontsize=14) + text_w, _ = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(block.markImg) + draw.text( + ((block_w + img_w - text_w) / 2, 55), + text, + font=font, + fill=font_color, + ) + await bg.paste(block, (sep_w, sep_h)) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data().values() + ] + self.ALL_ARMS = [ + GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False) + for value in self.load_data("guardian_arms.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) + self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR and self.UP_ARMS: + data = { + "char": json.loads(self.UP_CHAR.json()), + "arms": json.loads(self.UP_ARMS.json()), + } + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # guardian.json + guardian_info = {} + url = "https://wiki.biligame.com/gt/英雄筛选表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + # name = char.xpath("./td[1]/a/@title")[0] + # avatar = char.xpath("./td[1]/a/img/@src")[0] + # star = char.xpath("./td[1]/span/img/@alt")[0] + name = char.xpath("./th[1]/a[1]/@title")[0] + avatar = char.xpath("./th[1]/a/img/@src")[0] + star = char.xpath("./th[1]/span/img/@alt")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).split(" ")[0].replace("Rank", "")), + } + guardian_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_info) + logger.info(f"{self.game_name_cn} 更新成功") + # guardian_arms.json + guardian_arms_info = {} + url = "https://wiki.biligame.com/gt/武器" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 武器出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[2]/a/@title")[0] + avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] + url = char.xpath("./td[3]/img/@srcset")[0] + if r := re.search(r"Rank-mini-star_(\d).png", url): + star = r.group(1) + else: + continue + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + guardian_arms_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_arms_info, "guardian_arms.json") + logger.info(f"{self.game_name_cn} 武器更新成功") + url = "https://wiki.biligame.com/gt/盾牌" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 盾牌出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath( + "//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr" + ) + for char in char_list: + try: + name = char.xpath("./td[2]/a/@title")[0] + avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0] + star = char.xpath("./td[3]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar)), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + guardian_arms_info[member_dict["名称"]] = member_dict + self.dump_data(guardian_arms_info, "guardian_arms.json") + logger.info(f"{self.game_name_cn} 盾牌更新成功") + # 下载头像 + for value in guardian_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in guardian_arms_info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + idx = 1 + GT_URL = "https://patchwiki.biligame.com/images/gt" + for url in [ + "/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png", + "/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png", + "/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png", + ]: + await self.download_img(GT_URL + url, f"{idx}_star") + idx += 1 + # 另一种星星 + idx = 1 + for url in [ + "/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png", + "/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png", + "/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png", + "/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png", + "/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png", + ]: + await self.download_img(GT_URL + url, f"{idx}_star_rank") + idx += 1 + # 头像框 + await self.download_img( + GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame" + ) + await self.update_up_char() + + async def update_up_char(self): + url = "https://wiki.biligame.com/gt/首页" + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + try: + dom = etree.HTML(result, etree.HTMLParser()) + announcement = dom.xpath( + "//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]" + )[0] + title = announcement.xpath("./font/p/b/text()")[0] + match = re.search(r"从(.*?)开始.*?至(.*?)结束", title) + if not match: + logger.warning(f"{self.game_name_cn}找不到UP时间") + return + start, end = match.groups() + start_time = dateparser.parse(start.replace("月", "/").replace("日", "")) + end_time = dateparser.parse(end.replace("月", "/").replace("日", "")) + if not (start_time and end_time) or not ( + start_time <= datetime.now() <= end_time + ): + return + divs = announcement.xpath("./font/div") + char_index = 0 + arms_index = 0 + for index, div in enumerate(divs): + if div.xpath("string(.)") == "角色": + char_index = index + elif div.xpath("string(.)") == "武器": + arms_index = index + chars = divs[char_index + 1 : arms_index] + arms = divs[arms_index + 1 :] + up_chars = [] + up_arms = [] + for char in chars: + name = char.xpath("./p/a/@title")[0] + up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0)) + for arm in arms: + name = arm.xpath("./p/a/@title")[0] + up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0)) + self.UP_CHAR = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.UP_ARMS = UpEvent( + title=title, + pool_img="", + start_time=start_time, + end_time=end_time, + up_char=up_arms, + ) + self.dump_up_char() + logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}") + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR and self.UP_ARMS: + return MessageFactory( + [Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}")] + ) diff --git a/zhenxun/plugins/draw_card/handles/onmyoji_handle.py b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py new file mode 100644 index 00000000..25d05c38 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py @@ -0,0 +1,178 @@ +import random + +import ujson as json +from lxml import etree +from PIL import Image, ImageDraw +from PIL.Image import Image as IMG + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class OnmyojiChar(BaseData): + @property + def star_str(self) -> str: + return ["N", "R", "SR", "SSR", "SP"][self.star - 1] + + +class OnmyojiHandle(BaseHandle[OnmyojiChar]): + def __init__(self): + super().__init__("onmyoji", "阴阳师") + self.max_star = 5 + self.config = draw_config.onmyoji + self.ALL_CHAR: list[OnmyojiChar] = [] + + def get_card(self, **kwargs) -> OnmyojiChar: + star = self.get_star( + [5, 4, 3, 2], + [ + self.config.ONMYOJI_SP, + self.config.ONMYOJI_SSR, + self.config.ONMYOJI_SR, + self.config.ONMYOJI_R, + ], + ) + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + return random.choice(chars) + + def format_max_star(self, card_list: list[tuple[OnmyojiChar, int]]) -> str: + rst = "" + for card, index in card_list: + if card.star == self.max_star: + rst += f"第 {index} 抽获取SP {card.name}\n" + elif card.star == self.max_star - 1: + rst += f"第 {index} 抽获取SSR {card.name}\n" + return rst.strip() + + @staticmethod + def star_label(star: int) -> IMG: + text, color1, color2 = [ + ("N", "#7E7E82", "#F5F6F7"), + ("R", "#014FA8", "#37C6FD"), + ("SR", "#6E0AA4", "#E94EFD"), + ("SSR", "#E5511D", "#FAF905"), + ("SP", "#FA1F2D", "#FFBBAF"), + ][star - 1] + w = 200 + h = 110 + # 制作渐变色图片 + base = Image.new("RGBA", (w, h), color1) + top = Image.new("RGBA", (w, h), color2) + mask = Image.new("L", (w, h)) + mask_data = [] + for y in range(h): + mask_data.extend([int(255 * (y / h))] * w) + mask.putdata(mask_data) + base.paste(top, (0, 0), mask) + # 透明图层 + font = load_font("gorga.otf", 100) + alpha = Image.new("L", (w, h)) + draw = ImageDraw.Draw(alpha) + draw.text((20, -30), text, fill="white", font=font) + base.putalpha(alpha) + # stroke + bg = Image.new("RGBA", (w, h)) + draw = ImageDraw.Draw(bg) + draw.text( + (20, -30), + text, + font=font, + fill="gray", + stroke_width=3, + stroke_fill="gray", + ) + bg.paste(base, (0, 0), base) + return bg + + async def generate_img(self, card_list: list[OnmyojiChar]) -> BuildImage: + return await super().generate_img(card_list, num_per_line=10) + + async def generate_card_img(self, card: OnmyojiChar) -> BuildImage: + bg = BuildImage(73, 240, color="#F1EFE9") + img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png") + img = BuildImage(0, 0, background=img_path) + img = Image.open(img_path).convert("RGBA") + label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS) + await bg.paste(img, (0, 0)) + await bg.paste(label, (0, 135)) + font = load_font("msyh.ttf", 16) + draw = ImageDraw.Draw(bg.markImg) + text = "\n".join([t for t in card.name[:4]]) + _, text_h = font.getsize_multiline(text, spacing=0) + draw.text( + (40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0 + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + OnmyojiChar( + name=value["名称"], + star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1, + limited=( + True + if key + in [ + "奴良陆生", + "卖药郎", + "鬼灯", + "阿香", + "蜜桃&芥子", + "犬夜叉", + "杀生丸", + "桔梗", + "朽木露琪亚", + "黑崎一护", + "灶门祢豆子", + "灶门炭治郎", + ] + else False + ), + ) + for key, value in self.load_data().items() + ] + + async def _update_info(self): + info = {} + url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74=" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + data = json.loads(result) + for x in data: + name = remove_prohibited_str(x["name"]) + member_dict = { + "id": x["id"], + "名称": name, + "星级": x["level"], + } + info[name] = member_dict + # logger.info(f"{name} is update...") + # 更新头像 + for key in info.keys(): + url = f'https://yys.163.com/shishen/{info[key]["id"]}.html' + result = await self.get_url(url) + if not result: + info[key]["头像"] = "" + continue + try: + dom = etree.HTML(result, etree.HTMLParser()) + avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0] + avatar = "https:" + avatar + info[key]["头像"] = avatar + except IndexError: + info[key]["头像"] = "" + logger.warning(f"{self.game_name_cn} 获取头像错误 {key}") + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载书签形式的头像 + url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png" + await self.download_img(url, value["名称"] + "_mark_btn") diff --git a/zhenxun/plugins/draw_card/handles/pcr_handle.py b/zhenxun/plugins/draw_card/handles/pcr_handle.py new file mode 100644 index 00000000..666a6842 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/pcr_handle.py @@ -0,0 +1,149 @@ +import random +from urllib.parse import unquote + +from lxml import etree +from PIL import ImageDraw + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle + + +class PcrChar(BaseData): + pass + + +class PcrHandle(BaseHandle[PcrChar]): + def __init__(self): + super().__init__("pcr", "公主连结") + self.max_star = 3 + self.config = draw_config.pcr + self.ALL_CHAR: list[PcrChar] = [] + + def get_card(self, mode: int = 1) -> PcrChar: + if mode == 2: + star = self.get_star( + [3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P] + ) + else: + star = self.get_star( + [3, 2, 1], + [self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P], + ) + chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited] + return random.choice(chars) + + def get_cards(self, count: int, **kwargs) -> list[tuple[PcrChar, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(2) + card_count = 0 + else: + card = self.get_card(1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + async def generate_card_img(self, card: PcrChar) -> BuildImage: + sep_w = 5 + sep_h = 5 + star_h = 15 + img_w = 90 + img_h = 90 + font_h = 20 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, sep_h)) + for i in range(card.star): + await bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h)) + # 加名字 + text = card.name[:5] + "..." if len(card.name) > 6 else card.name + font = load_font(fontsize=14) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + PcrChar( + name=value["名称"], + star=int(value["星级"]), + limited=True if "(" in key else False, + ) + for key, value in self.load_data().items() + ] + + async def _update_info(self): + info = {} + if draw_config.PCR_TAI: + url = "https://wiki.biligame.com/pcr/角色图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + # TODO: PCR台湾更新失败 + char_list = dom.xpath( + "//*[@id='CardSelectCard']/div[@class='unit-icon trcard']" + ) + for char in char_list: + try: + name = char.xpath("./a/@title")[0] + avatar = char.xpath("./a/img/@srcset")[0] + star = len(char.xpath("./div[1]/img")) + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": star, + } + info[member_dict["名称"]] = member_dict + else: + url = "https://wiki.biligame.com/pcr/角色筛选表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = char.xpath("./td[4]/text()")[0] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "星级": int(str(star).strip()), + } + info[member_dict["名称"]] = member_dict + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + await self.download_img( + "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", + "star", + ) diff --git a/zhenxun/plugins/draw_card/handles/pretty_handle.py b/zhenxun/plugins/draw_card/handles/pretty_handle.py new file mode 100644 index 00000000..ecc2dfe3 --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/pretty_handle.py @@ -0,0 +1,422 @@ +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from bs4 import BeautifulSoup +from lxml import etree +from nonebot_plugin_saa import Image, MessageFactory, Text +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class PrettyData(BaseData): + pass + + +class PrettyChar(PrettyData): + pass + + +class PrettyCard(PrettyData): + @property + def star_str(self) -> str: + return ["R", "SR", "SSR"][self.star - 1] + + +class PrettyHandle(BaseHandle[PrettyData]): + def __init__(self): + super().__init__("pretty", "赛马娘") + self.data_files.append("pretty_card.json") + self.max_star = 3 + self.game_card_color = "#eff2f5" + self.config = draw_config.pretty + + self.ALL_CHAR: list[PrettyChar] = [] + self.ALL_CARD: list[PrettyCard] = [] + self.UP_CHAR: UpEvent | None = None + self.UP_CARD: UpEvent | None = None + + def get_card(self, pool_name: str, mode: int = 1) -> PrettyData: + if mode == 1: + star = self.get_star( + [3, 2, 1], + [ + self.config.PRETTY_THREE_P, + self.config.PRETTY_TWO_P, + self.config.PRETTY_ONE_P, + ], + ) + else: + star = self.get_star( + [3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P] + ) + up_pool = None + if pool_name == "char": + up_pool = self.UP_CHAR + all_list = self.ALL_CHAR + else: + up_pool = self.UP_CARD + all_list = self.ALL_CARD + + all_char = [x for x in all_list if x.star == star and not x.limited] + acquire_char = None + # 有UP池子 + if up_pool and star in [x.star for x in up_pool.up_char]: + up_list = [x.name for x in up_pool.up_char if x.star == star] + # 抽到UP + if random.random() < 1 / len(all_char) * (0.7 / 0.1385): + up_name = random.choice(up_list) + try: + acquire_char = [x for x in all_list if x.name == up_name][0] + except IndexError: + pass + if not acquire_char: + acquire_char = random.choice(all_char) + return acquire_char + + def get_cards(self, count: int, pool_name: str) -> list[tuple[PrettyData, int]]: + card_list = [] + card_count = 0 # 保底计算 + for i in range(count): + card_count += 1 + # 十连保底 + if card_count == 10: + card = self.get_card(pool_name, 2) + card_count = 0 + else: + card = self.get_card(pool_name, 1) + if card.star > self.max_star - 2: + card_count = 0 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self, pool_name: str) -> str: + info = "" + up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD + if up_event: + star3_list = [x.name for x in up_event.up_char if x.star == 3] + star2_list = [x.name for x in up_event.up_char if x.star == 2] + star1_list = [x.name for x in up_event.up_char if x.star == 1] + if star3_list: + if pool_name == "char": + info += f'三星UP:{" ".join(star3_list)}\n' + else: + info += f'SSR UP:{" ".join(star3_list)}\n' + if star2_list: + if pool_name == "char": + info += f'二星UP:{" ".join(star2_list)}\n' + else: + info += f'SR UP:{" ".join(star2_list)}\n' + if star1_list: + if pool_name == "char": + info += f'一星UP:{" ".join(star1_list)}\n' + else: + info += f'R UP:{" ".join(star1_list)}\n' + info = f"当前up池:{up_event.title}\n{info}" + return info.strip() + + async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory: + pool_name = "char" if not pool_name else pool_name + index2card = self.get_cards(count, pool_name) + cards = [card[0] for card in index2card] + up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD + up_list = [x.name for x in up_event.up_char] if up_event else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info(pool_name) + img = await self.generate_img(cards) + return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + + async def generate_card_img(self, card: PrettyData) -> BuildImage: + if isinstance(card, PrettyChar): + star_h = 30 + img_w = 200 + img_h = 219 + font_h = 50 + bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + star_w = star_h * card.star + for i in range(card.star): + await bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0)) + await bg.paste(img, (0, 0)) + # 加名字 + text = card.name[:5] + "..." if len(card.name) > 6 else card.name + font = load_font(fontsize=30) + text_w, _ = font.getsize(text) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + ((img_w - text_w) / 2, img_h), + text, + font=font, + fill="gray", + ) + return bg + else: + sep_w = 10 + img_w = 200 + img_h = 267 + font_h = 75 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5") + label_path = str(self.img_path / f"{card.star}_label.png") + label = BuildImage(40, 40, background=label_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, 0)) + await bg.paste(label, (30, 3)) + # 加名字 + text = "" + texts = [] + font = load_font(fontsize=25) + for t in card.name: + if BuildImage.get_text_size((text + t), font)[0] > 190: + texts.append(text) + text = "" + if len(texts) >= 2: + texts[-1] += "..." + break + else: + text += t + if text: + texts.append(text) + text = "\n".join(texts) + text_w, _ = font.getsize_multiline(text) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + ((img_w - text_w) / 2, img_h), + text, + font=font, + align="center", + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_CHAR = [ + PrettyChar( + name=value["名称"], + star=int(value["初始星级"]), + limited=False, + ) + for value in self.load_data().values() + ] + self.ALL_CARD = [ + PrettyCard( + name=value["中文名"], + star=["R", "SR", "SSR"].index(value["稀有度"]) + 1, + limited=True if "卡池" not in value["获取方式"] else False, + ) + for value in self.load_data("pretty_card.json").values() + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + self.UP_CHAR = UpEvent.parse_obj(data.get("char", {})) + self.UP_CARD = UpEvent.parse_obj(data.get("card", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_CHAR and self.UP_CARD: + data = { + "char": json.loads(self.UP_CHAR.json()), + "card": json.loads(self.UP_CARD.json()), + } + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + # pretty.json + pretty_info = {} + url = "https://wiki.biligame.com/umamusume/赛马娘图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/a/@title")[0] + avatar = char.xpath("./td[1]/a/img/@srcset")[0] + star = len(char.xpath("./td[3]/img")) + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "初始星级": star, + } + pretty_info[member_dict["名称"]] = member_dict + self.dump_data(pretty_info) + logger.info(f"{self.game_name_cn} 更新成功") + # pretty_card.json + pretty_card_info = {} + url = "https://wiki.biligame.com/umamusume/支援卡图鉴" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 卡牌出错") + else: + dom = etree.HTML(result, etree.HTMLParser()) + char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + name = char.xpath("./td[1]/div/a/@title")[0] + name_cn = char.xpath("./td[3]/a/text()")[0] + avatar = char.xpath("./td[1]/div/a/img/@srcset")[0] + star = str(char.xpath("./td[5]/text()")[0]).strip() + sources = str(char.xpath("./td[7]/text()")[0]).strip() + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(name), + "中文名": remove_prohibited_str(name_cn), + "稀有度": star, + "获取方式": [sources] if sources else [], + } + pretty_card_info[member_dict["中文名"]] = member_dict + self.dump_data(pretty_card_info, "pretty_card.json") + logger.info(f"{self.game_name_cn} 卡牌更新成功") + # 下载头像 + for value in pretty_info.values(): + await self.download_img(value["头像"], value["名称"]) + for value in pretty_card_info.values(): + await self.download_img(value["头像"], value["中文名"]) + # 下载星星 + PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume" + await self.download_img( + PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star" + ) + # 下载稀有度标志 + idx = 1 + for url in [ + "/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png", + "/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png", + "/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png", + ]: + await self.download_img(PRETTY_URL + url, f"{idx}_label") + idx += 1 + await self.update_up_char() + + async def update_up_char(self): + announcement_url = "https://wiki.biligame.com/umamusume/公告" + result = await self.get_url(announcement_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a") + title = "" + url = "" + for announcement in announcements: + try: + title = announcement.xpath("./@title")[0] + url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0] + if re.match(r".*?\d{8}$", title) or re.match( + r"^\d{1,2}月\d{1,2}日.*?", title + ): + break + except IndexError: + continue + if not title: + logger.warning(f"{self.game_name_cn}未找到新UP公告") + return + result = await self.get_url(url) + if not result: + logger.warning(f"{self.game_name_cn}获取UP公告出错") + return + try: + start_time = None + end_time = None + char_img = "" + card_img = "" + up_chars = [] + up_cards = [] + soup = BeautifulSoup(result, "lxml") + heads = soup.find_all("span", {"class": "mw-headline"}) + for head in heads: + if "时间" in head.text: + time = head.find_next("p").text.split("\n")[0] + if "~" in time: + start, end = time.split("~") + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + elif "赛马娘" in head.text: + char_img = head.find_next("a", {"class": "image"}).find("img")[ + "src" + ] + lines = str(head.find_next("p").text).split("\n") + chars = [ + line + for line in lines + if "★" in line and "(" in line and ")" in line + ] + for char in chars: + star = char.count("★") + name = re.split(r"[()]", char)[-2].strip() + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=70) + ) + elif "支援卡" in head.text: + card_img = head.find_next("a", {"class": "image"}).find("img")[ + "src" + ] + lines = str(head.find_next("p").text).split("\n") + cards = [ + line + for line in lines + if "R" in line and "(" in line and ")" in line + ] + for card in cards: + star = 3 if "SSR" in card else 2 if "SR" in card else 1 + name = re.split(r"[()]", card)[-2].strip() + up_cards.append( + UpChar(name=name, star=star, limited=False, zoom=70) + ) + if start_time and end_time: + if start_time <= datetime.now() <= end_time: + self.UP_CHAR = UpEvent( + title=title, + pool_img=char_img, + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.UP_CARD = UpEvent( + title=title, + pool_img=card_img, + start_time=start_time, + end_time=end_time, + up_char=up_cards, + ) + self.dump_up_char() + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}" + ) + except Exception as e: + logger.warning(f"{self.game_name_cn}UP更新出错", e=e) + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_CHAR and self.UP_CARD: + return MessageFactory( + [ + Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}"), + Image(self.UP_CHAR.pool_img), + Image(self.UP_CARD.pool_img), + ] + ) diff --git a/zhenxun/plugins/draw_card/handles/prts_handle.py b/zhenxun/plugins/draw_card/handles/prts_handle.py new file mode 100644 index 00000000..b06fc87c --- /dev/null +++ b/zhenxun/plugins/draw_card/handles/prts_handle.py @@ -0,0 +1,343 @@ +import random +import re +from datetime import datetime +from urllib.parse import unquote + +import dateparser +import ujson as json +from lxml import etree +from lxml.etree import _Element +from nonebot_plugin_saa import Image, MessageFactory, Text +from PIL import ImageDraw +from pydantic import ValidationError + +from zhenxun.services.log import logger +from zhenxun.utils.image_utils import BuildImage + +from ..config import draw_config +from ..util import cn2py, load_font, remove_prohibited_str +from .base_handle import BaseData, BaseHandle, UpChar, UpEvent + + +class Operator(BaseData): + recruit_only: bool # 公招限定 + event_only: bool # 活动获得干员 + core_only: bool # 中坚干员 + # special_only: bool # 升变/异格干员 + + +class PrtsHandle(BaseHandle[Operator]): + def __init__(self): + super().__init__(game_name="prts", game_name_cn="明日方舟") + self.max_star = 6 + self.game_card_color = "#eff2f5" + self.config = draw_config.prts + + self.ALL_OPERATOR: list[Operator] = [] + self.UP_EVENT: UpEvent | None = None + + def get_card(self, add: float) -> Operator: + star = self.get_star( + star_list=[6, 5, 4, 3], + probability_list=[ + self.config.PRTS_SIX_P + add, + self.config.PRTS_FIVE_P, + self.config.PRTS_FOUR_P, + self.config.PRTS_THREE_P, + ], + ) + + all_operators = [ + x + for x in self.ALL_OPERATOR + if x.star == star + and not any([x.limited, x.recruit_only, x.event_only, x.core_only]) + ] + acquire_operator = None + + if self.UP_EVENT: + up_operators = [x for x in self.UP_EVENT.up_char if x.star == star] + # UPs + try: + zooms = [x.zoom for x in up_operators] + zoom_sum = sum(zooms) + if random.random() < zoom_sum: + up_name = random.choices(up_operators, weights=zooms, k=1)[0].name + acquire_operator = [ + x for x in self.ALL_OPERATOR if x.name == up_name + ][0] + except IndexError: + pass + if not acquire_operator: + acquire_operator = random.choice(all_operators) + return acquire_operator + + def get_cards(self, count: int, **kwargs) -> list[tuple[Operator, int]]: + card_list = [] # 获取所有角色 + add = 0.0 + count_idx = 0 + for i in range(count): + count_idx += 1 + card = self.get_card(add) + if card.star == self.max_star: + add = 0.0 + count_idx = 0 + elif count_idx > 50: + add += 0.02 + card_list.append((card, i + 1)) + return card_list + + def format_pool_info(self) -> str: + info = "" + if self.UP_EVENT: + star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6] + star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5] + star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4] + if star6_list: + info += f"六星UP:{' '.join(star6_list)}\n" + if star5_list: + info += f"五星UP:{' '.join(star5_list)}\n" + if star4_list: + info += f"四星UP:{' '.join(star4_list)}\n" + info = f"当前up池: {self.UP_EVENT.title}\n{info}" + return info.strip() + + async def draw(self, count: int, **kwargs) -> MessageFactory: + index2card = self.get_cards(count) + """这里cards修复了抽卡图文不符的bug""" + cards = [card[0] for card in index2card] + up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] + result = self.format_result(index2card, up_list=up_list) + pool_info = self.format_pool_info() + img = await self.generate_img(cards) + return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + + async def generate_card_img(self, card: Operator) -> BuildImage: + sep_w = 5 + sep_h = 5 + star_h = 15 + img_w = 120 + img_h = 120 + font_h = 20 + bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") + star_path = str(self.img_path / "star.png") + star = BuildImage(star_h, star_h, background=star_path) + img_path = str(self.img_path / f"{cn2py(card.name)}.png") + img = BuildImage(img_w, img_h, background=img_path) + await bg.paste(img, (sep_w, sep_h)) + for i in range(card.star): + await bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h)) + # 加名字 + text = card.name[:7] + "..." if len(card.name) > 8 else card.name + font = load_font(fontsize=16) + text_w, text_h = BuildImage.get_text_size(text, font) + draw = ImageDraw.Draw(bg.markImg) + draw.text( + (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2), + text, + font=font, + fill="gray", + ) + return bg + + def _init_data(self): + self.ALL_OPERATOR = [ + Operator( + name=value["名称"], + star=int(value["星级"]), + limited="标准寻访" not in value["获取途径"] + and "中坚寻访" not in value["获取途径"], + recruit_only=( + True + if "标准寻访" not in value["获取途径"] + and "中坚寻访" not in value["获取途径"] + and "公开招募" in value["获取途径"] + else False + ), + event_only=True if "活动获取" in value["获取途径"] else False, + core_only=( + True + if "标准寻访" not in value["获取途径"] + and "中坚寻访" in value["获取途径"] + else False + ), + ) + for key, value in self.load_data().items() + if "阿米娅" not in key + ] + self.load_up_char() + + def load_up_char(self): + try: + data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json") + """这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下""" + if not data: + logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在") + else: + self.UP_EVENT = UpEvent.parse_obj(data.get("char", {})) + except ValidationError: + logger.warning(f"{self.game_name}_up_char 解析出错") + + def dump_up_char(self): + if self.UP_EVENT: + data = {"char": json.loads(self.UP_EVENT.json())} + self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json") + + async def _update_info(self): + """更新信息""" + info = {} + url = "https://wiki.biligame.com/arknights/干员数据表" + result = await self.get_url(url) + if not result: + logger.warning(f"更新 {self.game_name_cn} 出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + char_list: list[_Element] = dom.xpath("//table[@id='CardSelectTr']/tbody/tr") + for char in char_list: + try: + avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0] + name = char.xpath("./td[1]/center/a/text()")[0] + star = char.xpath("./td[2]/text()")[0] + """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因""" + sources = [_.strip("\n") for _ in char.xpath("./td[7]/text()")] + except IndexError: + continue + member_dict = { + "头像": unquote(str(avatar).split(" ")[-2]), + "名称": remove_prohibited_str(str(name).strip()), + "星级": int(str(star).strip()), + "获取途径": sources, + } + info[member_dict["名称"]] = member_dict + self.dump_data(info) + logger.info(f"{self.game_name_cn} 更新成功") + # 下载头像 + for value in info.values(): + await self.download_img(value["头像"], value["名称"]) + # 下载星星 + await self.download_img( + "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png", + "star", + ) + await self.update_up_char() + + async def update_up_char(self): + """重载卡池""" + announcement_url = "https://ak.hypergryph.com/news.html" + result = await self.get_url(announcement_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告出错") + return + dom = etree.HTML(result, etree.HTMLParser()) + activity_urls = dom.xpath( + "//ol[@class='articlelist' and @data-category-key='ACTIVITY']/li/a/@href" + ) + start_time = None + end_time = None + up_chars = [] + pool_img = "" + for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了 + activity_url = f"https://ak.hypergryph.com{activity_url}" + result = await self.get_url(activity_url) + if not result: + logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错") + continue + + """因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败""" + dom = etree.HTML(result, etree.HTMLParser()) + contents = dom.xpath( + "//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src" + ) + title = "" + time = "" + chars: list[str] = [] + for index, content in enumerate(contents): + if re.search("(.*)(寻访|复刻).*?开启", content): + title = re.split(r"[【】]", content) + title = "".join(title[1:-1]) if "-" in title else title[1] + lines = [ + contents[index - 2 + _] for _ in range(8) + ] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前 + lines.append("") # 防止IndexError,加个空字符串 + for idx, line in enumerate(lines): + match = re.search( + r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line + ) + if match: + time = match.group(1) + """因为

的诡异排版,所以有了下面的一段""" + if ("★★" in line and "%" in line) or ( + "★★" in line and "%" in lines[idx + 1] + ): + ( + chars.append(line) + if ("★★" in line and "%" in line) + else chars.append(line + lines[idx + 1]) + ) + if not time: + continue + start, end = ( + time.replace("月", "/").replace("日", " ").split("-")[:2] + ) # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题 + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + pool_img = contents[index - 2] + r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)""" + for char in chars: + star = char.split("(")[0].count("★") + name = ( + re.split(r"[:(]", char)[1] + if "★(" not in char + else re.split("):", char)[1] + ) # 有的括号在前面有的在后面 + dual_up = False + if "\\" in name: + names = name.split("\\") + dual_up = True + elif "/" in name: + names = name.split("/") + dual_up = True + else: + names = [name] # 既有用/分割的,又有用\分割的 + + names = [name.replace("[限定]", "").strip() for name in names] + zoom = 1 + if "权值" in char: + zoom = 0.03 + else: + match = re.search(r"(占.*?的.*?(\d+).*?%)", char) + if dual_up == True: + zoom = float(match.group(1)) / 2 + else: + zoom = float(match.group(1)) + zoom = zoom / 100 if zoom > 1 else zoom + for name in names: + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=zoom) + ) + break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决 + if title and start_time and end_time: + if start_time <= datetime.now() <= end_time: + self.UP_EVENT = UpEvent( + title=title, + pool_img=pool_img, + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.dump_up_char() + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}" + ) + break + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_EVENT: + return MessageFactory( + [ + Text(f"重载成功!\n当前UP池子:{self.UP_EVENT.title}"), + Image(self.UP_EVENT.pool_img), + ] + ) diff --git a/zhenxun/plugins/draw_card/rule.py b/zhenxun/plugins/draw_card/rule.py new file mode 100644 index 00000000..49746d95 --- /dev/null +++ b/zhenxun/plugins/draw_card/rule.py @@ -0,0 +1,10 @@ +from nonebot.internal.rule import Rule + +from zhenxun.configs.config import Config + + +def rule(game) -> Rule: + async def _rule() -> bool: + return Config.get_config("draw_card", game.config_name, True) + + return Rule(_rule) diff --git a/zhenxun/plugins/draw_card/util.py b/zhenxun/plugins/draw_card/util.py new file mode 100644 index 00000000..d0cefc91 --- /dev/null +++ b/zhenxun/plugins/draw_card/util.py @@ -0,0 +1,61 @@ +import platform +from pathlib import Path + +import pypinyin +from PIL import Image, ImageDraw, ImageFont +from PIL.Image import Image as IMG +from PIL.ImageFont import FreeTypeFont + +from zhenxun.configs.path_config import FONT_PATH +from zhenxun.utils._build_image import BuildImage + +dir_path = Path(__file__).parent.absolute() + + +def cn2py(word) -> str: + """保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题""" + temp = "" + for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3): + temp += "".join(i) + return temp + + +# 移除windows和linux下特殊字符 +def remove_prohibited_str(name: str) -> str: + if platform.system().lower() == "windows": + tmp = "" + for i in name: + if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]: + tmp += i + name = tmp + else: + name = name.replace("/", "\\") + return name + + +def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont: + return ImageFont.truetype( + str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8" + ) + + +def circled_number(num: int) -> IMG: + font = load_font(fontsize=450) + text = str(num) + text_w = BuildImage.get_text_size(text, font=font)[0] + w = 240 + text_w + w = w if w >= 500 else 500 + img = Image.new("RGBA", (w, 500)) + draw = ImageDraw.Draw(img) + draw.ellipse(((0, 0), (500, 500)), fill="red") + draw.ellipse(((w - 500, 0), (w, 500)), fill="red") + draw.rectangle(((250, 0), (w - 250, 500)), fill="red") + draw.text( + (120, -60), + text, + font=font, + fill="white", + stroke_width=10, + stroke_fill="white", + ) + return img From d4a49a47e5c040040fc7eb49edbf4bf0d13b86f8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 28 Jul 2024 20:29:03 +0800 Subject: [PATCH 061/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0B?= =?UTF-8?q?=E7=AB=99=E8=BD=AC=E5=8F=91=E8=A7=A3=E6=9E=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 407 ++++++++++++++---- pyproject.toml | 3 +- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 5 +- .../platform/qq/group_handle.py | 4 +- .../superuser/broadcast/_data_source.py | 2 +- zhenxun/plugins/parse_bilibili/__init__.py | 57 +++ zhenxun/plugins/parse_bilibili/data_source.py | 186 ++++++++ 7 files changed, 579 insertions(+), 85 deletions(-) create mode 100644 zhenxun/plugins/parse_bilibili/__init__.py create mode 100644 zhenxun/plugins/parse_bilibili/data_source.py diff --git a/poetry.lock b/poetry.lock index c526dd27..e00d88b1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -216,18 +216,18 @@ reference = "ali" [[package]] name = "arclet-alconna" -version = "1.7.44" +version = "1.8.19" description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." optional = false python-versions = ">=3.8" files = [ - {file = "arclet_alconna-1.7.44-py3-none-any.whl", hash = "sha256:e5751a2aa854b7b2c01cac87986ad11b397986a725c9536d5f9ff81a84e85614"}, - {file = "arclet_alconna-1.7.44.tar.gz", hash = "sha256:9c8a70a3f75e8358fa9c71befd3687c8c9781a19b1d28cb53cbe08fbc36cf720"}, + {file = "arclet_alconna-1.8.19-py3-none-any.whl", hash = "sha256:c78d5527d8ea13990e96f996a3480bf236ad63b81114f53ce2c010bc2a0ee1d8"}, + {file = "arclet_alconna-1.8.19.tar.gz", hash = "sha256:12064caad6854a4b00dc5b7376d86e15911072acd9278531bf86e4fb97568288"}, ] [package.dependencies] -nepattern = ">=0.5.14,<0.6.0" -tarina = ">=0.4.1" +nepattern = ">=0.7.3,<1.0.0" +tarina = ">=0.5.0" typing-extensions = ">=4.5.0" [package.extras] @@ -240,18 +240,18 @@ reference = "ali" [[package]] name = "arclet-alconna-tools" -version = "0.6.11" +version = "0.7.6" description = "Builtin Tools for Alconna" optional = false python-versions = ">=3.8" files = [ - {file = "arclet-alconna-tools-0.6.11.tar.gz", hash = "sha256:079f1ccd84120c65288e50014de2117a0dc7c52e5c2d2d718ad9fd95afb40232"}, - {file = "arclet_alconna_tools-0.6.11-py3-none-any.whl", hash = "sha256:d3bc7d70040fbc1c0b44a9b751089f87c6c487161d7c930dd4018d7ef468d91f"}, + {file = "arclet_alconna_tools-0.7.6-py3-none-any.whl", hash = "sha256:fdd1cb900603ce6bb00295bf7bf7f60dfdb764f0614abe248cdcb754e5149edd"}, + {file = "arclet_alconna_tools-0.7.6.tar.gz", hash = "sha256:7cb7dc54c1c2198529c63227739423401051b8489374f1a7a3efa0c4e70b2a22"}, ] [package.dependencies] -arclet-alconna = ">=1.7.39" -nepattern = ">=0.5.15" +arclet-alconna = ">=1.8.15" +nepattern = ">=0.7.3,<1.0.0" [package.source] type = "legacy" @@ -410,6 +410,31 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "bilireq" +version = "0.2.3.post0" +description = "" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "bilireq-0.2.3.post0-py3-none-any.whl", hash = "sha256:8d1f98bb8fb59c0ce1dec226329353ce51e2efaad0a6b4d240437b6132648322"}, + {file = "bilireq-0.2.3.post0.tar.gz", hash = "sha256:3185c3952a2becc7d31b0c01a12fda463fa477253504a68f81ea871594887ab4"}, +] + +[package.dependencies] +grpcio = ">=1.49.1,<2.0.0" +httpx = ">=0.23.0,<0.24.0" +protobuf = ">=4.21.7,<5.0.0" +rsa = ">=4.9,<5.0" + +[package.extras] +qrcode = ["qrcode[pil] (>=7.3.1,<8.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "binaryornot" version = "0.4.4" @@ -1079,6 +1104,69 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "grpcio" +version = "1.65.1" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio-1.65.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:3dc5f928815b8972fb83b78d8db5039559f39e004ec93ebac316403fe031a062"}, + {file = "grpcio-1.65.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8333ca46053c35484c9f2f7e8d8ec98c1383a8675a449163cea31a2076d93de8"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:7af64838b6e615fff0ec711960ed9b6ee83086edfa8c32670eafb736f169d719"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb64b4166362d9326f7efbf75b1c72106c1aa87f13a8c8b56a1224fac152f5c"}, + {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8422dc13ad93ec8caa2612b5032a2b9cd6421c13ed87f54db4a3a2c93afaf77"}, + {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4effc0562b6c65d4add6a873ca132e46ba5e5a46f07c93502c37a9ae7f043857"}, + {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a6c71575a2fedf259724981fd73a18906513d2f306169c46262a5bae956e6364"}, + {file = "grpcio-1.65.1-cp310-cp310-win32.whl", hash = "sha256:34966cf526ef0ea616e008d40d989463e3db157abb213b2f20c6ce0ae7928875"}, + {file = "grpcio-1.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:ca931de5dd6d9eb94ff19a2c9434b23923bce6f767179fef04dfa991f282eaad"}, + {file = "grpcio-1.65.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bbb46330cc643ecf10bd9bd4ca8e7419a14b6b9dedd05f671c90fb2c813c6037"}, + {file = "grpcio-1.65.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d827a6fb9215b961eb73459ad7977edb9e748b23e3407d21c845d1d8ef6597e5"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:6e71aed8835f8d9fbcb84babc93a9da95955d1685021cceb7089f4f1e717d719"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1c84560b3b2d34695c9ba53ab0264e2802721c530678a8f0a227951f453462"}, + {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27adee2338d697e71143ed147fe286c05810965d5d30ec14dd09c22479bfe48a"}, + {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f62652ddcadc75d0e7aa629e96bb61658f85a993e748333715b4ab667192e4e8"}, + {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:71a05fd814700dd9cb7d9a507f2f6a1ef85866733ccaf557eedacec32d65e4c2"}, + {file = "grpcio-1.65.1-cp311-cp311-win32.whl", hash = "sha256:b590f1ad056294dfaeac0b7e1b71d3d5ace638d8dd1f1147ce4bd13458783ba8"}, + {file = "grpcio-1.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:12e9bdf3b5fd48e5fbe5b3da382ad8f97c08b47969f3cca81dd9b36b86ed39e2"}, + {file = "grpcio-1.65.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:54cb822e177374b318b233e54b6856c692c24cdbd5a3ba5335f18a47396bac8f"}, + {file = "grpcio-1.65.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aaf3c54419a28d45bd1681372029f40e5bfb58e5265e3882eaf21e4a5f81a119"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:557de35bdfbe8bafea0a003dbd0f4da6d89223ac6c4c7549d78e20f92ead95d9"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bfd95ef3b097f0cc86ade54eafefa1c8ed623aa01a26fbbdcd1a3650494dd11"}, + {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e6a8f3d6c41e6b642870afe6cafbaf7b61c57317f9ec66d0efdaf19db992b90"}, + {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1faaf7355ceed07ceaef0b9dcefa4c98daf1dd8840ed75c2de128c3f4a4d859d"}, + {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:60f1f38eed830488ad2a1b11579ef0f345ff16fffdad1d24d9fbc97ba31804ff"}, + {file = "grpcio-1.65.1-cp312-cp312-win32.whl", hash = "sha256:e75acfa52daf5ea0712e8aa82f0003bba964de7ae22c26d208cbd7bc08500177"}, + {file = "grpcio-1.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff5a84907e51924973aa05ed8759210d8cdae7ffcf9e44fd17646cf4a902df59"}, + {file = "grpcio-1.65.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:1fbd6331f18c3acd7e09d17fd840c096f56eaf0ef830fbd50af45ae9dc8dfd83"}, + {file = "grpcio-1.65.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de5b6be29116e094c5ef9d9e4252e7eb143e3d5f6bd6d50a78075553ab4930b0"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e4a3cdba62b2d6aeae6027ae65f350de6dc082b72e6215eccf82628e79efe9ba"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941c4869aa229d88706b78187d60d66aca77fe5c32518b79e3c3e03fc26109a2"}, + {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f40cebe5edb518d78b8131e87cb83b3ee688984de38a232024b9b44e74ee53d3"}, + {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2ca684ba331fb249d8a1ce88db5394e70dbcd96e58d8c4b7e0d7b141a453dce9"}, + {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8558f0083ddaf5de64a59c790bffd7568e353914c0c551eae2955f54ee4b857f"}, + {file = "grpcio-1.65.1-cp38-cp38-win32.whl", hash = "sha256:8d8143a3e3966f85dce6c5cc45387ec36552174ba5712c5dc6fcc0898fb324c0"}, + {file = "grpcio-1.65.1-cp38-cp38-win_amd64.whl", hash = "sha256:76e81a86424d6ca1ce7c16b15bdd6a964a42b40544bf796a48da241fdaf61153"}, + {file = "grpcio-1.65.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb5175f45c980ff418998723ea1b3869cce3766d2ab4e4916fbd3cedbc9d0ed3"}, + {file = "grpcio-1.65.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b12c1aa7b95abe73b3e04e052c8b362655b41c7798da69f1eaf8d186c7d204df"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3019fb50128b21a5e018d89569ffaaaa361680e1346c2f261bb84a91082eb3d3"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ae15275ed98ea267f64ee9ddedf8ecd5306a5b5bb87972a48bfe24af24153e8"}, + {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f096ffb881f37e8d4f958b63c74bfc400c7cebd7a944b027357cd2fb8d91a57"}, + {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2f56b5a68fdcf17a0a1d524bf177218c3c69b3947cb239ea222c6f1867c3ab68"}, + {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:941596d419b9736ab548aa0feb5bbba922f98872668847bf0720b42d1d227b9e"}, + {file = "grpcio-1.65.1-cp39-cp39-win32.whl", hash = "sha256:5fd7337a823b890215f07d429f4f193d24b80d62a5485cf88ee06648591a0c57"}, + {file = "grpcio-1.65.1-cp39-cp39-win_amd64.whl", hash = "sha256:1bceeec568372cbebf554eae1b436b06c2ff24cfaf04afade729fb9035408c6c"}, + {file = "grpcio-1.65.1.tar.gz", hash = "sha256:3c492301988cd720cd145d84e17318d45af342e29ef93141228f9cd73222368b"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.65.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "h11" version = "0.14.0" @@ -1097,24 +1185,24 @@ reference = "ali" [[package]] name = "httpcore" -version = "1.0.2" +version = "0.16.3" description = "A minimal low-level HTTP client." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, + {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, ] [package.dependencies] +anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" +sniffio = "==1.*" [package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.23.0)"] [package.source] type = "legacy" @@ -1176,25 +1264,24 @@ reference = "ali" [[package]] name = "httpx" -version = "0.26.0" +version = "0.23.3" description = "The next generation HTTP client." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, + {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, + {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, ] [package.dependencies] -anyio = "*" certifi = "*" -httpcore = "==1.*" -idna = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1241,6 +1328,30 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "importlib-metadata" +version = "8.2.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, + {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "iso8601" version = "1.1.0" @@ -1747,17 +1858,17 @@ reference = "ali" [[package]] name = "nepattern" -version = "0.5.15" +version = "0.7.6" description = "a complex pattern, support typing" optional = false python-versions = ">=3.8" files = [ - {file = "nepattern-0.5.15-py3-none-any.whl", hash = "sha256:c68fc7c0c9b7835c956a89e0f91fd380b8e07880e183414871e83ef4a9fa0dbd"}, - {file = "nepattern-0.5.15.tar.gz", hash = "sha256:3b04b91b5856b9826b61737933911f570a75ba8116b9e2ff8fa83b4aa0211203"}, + {file = "nepattern-0.7.6-py3-none-any.whl", hash = "sha256:233d0befecc190f228ded3651a85faaf53f1308bba40ab8ddec379d0d3c88051"}, + {file = "nepattern-0.7.6.tar.gz", hash = "sha256:07bd5b2f3b9b9739b703bf723ffd642ca93738a32df7b699d57d6f338d46bad0"}, ] [package.dependencies] -tarina = ">=0.3.3" +tarina = ">=0.5.1" typing-extensions = ">=4.5.0" [package.source] @@ -1846,20 +1957,23 @@ reference = "ali" [[package]] name = "nonebot-plugin-alconna" -version = "0.37.1" +version = "0.50.2" description = "Alconna Adapter for Nonebot" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "nonebot_plugin_alconna-0.37.1-py3-none-any.whl", hash = "sha256:fcc46f04ac89bf43730afebd97fa46e5910bc404a9e24cab7950da58be36246d"}, - {file = "nonebot_plugin_alconna-0.37.1.tar.gz", hash = "sha256:5e9989ee7debd79d61c97aa41c88aac5fe452cc9c47f2d48b829d81d26dfe130"}, + {file = "nonebot_plugin_alconna-0.50.2-py3-none-any.whl", hash = "sha256:be641eaf539f6f9dfb2398be80e994fa27814064eeed89e7a46a03754756dfc1"}, + {file = "nonebot_plugin_alconna-0.50.2.tar.gz", hash = "sha256:ebae23723cee5cbbc350aa864d9e3d95cb1ab8324ba8674130df3302066277b1"}, ] [package.dependencies] -arclet-alconna = ">=1.7.44" -arclet-alconna-tools = ">=0.6.11" -nepattern = ">=0.5.15" -nonebot2 = ">=2.2.0" +arclet-alconna = ">=1.8.19" +arclet-alconna-tools = ">=0.7.6" +importlib-metadata = ">=4.13.0" +nepattern = ">=0.7.4" +nonebot-plugin-waiter = ">=0.6.0" +nonebot2 = ">=2.3.0" +tarina = ">=0.5.4" [package.source] type = "legacy" @@ -1977,15 +2091,34 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "nonebot-plugin-waiter" +version = "0.7.1" +description = "An alternative for got-and-reject in Nonebot" +optional = false +python-versions = ">=3.9" +files = [ + {file = "nonebot_plugin_waiter-0.7.1-py3-none-any.whl", hash = "sha256:b9967cc7aeea0db86053ada20929841830aea60bb8c7da26d0483eefda75635c"}, + {file = "nonebot_plugin_waiter-0.7.1.tar.gz", hash = "sha256:8be2adc175e45ca794881e3df449302b8e6e045cd9bae97a809907f4200b4110"}, +] + +[package.dependencies] +nonebot2 = ">=2.3.0" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "nonebot2" -version = "2.2.0" +version = "2.3.2" description = "An asynchronous python bot framework." optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.9,<4.0" files = [ - {file = "nonebot2-2.2.0-py3-none-any.whl", hash = "sha256:447fa63d384414c0e610f4ce6d2b3999db81ac2becd8d86716c4117013dc032f"}, - {file = "nonebot2-2.2.0.tar.gz", hash = "sha256:138800846fa3dc635bda9f2ddc589519ee8d9d3b401013fbb95e47676fc830fb"}, + {file = "nonebot2-2.3.2-py3-none-any.whl", hash = "sha256:c51aa3c1f23d8062ce6d13c8423dcb9a8bf0c44f21687916095f825da79a9a55"}, + {file = "nonebot2-2.3.2.tar.gz", hash = "sha256:af52e27e03e7fe147f2b642151eec81f264d058efe53b974eb08b5d90177cd14"}, ] [package.dependencies] @@ -2306,6 +2439,31 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "protobuf" +version = "4.25.4" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4"}, + {file = "protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d"}, + {file = "protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040"}, + {file = "protobuf-4.25.4-cp38-cp38-win32.whl", hash = "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1"}, + {file = "protobuf-4.25.4-cp38-cp38-win_amd64.whl", hash = "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1"}, + {file = "protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca"}, + {file = "protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f"}, + {file = "protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978"}, + {file = "protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "psutil" version = "5.9.8" @@ -2339,6 +2497,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pyasn1" +version = "0.6.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pydantic" version = "1.10.14" @@ -2873,6 +3047,28 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = "*" +files = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "rich" version = "13.7.0" @@ -2896,6 +3092,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "ruamel-yaml" version = "0.18.5" @@ -3159,54 +3374,66 @@ reference = "ali" [[package]] name = "tarina" -version = "0.4.2" +version = "0.5.4" description = "A collection of common utils for Arclet" optional = false python-versions = ">=3.8" files = [ - {file = "tarina-0.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c46b2b827a4d14f521c5f323e1cb8ed5350d3d9bf8e7828100265903526b9907"}, - {file = "tarina-0.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc78a3c653fdea3f2ae642584a6a55cf26856b4858875068a7cfca92b13bca6b"}, - {file = "tarina-0.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1943cabd1707e52b1bfc478c33c48c04d6c0d3ef9425ad808265d7965142c3b"}, - {file = "tarina-0.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872507bc155412ab71f202a9b28ee170bd395c7cf8dbee63bfe78845265717a2"}, - {file = "tarina-0.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61aab6935092373fd53565ec7ba894ff9567e4620535a26362aeb66826f6d0d7"}, - {file = "tarina-0.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5ae2ccd7aa409d33ea14944533b93f15cee71a1a7f4547f0cfef1ad6153ed142"}, - {file = "tarina-0.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ee72b6a55215ad6acc1d50fbd227d972138adacc9f7c6a3f1c63080780d968ed"}, - {file = "tarina-0.4.2-cp310-cp310-win32.whl", hash = "sha256:4f417bb80c18d5f87f27bcb1e70d5eaae125578440c6bbe4c5275c6c633a7475"}, - {file = "tarina-0.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:e54de934c6a754e27daf64a04d6ca287303f7b7e6f8ecee6cb8162577ffc2a6c"}, - {file = "tarina-0.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:30ec63fce2556f9e63d8d774a74ef688d9c46dd04f198d3b9a653cd5c539fd5e"}, - {file = "tarina-0.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03718dd5630daa7ee84f31ae258c979f168f4b58149a0647af706bff64268321"}, - {file = "tarina-0.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d49030aefd8486fdb5b91e72c188451de216fb71c636cbd33a7eee5e6c73daf"}, - {file = "tarina-0.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ad962153c9f63ad1e89acd3d9bbbcee21b5ea07ee0e8dac06c6d2471ee8337d"}, - {file = "tarina-0.4.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2a8a9bfd4a9a5907c2c64e163c374eaed24b1e0df3fe9d0bd7d52a9f730daa"}, - {file = "tarina-0.4.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:50a2bea62c32cf9a6cd89f7eb1b2a1a5a5ca3b7533960abdcc77956121267de1"}, - {file = "tarina-0.4.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c6c16b01783797cbd12eeef042a7ed96a5531b01b931e50ac5afb3d9d4d1634"}, - {file = "tarina-0.4.2-cp311-cp311-win32.whl", hash = "sha256:c4b54ebdfeb63f9a60ca4c70014784eb50c00ad892e9ad8211527e2d57108abb"}, - {file = "tarina-0.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b613afb8e3381f64e2ede5ad17b7c013515f601b8ce335dead5403aa5ac58281"}, - {file = "tarina-0.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a95da4a609cdf7e888e1688c0d332acf9c62633f81a40cf851ce3649c3fb9283"}, - {file = "tarina-0.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a3cb636ec94d5f7a6bb7a7bfc451e484c6e8ea7bc04144eb4662d692923ca8ca"}, - {file = "tarina-0.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b1f4a76784f47a89e7e2bb44136fe8e07a636f50fbc4b24f0d8da63cc28ebf78"}, - {file = "tarina-0.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45348f4ecfa21b84c103ef021b161020637c7a6aa02e02440fbe3418cc4a5654"}, - {file = "tarina-0.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1848011cf5707aad0899236bcc972f86ef5554ec4799a8ddf15ef53aea784ff"}, - {file = "tarina-0.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:494ccf76d4428de18c59464f5a9e1c8b6d0bf598941a8b0d9f407729f81e0a95"}, - {file = "tarina-0.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0086c91358536549f61a6ac24861764ef1a607a91bd82a2dbd574392de26dff0"}, - {file = "tarina-0.4.2-cp38-cp38-win32.whl", hash = "sha256:c30618a7c8586719ff46c12b653d52e969c0a5105698f3fdbff8e5293b135b63"}, - {file = "tarina-0.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:6b7b3bff3cbf0901f55c87739af297fd6692f53d3d3a55d0de12e7e41e4e7ff4"}, - {file = "tarina-0.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af75e381a0ae7481741f37221fa721816d78c6e34791ac14a29d41f148bfae49"}, - {file = "tarina-0.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:938fe8e9fe950eb4ab1824f72d55973cae98aaeacba1bf6ce2f6e5729f9b8555"}, - {file = "tarina-0.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46737007a43cfef9ba20409230beb49a6754de498930eb1105edf0687ce4d6d8"}, - {file = "tarina-0.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbbbdd2aee142d3c0888ebce2481ad15e4d505c040fee1b9c7809bf3c9e83649"}, - {file = "tarina-0.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4781c783266e2d4eccc975ba79e1e0b4b465d267e616b5b44f32981a289a7a"}, - {file = "tarina-0.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01d1b3b0e0079e5f372111b149815d70d4a063a83f1a656d6acd91e67fa9c862"}, - {file = "tarina-0.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d493c5492a27964aa4f8fa65ac6eab172624b8d25718b1a5e2261102ac363b26"}, - {file = "tarina-0.4.2-cp39-cp39-win32.whl", hash = "sha256:df510f8d8a2cb5e684f2412d98a3f2654986e40c2a697be686f0b27f51ea7c36"}, - {file = "tarina-0.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:646f74e7426acf2644461a2077b72677d964b5c064f6cb2df8c60c77580e1f89"}, - {file = "tarina-0.4.2-py3-none-any.whl", hash = "sha256:08650c08b1950e7346b13f20ff695d3d3d0d7a9d240521a7544c433ba326a736"}, - {file = "tarina-0.4.2.tar.gz", hash = "sha256:a719c31c1e65c5fb68c12fbacffe802f160678d2e6a9745d1241b924e9283791"}, + {file = "tarina-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49f20a447866ecc831acc82f09dec01f77a0ca1f89b12fa27268bccd29378449"}, + {file = "tarina-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5b24b5c07dc02c006d80930028e1c5f46945bf55effbeeaa426d5ac8f46eff88"}, + {file = "tarina-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed8fe5a1df3b32e69f99f5ae6615dc8c2e34459c7e7f828bbeadefb4ecd4fe4f"}, + {file = "tarina-0.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab6fac674c408bff3161a27473951df8994b54fff406680814079c9c0b82f804"}, + {file = "tarina-0.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfabcce37425aaf5db604ad916c9b69350174afcdb98192c6dbf1fc0cda2183f"}, + {file = "tarina-0.5.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:18900dc94388da4d322c56292cdab6a62da46d27ab5db30ed8809caab57c3502"}, + {file = "tarina-0.5.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b3f8b69949c85bb3cf5b27985961ba0c26e4359a42352f7d5870f6d455f4890"}, + {file = "tarina-0.5.4-cp310-cp310-win32.whl", hash = "sha256:8e4389a6147460b6ea6a795f21a6348190ca2fe0eb95faafb3120bb0d4de7033"}, + {file = "tarina-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:042bdbaac389334ab9c0851a5f1972dc9ed5c0387b4bcdee3ba1b2223aadb39f"}, + {file = "tarina-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:08964a6daa02d992be4b4bf2ace99c94549350195a749198f2d422221e93cc9f"}, + {file = "tarina-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81635455a307d65440c20645923041c8815c50dfeac046b64b64fd7840b7c30"}, + {file = "tarina-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f20ce1ecc06362bbfd7ca30b1dc19c3a049f69b7dc6061df95a0bf93ce627055"}, + {file = "tarina-0.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:539d239b35af0052be9cc7eeb3675c84b02a4b98c3d8ec51dbe7db2e9e5da92a"}, + {file = "tarina-0.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d8e9da2d450cdd93ac9a11af1ff02b6c9a305aa477cbada0d397c5b0b64e3"}, + {file = "tarina-0.5.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:15a2ac416e972b0318c53f20c3478d77fb770dfa9ab25ab43aa8975886ecb160"}, + {file = "tarina-0.5.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af522dc1ad30d7bcbbf9384f4f3aede3bebd7cecfc7127148ae0d12bd69b65d9"}, + {file = "tarina-0.5.4-cp311-cp311-win32.whl", hash = "sha256:781b1df4250e8f8f0b7902f3b7952135cbf43284e2cf490f57b738160d74b56f"}, + {file = "tarina-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:9d32bab544e7c74e56958b0ebcd430a80194492ca6e98ed2f6217708fabc4027"}, + {file = "tarina-0.5.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:95b1504e4241a28fe75fa0995ebfed1dad140381ad72541e5b69428c84d16735"}, + {file = "tarina-0.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9bbaefb3a627fcefc868d455cdc5d42297ba48369651821b04d8c8836307c39f"}, + {file = "tarina-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7cfec7c6a725bebb46b4e4a8ed64523c6deeae94dba1d3102b866c0247a32cdb"}, + {file = "tarina-0.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fccfd98ca925ec3597ca88f359f608f7762ad13a14dffcb17742b1e78e071306"}, + {file = "tarina-0.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bef0dfa5007f5138f48cbb9c2ef9564579def00b75caf47ebf53d32db7bf4044"}, + {file = "tarina-0.5.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8e6e2f0580d8dd956f92313ff51760df6893cd16fc009cdc2607130463d08bbb"}, + {file = "tarina-0.5.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:82f09edcf58b2e02622b173822c31c0ad5685f3e36667bd9de751f8c16b5305f"}, + {file = "tarina-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b56956862d70f0383973d8413ed0fca9623e930acea0d7bf11a67c79714b869f"}, + {file = "tarina-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:3ee6dafc31cceae46634314db0b547052790015abaec433ff39fef5bf5b3f0f6"}, + {file = "tarina-0.5.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2db7a3c9061ff6b8ba4ad3536850ac39ecc15b01bc41d6ee50468c8a8f06519c"}, + {file = "tarina-0.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a62297950d1448adaa3cc8ffb9ef1d076e1f51da07862f0205d660914cbee15"}, + {file = "tarina-0.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3b52205781e8b7dfc94ac90f6433a55e8025872b8ceb3bc0498ae2ba3e8b8cfb"}, + {file = "tarina-0.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17d95f0eb66785ef845b0f9567c738e2323f3e6ed56cf82b7c28ab9314dd7896"}, + {file = "tarina-0.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff02d7ae7718e48290dea13287c554928c09ea7859e3e0cf5bff91d031ad5b2"}, + {file = "tarina-0.5.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:13bc48018b78f2fa2707ae5dda3c38e482fdb38e911c38ac1c7208593b58c8a2"}, + {file = "tarina-0.5.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0dbf855e6f31397422cccd3816c2ffdc613fa746c0ff064730676eb8c59eb5a"}, + {file = "tarina-0.5.4-cp38-cp38-win32.whl", hash = "sha256:99767cdc271e35edb401c772c87e2dba9b24f93803a51d0979ef0c113aafb0e0"}, + {file = "tarina-0.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:64abd0da7146430c9dbce9a659861f09f03a0eecb4c65f42a6ac1c347961c534"}, + {file = "tarina-0.5.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aa01c6032226f996286d60bd7b3bfb95565e9288e89b64208649b584386cfd9e"}, + {file = "tarina-0.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c93781dfcf0c95c7e12c29fa788a32898aa090ba26bef9b1c970412b8cb7f59"}, + {file = "tarina-0.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b61ab72169c2289001a047694dbf6e0e73ed0b1c5405f65651b2500190928d43"}, + {file = "tarina-0.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3b9ee386d0a8558c9270ae2f4fd33ff2394482705a2849646aad3df870cf754"}, + {file = "tarina-0.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d6937a4911e5b7bf1f5a4bcc466e2cce3b1576eb6462459e568668f63a073f"}, + {file = "tarina-0.5.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d4d332b30374b2d8fec2852d6af77f121c0fb026c48593cebdfbed6d49c2b260"}, + {file = "tarina-0.5.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:077b99101ee19699c8791f2630ed7c40c592e5d75ab309a042f5303d89f382c6"}, + {file = "tarina-0.5.4-cp39-cp39-win32.whl", hash = "sha256:a553a8790215ecd6f1af2616769012f16e28eaae0b805ddc780fe543ec2a6a4b"}, + {file = "tarina-0.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:5c75b66d011cb7dd78149bf3911a78eaa96885dab4477fd4a96613349411f378"}, + {file = "tarina-0.5.4-py3-none-any.whl", hash = "sha256:1aa7d5c00e4bb6a35c5fd21bcbc536670df755922cd49bd9076a024fea191ade"}, + {file = "tarina-0.5.4.tar.gz", hash = "sha256:5d192a50d47b22ae8ca79e50ee760f171e563135eb04dc834a9b254211dbf32e"}, ] [package.dependencies] typing-extensions = ">=4.4.0" +[package.extras] +yaml = ["pyyaml (>=6.0.1)"] + [package.source] type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" @@ -3887,7 +4114,27 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "8259b57e9de269479dfb70be17e3060fedc0426ae22abf3317448e50edba9b23" +content-hash = "3a23c97b954472fca2124960e82ba695c52f3448bec4458df94b3d884d23c1e4" diff --git a/pyproject.toml b/pyproject.toml index a1b297e1..cbe43480 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ retrying = "^1.3.4" aiofiles = "^23.2.1" nonebot-plugin-htmlrender = "^0.3.0" nonebot-plugin-userinfo = "^0.1.3" -nonebot-plugin-alconna = "^0.37.1" pypinyin = "^0.51.0" beautifulsoup4 = "^4.12.3" lxml = "^5.1.0" @@ -44,6 +43,8 @@ black = "^24.4.2" cn2an = "^0.5.22" aiohttp = "^3.9.5" dateparser = "^1.2.0" +nonebot-plugin-alconna = "^0.50.2" +bilireq = "0.2.3post0" [tool.poetry.dev-dependencies] diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index f64b740e..610d90e2 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -78,7 +78,10 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): if user_id: command = state["_prefix"]["raw_command"] if state.get("_alc_result"): - command = state["_alc_result"].source.command + try: + command = state["_alc_result"].source.command + except AttributeError: + pass if command: if _blmt.check(f"{user_id}__{command}"): await BanConsole.ban( diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index 141b58bc..ad2d2ec9 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -225,7 +225,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent img_file = path / f"{i}.png" if img_file.exists(): msg_list.append(Image(img_file)) - if GroupConsole.is_block_task(group_id, "group_welcome"): + if not GroupConsole.is_block_task(group_id, "group_welcome"): logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id) if msg_list: await MessageFactory(msg_list).send() @@ -293,5 +293,5 @@ async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent ) operator_name = operator["card"] if operator["card"] else operator["nickname"] result = f"{user_name} 被 {operator_name} 送走了." - if GroupConsole.is_block_task(str(event.group_id), "refund_group_remind"): + if not GroupConsole.is_block_task(str(event.group_id), "refund_group_remind"): await group_decrease_handle.send(f"{result}") diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 83868088..92d02d70 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -45,7 +45,7 @@ class BroadcastManage: group.group_id, "broadcast", group.channel_id ): target = PlatformUtils.get_target( - bot, None, group.group_id, group.channel_id + bot, None, group.channel_id or group.group_id ) if target: await MessageFactory(message_list).send_to(target, bot) diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py new file mode 100644 index 00000000..3d16396a --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -0,0 +1,57 @@ +from nonebot import on_message +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger + +from .data_source import Parser + +__plugin_meta__ = PluginMetadata( + name="B站转发解析", + description="B站转发解析", + usage=""" + usage: + B站转发解析,解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv,且30秒内内不解析相同url + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + configs=[ + RegisterConfig( + module="_task", + key="DEFAULT_BILIBILI_PARSE", + value=True, + default_value=True, + help="被动 B站转发解析 进群默认开关状态", + type=bool, + ) + ], + tasks=[Task(module="bilibili_parse", name="b站转发解析")], + ).dict(), +) + + +async def _rule(session: EventSession) -> bool: + task = await TaskInfo.get_or_none(module="bilibili_parse") + if not task or not task.status: + logger.debug("b站转发解析被动全局关闭,已跳过...") + return False + if gid := session.id3 or session.id2: + return not await GroupConsole.is_block_task(gid, "bilibili_parse") + return False + + +_matcher = on_message(priority=1, block=False, rule=_rule) + + +@_matcher.handle() +async def _(session: EventSession, message: UniMsg): + data = message[0] + if result := await Parser.parse(data, message.extract_plain_text().strip()): + await result.send() + logger.info(f"b站转发解析: {result}", "BILIBILI_PARSE", session=session) diff --git a/zhenxun/plugins/parse_bilibili/data_source.py b/zhenxun/plugins/parse_bilibili/data_source.py new file mode 100644 index 00000000..d130b3b0 --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/data_source.py @@ -0,0 +1,186 @@ +import re +import time +import uuid +from pathlib import Path +from typing import Any + +import aiohttp +import ujson as json +from bilireq import video +from nonebot_plugin_alconna import Hyper +from nonebot_plugin_saa import Image, MessageFactory, Text + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncPlaywright +from zhenxun.utils.user_agent import get_user_agent + + +class Parser: + + time_watch: dict[str, float] = {} + + @classmethod + async def parse(cls, data: Any, raw: str | None = None) -> MessageFactory | None: + """解析 + + 参数: + data: data数据 + raw: 文本. + + 返回: + MessageFactory | None: 返回信息 + """ + if isinstance(data, Hyper) and data.raw: + json_data = json.loads(data.raw) + if video_info := await cls.__parse_video_share(json_data): + return await cls.__handle_video_info(video_info) + if path := await cls.__parse_news_share(json_data): + return MessageFactory([Image(path)]) + if raw: + return await cls.__search(raw) + return None + + @classmethod + async def __search(cls, message: str) -> MessageFactory | None: + """根据bv,av,链接获取视频信息 + + 参数: + message: 文本内容 + + 返回: + MessageFactory | None: 返回信息 + """ + if "BV" in message: + index = message.find("BV") + if len(message[index + 2 :]) >= 10: + msg = message[index : index + 12] + url = f"https://www.bilibili.com/video/{msg}" + return await cls.__handle_video_info( + await video.get_video_base_info(msg), url + ) + elif "av" in message: + index = message.find("av") + if len(message[index + 2 :]) >= 1: + if r := re.search(r"av(\d+)", message): + url = f"https://www.bilibili.com/video/av{r.group(1)}" + return await cls.__handle_video_info( + await video.get_video_base_info(f"av{r.group(1)}"), url + ) + elif "https://b23.tv" in message: + url = ( + "https://" + + message[message.find("b23.tv") : message.find("b23.tv") + 14] + ) + async with aiohttp.ClientSession(headers=get_user_agent()) as session: + async with session.get( + url, + timeout=7, + ) as response: + url = (str(response.url).split("?")[0]).strip("/") + bvid = url.split("/")[-1] + return await cls.__handle_video_info( + await video.get_video_base_info(bvid), url + ) + return None + + @classmethod + async def __handle_video_info( + cls, vd_info: dict, url: str = "" + ) -> MessageFactory | None: + """处理视频信息 + + 参数: + vd_info: 视频数据 + url: 视频url. + + 返回: + MessageFactory | None: 返回信息 + """ + if url: + if url in cls.time_watch.keys() and time.time() - cls.time_watch[url] < 30: + logger.debug("b站 url 解析在30秒内重复, 跳过解析...") + return None + cls.time_watch[url] = time.time() + aid = vd_info["aid"] + title = vd_info["title"] + author = vd_info["owner"]["name"] + reply = vd_info["stat"]["reply"] # 回复 + favorite = vd_info["stat"]["favorite"] # 收藏 + coin = vd_info["stat"]["coin"] # 投币 + # like = vd_info['stat']['like'] # 点赞 + # danmu = vd_info['stat']['danmaku'] # 弹幕 + date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) + return MessageFactory( + [ + Image(vd_info["pic"]), + Text( + f"\nav{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n{url}" + ), + ] + ) + + @classmethod + async def __parse_video_share(cls, data: dict) -> dict | None: + """解析视频转发 + + 参数: + data: data数据 + + 返回: + dict | None: 视频信息 + """ + try: + if data["meta"]["detail_1"]["title"] == "哔哩哔哩": + try: + async with aiohttp.ClientSession( + headers=get_user_agent() + ) as session: + async with session.get( + data["meta"]["detail_1"]["qqdocurl"], + timeout=7, + ) as response: + url = str(response.url).split("?")[0] + if url[-1] == "/": + url = url[:-1] + bvid = url.split("/")[-1] + return await video.get_video_base_info(bvid) + except Exception as e: + logger.warning("解析b站视频失败", e=e) + except Exception as e: + pass + return None + + @classmethod + async def __parse_news_share(cls, data: dict) -> Path | None: + """解析b站专栏 + + 参数: + data: data数据 + + 返回: + Path | None: 截图路径 + """ + try: + if data["meta"]["news"]["desc"] == "哔哩哔哩专栏": + try: + url = data["meta"]["news"]["jumpUrl"] + async with AsyncPlaywright.new_page() as page: + await page.goto(url, wait_until="networkidle", timeout=10000) + await page.set_viewport_size({"width": 2560, "height": 1080}) + try: + await page.locator("div.bili-mini-close-icon").click() + except Exception: + pass + if div := await page.query_selector("#app > div"): + path = TEMP_PATH / f"bl_share_{uuid.uuid1()}.png" + await div.screenshot( + path=path, + timeout=100000, + ) + return path + except Exception as e: + logger.warning("解析b站专栏失败", e=e) + except Exception as e: + pass + return None From c219264968aece2ce1d510d9ccb1eb8cef4d765e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 29 Jul 2024 23:31:11 +0800 Subject: [PATCH 062/132] =?UTF-8?q?feat=E2=9C=A8:=20=E4=BC=98=E5=8C=96b?= =?UTF-8?q?=E7=AB=99=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtin_plugins/sign_in/_data_source.py | 8 +- .../plugins/draw_card/handles/ba_handle.py | 10 +- zhenxun/plugins/parse_bilibili/__init__.py | 140 ++++++++++++- zhenxun/plugins/parse_bilibili/data_source.py | 186 ------------------ zhenxun/plugins/parse_bilibili/get_image.py | 107 ++++++++++ .../parse_bilibili/information_container.py | 60 ++++++ zhenxun/plugins/parse_bilibili/parse_url.py | 65 ++++++ zhenxun/utils/image_utils.py | 12 ++ 8 files changed, 391 insertions(+), 197 deletions(-) delete mode 100644 zhenxun/plugins/parse_bilibili/data_source.py create mode 100644 zhenxun/plugins/parse_bilibili/get_image.py create mode 100644 zhenxun/plugins/parse_bilibili/information_container.py create mode 100644 zhenxun/plugins/parse_bilibili/parse_url.py diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index c5d41de8..fea99e1c 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -1,4 +1,3 @@ -import os import random import secrets from datetime import datetime @@ -103,8 +102,13 @@ class SignManage: new_log = ( await SignLog.filter(user_id=session.id1).order_by("-create_time").first() ) + log_time = None + if new_log: + log_time = new_log.create_time.astimezone( + pytz.timezone("Asia/Shanghai") + ).date() if not is_card_view: - if not new_log or (new_log and new_log.create_time.date() != now.date()): + if not new_log or (log_time and log_time != now.date()): return await cls._handle_sign_in(user, nickname, session) return await get_card( user, nickname, -1, user_console.gold, "", is_card_view=is_card_view diff --git a/zhenxun/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py index a5d50743..61b0d78f 100644 --- a/zhenxun/plugins/draw_card/handles/ba_handle.py +++ b/zhenxun/plugins/draw_card/handles/ba_handle.py @@ -110,7 +110,7 @@ class BaHandle(BaseHandle[BaChar]): async def _update_info(self): # TODO: ba获取链接失效 info = {} - url = "https://lonqie.github.io/SchaleDB/data/cn/students.min.json?v=49" + url = "https://schale.gg/data/cn/students.min.json?v=49" result = (await AsyncHttpx.get(url)).json() if not result: logger.warning(f"更新 {self.game_name_cn} 出错") @@ -119,12 +119,14 @@ class BaHandle(BaseHandle[BaChar]): for char in result: try: name = char["Name"] + id = str(char["Id"]) avatar = ( - "https://github.com/lonqie/SchaleDB/raw/main/images/student/icon/" - + char["CollectionTexture"] - + ".png" + "https://github.com/SchaleDB/SchaleDB/raw/main/images/student/icon/" + + id + + ".webp" ) star = char["StarGrade"] + star = char["StarGrade"] except IndexError: continue member_dict = { diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index 3d16396a..bcc09772 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -1,14 +1,22 @@ +import re +import time + +import ujson as json from nonebot import on_message from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_alconna import Hyper, UniMsg +from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession +from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.models.group_console import GroupConsole from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncHttpx -from .data_source import Parser +from .information_container import InformationContainer +from .parse_url import parse_bili_url __plugin_meta__ = PluginMetadata( name="B站转发解析", @@ -48,10 +56,132 @@ async def _rule(session: EventSession) -> bool: _matcher = on_message(priority=1, block=False, rule=_rule) +_tmp = {} + @_matcher.handle() async def _(session: EventSession, message: UniMsg): + information_container = InformationContainer() + # 判断文本消息内容是否相关 + match = None + # 判断文本消息和小程序的内容是否指向一个b站链接 + get_url = None + # 判断文本消息是否包含视频相关内容 + vd_flag = False + # 设定时间阈值,阈值之下不会解析重复内容 + repet_second = 300 + # 尝试解析小程序消息 data = message[0] - if result := await Parser.parse(data, message.extract_plain_text().strip()): - await result.send() - logger.info(f"b站转发解析: {result}", "BILIBILI_PARSE", session=session) + if isinstance(data, Hyper) and data.raw: + try: + data = json.loads(data.raw) + except (IndexError, KeyError): + data = None + if data: + # 获取相关数据 + meta_data = data.get("meta", {}) + news_value = meta_data.get("news", {}) + detail_1_value = meta_data.get("detail_1", {}) + qqdocurl_value = detail_1_value.get("qqdocurl", {}) + jumpUrl_value = news_value.get("jumpUrl", {}) + get_url = (qqdocurl_value if qqdocurl_value else jumpUrl_value).split("?")[ + 0 + ] + # 解析文本消息 + elif msg := message.extract_plain_text(): + # 消息中含有视频号 + if "bv" in msg.lower() or "av" in msg.lower(): + match = re.search(r"((?=(?:bv|av))([A-Za-z0-9]+))", msg, re.IGNORECASE) + vd_flag = True + + # 消息中含有b23的链接,包括视频、专栏、动态、直播 + elif "https://b23.tv" in msg: + match = re.search(r"https://b23\.tv/[^?\s]+", msg, re.IGNORECASE) + + # 检查消息中是否含有直播、专栏、动态链接 + elif any( + keyword in msg + for keyword in [ + "https://live.bilibili.com/", + "https://www.bilibili.com/read/", + "https://www.bilibili.com/opus/", + "https://t.bilibili.com/", + ] + ): + pattern = r"https://(live|www\.bilibili\.com/read|www\.bilibili\.com/opus|t\.bilibili\.com)/[^?\s]+" + match = re.search(pattern, msg) + + # 匹配成功,则获取链接 + if match: + if vd_flag: + number = match.group(1) + get_url = f"https://www.bilibili.com/video/{number}" + else: + get_url = match.group() + + if get_url: + # 将链接统一发送给处理函数 + vd_info, live_info, vd_url, live_url, image_info, image_url = ( + await parse_bili_url(get_url, information_container) + ) + if vd_info: + # 判断一定时间内是否解析重复内容,或者是第一次解析 + if ( + vd_url in _tmp.keys() and time.time() - _tmp[vd_url] > repet_second + ) or vd_url not in _tmp.keys(): + pic = vd_info.get("pic", "") # 封面 + aid = vd_info.get("aid", "") # av号 + title = vd_info.get("title", "") # 标题 + author = vd_info.get("owner", {}).get("name", "") # UP主 + reply = vd_info.get("stat", {}).get("reply", "") # 回复 + favorite = vd_info.get("stat", {}).get("favorite", "") # 收藏 + coin = vd_info.get("stat", {}).get("coin", "") # 投币 + like = vd_info.get("stat", {}).get("like", "") # 点赞 + danmuku = vd_info.get("stat", {}).get("danmaku", "") # 弹幕 + ctime = vd_info["ctime"] + date = time.strftime("%Y-%m-%d", time.localtime(ctime)) + logger.info(f"解析bilibili转发 {vd_url}", "b站解析", session=session) + _tmp[vd_url] = time.time() + _path = TEMP_PATH / f"{aid}.jpg" + await AsyncHttpx.download_file(pic, _path) + await MessageFactory( + [ + Image(_path), + Text( + f"av{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n点赞:{like},弹幕:{danmuku}\n{vd_url}" + ), + ] + ).send() + + elif live_info: + if ( + live_url in _tmp.keys() and time.time() - _tmp[live_url] > repet_second + ) or live_url not in _tmp.keys(): + uid = live_info.get("uid", "") # 主播uid + title = live_info.get("title", "") # 直播间标题 + description = live_info.get("description", "") # 简介,可能会出现标签 + user_cover = live_info.get("user_cover", "") # 封面 + keyframe = live_info.get("keyframe", "") # 关键帧画面 + live_time = live_info.get("live_time", "") # 开播时间 + area_name = live_info.get("area_name", "") # 分区 + parent_area_name = live_info.get("parent_area_name", "") # 父分区 + logger.info(f"解析bilibili转发 {live_url}", "b站解析", session=session) + _tmp[live_url] = time.time() + await MessageFactory( + [ + Image(user_cover), + Text( + f"开播用户:https://space.bilibili.com/{uid}\n开播时间:{live_time}\n直播分区:{parent_area_name}——>{area_name}\n标题:{title}\n简介:{description}\n直播截图:\n" + ), + Image(keyframe), + Text(f"{live_url}"), + ] + ).send() + elif image_info: + if ( + image_url in _tmp.keys() + and time.time() - _tmp[image_url] > repet_second + ) or image_url not in _tmp.keys(): + logger.info(f"解析bilibili转发 {image_url}", "b站解析", session=session) + _tmp[image_url] = time.time() + await image_info.send() diff --git a/zhenxun/plugins/parse_bilibili/data_source.py b/zhenxun/plugins/parse_bilibili/data_source.py deleted file mode 100644 index d130b3b0..00000000 --- a/zhenxun/plugins/parse_bilibili/data_source.py +++ /dev/null @@ -1,186 +0,0 @@ -import re -import time -import uuid -from pathlib import Path -from typing import Any - -import aiohttp -import ujson as json -from bilireq import video -from nonebot_plugin_alconna import Hyper -from nonebot_plugin_saa import Image, MessageFactory, Text - -from zhenxun.configs.path_config import TEMP_PATH -from zhenxun.services.log import logger -from zhenxun.utils.http_utils import AsyncPlaywright -from zhenxun.utils.user_agent import get_user_agent - - -class Parser: - - time_watch: dict[str, float] = {} - - @classmethod - async def parse(cls, data: Any, raw: str | None = None) -> MessageFactory | None: - """解析 - - 参数: - data: data数据 - raw: 文本. - - 返回: - MessageFactory | None: 返回信息 - """ - if isinstance(data, Hyper) and data.raw: - json_data = json.loads(data.raw) - if video_info := await cls.__parse_video_share(json_data): - return await cls.__handle_video_info(video_info) - if path := await cls.__parse_news_share(json_data): - return MessageFactory([Image(path)]) - if raw: - return await cls.__search(raw) - return None - - @classmethod - async def __search(cls, message: str) -> MessageFactory | None: - """根据bv,av,链接获取视频信息 - - 参数: - message: 文本内容 - - 返回: - MessageFactory | None: 返回信息 - """ - if "BV" in message: - index = message.find("BV") - if len(message[index + 2 :]) >= 10: - msg = message[index : index + 12] - url = f"https://www.bilibili.com/video/{msg}" - return await cls.__handle_video_info( - await video.get_video_base_info(msg), url - ) - elif "av" in message: - index = message.find("av") - if len(message[index + 2 :]) >= 1: - if r := re.search(r"av(\d+)", message): - url = f"https://www.bilibili.com/video/av{r.group(1)}" - return await cls.__handle_video_info( - await video.get_video_base_info(f"av{r.group(1)}"), url - ) - elif "https://b23.tv" in message: - url = ( - "https://" - + message[message.find("b23.tv") : message.find("b23.tv") + 14] - ) - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get( - url, - timeout=7, - ) as response: - url = (str(response.url).split("?")[0]).strip("/") - bvid = url.split("/")[-1] - return await cls.__handle_video_info( - await video.get_video_base_info(bvid), url - ) - return None - - @classmethod - async def __handle_video_info( - cls, vd_info: dict, url: str = "" - ) -> MessageFactory | None: - """处理视频信息 - - 参数: - vd_info: 视频数据 - url: 视频url. - - 返回: - MessageFactory | None: 返回信息 - """ - if url: - if url in cls.time_watch.keys() and time.time() - cls.time_watch[url] < 30: - logger.debug("b站 url 解析在30秒内重复, 跳过解析...") - return None - cls.time_watch[url] = time.time() - aid = vd_info["aid"] - title = vd_info["title"] - author = vd_info["owner"]["name"] - reply = vd_info["stat"]["reply"] # 回复 - favorite = vd_info["stat"]["favorite"] # 收藏 - coin = vd_info["stat"]["coin"] # 投币 - # like = vd_info['stat']['like'] # 点赞 - # danmu = vd_info['stat']['danmaku'] # 弹幕 - date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) - return MessageFactory( - [ - Image(vd_info["pic"]), - Text( - f"\nav{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n{url}" - ), - ] - ) - - @classmethod - async def __parse_video_share(cls, data: dict) -> dict | None: - """解析视频转发 - - 参数: - data: data数据 - - 返回: - dict | None: 视频信息 - """ - try: - if data["meta"]["detail_1"]["title"] == "哔哩哔哩": - try: - async with aiohttp.ClientSession( - headers=get_user_agent() - ) as session: - async with session.get( - data["meta"]["detail_1"]["qqdocurl"], - timeout=7, - ) as response: - url = str(response.url).split("?")[0] - if url[-1] == "/": - url = url[:-1] - bvid = url.split("/")[-1] - return await video.get_video_base_info(bvid) - except Exception as e: - logger.warning("解析b站视频失败", e=e) - except Exception as e: - pass - return None - - @classmethod - async def __parse_news_share(cls, data: dict) -> Path | None: - """解析b站专栏 - - 参数: - data: data数据 - - 返回: - Path | None: 截图路径 - """ - try: - if data["meta"]["news"]["desc"] == "哔哩哔哩专栏": - try: - url = data["meta"]["news"]["jumpUrl"] - async with AsyncPlaywright.new_page() as page: - await page.goto(url, wait_until="networkidle", timeout=10000) - await page.set_viewport_size({"width": 2560, "height": 1080}) - try: - await page.locator("div.bili-mini-close-icon").click() - except Exception: - pass - if div := await page.query_selector("#app > div"): - path = TEMP_PATH / f"bl_share_{uuid.uuid1()}.png" - await div.screenshot( - path=path, - timeout=100000, - ) - return path - except Exception as e: - logger.warning("解析b站专栏失败", e=e) - except Exception as e: - pass - return None diff --git a/zhenxun/plugins/parse_bilibili/get_image.py b/zhenxun/plugins/parse_bilibili/get_image.py new file mode 100644 index 00000000..e2f4ddcb --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/get_image.py @@ -0,0 +1,107 @@ +import os +import re + +from nonebot_plugin_saa import Image + +from zhenxun.configs.path_config import TEMP_PATH +from zhenxun.services.log import logger +from zhenxun.utils.http_utils import AsyncPlaywright +from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.user_agent import get_user_agent_str + + +async def resize(path: str): + """调整图像大小的异步函数 + + 参数: + path (str): 图像文件路径 + """ + A = BuildImage(background=path) + await A.resize(0.5) + await A.save(path) + + +async def get_image(url) -> Image | None: + """获取Bilibili链接的截图,并返回base64格式的图片 + + 参数: + url (str): Bilibili链接 + + 返回: + Image: Image + """ + cv_match = None + opus_match = None + t_opus_match = None + + cv_number = None + opus_number = None + t_opus_number = None + + # 提取cv、opus、t_opus的编号 + url = url.split("?")[0] + cv_match = re.search(r"read/cv([A-Za-z0-9]+)", url, re.IGNORECASE) + opus_match = re.search(r"opus/([A-Za-z0-9]+)", url, re.IGNORECASE) + t_opus_match = re.search(r"https://t\.bilibili\.com/(\d+)", url, re.IGNORECASE) + + if cv_match: + cv_number = cv_match.group(1) + elif opus_match: + opus_number = opus_match.group(1) + elif t_opus_match: + t_opus_number = t_opus_match.group(1) + + screenshot_path = None + + # 根据编号构建保存路径 + if cv_number: + screenshot_path = f"{TEMP_PATH}/bilibili_cv_{cv_number}.png" + elif opus_number: + screenshot_path = f"{TEMP_PATH}/bilibili_opus_{opus_number}.png" + elif t_opus_number: + screenshot_path = f"{TEMP_PATH}/bilibili_opus_{t_opus_number}.png" + # t.bilibili.com和https://www.bilibili.com/opus在内容上是一样的,为便于维护,调整url至https://www.bilibili.com/opus/ + url = f"https://www.bilibili.com/opus/{t_opus_number}" + + if screenshot_path: + try: + # 如果文件不存在,进行截图 + if not os.path.exists(screenshot_path): + # 创建页面 + # random.choice(),从列表中随机抽取一个对象 + user_agent = get_user_agent_str() + try: + async with AsyncPlaywright.new_page() as page: + await page.set_viewport_size({"width": 5120, "height": 2560}) + # 设置请求拦截器 + await page.route( + re.compile(r"(\.png$)|(\.jpg$)"), + lambda route: route.abort(), + ) + # 访问链接 + await page.goto(url, wait_until="networkidle", timeout=10000) + # 根据不同的链接结构,设置对应的CSS选择器 + if cv_number: + css = "#app > div" + elif opus_number or t_opus_number: + css = "#app > div.opus-detail > div.bili-opus-view" + # 点击对应的元素 + await page.click(css) + # 查询目标元素 + div = await page.query_selector(css) + # 对目标元素进行截图 + await div.screenshot( # type: ignore + path=screenshot_path, + timeout=100000, + animations="disabled", + type="png", + ) + # 异步执行调整截图大小的操作 + await resize(screenshot_path) + except Exception as e: + logger.warning(f"尝试解析bilibili转发失败", e=e) + return None + return Image(screenshot_path) + except Exception as e: + logger.error(f"尝试解析bilibili转发失败", e=e) + return None diff --git a/zhenxun/plugins/parse_bilibili/information_container.py b/zhenxun/plugins/parse_bilibili/information_container.py new file mode 100644 index 00000000..1cbf651f --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/information_container.py @@ -0,0 +1,60 @@ +class InformationContainer: + def __init__( + self, + vd_info=None, + live_info=None, + vd_url=None, + live_url=None, + image_info=None, + image_url=None, + ): + self._vd_info = vd_info + self._live_info = live_info + self._vd_url = vd_url + self._live_url = live_url + self._image_info = image_info + self._image_url = image_url + + @property + def vd_info(self): + return self._vd_info + + @property + def live_info(self): + return self._live_info + + @property + def vd_url(self): + return self._vd_url + + @property + def live_url(self): + return self._live_url + + @property + def image_info(self): + return self._image_info + + @property + def image_url(self): + return self._image_url + + def update(self, updates): + """ + 更新多个信息的通用方法 + Args: + updates (dict): 包含信息类型和对应新值的字典 + """ + for info_type, new_value in updates.items(): + if hasattr(self, f"_{info_type}"): + setattr(self, f"_{info_type}", new_value) + + def get_information(self): + return ( + self.vd_info, + self.live_info, + self.vd_url, + self.live_url, + self.image_info, + self.image_url, + ) diff --git a/zhenxun/plugins/parse_bilibili/parse_url.py b/zhenxun/plugins/parse_bilibili/parse_url.py new file mode 100644 index 00000000..b4e2a1fe --- /dev/null +++ b/zhenxun/plugins/parse_bilibili/parse_url.py @@ -0,0 +1,65 @@ +import aiohttp +from bilireq import live, video + +from zhenxun.utils.user_agent import get_user_agent + +from .get_image import get_image +from .information_container import InformationContainer + + +async def parse_bili_url(get_url: str, information_container: InformationContainer): + """解析Bilibili链接,获取相关信息 + + 参数: + get_url (str): 待解析的Bilibili链接 + information_container (InformationContainer): 信息容器 + + 返回: + dict: 包含解析得到的信息的字典 + """ + response_url = "" + + # 去除链接末尾的斜杠 + if get_url[-1] == "/": + get_url = get_url[:-1] + + # 发起HTTP请求,获取重定向后的链接 + async with aiohttp.ClientSession(headers=get_user_agent()) as session: + async with session.get( + get_url, + timeout=7, + ) as response: + response_url = str(response.url).split("?")[0] + + # 去除重定向后链接末尾的斜杠 + if response_url[-1] == "/": + response_url = response_url[:-1] + + # 根据不同类型的链接进行处理 + if response_url.startswith( + ("https://www.bilibili.com/video", "https://m.bilibili.com/video/") + ): + vd_url = response_url + vid = vd_url.split("/")[-1] + vd_info = await video.get_video_base_info(vid) + information_container.update({"vd_info": vd_info, "vd_url": vd_url}) + + elif response_url.startswith("https://live.bilibili.com"): + live_url = response_url + liveid = live_url.split("/")[-1] + live_info = await live.get_room_info_by_id(liveid) + information_container.update({"live_info": live_info, "live_url": live_url}) + + elif response_url.startswith("https://www.bilibili.com/read"): + cv_url = response_url + image_info = await get_image(cv_url) + information_container.update({"image_info": image_info, "image_url": cv_url}) + + elif response_url.startswith( + ("https://www.bilibili.com/opus", "https://t.bilibili.com") + ): + opus_url = response_url + image_info = await get_image(opus_url) + information_container.update({"image_info": image_info, "image_url": opus_url}) + + return information_container.get_information() diff --git a/zhenxun/utils/image_utils.py b/zhenxun/utils/image_utils.py index 0c6a2b27..c193a565 100644 --- a/zhenxun/utils/image_utils.py +++ b/zhenxun/utils/image_utils.py @@ -1,6 +1,7 @@ import os import random import re +from io import BytesIO from pathlib import Path from typing import Awaitable, Callable @@ -408,3 +409,14 @@ async def get_download_image_hash(url: str, mark: str) -> str: except Exception as e: logger.warning(f"下载读取图片Hash出错", e=e) return "" + + +def pic2bytes(image) -> bytes: + """获取bytes + + 返回: + bytes: bytes + """ + buf = BytesIO() + image.save(buf, format="PNG") + return buf.getvalue() From cd4da389c349ed14908be8f8b624c2b299d2a536 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 29 Jul 2024 23:32:21 +0800 Subject: [PATCH 063/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=9B=B4=E6=96=B0B?= =?UTF-8?q?=E7=AB=99=E5=86=85=E5=AE=B9=E8=A7=A3=E6=9E=90=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/parse_bilibili/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index bcc09772..ae4d4b95 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -19,14 +19,14 @@ from .information_container import InformationContainer from .parse_url import parse_bili_url __plugin_meta__ = PluginMetadata( - name="B站转发解析", - description="B站转发解析", + name="B站内容解析", + description="B站内容解析", usage=""" usage: - B站转发解析,解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv,且30秒内内不解析相同url + 被动监听插件,解析B站视频、直播、专栏,支持小程序卡片及文本链接,5分钟内不解析相同内容 """.strip(), extra=PluginExtraData( - author="HibiKier", + author="leekooyo", version="0.1", menu_type="其他", configs=[ From b4b30e59c74a7b4af3d19b5c5b9ed797606569e1 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 30 Jul 2024 00:06:29 +0800 Subject: [PATCH 064/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E5=8C=85=E6=8B=ACuid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 15 +++++++++++---- zhenxun/models/user_console.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 91e350b8..4045c9aa 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -7,6 +7,7 @@ from tortoise import Tortoise from tortoise.exceptions import OperationalError from zhenxun.models.goods_info import GoodsInfo +from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.sign_user import SignUser from zhenxun.models.user_console import UserConsole from zhenxun.services.log import logger @@ -69,6 +70,8 @@ async def _(): and not await SignUser.annotate().count() ): try: + group_user = await GroupInfoUser.filter(uid__isnull=False).all() + user2uid = {u.user_id: u.uid for u in group_user} flag = False db = Tortoise.get_connection("default") old_sign_list = await db.execute_query_dict(SIGN_SQL) @@ -79,7 +82,7 @@ async def _(): } create_list = [] sign_id_list = [] - uid = await UserConsole.get_new_uid() + max_uid = max(user2uid.values()) + 1 for old_sign in old_sign_list: sign_id_list.append(old_sign["user_id"]) old_bag = [ @@ -97,16 +100,20 @@ async def _(): UserConsole( user_id=old_sign["user_id"], platform="qq", - uid=uid, + uid=user2uid.get(old_sign["user_id"]) or max_uid, props=props, gold=old_bag["gold"], ) ) + if not user2uid.get(old_sign["user_id"]): + max_uid += 1 else: create_list.append( - UserConsole(user_id=old_sign["user_id"], platform="qq", uid=uid) + UserConsole( + user_id=old_sign["user_id"], platform="qq", uid=max_uid + ) ) - uid += 1 + max_uid += 1 if create_list: logger.info("开始迁移用户数据...") await UserConsole.bulk_create(create_list, 10) diff --git a/zhenxun/models/user_console.py b/zhenxun/models/user_console.py index c743dd54..8ac2a204 100644 --- a/zhenxun/models/user_console.py +++ b/zhenxun/models/user_console.py @@ -131,7 +131,8 @@ class UserConsole(Model): platform: 平台. """ user, _ = await cls.get_or_create( - user_id=user_id, defaults={"platform": platform, "uid": cls.get_new_uid()} + user_id=user_id, + defaults={"platform": platform, "uid": await cls.get_new_uid()}, ) if goods_uuid not in user.props: user.props[goods_uuid] = 0 From f0b05ec5ed2bf06182dd428a28b79a25d02fe55c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 30 Jul 2024 22:36:09 +0800 Subject: [PATCH 065/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E7=9A=84=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 6 +- zhenxun/builtin_plugins/init/init_plugin.py | 259 +++++++++++++++++++- zhenxun/models/plugin_limit.py | 9 +- 3 files changed, 262 insertions(+), 12 deletions(-) diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index 4045c9aa..ef7ed6ff 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -33,7 +33,6 @@ for d in os.listdir(path): driver: Driver = nonebot.get_driver() -flag = True SIGN_SQL = """ select distinct on("user_id") t1.user_id, t1.checkin_count, t1.add_probability, t1.specify_probability, t1.impression @@ -58,15 +57,14 @@ from public.bag_users t1 @driver.on_startup async def _(): - global flag + """签到与用户的数据迁移""" if goods_list := await GoodsInfo.filter(uuid__isnull=True).all(): for goods in goods_list: goods.uuid = uuid.uuid1() # type: ignore await GoodsInfo.bulk_update(goods_list, ["uuid"], 10) await shop_register.load_register() if ( - flag - and not await UserConsole.annotate().count() + not await UserConsole.annotate().count() and not await SignUser.annotate().count() ): try: diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 7459d766..005f446e 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -1,15 +1,24 @@ import nonebot +import ujson as json from nonebot import get_loaded_plugins from nonebot.drivers import Driver from nonebot.plugin import Plugin from ruamel.yaml import YAML +from zhenxun.configs.path_config import DATA_PATH from zhenxun.configs.utils import PluginExtraData, PluginSetting +from zhenxun.models.group_console import GroupConsole from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_limit import PluginLimit from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType +from zhenxun.utils.enum import ( + BlockType, + LimitCheckType, + LimitWatchType, + PluginLimitType, + PluginType, +) _yaml = YAML(pure=True) _yaml.allow_unicode = True @@ -150,3 +159,251 @@ async def _(): ["run_time", "status", "name"], 10, ) + await data_migration() + + +async def data_migration(): + await limit_migration() + await plugin_migration() + await group_migration() + + +async def limit_migration(): + """插件限制迁移""" + cd_file = DATA_PATH / "configs" / "plugins2cd.yaml" + block_file = DATA_PATH / "configs" / "plugins2block.yaml" + count_file = DATA_PATH / "configs" / "plugins2count.yaml" + limit_data: dict[str, list[tuple[str, dict]]] = {} + if cd_file.exists(): + with open(cd_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginCdLimit"]: + limit_data[k] = [("CD", data["PluginCdLimit"][k])] + cd_file.unlink() + if block_file.exists(): + with open(block_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginBlockLimit"]: + if k in limit_data: + limit_data[k].append(("BLOCK", data["PluginBlockLimit"][k])) + else: + limit_data[k] = [("BLOCK", data["PluginBlockLimit"][k])] + block_file.unlink() + if count_file.exists(): + with open(count_file, encoding="utf8") as f: + if data := _yaml.load(f): + for k in data["PluginCountLimit"]: + if k in limit_data: + limit_data[k].append(("COUNT", data["PluginCountLimit"][k])) + else: + limit_data[k] = [("COUNT", data["PluginCountLimit"][k])] + count_file.unlink() + if limit_data: + logger.info("开始迁移插件限制数据...") + update_list = [] + create_list = [] + plugins = await PluginInfo.filter(module__in=limit_data.keys()) + for plugin in plugins: + limits: list[PluginLimit] = await plugin.plugin_limit.all() # type: ignore + exits_limit = [x[0] for x in limit_data[plugin.module]] + _not_create_type = [] + for limit in limits: + if _limit_list := [ + x[1] + for x in limit_data[plugin.module] + if x[0] == str(limit.limit_type) + ]: + """修改""" + _not_create_type.append(str(limit.limit_type)) + _limit = _limit_list[0] + watch_type = LimitWatchType.USER + if _limit.get("watch_type") == "group": + watch_type = LimitWatchType.GROUP + check_type = LimitCheckType.ALL + if _limit.get("check_type") == "private": + check_type = LimitCheckType.PRIVATE + elif _limit.get("check_type") == "group": + check_type = LimitCheckType.GROUP + limit.watch_type = watch_type + limit.result = _limit.get("rst", "") + limit.status = _limit.get("status", True) + if limit.watch_type != PluginLimitType.COUNT: + limit.check_type = check_type + if limit.watch_type == PluginLimitType.CD: + limit.cd = _limit["cd"] + if limit.watch_type == PluginLimitType.COUNT: + limit.max_count = _limit["count"] + await limit.save() + update_list.append(limit) + for s in [e for e in exits_limit if e not in _not_create_type]: + if _limit_list := [ + x[1] for x in limit_data[plugin.module] if s == x[0] + ]: + _limit = _limit_list[0] + limit_type = PluginLimitType.CD + if s == "BLOCK": + limit_type = PluginLimitType.BLOCK + elif s == "COUNT": + limit_type = PluginLimitType.COUNT + watch_type = LimitWatchType.USER + if _limit.get("watch_type") == "group": + watch_type = LimitWatchType.GROUP + check_type = LimitCheckType.ALL + if _limit.get("check_type") == "private": + check_type = LimitCheckType.PRIVATE + elif _limit.get("check_type") == "group": + check_type = LimitCheckType.GROUP + create_list.append( + PluginLimit( + module=plugin.module, + module_path=plugin.module_path, + plugin=plugin, + limit_type=limit_type, + watch_type=watch_type, + status=_limit.get("status", True), + check_type=check_type, + result=_limit.get("rst", ""), + cd=_limit.get("cd"), + max_count=_limit.get("max_count"), + ) + ) + # TODO: 批量错误 tortoise.exceptions.OperationalError: syntax error at or near "ALL" + # if update_list: + # await PluginLimit.bulk_update( + # update_list, + # [ + # "watch_type", + # "status", + # "check_type", + # "result", + # "cd", + # "max_count", + # ], + # 10, + # ) + if create_list: + await PluginLimit.bulk_create(create_list, 10) + logger.info("迁移插件限制数据完成!") + + +async def plugin_migration(): + """迁移插件数据""" + setting_file = DATA_PATH / "configs" / "plugins2settings.yaml" + plugin_file = DATA_PATH / "manager" / "plugins_manager.json" + if setting_file.exists(): + with open(setting_file, encoding="utf8") as f: + if data := _yaml.load(f): + logger.info("开始迁移插件setting数据...") + data = data["PluginSettings"] + plugins = await PluginInfo.filter(module__in=data.keys()) + for plugin in plugins: + if plugin_data_list := [ + data[p] for p in data if p == plugin.module + ]: + plugin_data = plugin_data_list[0] + plugin.default_status = plugin_data.get("default_status", True) + plugin.level = plugin_data.get("level", 5) + plugin.limit_superuser = plugin_data.get( + "limit_superuser", False + ) + plugin.menu_type = plugin_data.get("plugin_type", ["功能"])[0] + plugin.cost_gold = plugin_data.get("cost_gold", 0) + await PluginInfo.bulk_update( + plugins, + [ + "default_status", + "level", + "limit_superuser", + "menu_type", + "cost_gold", + ], + 10, + ) + setting_file.unlink() + logger.info("迁移插件setting数据完成!") + if plugin_file.exists(): + with open(plugin_file, encoding="utf8") as f: + if data := json.load(f): + logger.info("开始迁移插件数据...") + plugins = await PluginInfo.filter(module__in=data.keys()) + for plugin in plugins: + if plugin_data := data.get(plugin.module): + plugin.status = plugin_data.get("status", True) + block_type = None + get_block = plugin_data.get("block_type") + if get_block == "all": + block_type = BlockType.ALL + elif get_block == "private": + block_type = BlockType.PRIVATE + elif get_block == "group": + block_type = BlockType.GROUP + plugin.block_type = block_type + await PluginInfo.bulk_update(plugins, ["status", "block_type"], 10) + plugin_file.unlink() + logger.info("迁移插件数据完成!") + + +async def group_migration(): + """ + 群组数据迁移 + """ + group_file = DATA_PATH / "manager" / "group_manager.json" + if group_file.exists(): + with open(group_file, encoding="utf8") as f: + if data := json.load(f): + logger.info("开始迁移群组数据...") + update_list = [] + create_list = [] + white_group = data["white_group"] + close_task = data["close_task"] + old_group_list: dict = data["group_manager"] + if close_task: + """全局被动关闭""" + await TaskInfo.filter(module__in=close_task).update(status=False) + group_list = await GroupConsole.filter( + group_id__in=old_group_list.keys() + ) + for old_group_id in old_group_list: + old_group = old_group_list[old_group_id] + block_plugin = "" + block_task = "" + status = old_group.get("status", True) + level = old_group.get("level", 5) + if close_plugins := old_group.get("close_plugins"): + block_plugin = ",".join(close_plugins) + "," + if group_task_status := old_group.get("group_task_status"): + close_task = [ + t for t in group_task_status if not group_task_status[t] + ] + block_task = ",".join(close_task) + "," + if group_ := [g for g in group_list if g.group_id == old_group_id]: + group = group_[0] + if group.group_id in white_group: + group.is_super = True + group.status = status + group.block_plugin = block_plugin + group.block_task = block_task + group.level = level + update_list.append(group) + else: + """添加""" + create_list.append( + GroupConsole( + group_id=old_group_id, + status=status, + level=level, + block_plugin=block_plugin, + block_task=block_task, + is_super=old_group_id in white_group, + ) + ) + if update_list: + await GroupConsole.bulk_update( + update_list, + ["is_super", "status", "block_plugin", "block_task"], + 10, + ) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + group_file.unlink() + logger.info("迁移群组数据完成!") diff --git a/zhenxun/models/plugin_limit.py b/zhenxun/models/plugin_limit.py index 96538b64..e6b185e7 100644 --- a/zhenxun/models/plugin_limit.py +++ b/zhenxun/models/plugin_limit.py @@ -1,12 +1,7 @@ from tortoise import fields from zhenxun.services.db_context import Model -from zhenxun.utils.enum import ( - BlockType, - LimitCheckType, - LimitWatchType, - PluginLimitType, -) +from zhenxun.utils.enum import LimitCheckType, LimitWatchType, PluginLimitType class PluginLimit(Model): @@ -30,7 +25,7 @@ class PluginLimit(Model): status = fields.BooleanField(default=True, description="限制的开关状态") """限制的开关状态""" check_type = fields.CharEnumField( - LimitCheckType, default=BlockType.ALL, description="检查类型" + LimitCheckType, default=LimitCheckType.ALL, description="检查类型" ) """检查类型""" result = fields.CharField(max_length=255, null=True, description="返回信息") From 2bf5fd1a374fd61c52e4368b3d600c32aaf17b2e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 04:58:29 +0800 Subject: [PATCH 066/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=96=B0=E5=A2=9EWeb?= =?UTF-8?q?=20UI=E5=8A=9F=E8=83=BD=E5=8F=8A=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E3=80=81=E6=97=A5=E5=BF=97=E7=AD=89API=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 210 ++++++- pyproject.toml | 2 + zhenxun/configs/utils/__init__.py | 2 + zhenxun/models/fg_request.py | 48 +- zhenxun/plugins/web_ui/__init__.py | 76 +++ zhenxun/plugins/web_ui/api/__init__.py | 1 + zhenxun/plugins/web_ui/api/logs/__init__.py | 1 + .../plugins/web_ui/api/logs/log_manager.py | 35 ++ zhenxun/plugins/web_ui/api/logs/logs.py | 40 ++ zhenxun/plugins/web_ui/api/tabs/__init__.py | 5 + .../web_ui/api/tabs/database/__init__.py | 121 ++++ .../web_ui/api/tabs/database/models/model.py | 24 + .../api/tabs/database/models/sql_log.py | 37 ++ .../plugins/web_ui/api/tabs/main/__init__.py | 290 ++++++++++ .../web_ui/api/tabs/main/data_source.py | 35 ++ zhenxun/plugins/web_ui/api/tabs/main/model.py | 105 ++++ .../web_ui/api/tabs/manage/__init__.py | 529 ++++++++++++++++++ .../plugins/web_ui/api/tabs/manage/model.py | 265 +++++++++ .../web_ui/api/tabs/plugin_manage/__init__.py | 187 +++++++ .../web_ui/api/tabs/plugin_manage/model.py | 125 +++++ .../web_ui/api/tabs/system/__init__.py | 121 ++++ .../plugins/web_ui/api/tabs/system/model.py | 64 +++ zhenxun/plugins/web_ui/auth/__init__.py | 47 ++ zhenxun/plugins/web_ui/base_model.py | 108 ++++ zhenxun/plugins/web_ui/config.py | 36 ++ zhenxun/plugins/web_ui/utils.py | 136 +++++ zhenxun/utils/enum.py | 2 + zhenxun/utils/plugin_models/base.py | 9 + 28 files changed, 2643 insertions(+), 18 deletions(-) create mode 100644 zhenxun/plugins/web_ui/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/logs/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/logs/log_manager.py create mode 100644 zhenxun/plugins/web_ui/api/logs/logs.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/models/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/data_source.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/main/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/manage/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/manage/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/system/__init__.py create mode 100644 zhenxun/plugins/web_ui/api/tabs/system/model.py create mode 100644 zhenxun/plugins/web_ui/auth/__init__.py create mode 100644 zhenxun/plugins/web_ui/base_model.py create mode 100644 zhenxun/plugins/web_ui/config.py create mode 100644 zhenxun/plugins/web_ui/utils.py create mode 100644 zhenxun/utils/plugin_models/base.py diff --git a/poetry.lock b/poetry.lock index e00d88b1..53a0678d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -591,6 +591,75 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "chardet" version = "5.2.0" @@ -792,6 +861,60 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "cryptography" +version = "43.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "dateparser" version = "1.2.0" @@ -835,6 +958,29 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "ecdsa" +version = "0.19.0" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, +] + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "emoji" version = "2.10.1" @@ -2513,6 +2659,22 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "pydantic" version = "1.10.14" @@ -2737,6 +2899,33 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +optional = false +python-versions = "*" +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-markdown-math" version = "0.8" @@ -2756,6 +2945,25 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "python-slugify" version = "8.0.2" @@ -4137,4 +4345,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "3a23c97b954472fca2124960e82ba695c52f3448bec4458df94b3d884d23c1e4" +content-hash = "1069f396df7f09336b9ea7737997061e4dfea458a561995a2afee74fd9cf36ad" diff --git a/pyproject.toml b/pyproject.toml index cbe43480..8faaa329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,8 @@ aiohttp = "^3.9.5" dateparser = "^1.2.0" nonebot-plugin-alconna = "^0.50.2" bilireq = "0.2.3post0" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +python-multipart = "^0.0.9" [tool.poetry.dev-dependencies] diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index 4a192b3d..d9f4367e 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -168,6 +168,8 @@ class PluginExtraData(BaseModel): """超级用户帮助""" aliases: Set[str] = set() """额外名称""" + sql_list: list[str] | None = None + """常用sql""" class NoSuchConfig(Exception): diff --git a/zhenxun/models/fg_request.py b/zhenxun/models/fg_request.py index ad740eff..43cbfdbc 100644 --- a/zhenxun/models/fg_request.py +++ b/zhenxun/models/fg_request.py @@ -9,7 +9,9 @@ from zhenxun.utils.exception import NotFoundError class FgRequest(Model): id = fields.IntField(pk=True, generated=True, auto_increment=True) """自增id""" - request_type = fields.CharEnumField(RequestType, default=None, description="请求类型") + request_type = fields.CharEnumField( + RequestType, default=None, description="请求类型" + ) """请求类型""" platform = fields.CharField(255, description="平台") """平台""" @@ -25,7 +27,9 @@ class FgRequest(Model): """对象名称""" comment = fields.CharField(max_length=255, null=True, description="验证信息") """验证信息""" - handle_type = fields.CharEnumField(RequestHandleType, null=True, description="处理类型") + handle_type = fields.CharEnumField( + RequestHandleType, null=True, description="处理类型" + ) """处理类型""" class Meta: @@ -33,53 +37,61 @@ class FgRequest(Model): table_description = "好友群组请求" @classmethod - async def approve(cls, bot: Bot, id: int, request_type: RequestType): + async def approve(cls, bot: Bot, id: int): """同意请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.APPROVE) + await cls._handle_request(bot, id, RequestHandleType.APPROVE) @classmethod - async def refused(cls, bot: Bot, id: int, request_type: RequestType): + async def refused(cls, bot: Bot, id: int): """拒绝请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.REFUSED) + await cls._handle_request(bot, id, RequestHandleType.REFUSED) @classmethod - async def ignore(cls, bot: Bot, id: int, request_type: RequestType): + async def ignore(cls, id: int): + """忽略请求 + + 参数: + id: 请求id + + 异常: + NotFoundError: 未发现请求 + """ + await cls._handle_request(None, id, RequestHandleType.IGNORE) + + @classmethod + async def expire(cls, id: int): """忽略请求 参数: bot: Bot id: 请求id - request_type: 请求类型 异常: NotFoundError: 未发现请求 """ - await cls._handle_request(bot, id, request_type, RequestHandleType.IGNORE) + await cls._handle_request(None, id, RequestHandleType.EXPIRE) @classmethod async def _handle_request( cls, - bot: Bot, + bot: Bot | None, id: int, - request_type: RequestType, handle_type: RequestHandleType, ): """处理请求 @@ -87,19 +99,21 @@ class FgRequest(Model): 参数: bot: Bot id: 请求id - request_type: 请求类型 handle_type: 处理类型 异常: NotFoundError: 未发现请求 """ - req = await cls.get_or_none(id=id, request_type=request_type) + req = await cls.get_or_none(id=id) if not req: raise NotFoundError req.handle_type = RequestHandleType await req.save(update_fields=["handle_type"]) - if handle_type != RequestHandleType.IGNORE: - if request_type == RequestType.FRIEND: + if bot and handle_type not in [ + RequestHandleType.IGNORE, + RequestHandleType.EXPIRE, + ]: + if req.request_type == RequestType.FRIEND: await bot.set_friend_add_request( flag=req.flag, approve=handle_type == RequestHandleType.APPROVE ) diff --git a/zhenxun/plugins/web_ui/__init__.py b/zhenxun/plugins/web_ui/__init__.py new file mode 100644 index 00000000..37684372 --- /dev/null +++ b/zhenxun/plugins/web_ui/__init__.py @@ -0,0 +1,76 @@ +import asyncio + +import nonebot +from fastapi import APIRouter, FastAPI +from nonebot.adapters.onebot.v11 import Bot, MessageEvent +from nonebot.log import default_filter, default_format +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.typing import T_State + +from zhenxun.configs.config import Config as gConfig +from zhenxun.services.log import logger, logger_ + +from .api.logs import router as ws_log_routes +from .api.logs.log_manager import LOG_STORAGE +from .api.tabs.database import router as database_router +from .api.tabs.main import router as main_router +from .api.tabs.main import ws_router as status_routes +from .api.tabs.manage import router as manage_router +from .api.tabs.manage import ws_router as chat_routes +from .api.tabs.plugin_manage import router as plugin_router +from .api.tabs.system import router as system_router +from .auth import router as auth_router + +driver = nonebot.get_driver() + +gConfig.add_plugin_config("web-ui", "username", "admin", help="前端管理用户名") + +gConfig.add_plugin_config("web-ui", "password", None, help="前端管理密码") + +gConfig.set_name("web-ui", "web-ui") + + +BaseApiRouter = APIRouter(prefix="/zhenxun/api") + + +BaseApiRouter.include_router(auth_router) +BaseApiRouter.include_router(main_router) +BaseApiRouter.include_router(manage_router) +BaseApiRouter.include_router(database_router) +BaseApiRouter.include_router(plugin_router) +BaseApiRouter.include_router(system_router) + + +WsApiRouter = APIRouter(prefix="/zhenxun/socket") + +WsApiRouter.include_router(ws_log_routes) +WsApiRouter.include_router(status_routes) +WsApiRouter.include_router(chat_routes) + + +@driver.on_startup +def _(): + try: + + async def log_sink(message: str): + loop = None + if not loop: + try: + loop = asyncio.get_running_loop() + except Exception as e: + logger.warning("Web Ui log_sink", e=e) + if not loop: + loop = asyncio.new_event_loop() + loop.create_task(LOG_STORAGE.add(message.rstrip("\n"))) + + logger_.add( + log_sink, colorize=True, filter=default_filter, format=default_format + ) + + app: FastAPI = nonebot.get_app() + app.include_router(BaseApiRouter) + app.include_router(WsApiRouter) + logger.info("API启动成功", "Web UI") + except Exception as e: + logger.error("API启动失败", "Web UI", e=e) diff --git a/zhenxun/plugins/web_ui/api/__init__.py b/zhenxun/plugins/web_ui/api/__init__.py new file mode 100644 index 00000000..32d31b27 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/__init__.py @@ -0,0 +1 @@ +from .tabs import * diff --git a/zhenxun/plugins/web_ui/api/logs/__init__.py b/zhenxun/plugins/web_ui/api/logs/__init__.py new file mode 100644 index 00000000..d6684888 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/__init__.py @@ -0,0 +1 @@ +from .logs import * diff --git a/zhenxun/plugins/web_ui/api/logs/log_manager.py b/zhenxun/plugins/web_ui/api/logs/log_manager.py new file mode 100644 index 00000000..71992c91 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/log_manager.py @@ -0,0 +1,35 @@ +import asyncio +from typing import Awaitable, Callable, Generic, TypeVar + +PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" + +_T = TypeVar("_T") +LogListener = Callable[[_T], Awaitable[None]] + + +class LogStorage(Generic[_T]): + """ + 日志存储 + """ + + def __init__(self, rotation: float = 5 * 60): + self.count, self.rotation = 0, rotation + self.logs: dict[int, str] = {} + self.listeners: set[LogListener[str]] = set() + + async def add(self, log: str): + seq = self.count = self.count + 1 + self.logs[seq] = log + asyncio.get_running_loop().call_later(self.rotation, self.remove, seq) + await asyncio.gather( + *map(lambda listener: listener(log), self.listeners), + return_exceptions=True, + ) + return seq + + def remove(self, seq: int): + del self.logs[seq] + return + + +LOG_STORAGE: LogStorage[str] = LogStorage[str]() diff --git a/zhenxun/plugins/web_ui/api/logs/logs.py b/zhenxun/plugins/web_ui/api/logs/logs.py new file mode 100644 index 00000000..01c78096 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/logs/logs.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, WebSocket +from loguru import logger +from nonebot.utils import escape_tag +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState + +from .log_manager import LOG_STORAGE + +router = APIRouter() + + +@router.get("/logs", response_model=list[str]) +async def system_logs_history(reverse: bool = False): + """历史日志 + + 参数: + reverse: 反转顺序. + """ + return LOG_STORAGE.list(reverse=reverse) # type: ignore + + +@router.websocket("/logs") +async def system_logs_realtime(websocket: WebSocket): + await websocket.accept() + + async def log_listener(log: str): + await websocket.send_text(log) + + LOG_STORAGE.listeners.add(log_listener) + try: + while websocket.client_state == WebSocketState.CONNECTED: + recv = await websocket.receive() + logger.trace( + f"{system_logs_realtime.__name__!r} received " + f"{escape_tag(repr(recv))}" + ) + except WebSocketDisconnect: + pass + finally: + LOG_STORAGE.listeners.remove(log_listener) + return diff --git a/zhenxun/plugins/web_ui/api/tabs/__init__.py b/zhenxun/plugins/web_ui/api/tabs/__init__.py new file mode 100644 index 00000000..99ed6ea1 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/__init__.py @@ -0,0 +1,5 @@ +from .database import * +from .main import * +from .manage import * +from .plugin_manage import * +from .system import * diff --git a/zhenxun/plugins/web_ui/api/tabs/database/__init__.py b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py new file mode 100644 index 00000000..2fd77085 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/__init__.py @@ -0,0 +1,121 @@ +import nonebot +from fastapi import APIRouter, Request +from nonebot.drivers import Driver +from tortoise import Tortoise +from tortoise.exceptions import OperationalError + +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.services.db_context import TestSQL + +from ....base_model import BaseResultModel, QueryModel, Result +from ....utils import authentication +from .models.model import SqlModel, SqlText +from .models.sql_log import SqlLog + +router = APIRouter(prefix="/database") + + +driver: Driver = nonebot.get_driver() + + +SQL_DICT = {} + + +SELECT_TABLE_SQL = """ +select a.tablename as name,d.description as desc from pg_tables a + left join pg_class c on relname=tablename + left join pg_description d on oid=objoid and objsubid=0 where a.schemaname = 'public' +""" + +SELECT_TABLE_COLUMN_SQL = """ +SELECT column_name, data_type, character_maximum_length as max_length, is_nullable +FROM information_schema.columns +WHERE table_name = '{}'; +""" + + +@driver.on_startup +async def _(): + for plugin in nonebot.get_loaded_plugins(): + module = plugin.name + sql_list = [] + if plugin.metadata and plugin.metadata.extra: + sql_list = plugin.metadata.extra.get("sql_list") + if module in SQL_DICT: + raise ValueError(f"{module} 常用SQL module 重复") + if sql_list: + SqlModel( + name="", + module=module, + sql_list=sql_list, + ) + SQL_DICT[module] = SqlModel + if SQL_DICT: + result = await PluginInfo.filter(module__in=SQL_DICT.keys()).values_list( + "module", "name" + ) + module2name = {r[0]: r[1] for r in result} + for s in SQL_DICT: + module = SQL_DICT[s].module + if module in module2name: + SQL_DICT[s].name = module2name[module] + else: + SQL_DICT[s].name = module + + +@router.get( + "/get_table_list", dependencies=[authentication()], description="获取数据库表" +) +async def _() -> Result: + db = Tortoise.get_connection("default") + query = await db.execute_query_dict(SELECT_TABLE_SQL) + return Result.ok(query) + + +@router.get( + "/get_table_column", dependencies=[authentication()], description="获取表字段" +) +async def _(table_name: str) -> Result: + db = Tortoise.get_connection("default") + print(SELECT_TABLE_COLUMN_SQL.format(table_name)) + query = await db.execute_query_dict(SELECT_TABLE_COLUMN_SQL.format(table_name)) + return Result.ok(query) + + +@router.post("/exec_sql", dependencies=[authentication()], description="执行sql") +async def _(sql: SqlText, request: Request) -> Result: + ip = request.client.host if request.client else "unknown" + try: + if sql.sql.lower().startswith("select"): + db = Tortoise.get_connection("default") + res = await db.execute_query_dict(sql.sql) + await SqlLog.add(ip or "0.0.0.0", sql.sql, "") + return Result.ok(res, "执行成功啦!") + else: + result = await TestSQL.raw(sql.sql) + await SqlLog.add(ip or "0.0.0.0", sql.sql, str(result)) + return Result.ok(info="执行成功啦!") + except OperationalError as e: + await SqlLog.add(ip or "0.0.0.0", sql.sql, str(e), False) + return Result.warning_(f"sql执行错误: {e}") + + +@router.post("/get_sql_log", dependencies=[authentication()], description="sql日志列表") +async def _(query: QueryModel) -> Result: + total = await SqlLog.all().count() + if total % query.size: + total += 1 + data = ( + await SqlLog.all() + .order_by("-id") + .offset((query.index - 1) * query.size) + .limit(query.size) + ) + return Result.ok(BaseResultModel(total=total, data=data)) + + +@router.get("/get_common_sql", dependencies=[authentication()], description="常用sql") +async def _(plugin_name: str | None = None) -> Result: + if plugin_name: + return Result.ok(SQL_DICT.get(plugin_name)) + return Result.ok(str(SQL_DICT)) diff --git a/zhenxun/plugins/web_ui/api/tabs/database/models/model.py b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py new file mode 100644 index 00000000..e18e4cfb --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/model.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel + +from zhenxun.utils.plugin_models.base import CommonSql + + +class SqlText(BaseModel): + """ + sql语句 + """ + + sql: str + + +class SqlModel(BaseModel): + """ + 常用sql + """ + + name: str + """插件中文名称""" + module: str + """插件名称""" + sql_list: list[CommonSql] + """插件列表""" diff --git a/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py new file mode 100644 index 00000000..691f1b5a --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/database/models/sql_log.py @@ -0,0 +1,37 @@ +from tortoise import fields + +from zhenxun.services.db_context import Model + + +class SqlLog(Model): + + id = fields.IntField(pk=True, generated=True, auto_increment=True) + """自增id""" + ip = fields.CharField(255) + """ip""" + sql = fields.CharField(255) + """sql""" + result = fields.CharField(255, null=True) + """结果""" + is_suc = fields.BooleanField(default=True) + """是否成功""" + create_time = fields.DatetimeField(auto_now_add=True) + """创建时间""" + + class Meta: + table = "sql_log" + table_description = "sql执行日志" + + @classmethod + async def add( + cls, ip: str, sql: str, result: str | None = None, is_suc: bool = True + ): + """获取用户在群内的等级 + + 参数: + ip: ip + sql: sql + result: 返回结果 + is_suc: 是否成功 + """ + await cls.create(ip=ip, sql=sql, result=result, is_suc=is_suc) diff --git a/zhenxun/plugins/web_ui/api/tabs/main/__init__.py b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py new file mode 100644 index 00000000..ed8bb576 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/__init__.py @@ -0,0 +1,290 @@ +import asyncio +import time +from datetime import datetime, timedelta +from pathlib import Path + +import nonebot +from fastapi import APIRouter, WebSocket +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState +from tortoise.functions import Count +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK + +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_info import GroupInfo +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + +from ....base_model import Result +from ....config import AVA_URL, GROUP_AVA_URL, QueryDateType +from ....utils import authentication, get_system_status +from .data_source import bot_live +from .model import ActiveGroup, BaseInfo, ChatHistoryCount, HotPlugin + +run_time = time.time() + +ws_router = APIRouter() +router = APIRouter(prefix="/main") + + +@router.get("/get_base_info", dependencies=[authentication()], description="基础信息") +async def _(bot_id: str | None = None) -> Result: + """获取Bot基础信息 + + 参数: + bot_id (Optional[str], optional): bot_id. Defaults to None. + + 返回: + Result: 获取指定bot信息与bot列表 + """ + bot_list: list[BaseInfo] = [] + if bots := nonebot.get_bots(): + select_bot: BaseInfo + for key, bot in bots.items(): + login_info = await bot.get_login_info() + bot_list.append( + BaseInfo( + bot=bot, # type: ignore + self_id=bot.self_id, + nickname=login_info["nickname"], + ava_url=AVA_URL.format(bot.self_id), + ) + ) + # 获取指定qq号的bot信息,若无指定 则获取第一个 + if _bl := [b for b in bot_list if b.self_id == bot_id]: + select_bot = _bl[0] + else: + select_bot = bot_list[0] + select_bot.is_select = True + select_bot.config = select_bot.bot.config + now = datetime.now() + # 今日累计接收消息 + select_bot.received_messages = await ChatHistory.filter( + bot_id=select_bot.self_id, + create_time__gte=now - timedelta(hours=now.hour), + ).count() + # 群聊数量 + select_bot.group_count = len(await select_bot.bot.get_group_list()) + # 好友数量 + select_bot.friend_count = len(await select_bot.bot.get_friend_list()) + for bot in bot_list: + bot.bot = None # type: ignore + # 插件加载数量 + select_bot.plugin_count = await PluginInfo.all().count() + fail_count = await PluginInfo.filter(load_status=False).count() + select_bot.fail_plugin_count = fail_count + select_bot.success_plugin_count = ( + select_bot.plugin_count - select_bot.fail_plugin_count + ) + # 连接时间 + select_bot.connect_time = bot_live.get(select_bot.self_id) or 0 + if select_bot.connect_time: + connect_date = datetime.fromtimestamp(select_bot.connect_time) + connect_date_str = connect_date.strftime("%Y-%m-%d %H:%M:%S") + select_bot.connect_date = datetime.strptime( + connect_date_str, "%Y-%m-%d %H:%M:%S" + ) + version_file = Path() / "__version__" + if version_file.exists(): + if text := version_file.open().read(): + if ver := text.replace("__version__: ", "").strip(): + select_bot.version = ver + day_call = await Statistics.filter( + create_time__gte=now - timedelta(hours=now.hour) + ).count() + select_bot.day_call = day_call + return Result.ok(bot_list, "拿到信息啦!") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_all_ch_count", dependencies=[authentication()], description="获取接收消息数量" +) +async def _(bot_id: str) -> Result: + now = datetime.now() + all_count = await ChatHistory.filter(bot_id=bot_id).count() + day_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) + ).count() + week_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=7) + ).count() + month_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=30) + ).count() + year_count = await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=365) + ).count() + return Result.ok( + ChatHistoryCount( + num=all_count, + day=day_count, + week=week_count, + month=month_count, + year=year_count, + ) + ) + + +@router.get( + "/get_ch_count", dependencies=[authentication()], description="获取接收消息数量" +) +async def _(bot_id: str, query_type: QueryDateType | None = None) -> Result: + if bots := nonebot.get_bots(): + if not query_type: + return Result.ok(await ChatHistory.filter(bot_id=bot_id).count()) + now = datetime.now() + if query_type == QueryDateType.DAY: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(hours=now.hour) + ).count() + ) + if query_type == QueryDateType.WEEK: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=7) + ).count() + ) + if query_type == QueryDateType.MONTH: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=30) + ).count() + ) + if query_type == QueryDateType.YEAR: + return Result.ok( + await ChatHistory.filter( + bot_id=bot_id, create_time__gte=now - timedelta(days=365) + ).count() + ) + return Result.warning_("无Bot连接...") + + +@router.get( + "get_fg_count", dependencies=[authentication()], description="好友/群组数量" +) +async def _(bot_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + bot = bots[bot_id] + platform = PlatformUtils.get_platform(bot) + if platform == "qq": + data = { + "friend_count": len(await bot.get_friend_list()), + "group_count": len(await bot.get_group_list()), + } + return Result.ok(data) + return Result.warning_("暂不支持该平台...") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_run_time", dependencies=[authentication()], description="获取nb运行时间" +) +async def _() -> Result: + return Result.ok(int(time.time() - run_time)) + + +@router.get( + "/get_active_group", dependencies=[authentication()], description="获取活跃群聊" +) +async def _(date_type: QueryDateType | None = None) -> Result: + query = ChatHistory + now = datetime.now() + if date_type == QueryDateType.DAY: + query = ChatHistory.filter(create_time__gte=now - timedelta(hours=now.hour)) + if date_type == QueryDateType.WEEK: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=7)) + if date_type == QueryDateType.MONTH: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=30)) + if date_type == QueryDateType.YEAR: + query = ChatHistory.filter(create_time__gte=now - timedelta(days=365)) + data_list = ( + await query.annotate(count=Count("id")) + .filter(group_id__not_isnull=True) + .group_by("group_id") + .order_by("-count") + .limit(5) + .values_list("group_id", "count") + ) + active_group_list = [] + id2name = {} + if data_list: + if info_list := await GroupInfo.filter( + group_id__in=[x[0] for x in data_list] + ).all(): + for group_info in info_list: + id2name[group_info.group_id] = group_info.group_name + for data in data_list: + active_group_list.append( + ActiveGroup( + group_id=data[0], + name=id2name.get(data[0]) or data[0], + chat_num=data[1], + ava_img=GROUP_AVA_URL.format(data[0], data[0]), + ) + ) + active_group_list = sorted( + active_group_list, key=lambda x: x.chat_num, reverse=True + ) + if len(active_group_list) > 5: + active_group_list = active_group_list[:5] + return Result.ok(active_group_list) + + +@router.get( + "/get_hot_plugin", dependencies=[authentication()], description="获取热门插件" +) +async def _(date_type: QueryDateType | None = None) -> Result: + query = Statistics + now = datetime.now() + if date_type == QueryDateType.DAY: + query = Statistics.filter(create_time__gte=now - timedelta(hours=now.hour)) + if date_type == QueryDateType.WEEK: + query = Statistics.filter(create_time__gte=now - timedelta(days=7)) + if date_type == QueryDateType.MONTH: + query = Statistics.filter(create_time__gte=now - timedelta(days=30)) + if date_type == QueryDateType.YEAR: + query = Statistics.filter(create_time__gte=now - timedelta(days=365)) + data_list = ( + await query.annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + hot_plugin_list = [] + module_list = [x[0] for x in data_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in data_list: + module = data[0] + name = module2name.get(module) or module + hot_plugin_list.append( + HotPlugin( + module=data[0], + name=name, + count=data[1], + ) + ) + hot_plugin_list = sorted(hot_plugin_list, key=lambda x: x.count, reverse=True) + if len(hot_plugin_list) > 5: + hot_plugin_list = hot_plugin_list[:5] + return Result.ok(hot_plugin_list) + + +@ws_router.websocket("/system_status") +async def system_logs_realtime(websocket: WebSocket, sleep: int = 5): + await websocket.accept() + logger.debug("ws system_status is connect") + try: + while websocket.client_state == WebSocketState.CONNECTED: + system_status = await get_system_status() + await websocket.send_text(system_status.json()) + await asyncio.sleep(sleep) + except (WebSocketDisconnect, ConnectionClosedError, ConnectionClosedOK): + pass + return diff --git a/zhenxun/plugins/web_ui/api/tabs/main/data_source.py b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py new file mode 100644 index 00000000..ca445016 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/data_source.py @@ -0,0 +1,35 @@ +import time + +import nonebot +from nonebot.adapters.onebot.v11 import Bot +from nonebot.drivers import Driver + +driver: Driver = nonebot.get_driver() + + +class BotLive: + def __init__(self): + self._data = {} + + def add(self, bot_id: str): + self._data[bot_id] = time.time() + + def get(self, bot_id: str) -> int | None: + return self._data.get(bot_id) + + def remove(self, bot_id: str): + if bot_id in self._data: + del self._data[bot_id] + + +bot_live = BotLive() + + +@driver.on_bot_connect +async def _(bot: Bot): + bot_live.add(bot.self_id) + + +@driver.on_bot_disconnect +async def _(bot: Bot): + bot_live.remove(bot.self_id) diff --git a/zhenxun/plugins/web_ui/api/tabs/main/model.py b/zhenxun/plugins/web_ui/api/tabs/main/model.py new file mode 100644 index 00000000..c9d76706 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/main/model.py @@ -0,0 +1,105 @@ +from datetime import datetime + +from nonebot.adapters import Bot +from nonebot.config import Config +from pydantic import BaseModel + + +class SystemStatus(BaseModel): + """ + 系统状态 + """ + + cpu: float + memory: float + disk: float + + +class BaseInfo(BaseModel): + """ + 基础信息 + """ + + bot: Bot + """Bot""" + self_id: str + """SELF ID""" + nickname: str + """昵称""" + ava_url: str + """头像url""" + friend_count: int = 0 + """好友数量""" + group_count: int = 0 + """群聊数量""" + received_messages: int = 0 + """今日 累计接收消息""" + connect_time: int = 0 + """连接时间""" + connect_date: datetime | None = None + """连接日期""" + + plugin_count: int = 0 + """加载插件数量""" + success_plugin_count: int = 0 + """加载成功插件数量""" + fail_plugin_count: int = 0 + """加载失败插件数量""" + + is_select: bool = False + """当前选择""" + + config: Config | None = None + """nb配置""" + day_call: int = 0 + """今日调用插件次数""" + version: str = "unknown" + """真寻版本""" + + class Config: + arbitrary_types_allowed = True + + +class ChatHistoryCount(BaseModel): + """ + 聊天记录数量 + """ + + num: int + """总数""" + day: int + """一天内""" + week: int + """一周内""" + month: int + """一月内""" + year: int + """一年内""" + + +class ActiveGroup(BaseModel): + """ + 活跃群聊数据 + """ + + group_id: str + """群组id""" + name: str + """群组名称""" + chat_num: int + """发言数量""" + ava_img: str + """群组头像""" + + +class HotPlugin(BaseModel): + """ + 热门插件 + """ + + module: str + """模块名""" + name: str + """插件名称""" + count: int + """调用次数""" diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py new file mode 100644 index 00000000..82a34d56 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py @@ -0,0 +1,529 @@ +import re +from typing import Literal + +import nonebot +from fastapi import APIRouter +from nonebot.adapters.onebot.v11 import ActionFailed +from starlette.websockets import WebSocket, WebSocketDisconnect, WebSocketState +from tortoise.functions import Count + +from zhenxun.configs.config import NICKNAME +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.fg_request import FgRequest +from zhenxun.models.friend_user import FriendUser +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.group_member_info import GroupInfoUser +from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.statistics import Statistics +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import RequestHandleType, RequestType +from zhenxun.utils.exception import NotFoundError +from zhenxun.utils.platform import PlatformUtils + +from ....base_model import Result +from ....config import AVA_URL, GROUP_AVA_URL +from ....utils import authentication +from ...logs.log_manager import LOG_STORAGE +from .model import ( + DeleteFriend, + Friend, + FriendRequestResult, + GroupDetail, + GroupRequestResult, + GroupResult, + HandleRequest, + LeaveGroup, + Message, + MessageItem, + Plugin, + ReqResult, + SendMessage, + Task, + UpdateGroup, + UserDetail, +) + +ws_router = APIRouter() +router = APIRouter(prefix="/manage") + +SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" + +GROUP_PATTERN = r'.*?Message (-?\d*) from (\d*)@\[群:(\d*)] "(.*)"' + +PRIVATE_PATTERN = r'.*?Message (-?\d*) from (\d*) "(.*)"' + +AT_PATTERN = r"\[CQ:at,qq=(.*)\]" + +IMAGE_PATTERN = r"\[CQ:image,.*,url=(.*);.*?\]" + + +@router.get( + "/get_group_list", dependencies=[authentication()], description="获取群组列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + group_list_result = [] + try: + group_info = {} + group_list = await bots[bot_id].get_group_list() + for g in group_list: + gid = g["group_id"] + g["ava_url"] = GROUP_AVA_URL.format(gid, gid) + group_list_result.append(GroupResult(**g)) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(group_list_result, "拿到了新鲜出炉的数据!") + return Result.warning_("无Bot连接...") + + +@router.post( + "/update_group", dependencies=[authentication()], description="修改群组信息" +) +async def _(group: UpdateGroup) -> Result: + try: + group_id = group.group_id + if db_group := await GroupConsole.get_or_none(group_id=group_id): + db_group.level = group.level + db_group.status = group.status + if group.close_plugins: + db_group.block_plugin = ",".join(group.close_plugins) + "," + # TODO: 关闭task + await db_group.save(update_fields=["level", "status", "block_plugin"]) + except Exception as e: + logger.error("调用API错误", "/get_group", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已完成记录!") + + +@router.get( + "/get_friend_list", dependencies=[authentication()], description="获取好友列表" +) +async def _(bot_id: str) -> Result: + """ + 获取群信息 + """ + if bots := nonebot.get_bots(): + if bot_id not in bots: + return Result.warning_("指定Bot未连接...") + try: + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台暂不支持该功能...") + friend_list = await bots[bot_id].get_friend_list() + for f in friend_list: + f["ava_url"] = AVA_URL.format(f["user_id"]) + return Result.ok( + [Friend(**f) for f in friend_list if str(f["user_id"]) != bot_id], + "拿到了新鲜出炉的数据!", + ) + except Exception as e: + logger.error("调用API错误", "/get_group_list", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_request_count", dependencies=[authentication()], description="获取请求数量" +) +async def _() -> Result: + f_count = await FgRequest.filter(request_type=RequestType.FRIEND).count() + g_count = await FgRequest.filter(request_type=RequestType.GROUP).count() + data = { + "friend_count": f_count, + "group_count": g_count, + } + return Result.ok(data, f"{NICKNAME}带来了最新的数据!") + + +@router.get( + "/get_request_list", dependencies=[authentication()], description="获取请求列表" +) +async def _() -> Result: + try: + req_result = ReqResult() + data_list = await FgRequest.filter(handle_type__not_isnull=True).all() + for req in data_list: + if req.request_type == RequestType.FRIEND: + req_result.friend.append( + FriendRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=AVA_URL.format(req.user_id), + type=str(req.request_type).lower(), + ) + ) + else: + req_result.group.append( + GroupRequestResult( + oid=req.id, + bot_id=req.bot_id, + id=req.user_id, + flag=req.flag, + nickname=req.nickname, + comment=req.comment, + ava_url=GROUP_AVA_URL.format(req.group_id, req.group_id), + type=str(req.request_type).lower(), + invite_group=req.group_id, + group_name=None, + ) + ) + req_result.friend.reverse() + req_result.group.reverse() + except Exception as e: + logger.error("调用API错误", "/get_request", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(req_result, f"{NICKNAME}带来了最新的数据!") + + +@router.delete( + "/clear_request", dependencies=[authentication()], description="清空请求列表" +) +async def _(request_type: Literal["private", "group"]) -> Result: + await FgRequest.filter(handle_type__not_isnull=True).update( + handle_type=RequestHandleType.IGNORE + ) + return Result.ok(info="成功清除了数据!") + + +@router.post("/refuse_request", dependencies=[authentication()], description="拒绝请求") +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + try: + await FgRequest.refused(bots[bot_id], parma.id) + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + except NotFoundError: + return Result.warning_("未找到此Id请求...") + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/refuse_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_request", dependencies=[authentication()], description="忽略请求") +async def _(parma: HandleRequest) -> Result: + await FgRequest.expire(parma.id) + return Result.ok(info="成功处理了请求!") + + +@router.post( + "/approve_request", dependencies=[authentication()], description="同意请求" +) +async def _(parma: HandleRequest) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = parma.bot_id + if bot_id not in nonebot.get_bots(): + return Result.warning_("指定Bot未连接...") + if parma.request_type == "group": + if req := await FgRequest.get_or_none(id=parma.id): + if group := await GroupConsole.get_or_none(group_id=req.group_id): + await group.update_or_create(group_flag=1) + else: + group_info = await bots[bot_id].get_group_info( + group_id=req.group_id + ) + await GroupConsole.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + else: + return Result.warning_("未找到此Id请求...") + try: + await FgRequest.approve(bots[bot_id], parma.id) + return Result.ok(info="成功处理了请求!") + except ActionFailed as e: + await FgRequest.expire(parma.id) + return Result.warning_("请求失败,可能该请求已失效或请求数据错误...") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/approve_request", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/leave_group", dependencies=[authentication()], description="退群") +async def _(param: LeaveGroup) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持退群操作...") + group_list = await bots[bot_id].get_group_list() + if param.group_id not in [str(g["group_id"]) for g in group_list]: + return Result.warning_("Bot未在该群聊中...") + await bots[bot_id].set_group_leave(group_id=param.group_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("无Bot连接...") + except Exception as e: + logger.error("调用API错误", "/leave_group", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.post("/delete_friend", dependencies=[authentication()], description="删除好友") +async def _(param: DeleteFriend) -> Result: + try: + if bots := nonebot.get_bots(): + bot_id = param.bot_id + platform = PlatformUtils.get_platform(bots[bot_id]) + if platform != "qq": + return Result.warning_("该平台不支持删除好友操作...") + friend_list = await bots[bot_id].get_friend_list() + if param.user_id not in [str(g["user_id"]) for g in friend_list]: + return Result.warning_("Bot未有其好友...") + await bots[bot_id].delete_friend(user_id=param.user_id) + return Result.ok(info="成功处理了请求!") + return Result.warning_("Bot未连接...") + except Exception as e: + logger.error("调用API错误", "/delete_friend", e=e) + return Result.fail(f"{type(e)}: {e}") + + +@router.get( + "/get_friend_detail", dependencies=[authentication()], description="获取好友详情" +) +async def _(bot_id: str, user_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + if fd := [ + x + for x in await bots[bot_id].get_friend_list() + if str(x["user_id"]) == user_id + ]: + like_plugin_list = ( + await Statistics.filter(user_id=user_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + module_list = [x[0] for x in like_plugin_list] + plugins = await PluginInfo.filter(module__in=module_list).all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + user = fd[0] + user_detail = UserDetail( + user_id=user_id, + ava_url=AVA_URL.format(user_id), + nickname=user["nickname"], + remark=user["remark"], + is_ban=await BanConsole.is_ban(user_id), + chat_count=await ChatHistory.filter(user_id=user_id).count(), + call_count=await Statistics.filter(user_id=user_id).count(), + like_plugin=like_plugin, + ) + return Result.ok(user_detail) + else: + return Result.warning_("未添加指定好友...") + return Result.warning_("无Bot连接...") + + +@router.get( + "/get_group_detail", dependencies=[authentication()], description="获取群组详情" +) +async def _(bot_id: str, group_id: str) -> Result: + if bots := nonebot.get_bots(): + if bot_id in bots: + group = await GroupConsole.get_or_none(group_id=group_id) + if not group: + return Result.warning_("指定群组未被收录...") + like_plugin_list = ( + await Statistics.filter(group_id=group_id) + .annotate(count=Count("id")) + .group_by("plugin_name") + .order_by("-count") + .limit(5) + .values_list("plugin_name", "count") + ) + like_plugin = {} + plugins = await PluginInfo.all() + module2name = {p.module: p.name for p in plugins} + for data in like_plugin_list: + name = module2name.get(data[0]) or data[0] + like_plugin[name] = data[1] + close_plugins = [] + if group.block_plugin: + for module in group.block_plugin.split(","): + module_ = module.replace(":super", "") + is_super_block = module.endswith(":super") + plugin = Plugin( + module=module_, + plugin_name=module, + is_super_block=is_super_block, + ) + plugin.plugin_name = module2name.get(module) or module + close_plugins.append(plugin) + all_task = await TaskInfo.annotate().values_list("module", "name") + task_module2name = {x[0]: x[1] for x in all_task} + task_list = [] + if group.block_task: + split_task = group.block_task.split(",") + for task in all_task: + task_list.append( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=task[0] not in split_task, + ) + ) + group_detail = GroupDetail( + group_id=group_id, + ava_url=GROUP_AVA_URL.format(group_id, group_id), + name=group.group_name, + member_count=group.member_count, + max_member_count=group.max_member_count, + chat_count=await ChatHistory.filter(group_id=group_id).count(), + call_count=await Statistics.filter(group_id=group_id).count(), + like_plugin=like_plugin, + level=group.level, + status=group.status, + close_plugins=close_plugins, + task=task_list, + ) + return Result.ok(group_detail) + else: + return Result.warning_("未添加指定群组...") + return Result.warning_("无Bot连接...") + + +@router.post( + "/send_message", dependencies=[authentication()], description="获取群组详情" +) +async def _(param: SendMessage) -> Result: + if bots := nonebot.get_bots(): + if param.bot_id in bots: + platform = PlatformUtils.get_platform(bots[param.bot_id]) + if platform != "qq": + return Result.warning_("暂不支持该平台...") + try: + if param.user_id: + await bots[param.bot_id].send_private_msg( + user_id=str(param.user_id), message=param.message + ) + else: + await bots[param.bot_id].send_group_msg( + group_id=str(param.group_id), message=param.message + ) + except Exception as e: + return Result.fail(str(e)) + return Result.ok("发送成功!") + return Result.warning_("指定Bot未连接...") + return Result.warning_("无Bot连接...") + + +MSG_LIST = [] + +ID2NAME = {} + + +async def message_handle( + sub_log: str, type: Literal["private", "group"] +) -> Message | None: + global MSG_LIST, ID2NAME + pattern = PRIVATE_PATTERN if type == "private" else GROUP_PATTERN + msg_id = None + uid = None + gid = None + msg = None + img_list = re.findall(IMAGE_PATTERN, sub_log) + if r := re.search(pattern, sub_log): + if type == "private": + msg_id = r.group(1) + uid = r.group(2) + msg = r.group(3) + if uid not in ID2NAME: + if user := await FriendUser.get_or_none(user_id=uid): + ID2NAME[uid] = user.user_name or user.nickname + else: + msg_id = r.group(1) + uid = r.group(2) + gid = r.group(3) + msg = r.group(4) + if gid not in ID2NAME: + if user := await GroupInfoUser.get_or_none(user_id=uid, group_id=gid): + ID2NAME[uid] = user.user_name or user.nickname + if at_list := re.findall(AT_PATTERN, msg): + user_list = await GroupInfoUser.filter( + user_id__in=at_list, group_id=gid + ).all() + id2name = {u.user_id: (u.user_name or u.nickname) for u in user_list} + for qq in at_list: + msg = re.sub(rf"\[CQ:at,qq={qq}\]", f"@{id2name[qq] or ''}", msg) + if msg_id in MSG_LIST: + return + MSG_LIST.append(msg_id) + messages = [] + if msg and uid: + rep = re.split(r"\[CQ:image.*\]", msg) + if img_list: + for i in range(len(rep)): + messages.append(MessageItem(type="text", msg=rep[i])) + if i < len(img_list): + messages.append(MessageItem(type="img", msg=img_list[i])) + else: + messages = [MessageItem(type="text", msg=x) for x in rep] + return Message( + object_id=uid if type == "private" else gid, # type: ignore + user_id=uid, + group_id=gid, + message=messages, + name=ID2NAME.get(uid) or "", + ava_url=AVA_URL.format(uid), + ) + return None + + +@ws_router.websocket("/chat") +async def _(websocket: WebSocket): + await websocket.accept() + + async def log_listener(log: str): + global MSG_LIST, ID2NAME + sub_log = re.sub(SUB_PATTERN, "", log) + img_list = re.findall(IMAGE_PATTERN, sub_log) + if "message.private.friend" in log: + if message := await message_handle(sub_log, "private"): + await websocket.send_json(message.dict()) + else: + if r := re.search(GROUP_PATTERN, sub_log): + if message := await message_handle(sub_log, "group"): + await websocket.send_json(message.dict()) + if len(MSG_LIST) > 30: + MSG_LIST = MSG_LIST[-1:] + + LOG_STORAGE.listeners.add(log_listener) + try: + while websocket.client_state == WebSocketState.CONNECTED: + recv = await websocket.receive() + except WebSocketDisconnect: + pass + finally: + LOG_STORAGE.listeners.remove(log_listener) + return diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/model.py b/zhenxun/plugins/web_ui/api/tabs/manage/model.py new file mode 100644 index 00000000..a1e16bf4 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/manage/model.py @@ -0,0 +1,265 @@ +from typing import Literal + +from pydantic import BaseModel + + +class Group(BaseModel): + """ + 群组信息 + """ + + group_id: str + """群组id""" + group_name: str + """群组名称""" + member_count: int + """成员人数""" + max_member_count: int + """群组最大人数""" + + +class Task(BaseModel): + """ + 被动技能 + """ + + name: str + """被动名称""" + zh_name: str + """被动中文名称""" + status: bool + """状态""" + + +class Plugin(BaseModel): + """ + 插件 + """ + + module: str + """模块名""" + plugin_name: str + """中文名""" + is_super_block: bool + """是否超级用户禁用""" + + +class GroupResult(BaseModel): + """ + 群组返回数据 + """ + + group_id: str + """群组id""" + group_name: str + """群组名称""" + ava_url: str + """群组头像""" + + +class Friend(BaseModel): + """ + 好友数据 + """ + + user_id: str + """用户id""" + nickname: str = "" + """昵称""" + remark: str = "" + """备注""" + ava_url: str = "" + """头像url""" + + +class UpdateGroup(BaseModel): + """ + 更新群组信息 + """ + + group_id: str + """群号""" + status: bool + """状态""" + level: int + """群权限""" + task: list[str] + """被动状态""" + close_plugins: list[str] + """关闭插件""" + + +class FriendRequestResult(BaseModel): + """ + 好友/群组请求管理 + """ + + bot_id: str + """bot_id""" + oid: int + """排序""" + id: str + """id""" + flag: str + """flag""" + nickname: str | None + """昵称""" + comment: str | None + """备注信息""" + ava_url: str + """头像""" + type: str + """类型 private group""" + + +class GroupRequestResult(FriendRequestResult): + """ + 群聊邀请请求 + """ + + invite_group: str + """邀请群聊""" + group_name: str | None + """群聊名称""" + + +class HandleRequest(BaseModel): + """ + 操作请求接收数据 + """ + + bot_id: str | None = None + """bot_id""" + id: int + """数据id""" + request_type: Literal["private", "group"] + """类型""" + + +class LeaveGroup(BaseModel): + """ + 退出群聊 + """ + + bot_id: str + """bot_id""" + group_id: str + """群聊id""" + + +class DeleteFriend(BaseModel): + """ + 删除好友 + """ + + bot_id: str + """bot_id""" + user_id: str + """用户id""" + + +class ReqResult(BaseModel): + """ + 好友/群组请求列表 + """ + + friend: list[FriendRequestResult] = [] + """好友请求列表""" + group: list[GroupRequestResult] = [] + """群组请求列表""" + + +class UserDetail(BaseModel): + """ + 用户详情 + """ + + user_id: str + """用户id""" + ava_url: str + """头像url""" + nickname: str + """昵称""" + remark: str + """备注""" + is_ban: bool + """是否被ban""" + chat_count: int + """发言次数""" + call_count: int + """功能调用次数""" + like_plugin: dict[str, int] + """最喜爱的功能""" + + +class GroupDetail(BaseModel): + """ + 用户详情 + """ + + group_id: str + """群组id""" + ava_url: str + """头像url""" + name: str + """名称""" + member_count: int + """成员数""" + max_member_count: int + """最大成员数""" + chat_count: int + """发言次数""" + call_count: int + """功能调用次数""" + like_plugin: dict[str, int] + """最喜爱的功能""" + level: int + """群权限""" + status: bool + """状态(睡眠)""" + close_plugins: list[Plugin] + """关闭的插件""" + task: list[Task] + """被动列表""" + + +class MessageItem(BaseModel): + + type: str + """消息类型""" + msg: str + """内容""" + + +class Message(BaseModel): + """ + 消息 + """ + + object_id: str + """主体id user_id 或 group_id""" + user_id: str + """用户id""" + group_id: str | None = None + """群组id""" + message: list[MessageItem] + """消息""" + name: str + """用户名称""" + ava_url: str + """用户头像""" + + +class SendMessage(BaseModel): + """ + 发送消息 + """ + + bot_id: str + """bot id""" + user_id: str | None = None + """用户id""" + group_id: str | None = None + """群组id""" + message: str + """消息""" diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py new file mode 100644 index 00000000..fce8f8e0 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -0,0 +1,187 @@ +import re + +import cattrs +from fastapi import APIRouter, Query + +from zhenxun.configs.config import Config +from zhenxun.models.plugin_info import PluginInfo as DbPluginInfo +from zhenxun.services.log import logger +from zhenxun.utils.enum import BlockType, PluginType + +from ....base_model import Result +from ....utils import authentication +from .model import ( + PluginConfig, + PluginCount, + PluginDetail, + PluginInfo, + PluginSwitch, + UpdatePlugin, +) + +router = APIRouter(prefix="/plugin") + + +@router.get( + "/get_plugin_list", dependencies=[authentication()], deprecated="获取插件列表" # type: ignore +) +async def _( + plugin_type: list[PluginType] = Query(None), menu_type: str | None = None +) -> Result: + try: + plugin_list: list[PluginInfo] = [] + query = DbPluginInfo + if plugin_type: + query = query.filter(plugin_type__in=plugin_type) + if menu_type: + query = query.filter(menu_type=menu_type) + plugins = await query.all() + for plugin in plugins: + plugin_info = PluginInfo( + module=plugin.module, + plugin_name=plugin.name, + default_status=plugin.default_status, + limit_superuser=plugin.limit_superuser, + cost_gold=plugin.cost_gold, + menu_type=plugin.menu_type, + version=plugin.version or 0, + level=plugin.level, + status=plugin.status, + author=plugin.author, + ) + plugin_list.append(plugin_info) + except Exception as e: + logger.error("调用API错误", "/get_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(plugin_list, "拿到了新鲜出炉的数据!") + + +@router.get( + "/get_plugin_count", dependencies=[authentication()], deprecated="获取插件数量" # type: ignore +) +async def _() -> Result: + plugin_count = PluginCount() + plugin_count.normal = await DbPluginInfo.filter( + plugin_type=PluginType.NORMAL + ).count() + plugin_count.admin = await DbPluginInfo.filter( + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN] + ).count() + plugin_count.superuser = await DbPluginInfo.filter( + plugin_type=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN] + ).count() + plugin_count.other = await DbPluginInfo.filter( + plugin_type=PluginType.HIDDEN + ).count() + return Result.ok(plugin_count) + + +@router.post( + "/update_plugin", dependencies=[authentication()], description="更新插件参数" +) +async def _(plugin: UpdatePlugin) -> Result: + try: + db_plugin = await DbPluginInfo.get_or_none(module=plugin.module) + if not db_plugin: + return Result.fail("插件不存在...") + db_plugin.default_status = plugin.default_status + db_plugin.limit_superuser = plugin.limit_superuser + db_plugin.cost_gold = plugin.cost_gold + db_plugin.level = plugin.level + db_plugin.menu_type = plugin.menu_type + db_plugin.block_type = plugin.block_type + if plugin.block_type == BlockType.ALL: + db_plugin.status = False + else: + db_plugin.status = True + await db_plugin.save() + # 配置项 + if plugin.configs and (configs := Config.get(plugin.module)): + for key in plugin.configs: + if c := configs.configs.get(key): + value = plugin.configs[key] + if c.type and value is not None: + value = cattrs.structure(value, c.type) + Config.set_config(plugin.module, key, value) + except Exception as e: + logger.error("调用API错误", "/update_plugins", e=e) + return Result.fail(f"{type(e)}: {e}") + return Result.ok(info="已经帮你写好啦!") + + +@router.post("/change_switch", dependencies=[authentication()], description="开关插件") +async def _(param: PluginSwitch) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=param.module) + if not db_plugin: + return Result.fail("插件不存在...") + if not param.status: + db_plugin.block_type = BlockType.ALL + db_plugin.status = False + else: + db_plugin.block_type = None + db_plugin.status = True + await db_plugin.save() + return Result.ok(info="成功改变了开关状态!") + + +@router.get( + "/get_plugin_menu_type", dependencies=[authentication()], description="获取插件类型" +) +async def _() -> Result: + menu_type_list = [] + result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) + for r in result: + if r not in menu_type_list: + menu_type_list.append(r) + return Result.ok(menu_type_list) + + +@router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") +async def _(module: str) -> Result: + db_plugin = await DbPluginInfo.get_or_none(module=module) + if not db_plugin: + return Result.fail("插件不存在...") + config_list = [] + if config := Config.get(module): + for cfg in config.configs: + type_str = "" + type_inner = None + x = str(config.configs[cfg].type) + r = re.search(r"", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + else: + r = re.search(r"typing\.(.*)\[(.*)\]", str(config.configs[cfg].type)) + if r: + type_str = r.group(1) + if type_str: + type_str = type_str.lower() + type_inner = r.group(2) + if type_inner: + type_inner = [x.strip() for x in type_inner.split(",")] + config_list.append( + PluginConfig( + module=module, + key=cfg, + value=config.configs[cfg].value, + help=config.configs[cfg].help, + default_value=config.configs[cfg].default_value, + type=type_str, + type_inner=type_inner, # type: ignore + ) + ) + plugin_info = PluginDetail( + module=module, + plugin_name=db_plugin.name, + default_status=db_plugin.default_status, + limit_superuser=db_plugin.limit_superuser, + cost_gold=db_plugin.cost_gold, + menu_type=db_plugin.menu_type, + version=db_plugin.version or "0", + level=db_plugin.level, + status=db_plugin.status, + author=db_plugin.author, + config_list=config_list, + block_type=db_plugin.block_type, + ) + return Result.ok(plugin_info) diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py new file mode 100644 index 00000000..e2952038 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/model.py @@ -0,0 +1,125 @@ +from typing import Any + +from pydantic import BaseModel + +from zhenxun.utils.enum import BlockType + + +class PluginSwitch(BaseModel): + """ + 插件开关 + """ + + module: str + """模块""" + status: bool + """开关状态""" + + +class UpdateConfig(BaseModel): + """ + 配置项修改参数 + """ + + module: str + """模块""" + key: str + """配置项key""" + value: Any + """配置项值""" + + +class UpdatePlugin(BaseModel): + """ + 插件修改参数 + """ + + module: str + """模块""" + default_status: bool + """默认开关""" + limit_superuser: bool + """限制超级用户""" + cost_gold: int + """金币花费""" + menu_type: str + """插件菜单类型""" + level: int + """插件所需群权限""" + block_type: BlockType | None = None + """禁用类型""" + configs: dict[str, Any] | None = None + """配置项""" + + +class PluginInfo(BaseModel): + """ + 基本插件信息 + """ + + module: str + """插件名称""" + plugin_name: str + """插件中文名称""" + default_status: bool + """默认开关""" + limit_superuser: bool + """限制超级用户""" + cost_gold: int + """花费金币""" + menu_type: str + """插件菜单类型""" + version: str + """插件版本""" + level: int + """群权限""" + status: bool + """当前状态""" + author: str | None = None + """作者""" + block_type: BlockType | None = None + """禁用类型""" + + +class PluginConfig(BaseModel): + """ + 插件配置项 + """ + + module: str + """模块""" + key: str + """键""" + value: Any + """值""" + help: str | None = None + """帮助""" + default_value: Any + """默认值""" + type: Any = None + """值类型""" + type_inner: list[str] | None = None + """List Tuple等内部类型检验""" + + +class PluginCount(BaseModel): + """ + 插件数量 + """ + + normal: int = 0 + """普通插件""" + admin: int = 0 + """管理员插件""" + superuser: int = 0 + """超级用户插件""" + other: int = 0 + """其他插件""" + + +class PluginDetail(PluginInfo): + """ + 插件详情 + """ + + config_list: list[PluginConfig] diff --git a/zhenxun/plugins/web_ui/api/tabs/system/__init__.py b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py new file mode 100644 index 00000000..61430144 --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/system/__init__.py @@ -0,0 +1,121 @@ +import os +import shutil +from pathlib import Path +from typing import List, Optional + +from fastapi import APIRouter + +from ....base_model import Result +from ....utils import authentication, get_system_disk +from .model import AddFile, DeleteFile, DirFile, RenameFile, SaveFile + +router = APIRouter(prefix="/system") + + + +@router.get("/get_dir_list", dependencies=[authentication()], description="获取文件列表") +async def _(path: Optional[str] = None) -> Result: + base_path = Path(path) if path else Path() + data_list = [] + for file in os.listdir(base_path): + data_list.append(DirFile(is_file=not (base_path / file).is_dir(), name=file, parent=path)) + return Result.ok(data_list) + + +@router.get("/get_resources_size", dependencies=[authentication()], description="获取文件列表") +async def _(full_path: Optional[str] = None) -> Result: + return Result.ok(await get_system_disk(full_path)) + + +@router.post("/delete_file", dependencies=[authentication()], description="删除文件") +async def _(param: DeleteFile) -> Result: + path = Path(param.full_path) + if not path or not path.exists(): + return Result.warning_("文件不存在...") + try: + path.unlink() + return Result.ok('删除成功!') + except Exception as e: + return Result.warning_('删除失败: ' + str(e)) + +@router.post("/delete_folder", dependencies=[authentication()], description="删除文件夹") +async def _(param: DeleteFile) -> Result: + path = Path(param.full_path) + if not path or not path.exists() or path.is_file(): + return Result.warning_("文件夹不存在...") + try: + shutil.rmtree(path.absolute()) + return Result.ok('删除成功!') + except Exception as e: + return Result.warning_('删除失败: ' + str(e)) + + +@router.post("/rename_file", dependencies=[authentication()], description="重命名文件") +async def _(param: RenameFile) -> Result: + path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) + if not path or not path.exists(): + return Result.warning_("文件不存在...") + try: + path.rename(path.parent / param.name) + return Result.ok('重命名成功!') + except Exception as e: + return Result.warning_('重命名失败: ' + str(e)) + + +@router.post("/rename_folder", dependencies=[authentication()], description="重命名文件夹") +async def _(param: RenameFile) -> Result: + path = (Path(param.parent) / param.old_name) if param.parent else Path(param.old_name) + if not path or not path.exists() or path.is_file(): + return Result.warning_("文件夹不存在...") + try: + new_path = path.parent / param.name + shutil.move(path.absolute(), new_path.absolute()) + return Result.ok('重命名成功!') + except Exception as e: + return Result.warning_('重命名失败: ' + str(e)) + + +@router.post("/add_file", dependencies=[authentication()], description="新建文件") +async def _(param: AddFile) -> Result: + path = (Path(param.parent) / param.name) if param.parent else Path(param.name) + if path.exists(): + return Result.warning_("文件已存在...") + try: + path.open('w') + return Result.ok('新建文件成功!') + except Exception as e: + return Result.warning_('新建文件失败: ' + str(e)) + + +@router.post("/add_folder", dependencies=[authentication()], description="新建文件夹") +async def _(param: AddFile) -> Result: + path = (Path(param.parent) / param.name) if param.parent else Path(param.name) + if path.exists(): + return Result.warning_("文件夹已存在...") + try: + path.mkdir() + return Result.ok('新建文件夹成功!') + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) + + +@router.get("/read_file", dependencies=[authentication()], description="读取文件") +async def _(full_path: str) -> Result: + path = Path(full_path) + if not path.exists(): + return Result.warning_("文件不存在...") + try: + text = path.read_text(encoding='utf-8') + return Result.ok(text) + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) + +@router.post("/save_file", dependencies=[authentication()], description="读取文件") +async def _(param: SaveFile) -> Result: + path = Path(param.full_path) + try: + with path.open('w') as f: + f.write(param.content) + return Result.ok("更新成功!") + except Exception as e: + return Result.warning_('新建文件夹失败: ' + str(e)) \ No newline at end of file diff --git a/zhenxun/plugins/web_ui/api/tabs/system/model.py b/zhenxun/plugins/web_ui/api/tabs/system/model.py new file mode 100644 index 00000000..b3b5a45f --- /dev/null +++ b/zhenxun/plugins/web_ui/api/tabs/system/model.py @@ -0,0 +1,64 @@ + + + +from datetime import datetime +from typing import Literal, Optional + +from pydantic import BaseModel + + +class DirFile(BaseModel): + + """ + 文件或文件夹 + """ + + is_file: bool + """是否为文件""" + name: str + """文件夹或文件名称""" + parent: Optional[str] = None + """父级""" + +class DeleteFile(BaseModel): + + """ + 删除文件 + """ + + full_path: str + """文件全路径""" + +class RenameFile(BaseModel): + + """ + 删除文件 + """ + parent: Optional[str] + """父路径""" + old_name: str + """旧名称""" + name: str + """新名称""" + + +class AddFile(BaseModel): + + """ + 新建文件 + """ + parent: Optional[str] + """父路径""" + name: str + """新名称""" + + +class SaveFile(BaseModel): + + """ + 保存文件 + """ + full_path: str + """全路径""" + content: str + """内容""" diff --git a/zhenxun/plugins/web_ui/auth/__init__.py b/zhenxun/plugins/web_ui/auth/__init__.py new file mode 100644 index 00000000..6551d1ad --- /dev/null +++ b/zhenxun/plugins/web_ui/auth/__init__.py @@ -0,0 +1,47 @@ +import json +from datetime import timedelta + +import nonebot +from fastapi import APIRouter, Depends +from fastapi.security import OAuth2PasswordRequestForm + +from zhenxun.configs.config import Config + +from ..base_model import Result +from ..utils import ( + ACCESS_TOKEN_EXPIRE_MINUTES, + create_token, + get_user, + token_data, + token_file, +) + +app = nonebot.get_app() + + +router = APIRouter() + + +@router.post("/login") +async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): + username = Config.get_config("web-ui", "username") + password = Config.get_config("web-ui", "password") + if not username or not password: + return Result.fail("你滴配置文件里用户名密码配置项为空", 998) + if username != form_data.username or password != form_data.password: + return Result.fail("真笨, 账号密码都能记错!", 999) + user = get_user(form_data.username) + if not user: + return Result.fail("用户不存在...", 997) + access_token = create_token( + user=user, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), + ) + token_data["token"].append(access_token) + if len(token_data["token"]) > 3: + token_data["token"] = token_data["token"][1:] + with open(token_file, "w", encoding="utf8") as f: + json.dump(token_data, f, ensure_ascii=False, indent=4) + return Result.ok( + {"access_token": access_token, "token_type": "bearer"}, "欢迎回家, 欧尼酱!" + ) diff --git a/zhenxun/plugins/web_ui/base_model.py b/zhenxun/plugins/web_ui/base_model.py new file mode 100644 index 00000000..67bb280f --- /dev/null +++ b/zhenxun/plugins/web_ui/base_model.py @@ -0,0 +1,108 @@ +from datetime import datetime +from typing import Any, Generic, Optional, TypeVar + +from pydantic import BaseModel, validator +from typing_extensions import Self + +T = TypeVar("T") + + +class User(BaseModel): + username: str + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + + +class Result(BaseModel): + """ + 总体返回 + """ + + suc: bool + """调用状态""" + code: int = 200 + """code""" + info: str = "操作成功" + """info""" + warning: Optional[str] = None + """警告信息""" + data: Any = None + """返回数据""" + + @classmethod + def warning_(cls, info: str, code: int = 200) -> Self: + return cls(suc=True, warning=info, code=code) + + @classmethod + def fail(cls, info: str = "异常错误", code: int = 500) -> Self: + return cls(suc=False, info=info, code=code) + + @classmethod + def ok(cls, data: Any = None, info: str = "操作成功", code: int = 200) -> Self: + return cls(suc=True, info=info, code=code, data=data) + + +class QueryModel(BaseModel, Generic[T]): + """ + 基本查询条件 + """ + + index: int + """页数""" + size: int + """每页数量""" + data: T + """携带数据""" + + @validator("index") + def index_validator(cls, index): + if index < 1: + raise ValueError("查询下标小于1...") + return index + + @validator("size") + def size_validator(cls, size): + if size < 1: + raise ValueError("每页数量小于1...") + return size + + +class BaseResultModel(BaseModel): + """ + 基础返回 + """ + + total: int + """总页数""" + data: Any + """数据""" + + +class SystemStatus(BaseModel): + """ + 系统状态 + """ + + cpu: float + memory: float + disk: float + check_time: datetime + + +class SystemFolderSize(BaseModel): + """ + 资源文件占比 + """ + + name: str + """名称""" + size: float + """大小""" + full_path: Optional[str] + """完整路径""" + is_dir: bool + """是否为文件夹""" diff --git a/zhenxun/plugins/web_ui/config.py b/zhenxun/plugins/web_ui/config.py new file mode 100644 index 00000000..0f16949a --- /dev/null +++ b/zhenxun/plugins/web_ui/config.py @@ -0,0 +1,36 @@ +import nonebot +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from strenum import StrEnum + +app = nonebot.get_app() + +origins = ["*"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160" + +GROUP_AVA_URL = "http://p.qlogo.cn/gh/{}/{}/640/" + + +class QueryDateType(StrEnum): + """ + 查询日期类型 + """ + + DAY = "day" + """日""" + WEEK = "week" + """周""" + MONTH = "month" + """月""" + YEAR = "year" + """年""" diff --git a/zhenxun/plugins/web_ui/utils.py b/zhenxun/plugins/web_ui/utils.py new file mode 100644 index 00000000..f39f36ac --- /dev/null +++ b/zhenxun/plugins/web_ui/utils.py @@ -0,0 +1,136 @@ +import os +from datetime import datetime, timedelta +from pathlib import Path + +import psutil +import ujson as json +from fastapi import Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError, jwt +from nonebot.utils import run_sync + +from zhenxun.configs.config import Config +from zhenxun.configs.path_config import DATA_PATH + +from .base_model import SystemFolderSize, SystemStatus, User + +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login") + +token_file = DATA_PATH / "web_ui" / "token.json" +token_file.parent.mkdir(parents=True, exist_ok=True) +token_data = {"token": []} +if token_file.exists(): + try: + token_data = json.load(open(token_file, "r", encoding="utf8")) + except json.JSONDecodeError: + pass + + +def get_user(uname: str) -> User | None: + """获取账号密码 + + 参数: + uname: uname + + 返回: + Optional[User]: 用户信息 + """ + username = Config.get_config("web-ui", "username") + password = Config.get_config("web-ui", "password") + if username and password and uname == username: + return User(username=username, password=password) + + +def create_token(user: User, expires_delta: timedelta | None = None): + """创建token + + 参数: + user: 用户信息 + expires_delta: 过期时间. + """ + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) + return jwt.encode( + claims={"sub": user.username, "exp": expire}, + key=SECRET_KEY, + algorithm=ALGORITHM, + ) + + +def authentication(): + """权限验证 + + 异常: + JWTError: JWTError + HTTPException: HTTPException + """ + + # if token not in token_data["token"]: + def inner(token: str = Depends(oauth2_scheme)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username, expire = payload.get("sub"), payload.get("exp") + user = get_user(username) # type: ignore + if user is None: + raise JWTError + except JWTError: + raise HTTPException( + status_code=400, detail="登录验证失败或已失效, 踢出房间!" + ) + + return Depends(inner) + + +def _get_dir_size(dir_path: Path) -> float: + """获取文件夹大小 + + 参数: + dir_path: 文件夹路径 + """ + size = 0 + for root, dirs, files in os.walk(dir_path): + size += sum([os.path.getsize(os.path.join(root, name)) for name in files]) + return size + + +@run_sync +def get_system_status() -> SystemStatus: + """获取系统信息等""" + cpu = psutil.cpu_percent() + memory = psutil.virtual_memory().percent + disk = psutil.disk_usage("/").percent + return SystemStatus( + cpu=cpu, + memory=memory, + disk=disk, + check_time=datetime.now().replace(microsecond=0), + ) + + +@run_sync +def get_system_disk( + full_path: str | None, +) -> list[SystemFolderSize]: + """获取资源文件大小等""" + base_path = Path(full_path) if full_path else Path() + other_size = 0 + data_list = [] + for file in os.listdir(base_path): + f = base_path / file + if f.is_dir(): + size = _get_dir_size(f) / 1024 / 1024 + data_list.append( + SystemFolderSize(name=file, size=size, full_path=str(f), is_dir=True) + ) + else: + other_size += f.stat().st_size / 1024 / 1024 + if other_size: + data_list.append( + SystemFolderSize( + name="other_file", size=other_size, full_path=full_path, is_dir=False + ) + ) + return data_list diff --git a/zhenxun/utils/enum.py b/zhenxun/utils/enum.py index 681f9768..bf6c5daa 100644 --- a/zhenxun/utils/enum.py +++ b/zhenxun/utils/enum.py @@ -97,3 +97,5 @@ class RequestHandleType(StrEnum): """拒绝""" IGNORE = "IGNORE" """忽略""" + EXPIRE = "EXPIRE" + """过期或失效""" diff --git a/zhenxun/utils/plugin_models/base.py b/zhenxun/utils/plugin_models/base.py new file mode 100644 index 00000000..a50f6b4a --- /dev/null +++ b/zhenxun/utils/plugin_models/base.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel + + +class CommonSql(BaseModel): + + sql: str + """sql语句""" + remark: str + """备注""" From 870fa0b8b64cb441c189f8c8d85b3f0908108a18 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 05:02:51 +0800 Subject: [PATCH 067/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py index fce8f8e0..f9aea30b 100644 --- a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -44,7 +44,7 @@ async def _( limit_superuser=plugin.limit_superuser, cost_gold=plugin.cost_gold, menu_type=plugin.menu_type, - version=plugin.version or 0, + version=plugin.version or "0", level=plugin.level, status=plugin.status, author=plugin.author, From d55d1c558c0febe19778d194d3e808e15a8536bf Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 13:00:46 +0800 Subject: [PATCH 068/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/init/init_plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 005f446e..3e77471d 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -338,7 +338,9 @@ async def plugin_migration(): elif get_block == "group": block_type = BlockType.GROUP plugin.block_type = block_type - await PluginInfo.bulk_update(plugins, ["status", "block_type"], 10) + await plugin.save(update_fields=["status", "block_type"]) + # TODO: tortoise.exceptions.OperationalError: syntax error at or near "ALL" + # await PluginInfo.bulk_update(plugins, ["status", "block_type"], 10) plugin_file.unlink() logger.info("迁移插件数据完成!") From 9fc0dbfc2b1715fce32209621d9c93773ac66dcd Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 13:26:26 +0800 Subject: [PATCH 069/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=AD=BE=E5=88=B0=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=BD=8D=E7=BD=AE=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 75ee333f..68b2da11 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -237,7 +237,7 @@ async def _generate_card( await bk.paste(A, (0, 150)) await bk.text((30, 167), "Accumulative check-in for") _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45 - await bk.paste(sign_day_img, (380, 158)) + await bk.paste(sign_day_img, (398, 158)) await bk.text((_x, 167), "days") await bk.paste(data_img, (220, 370)) await bk.paste(lik_text1_img, (220, 240)) From 930ff5b09da454e98e87ba72bf51bf7973f572d5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 17:08:33 +0800 Subject: [PATCH 070/132] =?UTF-8?q?=F0=9F=90=9B=20:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BC=80=E7=AE=B1=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E6=AD=A3?= =?UTF-8?q?=E5=88=99=E8=A1=A8=E8=BE=BE=E5=BC=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/open_cases/__init__.py | 22 ---------------------- zhenxun/plugins/open_cases/command.py | 2 +- zhenxun/plugins/open_cases/open_cases_c.py | 2 +- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/zhenxun/plugins/open_cases/__init__.py b/zhenxun/plugins/open_cases/__init__.py index e4ee738a..bac0bcc2 100644 --- a/zhenxun/plugins/open_cases/__init__.py +++ b/zhenxun/plugins/open_cases/__init__.py @@ -112,28 +112,6 @@ __plugin_meta__ = PluginMetadata( ) -# cases_matcher_group = MatcherGroup(priority=5, permission=GROUP, block=True) - - -# k_open_case = cases_matcher_group.on_command("开箱") -# reload_count = cases_matcher_group.on_command("重置开箱", permission=SUPERUSER) -# total_case_data = cases_matcher_group.on_command( -# "我的开箱", aliases={"开箱统计", "开箱查询", "查询开箱"} -# ) -# group_open_case_statistics = cases_matcher_group.on_command("群开箱统计") -# open_multiple = cases_matcher_group.on_regex("(.*)连开箱(.*)?") -# update_case = on_command( -# "更新武器箱", aliases={"更新皮肤"}, priority=1, permission=SUPERUSER, block=True -# ) -# update_case_image = on_command( -# "更新武器箱图片", priority=1, permission=SUPERUSER, block=True -# ) -# show_case = on_command("查看武器箱", priority=5, block=True) -# my_knifes = on_command("我的金色", priority=1, permission=GROUP, block=True) -# show_skin = on_command("查看皮肤", priority=5, block=True) -# price_trends = on_command("价格趋势", priority=5, block=True) - - @_price_matcher.handle() async def _( session: EventSession, diff --git a/zhenxun/plugins/open_cases/command.py b/zhenxun/plugins/open_cases/command.py index a2f85c38..ea86c2fc 100644 --- a/zhenxun/plugins/open_cases/command.py +++ b/zhenxun/plugins/open_cases/command.py @@ -31,7 +31,7 @@ _multiple_matcher = on_alconna( ) _multiple_matcher.shortcut( - r"(?P\d)连开箱(?P.*?)", + r"(?P\d+)连开箱(?P.*?)", command="multiple-open", arguments=["{num}", "{name}"], prefix=True, diff --git a/zhenxun/plugins/open_cases/open_cases_c.py b/zhenxun/plugins/open_cases/open_cases_c.py index 8cdd5b32..74f642aa 100644 --- a/zhenxun/plugins/open_cases/open_cases_c.py +++ b/zhenxun/plugins/open_cases/open_cases_c.py @@ -319,7 +319,7 @@ async def open_multiple_case( Text(f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n"), Image(mark_image.pic2bytes()), Text( - f"\nresult[:-1]\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" + f"\n{result[:-1]}\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" ), ] ) From a15982df5e73bab8d40d63059c113c2750aee168 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 17:12:10 +0800 Subject: [PATCH 071/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=AD=BE=E5=88=B0=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8E=92=E5=BA=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/_data_source.py | 2 +- zhenxun/plugins/ai/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index fea99e1c..9a9ee8f9 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -41,7 +41,7 @@ class SignManage: .values_list("user_id", flat=True) ) index = all_list.index(user_id) + 1 # type: ignore - user_list = await SignUser.annotate().order_by("impression").limit(num).all() + user_list = await SignUser.annotate().order_by("-impression").limit(num).all() user_id_list = [u.user_id for u in user_list] log_list = ( await SignLog.filter(user_id__in=user_id_list) diff --git a/zhenxun/plugins/ai/__init__.py b/zhenxun/plugins/ai/__init__.py index 3b6d694a..52862676 100644 --- a/zhenxun/plugins/ai/__init__.py +++ b/zhenxun/plugins/ai/__init__.py @@ -19,7 +19,7 @@ from .data_source import get_chat_result, hello, no_result __plugin_meta__ = PluginMetadata( name="AI", description="屑Ai", - usage=""" + usage=f""" 与{NICKNAME}普普通通的对话吧! """.strip(), extra=PluginExtraData( From 0e0e37437e35774267a56ee88ee1dfa9001b3d7a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 17:14:46 +0800 Subject: [PATCH 072/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=AD=BE=E5=88=B0=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8E=92=E5=BA=8F=E9=94=99=E8=AF=AF=E5=8F=8A=E5=A5=BD?= =?UTF-8?q?=E5=8F=8B=E5=88=97=E8=A1=A8=E8=8E=B7=E5=8F=96=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/_data_source.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 9a9ee8f9..0b769c43 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -37,7 +37,7 @@ class SignManage: async def rank(cls, user_id: str, num: int) -> BuildImage: all_list = ( await SignUser.annotate() - .order_by("impression") + .order_by("-impression") .values_list("user_id", flat=True) ) index = all_list.index(user_id) + 1 # type: ignore @@ -49,7 +49,6 @@ class SignManage: .group_by("user_id") .values_list("user_id", "count") ) - uid2cnt = {l[0]: l[1] for l in log_list} column_name = ["排名", "-", "名称", "好感度", "签到次数", "平台"] friend_list = await FriendUser.filter(user_id__in=user_id_list).values_list( "user_id", "user_name" @@ -69,7 +68,7 @@ class SignManage: (bytes, 30, 30) if user.platform == "qq" else "", uid2name.get(user.user_id), user.impression, - uid2cnt.get(user.user_id) or 0, + user.sign_count, (PLATFORM_PATH.get(user.platform), 30, 30), ] ) From a815c3bcc59aedfb22e25d74b6ac5bd00d286999 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 17:30:59 +0800 Subject: [PATCH 073/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=A5=BD=E6=84=9F=E5=BA=A6=E6=80=BB=E6=8E=92=E8=A1=8C=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=85=B3=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/__init__.py | 36 +++++++++------- .../builtin_plugins/sign_in/_data_source.py | 42 +++++++++++++------ 2 files changed, 51 insertions(+), 27 deletions(-) diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index 964c1d5e..5818852f 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -30,6 +30,7 @@ __plugin_meta__ = PluginMetadata( 签到 我的签到 好感度排行 + 好感度总排行 * 签到时有 3% 概率 * 2 * """.strip(), extra=PluginExtraData( @@ -83,8 +84,12 @@ _sign_matcher = on_alconna( "签到", Option("--my", action=store_true, help_text="我的签到"), Option( - "-l|--list", Args["num", int, 10], action=store_true, help_text="好感度排行" + "-l|--list", + Args["num", int, 10], + action=store_true, + help_text="好感度排行", ), + Option("-g|--global", action=store_true, help_text="全局排行"), ), priority=5, block=True, @@ -104,11 +109,16 @@ _sign_matcher.shortcut( prefix=True, ) +_sign_matcher.shortcut( + "好感度总排行", + command="签到", + arguments=["--list", "--global"], + prefix=True, +) + @_sign_matcher.assign("$main") -async def _( - session: EventSession, arparma: Arparma, nickname: str = UserName() -): +async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()): if session.id1: if path := await SignManage.sign(session, nickname): logger.info("签到成功", arparma.header_result, session=session) @@ -117,9 +127,7 @@ async def _( @_sign_matcher.assign("my") -async def _( - session: EventSession, arparma: Arparma, nickname: str = UserName() -): +async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()): if session.id1: if image := await SignManage.sign(session, nickname, True): logger.info("查看我的签到", arparma.header_result, session=session) @@ -128,14 +136,14 @@ async def _( @_sign_matcher.assign("list") -async def _( - session: EventSession, - arparma: Arparma, - num: int, - nickname: str = UserName() -): +async def _(session: EventSession, arparma: Arparma, num: int): + gid = session.id3 or session.id2 + if not arparma.find("global") and not gid: + await Text("私聊中无法查看 '好感度排行',请发送 '好感度总排行'").finish() if session.id1: - if image := await SignManage.rank(session.id1, num): + if arparma.find("global"): + gid = None + if image := await SignManage.rank(session.id1, num, gid): logger.info("查看签到排行", arparma.header_result, session=session) await Image(image.pic2bytes()).finish() return Text("用户id为空...").send() diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 0b769c43..0607619c 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -19,7 +19,7 @@ from zhenxun.utils.utils import get_user_avatar from ._random_event import random_event from .goods_register import driver -from .utils import SIGN_TODAY_CARD_PATH, get_card +from .utils import get_card ICON_PATH = IMAGE_PATH / "_icon" @@ -34,21 +34,33 @@ PLATFORM_PATH = { class SignManage: @classmethod - async def rank(cls, user_id: str, num: int) -> BuildImage: + async def rank( + cls, user_id: str, num: int, group_id: str | None = None + ) -> BuildImage: + """好感度排行 + + 参数: + user_id: 用户id + num: 排行榜数量 + group_id: 群组id + + 返回: + BuildImage: 构造图片 + """ + query = SignUser + if group_id: + user_list = await GroupInfoUser.filter(group_id=group_id).values_list( + "user_id", flat=True + ) + query = query.filter(user_id__in=user_list) all_list = ( - await SignUser.annotate() + await query.annotate() .order_by("-impression") .values_list("user_id", flat=True) ) index = all_list.index(user_id) + 1 # type: ignore - user_list = await SignUser.annotate().order_by("-impression").limit(num).all() + user_list = await query.annotate().order_by("-impression").limit(num).all() user_id_list = [u.user_id for u in user_list] - log_list = ( - await SignLog.filter(user_id__in=user_id_list) - .annotate(count=Count("id")) - .group_by("user_id") - .values_list("user_id", "count") - ) column_name = ["排名", "-", "名称", "好感度", "签到次数", "平台"] friend_list = await FriendUser.filter(user_id__in=user_id_list).values_list( "user_id", "user_name" @@ -72,9 +84,13 @@ class SignManage: (PLATFORM_PATH.get(user.platform), 30, 30), ] ) - return await ImageTemplate.table_page( - "好感度排行", f"你的排名在第 {index} 位哦!", column_name, data_list - ) + if group_id: + title = "好感度群组内排行" + tip = f"你的排名在本群第 {index} 位哦!" + else: + title = "好感度全局排行" + tip = f"你的排名在全局第 {index} 位哦!" + return await ImageTemplate.table_page(title, tip, column_name, data_list) @classmethod async def sign( From 714f5564d0c6a84c062df69dbe58db11aa4ac91c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 17:35:08 +0800 Subject: [PATCH 074/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E7=BB=9F=E8=AE=A1=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=B8=E5=85=B3=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/chat_history/chat_message_handle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index c7766eee..6e2cc8c9 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -54,6 +54,7 @@ _matcher = on_alconna( Option("--des", action=store_true, help_text="逆序"), Args["type?", ["日", "周", "月", "年"]]["count?", int, 10], ), + aliases={"消息统计"}, priority=5, block=True, ) From 2a49d86d9ff358da80c0fbd001c70b68671b9ddc Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 18:34:22 +0800 Subject: [PATCH 075/132] =?UTF-8?q?feat=E2=9C=A8:=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8A=9F=E8=83=BDID=E6=90=9C=E7=B4=A2=E5=8F=8A=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=9B=B8=E5=85=B3=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help/_data_source.py | 10 +++++++--- zhenxun/builtin_plugins/help/_utils.py | 15 +++++++++------ zhenxun/builtin_plugins/init/init_plugin.py | 3 +++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/zhenxun/builtin_plugins/help/_data_source.py b/zhenxun/builtin_plugins/help/_data_source.py index 68da9f41..72467b2e 100644 --- a/zhenxun/builtin_plugins/help/_data_source.py +++ b/zhenxun/builtin_plugins/help/_data_source.py @@ -25,10 +25,14 @@ async def get_plugin_help(name: str, is_superuser: bool) -> str | BuildImage: """获取功能的帮助信息 参数: - name: 插件名称 + name: 插件名称或id is_superuser: 是否为超级用户 """ - if plugin := await PluginInfo.get_or_none(name__iexact=name): + if name.isdigit(): + plugin = await PluginInfo.get_or_none(id=int(name), load_status=True) + else: + plugin = await PluginInfo.get_or_none(name__iexact=name, load_status=True) + if plugin: _plugin = nonebot.get_plugin_by_module_name(plugin.module_path) if _plugin and _plugin.metadata: items = None @@ -45,6 +49,6 @@ async def get_plugin_help(name: str, is_superuser: bool) -> str | BuildImage: "用法": _plugin.metadata.usage, } if items: - return await ImageTemplate.hl_page(name, items) + return await ImageTemplate.hl_page(plugin.name, items) return "糟糕! 该功能没有帮助喔..." return "没有查找到这个功能噢..." diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py index 9b2e783d..4e5c6247 100644 --- a/zhenxun/builtin_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -46,7 +46,9 @@ class HelpImageBuild: 对插件按照菜单类型分类 """ if not self._data: - self._data = await PluginInfo.filter(plugin_type=PluginType.NORMAL) + self._data = await PluginInfo.filter( + plugin_type=PluginType.NORMAL, load_status=True + ) if not self._sort_data: for plugin in self._data: menu_type = plugin.menu_type or "normal" @@ -143,18 +145,19 @@ class HelpImageBuild: await self.sort_type() font_size = 24 build_type = Config.get_config("help", "TYPE") - _image = BuildImage.build_text_image("1", size=font_size) font = BuildImage.load_font("HYWenHei-85W.ttf", 20) for idx, menu_type in enumerate(self._sort_data.keys()): plugin_list = self._sort_data[menu_type] - wh_list = [BuildImage.get_text_size(x.name, font) for x in plugin_list] + wh_list = [ + BuildImage.get_text_size(f"{x.id}.{x.name}", font) for x in plugin_list + ] wh_list.append(BuildImage.get_text_size(menu_type, font)) # sum_height = sum([x[1] for x in wh_list]) if build_type == "VV": sum_height = 50 * len(plugin_list) + 10 else: sum_height = (font_size + 6) * len(plugin_list) + 10 - max_width = max([x[0] for x in wh_list]) + 20 + max_width = max([x[0] for x in wh_list]) + 30 bk = BuildImage( max_width + 40, sum_height + 50, @@ -199,7 +202,7 @@ class HelpImageBuild: await B.paste(name_image, (0, curr_h), center_type="width") curr_h += name_image.h + 5 else: - await B.text((10, curr_h), f"{i + 1}.{plugin.name}", text_color) + await B.text((10, curr_h), f"{plugin.id}.{plugin.name}", text_color) if pos: await B.line(pos, (236, 66, 7), 3) curr_h += font_size + 5 @@ -221,7 +224,7 @@ class HelpImageBuild: h = 10 for msg in [ "目前支持的功能列表:", - "可以通过 ‘帮助[功能名称]’ 来获取对应功能的使用方法", + "可以通过 ‘帮助 [功能名称或功能Id]’ 来获取对应功能的使用方法", ]: text = await BuildImage.build_text_image(msg, "HYWenHei-85W.ttf", 24) await B.paste(text, (w, h)) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index 3e77471d..a74a72ba 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -101,9 +101,11 @@ async def _(): limit_list: list[PluginLimit] = [] task_list: list[TaskInfo] = [] module2id = {} + load_plugin = [] if module_list := await PluginInfo.all().values("id", "module_path"): module2id = {m["module_path"]: m["id"] for m in module_list} for plugin in get_loaded_plugins(): + load_plugin.append(plugin.name) if plugin.metadata: await _handle_setting(plugin, plugin_list, limit_list, task_list) create_list = [] @@ -160,6 +162,7 @@ async def _(): 10, ) await data_migration() + await PluginInfo.filter(module__not_in=load_plugin).update(load_status=False) async def data_migration(): From 9899f37528d8aa81f572238a73fd2d3aaa0cc56d Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 18:36:55 +0800 Subject: [PATCH 076/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E6=95=B0=E6=8D=AE=E8=BF=81=E7=A7=BB=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/init/init_plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index a74a72ba..a2380d5c 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -162,6 +162,7 @@ async def _(): 10, ) await data_migration() + await PluginInfo.filter(module__in=load_plugin).update(load_status=True) await PluginInfo.filter(module__not_in=load_plugin).update(load_status=False) From 3b2273e75b89d2b09fb2ac83dcd0ace9c3259602 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 19:10:31 +0800 Subject: [PATCH 077/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B8=AE=E5=8A=A9=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E7=B1=BB=E5=9E=8B=E9=87=8D=E5=A4=8D=E5=8F=8A=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=A8=A1=E7=B3=8A=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help/_utils.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/zhenxun/builtin_plugins/help/_utils.py b/zhenxun/builtin_plugins/help/_utils.py index 4e5c6247..89588314 100644 --- a/zhenxun/builtin_plugins/help/_utils.py +++ b/zhenxun/builtin_plugins/help/_utils.py @@ -52,6 +52,8 @@ class HelpImageBuild: if not self._sort_data: for plugin in self._data: menu_type = plugin.menu_type or "normal" + if menu_type == "normal": + menu_type = "功能" if not self._sort_data.get(menu_type): self._sort_data[menu_type] = [] self._sort_data[menu_type].append(plugin) @@ -206,19 +208,21 @@ class HelpImageBuild: if pos: await B.line(pos, (236, 66, 7), 3) curr_h += font_size + 5 - if menu_type == "normal": - menu_type = "功能" await bk.text((0, 14), menu_type, center_type="width") await bk.paste(B, (0, 50)) await bk.transparent(2) # await bk.acircle_corner(point_list=['lt', 'rt']) self._image_list.append(bk) image_group, h = group_image(self._image_list) + + async def _a(image: BuildImage): + await image.filter("GaussianBlur", 5) + B = await build_sort_image( image_group, h, background_path=BACKGROUND_PATH, - background_handle=lambda image: image.filter("GaussianBlur", 5), + background_handle=_a, ) w = 10 h = 10 From 6120c3bc449d93c58b2825347bd9f4e98baf6720 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 19:54:46 +0800 Subject: [PATCH 078/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD=E4=B8=AD=E7=9A=84=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 1 + zhenxun/services/log.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index ef7ed6ff..c529ba8c 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -17,6 +17,7 @@ require("nonebot_plugin_apscheduler") require("nonebot_plugin_alconna") require("nonebot_plugin_session") require("nonebot_plugin_saa") +require("nonebot_plugin_userinfo") from nonebot_plugin_saa import enable_auto_select_bot diff --git a/zhenxun/services/log.py b/zhenxun/services/log.py index e7bfed4a..a2b5e07a 100644 --- a/zhenxun/services/log.py +++ b/zhenxun/services/log.py @@ -100,7 +100,10 @@ class logger: template = cls.__parser_template( info, command, user_id, group_id, adapter, target, platform ) - logger_.opt(colors=True).info(template) + try: + logger_.opt(colors=True).info(template) + except Exception as e: + logger_.info(template) @classmethod def success( @@ -174,7 +177,10 @@ class logger: ) if e: template += f" || 错误{type(e)}: {e}" - logger_.opt(colors=True).warning(template) + try: + logger_.opt(colors=True).warning(template) + except Exception as e: + logger_.warning(template) @overload @classmethod @@ -232,7 +238,10 @@ class logger: ) if e: template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).error(template) + try: + logger_.opt(colors=True).error(template) + except Exception as e: + logger_.error(template) @overload @classmethod @@ -290,7 +299,10 @@ class logger: ) if e: template += f" || 错误 {type(e)}: {e}" - logger_.opt(colors=True).debug(template) + try: + logger_.opt(colors=True).debug(template) + except Exception as e: + logger_.debug(template) @classmethod def __parser_template( From 19fa40e9671bff77119632b66c6b8a26213caf93 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 31 Jul 2024 22:50:36 +0800 Subject: [PATCH 079/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BE=A4=E6=9D=83=E9=99=90=E6=A3=80=E6=B5=8B=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=BE=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/hooks/_auth_checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 2aef17c1..e5cd6be9 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -417,7 +417,9 @@ class AuthChecker: """ if group_id := session.id3 or session.id2: text = message.extract_plain_text() - group, _ = await GroupConsole.get_or_create(group_id=group_id) + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) if group.level < 0: """群权限小于0""" logger.debug( From 1ecb364f307b3c10de351c1c28a1f921eab15e14 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 2 Aug 2024 19:43:13 +0800 Subject: [PATCH 080/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E8=AF=B7=E6=B1=82=E5=8F=8A=E7=BE=A4=E8=81=8A?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/record_request.py | 46 ++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py index 367031fc..95ad2a58 100644 --- a/zhenxun/builtin_plugins/record_request.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -1,15 +1,12 @@ import time from datetime import datetime -from typing import Dict import nonebot -from nonebot import drivers, on_message, on_request -from nonebot.adapters.onebot.v11 import ( - ActionFailed, - Bot, - FriendRequestEvent, - GroupRequestEvent, -) +from nonebot import on_message, on_request +from nonebot.adapters.onebot.v11 import ActionFailed +from nonebot.adapters.onebot.v11 import Bot as v11Bot +from nonebot.adapters.onebot.v11 import FriendRequestEvent, GroupRequestEvent +from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_saa import TargetQQPrivate, Text @@ -48,7 +45,7 @@ __plugin_meta__ = PluginMetadata( class Timer: - data: Dict[str, float] = {} + data: dict[str, float] = {} @classmethod def check(cls, uid: int | str): @@ -70,9 +67,9 @@ _t = on_message(priority=999, block=False, rule=lambda: False) @friend_req.handle() -async def _(bot: Bot, event: FriendRequestEvent, session: EventSession): +async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSession): + superuser = nonebot.get_driver().config.platform_superusers["qq"][0] if event.user_id and Timer.check(event.user_id): - superuser = nonebot.get_driver().config.platform_superusers["qq"][0] logger.debug(f"收录好友请求...", "好友请求", target=event.user_id) user = await bot.get_stranger_info(user_id=event.user_id) nickname = user["nickname"] @@ -112,7 +109,8 @@ async def _(bot: Bot, event: FriendRequestEvent, session: EventSession): @group_req.handle() -async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): +async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSession): + superuser = nonebot.get_driver().config.platform_superusers["qq"][0] # 邀请 if event.sub_type == "invite": if str(event.user_id) in bot.config.superusers: @@ -126,13 +124,20 @@ async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): await bot.set_group_add_request( flag=event.flag, sub_type="invite", approve=True ) - group_info = await bot.get_group_info(group_id=event.group_id) + if isinstance(bot, v11Bot): + group_info = await bot.get_group_info(group_id=event.group_id) + max_member_count = group_info["max_member_count"] + member_count = group_info["member_count"] + else: + group_info = await bot.get_group_info(group_id=str(event.group_id)) + max_member_count = 0 + member_count = 0 await GroupConsole.update_or_create( - group_id=str(group_info["group_id"]), + group_id=str(event.group_id), defaults={ "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], + "max_member_count": max_member_count, + "member_count": member_count, "group_flag": 1, }, ) @@ -151,9 +156,7 @@ async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): "群聊请求", target=event.group_id, ) - user = await bot.get_stranger_info(user_id=event.user_id) - nickname = await FriendUser.get_user_name(event.user_id) - superuser = int(list(bot.config.superusers)[0]) + nickname = await FriendUser.get_user_name(str(event.user_id)) await Text( f"*****一份入群申请*****\n" f"申请人:{nickname}({event.user_id})\n" @@ -167,12 +170,13 @@ async def _(bot: Bot, event: GroupRequestEvent, session: EventSession): "等待管理员处理吧!", ) await FgRequest.create( - request_type=RequestType.FRIEND, + request_type=RequestType.GROUP, platform=session.platform, bot_id=bot.self_id, flag=event.flag, - user_id=event.user_id, + user_id=str(event.user_id), nickname=nickname, + group_id=str(event.group_id), ) else: logger.debug( From fdb62f72268cbc67e38819e15c9aa6f77602aae5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 2 Aug 2024 19:51:52 +0800 Subject: [PATCH 081/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/__init__.py | 7 ------ zhenxun/builtin_plugins/admin/admin_help.py | 5 ++-- zhenxun/builtin_plugins/admin/admin_watch.py | 6 ----- zhenxun/builtin_plugins/platform/__init__.py | 11 +++++++++ .../platform/qq/group_handle.py | 23 ++++++++++--------- 5 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 zhenxun/builtin_plugins/platform/__init__.py diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index c529ba8c..aae5acb0 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -1,4 +1,3 @@ -import os import uuid from nonebot import require @@ -22,16 +21,10 @@ require("nonebot_plugin_userinfo") from nonebot_plugin_saa import enable_auto_select_bot enable_auto_select_bot() -from pathlib import Path import nonebot import ujson as json -path = Path(__file__).parent / "platform" -for d in os.listdir(path): - nonebot.load_plugins(str((path / d).resolve())) - - driver: Driver = nonebot.get_driver() diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index b151ed09..45c1773e 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -1,5 +1,4 @@ import nonebot -from arclet.alconna import Args, Option from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_alconna.matcher import AlconnaMatcher @@ -7,8 +6,8 @@ from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config -from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH -from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger diff --git a/zhenxun/builtin_plugins/admin/admin_watch.py b/zhenxun/builtin_plugins/admin/admin_watch.py index 02fe9417..919e4bd3 100644 --- a/zhenxun/builtin_plugins/admin/admin_watch.py +++ b/zhenxun/builtin_plugins/admin/admin_watch.py @@ -4,7 +4,6 @@ from nonebot.plugin import PluginMetadata from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData -from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.level_user import LevelUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType @@ -26,11 +25,6 @@ base_config = Config.get("admin_bot_manage") @admin_notice.handle() async def _(event: GroupAdminNoticeEvent): - nickname = event.user_id - if user := await GroupInfoUser.get_or_none( - user_id=str(event.user_id), group_id=str(event.group_id) - ): - nickname = user.user_name if event.sub_type == "set": admin_default_auth = base_config.get("ADMIN_DEFAULT_AUTH") if admin_default_auth is not None: diff --git a/zhenxun/builtin_plugins/platform/__init__.py b/zhenxun/builtin_plugins/platform/__init__.py new file mode 100644 index 00000000..448afcf7 --- /dev/null +++ b/zhenxun/builtin_plugins/platform/__init__.py @@ -0,0 +1,11 @@ +import os +from pathlib import Path + +import nonebot + +path = Path(__file__).parent + +for f in os.listdir(path): + _p = path / f + if _p.is_dir(): + nonebot.load_plugins(str(_p.resolve())) diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index ad2d2ec9..48037a8c 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -27,7 +27,7 @@ from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.level_user import LevelUser from zhenxun.models.plugin_info import PluginInfo from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.enum import PluginType, RequestHandleType from zhenxun.utils.utils import FreqLimiter __plugin_meta__ = PluginMetadata( @@ -110,7 +110,9 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent group_id = str(event.group_id) if user_id == bot.self_id: """新成员为bot本身""" - group = await GroupConsole.get_or_none(group_id=group_id) + group = await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ) if (not group or group.group_flag == 0) and base_config.get("flag"): """群聊不存在或被强制拉群,退出该群""" try: @@ -142,9 +144,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent user_id=int(superuser), message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", ) - elif group_id not in await GroupConsole.all().values_list( - "group_id", flat=True - ): + elif not GroupConsole.exists(group_id=group_id, channel_id__isnull=True): """默认群功能开关""" block_plugin = "" if plugin_list := await PluginInfo.filter(default_status=False).all(): @@ -230,14 +230,15 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent if msg_list: await MessageFactory(msg_list).send() else: + image = ( + IMAGE_PATH + / "qxz" + / random.choice(os.listdir(IMAGE_PATH / "qxz")) + ) await MessageFactory( [ Text("新人快跑啊!!本群现状↓(快使用自定义!)"), - Image( - IMAGE_PATH - / "qxz" - / random.choice(os.listdir(IMAGE_PATH / "qxz")) - ), + Image(image), ] ).send() @@ -256,7 +257,7 @@ async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent operator_name = "None" group = await GroupConsole.filter(group_id=str(group_id)).first() group_name = group.group_name if group else "" - coffee = int(list(bot.config.superusers)[0]) + coffee = int(superuser) await bot.send_private_msg( user_id=coffee, message=f"****呜..一份踢出报告****\n" From 36047693aa079a8369f245fb72ca7ebd31389fc8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 2 Aug 2024 20:46:51 +0800 Subject: [PATCH 082/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=97=AE=E9=A2=98=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/qq/group_handle.py | 2 ++ zhenxun/builtin_plugins/record_request.py | 21 +++++++++++++--- .../superuser/request_manage.py | 25 ++++++++++++------- zhenxun/models/fg_request.py | 2 +- zhenxun/utils/_build_image.py | 12 +++++++++ 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index 48037a8c..a0bb61cc 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -265,6 +265,8 @@ async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent f"踢出了 {group_name}({group_id})\n" f"日期:{str(datetime.now()).split('.')[0]}", ) + if group: + await group.delete() return if str(event.user_id) == bot.self_id: """踢出Bot""" diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py index 95ad2a58..0628dca7 100644 --- a/zhenxun/builtin_plugins/record_request.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -18,7 +18,7 @@ from zhenxun.models.fg_request import FgRequest from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType, RequestType +from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType base_config = Config.get("invite_manager") @@ -95,6 +95,12 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi user_id=str(user["user_id"]), user_name=user["nickname"] ) else: + # 旧请求全部设置为过期 + await FgRequest.filter( + request_type=RequestType.FRIEND, + user_id=str(event.user_id), + handle_type__isnull=True, + ).update(handle_type=RequestHandleType.EXPIRE) await FgRequest.create( request_type=RequestType.FRIEND, platform=session.platform, @@ -121,9 +127,6 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio session=event.user_id, target=event.group_id, ) - await bot.set_group_add_request( - flag=event.flag, sub_type="invite", approve=True - ) if isinstance(bot, v11Bot): group_info = await bot.get_group_info(group_id=event.group_id) max_member_count = group_info["max_member_count"] @@ -141,6 +144,9 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio "group_flag": 1, }, ) + await bot.set_group_add_request( + flag=event.flag, sub_type="invite", approve=True + ) except ActionFailed as e: logger.error( "超级用户自动同意加入群聊发生错误", @@ -169,6 +175,13 @@ async def _(bot: v12Bot | v11Bot, event: GroupRequestEvent, session: EventSessio "请确保已经群主或群管理沟通过!\n" "等待管理员处理吧!", ) + # 旧请求全部设置为过期 + await FgRequest.filter( + request_type=RequestType.GROUP, + user_id=str(event.user_id), + group_id=str(event.group_id), + handle_type__isnull=True, + ).update(handle_type=RequestHandleType.EXPIRE) await FgRequest.create( request_type=RequestType.GROUP, platform=session.platform, diff --git a/zhenxun/builtin_plugins/superuser/request_manage.py b/zhenxun/builtin_plugins/superuser/request_manage.py index 71d6e37f..aa4a1e34 100644 --- a/zhenxun/builtin_plugins/superuser/request_manage.py +++ b/zhenxun/builtin_plugins/superuser/request_manage.py @@ -64,6 +64,7 @@ _req_matcher = on_alconna( permission=SUPERUSER, priority=1, rule=to_me(), + block=True, ) _read_matcher = on_alconna( @@ -81,6 +82,7 @@ _read_matcher = on_alconna( permission=SUPERUSER, priority=1, rule=to_me(), + block=True, ) _clear_matcher = on_alconna( @@ -98,6 +100,7 @@ _clear_matcher = on_alconna( permission=SUPERUSER, priority=1, rule=to_me(), + block=True, ) reg_arg_list = [ @@ -126,7 +129,6 @@ async def _( id: int, arparma: Arparma, ): - request_type = RequestType.FRIEND if handle.startswith("-f") else RequestType.GROUP type_dict = { "a": RequestHandleType.APPROVE, "r": RequestHandleType.REFUSED, @@ -135,11 +137,11 @@ async def _( handle_type = type_dict[handle[-1]] try: if handle_type == RequestHandleType.APPROVE: - await FgRequest.approve(bot, id, request_type) + await FgRequest.approve(bot, id) if handle_type == RequestHandleType.REFUSED: - await FgRequest.refused(bot, id, request_type) + await FgRequest.refused(bot, id) if handle_type == RequestHandleType.IGNORE: - await FgRequest.ignore(bot, id, request_type) + await FgRequest.ignore(id) except NotFoundError: await Text("未发现此id的请求...").finish(reply=True) except Exception: @@ -158,8 +160,8 @@ async def _( if all_request := await FgRequest.filter(handle_type__isnull=True).all(): req_list = list(all_request) req_list.reverse() - friend_req = [] - group_req = [] + friend_req: list[FgRequest] = [] + group_req: list[FgRequest] = [] for req in req_list: if req.request_type == RequestType.FRIEND: friend_req.append(req) @@ -193,9 +195,14 @@ async def _( ) await background.paste(platform_icon, (46, 10)) await background.text((150, 12), req.nickname) - comment_img = await BuildImage.build_text_image( - f"对方留言:{req.comment}", size=15, font_color=(140, 140, 143) - ) + if i == 0: + comment_img = await BuildImage.build_text_image( + f"对方留言:{req.comment}", size=15, font_color=(140, 140, 143) + ) + else: + comment_img = await BuildImage.build_text_image( + f"群组:{req.group_id}", size=15, font_color=(140, 140, 143) + ) await background.paste(comment_img, (150, 65)) tag = await BuildImage.build_text_image( f"{req.platform}", diff --git a/zhenxun/models/fg_request.py b/zhenxun/models/fg_request.py index 43cbfdbc..84f2e4c8 100644 --- a/zhenxun/models/fg_request.py +++ b/zhenxun/models/fg_request.py @@ -107,7 +107,7 @@ class FgRequest(Model): req = await cls.get_or_none(id=id) if not req: raise NotFoundError - req.handle_type = RequestHandleType + req.handle_type = handle_type await req.save(update_fields=["handle_type"]) if bot and handle_type not in [ RequestHandleType.IGNORE, diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index 665d4a5b..deeb3255 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -73,6 +73,18 @@ class BuildImage: def size(self) -> Tuple[int, int]: return self.markImg.size + @classmethod + def open(cls, path: str | Path) -> Self: + """打开图片 + + 参数: + path: 图片路径 + + 返回: + Self: BuildImage + """ + return cls(background=path) + @classmethod async def build_text_image( cls, From 51fcdf649ed56684e70869256987f8680b42326b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 2 Aug 2024 21:38:32 +0800 Subject: [PATCH 083/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BE=A4=E6=9D=83=E9=99=90=E6=A3=80=E6=B5=8B=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E7=BE=A4=E9=97=AE=E9=A2=98=E5=B9=B6=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/admin_help.py | 2 - zhenxun/builtin_plugins/admin/admin_watch.py | 15 +- .../builtin_plugins/admin/welcome_message.py | 1 + .../builtin_plugins/hooks/_auth_checker.py | 5 +- .../platform/qq/group_handle.py | 181 ++++++++++-------- zhenxun/configs/utils/__init__.py | 2 +- 6 files changed, 117 insertions(+), 89 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index 45c1773e..0b56da42 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -21,8 +21,6 @@ from zhenxun.utils.image_utils import ( ) from zhenxun.utils.rules import admin_check, ensure_group -base_config = Config.get("admin_bot_manage") - __plugin_meta__ = PluginMetadata( name="群组管理员帮助", description="管理员帮助列表", diff --git a/zhenxun/builtin_plugins/admin/admin_watch.py b/zhenxun/builtin_plugins/admin/admin_watch.py index 919e4bd3..7fe7fdb5 100644 --- a/zhenxun/builtin_plugins/admin/admin_watch.py +++ b/zhenxun/builtin_plugins/admin/admin_watch.py @@ -3,7 +3,7 @@ from nonebot.adapters.onebot.v11 import GroupAdminNoticeEvent from nonebot.plugin import PluginMetadata from zhenxun.configs.config import Config -from zhenxun.configs.utils import PluginExtraData +from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.models.level_user import LevelUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType @@ -13,7 +13,18 @@ __plugin_meta__ = PluginMetadata( description="检测群管理员变动, 添加与删除管理员默认权限, 当配置项 ADMIN_DEFAULT_AUTH 为空时, 不会添加管理员权限", usage="", extra=PluginExtraData( - author="HibiKier", version="0.1", plugin_type=PluginType.HIDDEN + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + configs=[ + RegisterConfig( + module="admin_bot_manage", + key="ADMIN_DEFAULT_AUTH", + value=5, + help="设置群欢迎消息所需要的管理员权限等级", + default_value=5, + ) + ], ).dict(), ) diff --git a/zhenxun/builtin_plugins/admin/welcome_message.py b/zhenxun/builtin_plugins/admin/welcome_message.py index 7909a036..6cb6c8dc 100644 --- a/zhenxun/builtin_plugins/admin/welcome_message.py +++ b/zhenxun/builtin_plugins/admin/welcome_message.py @@ -35,6 +35,7 @@ __plugin_meta__ = PluginMetadata( admin_level=base_config.get("SET_GROUP_WELCOME_MESSAGE_LEVEL", 2), configs=[ RegisterConfig( + module="admin_bot_manage", key="SET_GROUP_WELCOME_MESSAGE_LEVEL", value=2, help="设置群欢迎消息所需要的管理员权限等级", diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index e5cd6be9..edc71a6e 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -417,9 +417,12 @@ class AuthChecker: """ if group_id := session.id3 or session.id2: text = message.extract_plain_text() - group, _ = await GroupConsole.get_or_create( + group = await GroupConsole.get_or_none( group_id=group_id, channel_id__isnull=True ) + if not group: + """群不存在""" + raise IgnoredException("群不存在") if group.level < 0: """群权限小于0""" logger.debug( diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index a0bb61cc..5f9349e6 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -49,7 +49,7 @@ __plugin_meta__ = PluginMetadata( module="invite_manager", key="flag", value=True, - help="强制拉群后进群回复的内容", + help="强制拉群后进群退出并回复内容", default_value=True, type=bool, ), @@ -113,91 +113,106 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent group = await GroupConsole.get_or_none( group_id=group_id, channel_id__isnull=True ) - if (not group or group.group_flag == 0) and base_config.get("flag"): - """群聊不存在或被强制拉群,退出该群""" - try: - if result_msg := base_config.get("message"): - await bot.send_group_msg( - group_id=event.group_id, message=result_msg + if not group or group.group_flag == 0: + """群聊不存在或被强制拉群""" + if base_config.get("flag"): + """退出群组""" + try: + if result_msg := base_config.get("message"): + await bot.send_group_msg( + group_id=event.group_id, message=result_msg + ) + await bot.set_group_leave(group_id=event.group_id) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,已成功退出群聊 {group_id}...", ) - await bot.set_group_leave(group_id=event.group_id) - await bot.send_private_msg( - user_id=int(superuser), - message=f"触发强制入群保护,已成功退出群聊 {group_id}...", - ) - logger.info( - f"强制拉群或未有群信息,退出群聊成功", - "入群检测", - group_id=event.group_id, - ) - if req := await FgRequest.get_or_none(group_id=group_id): - req.handle_type = RequestHandleType.IGNORE - await req.save(update_fields=["handle_type"]) - except Exception as e: - logger.error( - f"强制拉群或未有群信息,退出群聊失败", - "入群检测", - group_id=event.group_id, - e=e, - ) - await bot.send_private_msg( - user_id=int(superuser), - message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", - ) - elif not GroupConsole.exists(group_id=group_id, channel_id__isnull=True): - """默认群功能开关""" - block_plugin = "" - if plugin_list := await PluginInfo.filter(default_status=False).all(): - for plugin in plugin_list: - block_plugin += f"{plugin.module}," - group_info = await bot.get_group_info(group_id=event.group_id) - await GroupConsole.create( - group_id=group_info["group_id"], - group_name=group_info["group_name"], - max_member_count=group_info["max_member_count"], - member_count=group_info["member_count"], - group_flag=1, - block_plugin=block_plugin, - platform="qq", - ) - admin_default_auth = Config.get_config( - "admin_bot_manage", "ADMIN_DEFAULT_AUTH" - ) - # 即刻刷新权限 - for user_info in await bot.get_group_member_list(group_id=event.group_id): - """即刻刷新权限""" - if ( - user_info["role"] - in [ - "owner", - "admin", - ] - and not await LevelUser.is_group_flag( - user_info["user_id"], group_id + logger.info( + f"强制拉群或未有群信息,退出群聊成功", + "入群检测", + group_id=event.group_id, ) - and admin_default_auth is not None + if req := await FgRequest.get_or_none( + group_id=group_id, handle_type__isnull=True + ): + req.handle_type = RequestHandleType.IGNORE + await req.save(update_fields=["handle_type"]) + except Exception as e: + logger.error( + f"强制拉群或未有群信息,退出群聊失败", + "入群检测", + group_id=event.group_id, + e=e, + ) + await bot.send_private_msg( + user_id=int(superuser), + message=f"触发强制入群保护,退出群聊 {event.group_id} 失败...", + ) + await GroupConsole.filter(group_id=group_id).delete() + else: + """允许群组并设置群认证,默认群功能开关""" + if group: + await GroupConsole.filter( + group_id=group_id, channel_id__isnull=True + ).update(group_flag=1) + else: + block_plugin = "" + if plugin_list := await PluginInfo.filter( + default_status=False + ).all(): + for plugin in plugin_list: + block_plugin += f"{plugin.module}," + group_info = await bot.get_group_info(group_id=event.group_id) + await GroupConsole.create( + group_id=group_info["group_id"], + group_name=group_info["group_name"], + max_member_count=group_info["max_member_count"], + member_count=group_info["member_count"], + group_flag=1, + block_plugin=block_plugin, + platform="qq", + ) + """刷新群管理员权限""" + admin_default_auth = Config.get_config( + "admin_bot_manage", "ADMIN_DEFAULT_AUTH" + ) + # 即刻刷新权限 + for user_info in await bot.get_group_member_list( + group_id=event.group_id ): - await LevelUser.set_level( - user_info["user_id"], - user_info["group_id"], - admin_default_auth, - ) - logger.debug( - f"添加默认群管理员权限: {admin_default_auth}", - "入群检测", - session=user_info["user_id"], - group_id=user_info["group_id"], - ) - if str(user_info["user_id"]) in bot.config.superusers: - await LevelUser.set_level( - user_info["user_id"], user_info["group_id"], 9 - ) - logger.debug( - f"添加超级用户权限: 9", - "入群检测", - session=user_info["user_id"], - group_id=user_info["group_id"], - ) + """即刻刷新权限""" + if ( + user_info["role"] + in [ + "owner", + "admin", + ] + and not await LevelUser.is_group_flag( + user_info["user_id"], group_id + ) + and admin_default_auth is not None + ): + await LevelUser.set_level( + user_info["user_id"], + user_info["group_id"], + admin_default_auth, + ) + logger.debug( + f"添加默认群管理员权限: {admin_default_auth}", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) + if str(user_info["user_id"]) in bot.config.superusers: + await LevelUser.set_level( + user_info["user_id"], user_info["group_id"], 9 + ) + logger.debug( + f"添加超级用户权限: 9", + "入群检测", + session=user_info["user_id"], + group_id=user_info["group_id"], + ) else: join_time = datetime.now() user_info = await bot.get_group_member_info( diff --git a/zhenxun/configs/utils/__init__.py b/zhenxun/configs/utils/__init__.py index d9f4367e..4ba9c47c 100644 --- a/zhenxun/configs/utils/__init__.py +++ b/zhenxun/configs/utils/__init__.py @@ -67,7 +67,7 @@ class ConfigGroup(BaseModel): """配置项列表""" def get(self, c: str, default: Any = None) -> Any: - cfg = self.configs.get(c) + cfg = self.configs.get(c.upper()) if cfg is not None: if cfg.value is not None: return cfg.value From 9fc189b3b746c94030367fd4b48be7e59f79914e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 2 Aug 2024 22:21:03 +0800 Subject: [PATCH 084/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93=E6=97=B6?= =?UTF-8?q?=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/services/db_context.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 4bd8db7b..33b612ad 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -53,9 +53,7 @@ async def init(): i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" try: await Tortoise.init( - db_url=i_bind, - modules={"models": MODELS}, - # timezone="Asia/Shanghai" + db_url=i_bind, modules={"models": MODELS}, timezone="Asia/Shanghai" ) if SCRIPT_METHOD: db = Tortoise.get_connection("default") From 7ab800be979de213fc6418d5a7980a041361e24e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 3 Aug 2024 00:10:24 +0800 Subject: [PATCH 085/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=81=93=E5=85=B7=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/shop/_data_source.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 71a0f8f1..e6e0d9cc 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -386,16 +386,16 @@ class ShopManage: uuid2goods = {item.uuid: item for item in result} column_name = ["-", "使用ID", "名称", "数量", "简介"] for i, p in enumerate(user.props): - prop = uuid2goods[p] - data_list.append( - [ - (ICON_PATH / prop.icon, 33, 33) if prop.icon else "", - i, - prop.goods_name, - user.props[p], - prop.goods_description, - ] - ) + if prop := uuid2goods.get(p): + data_list.append( + [ + (ICON_PATH / prop.icon, 33, 33) if prop.icon else "", + i, + prop.goods_name, + user.props[p], + prop.goods_description, + ] + ) return await ImageTemplate.table_page( f"{name}的道具仓库", "", column_name, data_list From 131cd5ea9aafc1fc3d0789c9f96872f047ab42cd Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 3 Aug 2024 00:34:19 +0800 Subject: [PATCH 086/132] =?UTF-8?q?=F0=9F=90=9B=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=A2=AB=E5=8A=A8=E6=8A=80=E8=83=BD=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/platform/qq/group_handle.py | 5 +++-- zhenxun/builtin_plugins/scheduler/morning.py | 7 ++----- .../builtin_plugins/superuser/broadcast/_data_source.py | 6 ++++-- zhenxun/plugins/fudu.py | 9 +++------ zhenxun/plugins/parse_bilibili/__init__.py | 8 +------- zhenxun/utils/platform.py | 2 +- 6 files changed, 14 insertions(+), 23 deletions(-) diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index 5f9349e6..bb6aee49 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -26,6 +26,7 @@ from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.level_user import LevelUser from zhenxun.models.plugin_info import PluginInfo +from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType, RequestHandleType from zhenxun.utils.utils import FreqLimiter @@ -240,7 +241,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent img_file = path / f"{i}.png" if img_file.exists(): msg_list.append(Image(img_file)) - if not GroupConsole.is_block_task(group_id, "group_welcome"): + if not TaskInfo.is_block("group_welcome", group_id): logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id) if msg_list: await MessageFactory(msg_list).send() @@ -311,5 +312,5 @@ async def _(bot: Bot, event: GroupDecreaseNoticeEvent | GroupMemberDecreaseEvent ) operator_name = operator["card"] if operator["card"] else operator["nickname"] result = f"{user_name} 被 {operator_name} 送走了." - if not GroupConsole.is_block_task(str(event.group_id), "refund_group_remind"): + if not TaskInfo.is_block("refund_group_remind", str(event.group_id)): await group_decrease_handle.send(f"{result}") diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index 6ed2ff1c..1a7aac6a 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -40,11 +40,8 @@ async def _(): ) -async def check(group_id: str, channel_id: str | None) -> bool: - task = await TaskInfo.get_or_none(module="morning_goodnight") - if not task or not task.status: - return False - return await GroupConsole.is_block_task(group_id, "morning_goodnight") +async def check(group_id: str) -> bool: + return not await TaskInfo.is_block("morning_goodnight", group_id) # 早上好 diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 92d02d70..1544506b 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -10,6 +10,7 @@ from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.platform import PlatformUtils @@ -41,8 +42,9 @@ class BroadcastManage: error_count = 0 for group in group_list: try: - if not await GroupConsole.is_block_task( - group.group_id, "broadcast", group.channel_id + if not await TaskInfo.is_block( + group.group_id, + "broadcast", # group.channel_id ): target = PlatformUtils.get_target( bot, None, group.channel_id or group.group_id diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py index db1c0766..1b05b983 100644 --- a/zhenxun/plugins/fudu.py +++ b/zhenxun/plugins/fudu.py @@ -12,10 +12,8 @@ from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.models.task_info import TaskInfo -from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType -from zhenxun.utils.http_utils import AsyncHttpx -from zhenxun.utils.image_utils import get_download_image_hash, get_img_hash +from zhenxun.utils.image_utils import get_download_image_hash from zhenxun.utils.rules import ensure_group __plugin_meta__ = PluginMetadata( @@ -98,12 +96,11 @@ _matcher = on_message(rule=ensure_group, priority=999) @_matcher.handle() async def _(message: UniMsg, event: Event, session: EventSession): - task = await TaskInfo.get_or_none(module="fudu") - if task and not task.status: + group_id = session.id2 or "" + if await TaskInfo.is_block("fudu", group_id): return if event.is_tome(): return - group_id = session.id2 or "" plain_text = message.extract_plain_text() image_list = [] for m in message: diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index ae4d4b95..9d5d4ba2 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -45,13 +45,7 @@ __plugin_meta__ = PluginMetadata( async def _rule(session: EventSession) -> bool: - task = await TaskInfo.get_or_none(module="bilibili_parse") - if not task or not task.status: - logger.debug("b站转发解析被动全局关闭,已跳过...") - return False - if gid := session.id3 or session.id2: - return not await GroupConsole.is_block_task(gid, "bilibili_parse") - return False + return not TaskInfo.is_block("bilibili_parse", session.id3 or session.id2) _matcher = on_message(priority=1, block=False, rule=_rule) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index ac4844e5..d56beb99 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -560,7 +560,7 @@ async def broadcast_group( bot: Bot | list[Bot] | None = None, bot_id: str | Set[str] | None = None, ignore_group: Set[int] | None = None, - check_func: Callable[[str, str | None], Awaitable] | None = None, + check_func: Callable[[str], Awaitable] | None = None, log_cmd: str | None = None, platform: Literal["qq", "dodo", "kaiheila"] | None = None, ): From e2d20b0eb4d074abcf5aaf8da525b5c92909e4fa Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 3 Aug 2024 01:28:08 +0800 Subject: [PATCH 087/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BF=84=E7=BD=97=E6=96=AF=E8=BD=AE=E7=9B=98=E5=81=B6=E5=B0=94?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/models/task_info.py | 20 +++++++++++++++++++ zhenxun/plugins/russian/__init__.py | 14 ++++++++++--- zhenxun/plugins/russian/data_source.py | 27 ++++++++++++++++++-------- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py index 7e02a3d0..7ed1c1c5 100644 --- a/zhenxun/models/task_info.py +++ b/zhenxun/models/task_info.py @@ -2,6 +2,8 @@ from tortoise import fields from zhenxun.services.db_context import Model +from .group_console import GroupConsole + class TaskInfo(Model): id = fields.IntField(pk=True, generated=True, auto_increment=True) @@ -20,3 +22,21 @@ class TaskInfo(Model): class Meta: table = "task_info" table_description = "被动技能基本信息" + + @classmethod + async def is_block(cls, module: str, group_id: str | None) -> bool: + """判断被动技能是否被禁用 + + 参数: + module: 被动技能模块名 + group_id: 群组id + + 返回: + bool: 是否被禁用 + """ + if task := await cls.get_or_none(module=module): + if task.status: + return True + if group_id: + return await GroupConsole.is_block_task(group_id, module) + return False diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index b5396843..2734b704 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -2,7 +2,7 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Arparma from nonebot_plugin_alconna import At as alcAt -from nonebot_plugin_alconna import Match +from nonebot_plugin_alconna import Match, UniMsg from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession @@ -64,10 +64,13 @@ async def _(money: int, num: Match[int], at_user: Match[alcAt]): _russian_matcher.set_path_arg("at_user", at_user.result.target) -@_russian_matcher.got_path("num", prompt="请输入装填子弹的数量!(最多6颗)") +@_russian_matcher.got_path( + "num", prompt="请输入装填子弹的数量!(最多6颗,输入取消来取消装弹)" +) async def _( bot: Bot, session: EventSession, + message: UniMsg, arparma: Arparma, money: int, num: int, @@ -75,10 +78,16 @@ async def _( uname: str = UserName(), ): gid = session.id2 + if message.extract_plain_text() == "取消": + await Text("已取消装弹...").finish() if not session.id1: await Text("用户id为空...").finish() if not gid: await Text("群组id为空...").finish() + if money <= 0: + await Text("赌注金额必须大于0!").finish(reply=True) + if num < 0 or num > 6: + await Text("子弹数量必须在1-6之间!").finish(reply=True) _at_user = at_user.result.target if at_user.available else None rus = Russian( at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=num @@ -94,7 +103,6 @@ async def _( @_accept_matcher.handle() async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): - global a gid = session.id2 if not session.id1: await Text("用户id为空...").finish() diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index 4d1f4568..d7d0c333 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -77,6 +77,17 @@ class RussianManage: bullet_list[i] = 1 return bullet_list + def __remove_job(self, group_id: str): + """移除定时任务 + + 参数: + group_id: 群组id + """ + try: + scheduler.remove_job(f"russian_job_{group_id}") + except JobLookupError: + pass + def __build_job( self, bot: Bot, group_id: str, is_add: bool = False, platform: str | None = None ): @@ -88,10 +99,7 @@ class RussianManage: is_add: 是否添加新定时任务. platform: 平台 """ - try: - scheduler.remove_job(f"russian_job_{group_id}") - except JobLookupError: - pass + self.__remove_job(group_id) if is_add: date = datetime.now() + timedelta(seconds=31) scheduler.add_job( @@ -164,7 +172,7 @@ class RussianManage: else: message_list = [ Text( - "若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】" + "若30秒内无人接受挑战则此次对决作废【首次游玩请at我发送 ’帮助俄罗斯轮盘‘ 来查看命令】" ) ] result = Text( @@ -191,6 +199,8 @@ class RussianManage: return Text("又不是找你决斗,你接受什么啊!气!") if russian.player2: return Text("当前决斗已被其他玩家接受!请等待下局对决!") + if russian.player1[0] == user_id: + return Text("你发起的对决,你接受什么啊!气!") russian.player2 = (user_id, uname) russian.next_user = russian.player1[0] return MessageFactory( @@ -321,11 +331,11 @@ class RussianManage: del self._data[group_id] return Text("规定时间内还未有人接受决斗,当前决斗过期...") return Text("决斗还未开始,,无法结算哦...") - if user_id and user_id not in [russian.player1[0], russian.player1[0]]: - return Text("吃瓜群众不要捣乱!黄牌警告!") + if user_id and user_id not in [russian.player1[0], russian.player2[0]]: + return Text(f"吃瓜群众不要捣乱!黄牌警告!") if not self.__check_is_timeout(group_id): return Text( - f"{russian.player1[1]} 和 {russian.player1[1]} 比赛并未超时,请继续比赛..." + f"{russian.player1[1]} 和 {russian.player2[1]} 比赛并未超时,请继续比赛..." ) win_user = None lose_user = None @@ -379,6 +389,7 @@ class RussianManage: padding=10, color="#f9f6f2", ) + self.__remove_job(group_id) result.append(Image(image.pic2bytes())) del self._data[group_id] return MessageFactory(result) From 321735497f0b10b9b77effa0d2aa7a7411bd9069 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 3 Aug 2024 01:43:52 +0800 Subject: [PATCH 088/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E7=BB=9F=E8=AE=A1=E8=8F=9C=E5=8D=95=E5=92=8C?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/statistics/statistics_handle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py index 862b6595..0179fd9a 100644 --- a/zhenxun/plugins/statistics/statistics_handle.py +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -17,7 +17,7 @@ from zhenxun.utils.enum import PluginType from ._data_source import StatisticsManage __plugin_meta__ = PluginMetadata( - name="功能调用统计可视化", + name="功能调用统计", description="功能调用统计可视化", usage=""" usage: @@ -36,7 +36,7 @@ __plugin_meta__ = PluginMetadata( extra=PluginExtraData( author="HibiKier", version="0.1", - plugin_type=PluginType.ADMIN, + plugin_type=PluginType.NORMAL, menu_type="数据统计", aliases={"功能调用统计"}, superuser_help=""" From 6258a421818f39ab4a7cd049a40cfffbdb29d65b Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 3 Aug 2024 01:46:55 +0800 Subject: [PATCH 089/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=A7=81=E8=81=8A=E4=B8=8B=E5=8A=9F=E8=83=BD=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/statistics/statistics_handle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py index 0179fd9a..f756ad2a 100644 --- a/zhenxun/plugins/statistics/statistics_handle.py +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -145,8 +145,8 @@ async def _( ): plugin_name = name.result if name.available else None st = search_type.result if search_type.available else None - uid = session.id1 if arparma.find("my") else None gid = session.id3 or session.id2 + uid = session.id1 if (arparma.find("my") or not gid) else None is_global = arparma.find("global") if uid and is_global: """个人全局""" From 3eb107a79dc370e66d3a27f82517bf06ea8ee813 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 17:22:30 +0800 Subject: [PATCH 090/132] =?UTF-8?q?=F0=9F=90=9B=20=E6=98=B5=E7=A7=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=B7=BB=E5=8A=A0shortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/nickname.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py index b02b4ac1..4eda8187 100644 --- a/zhenxun/builtin_plugins/nickname.py +++ b/zhenxun/builtin_plugins/nickname.py @@ -3,11 +3,10 @@ from typing import Any, List from nonebot import on_regex from nonebot.adapters import Bot -from nonebot.matcher import Matcher from nonebot.params import Depends, RegexGroup from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import Alconna, Option, UniMsg, on_alconna, store_true +from nonebot_plugin_alconna import Alconna, Option, on_alconna, store_true from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo @@ -70,6 +69,20 @@ _matcher = on_alconna( block=True, ) +_matcher.shortcut( + "我(是谁|叫什么)", + command="nickname", + arguments=["--name"], + prefix=True, +) + +_matcher.shortcut( + "取消昵称", + command="nickname", + arguments=["--cancel"], + prefix=True, +) + CALL_NAME = [ "好啦好啦,我知道啦,{},以后就这么叫你吧", @@ -104,9 +117,7 @@ def CheckNickname(): async def dependency( bot: Bot, - matcher: Matcher, session: EventSession, - message: UniMsg, reg_group: tuple[Any, ...] = RegexGroup(), ): black_word = Config.get_config("nickname", "BLACK_WORD") From ecc8ec3fd8a5787470f96fa6acf5dcc75694a6c7 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 18:49:39 +0800 Subject: [PATCH 091/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=E4=BC=98=E5=8C=96ban?= =?UTF-8?q?=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/ban/__init__.py | 109 ++++++++++-------- .../builtin_plugins/admin/ban/_data_source.py | 21 +++- .../builtin_plugins/hooks/_auth_checker.py | 1 + zhenxun/builtin_plugins/hooks/ban_hook.py | 7 +- zhenxun/models/ban_console.py | 4 +- 5 files changed, 83 insertions(+), 59 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index f57b7b07..d21641c6 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -30,12 +30,12 @@ __plugin_meta__ = PluginMetadata( usage=""" 普通管理员 格式: - ban [At用户] [时长(分钟)] + ban [At用户] -t [时长(分钟)] 示例: - ban @用户 : 永久拉黑用户 - ban @用户 100 : 拉黑用户100分钟 - unban @用户 : 从小黑屋中拉出来 + ban @用户 : 永久拉黑用户 + ban @用户 -t 100 : 拉黑用户100分钟 + unban @用户 : 从小黑屋中拉出来 """.strip(), extra=PluginExtraData( author="HibiKier", @@ -46,15 +46,23 @@ __plugin_meta__ = PluginMetadata( 格式: ban [At用户/用户Id] [时长] ban列表: 获取所有Ban数据 + 群组ban列表: 获取群组Ban数据 用户ban列表: 获取用户Ban数据 + ban列表 -u [用户Id]: 查找指定用户ban数据 + ban列表 -g [群组Id]: 查找指定群组ban数据 + 示例: + ban列表 -u 123456789 : 查找用户123456789的ban数据 + ban列表 -g 123456789 : 查找群组123456789的ban数据 + 私聊下: 示例: - ban 123456789 : 永久拉黑用户123456789 - ban 123456789 100 : 拉黑用户123456789 100分钟 + ban 123456789 : 永久拉黑用户123456789 + ban 123456789 -t 100 : 拉黑用户123456789 100分钟 - ban -g 999999 : 拉黑群组为999999的群组 + ban -g 999999 : 拉黑群组为999999的群组 + ban -g 999999 -t 100 : 拉黑群组为999999的群组 100分钟 unban 123456789 : 从小黑屋中拉出来 unban -g 999999 : 将群组9999999从小黑屋中拉出来 @@ -76,8 +84,9 @@ __plugin_meta__ = PluginMetadata( _ban_matcher = on_alconna( Alconna( "ban", - Args["user?", [str, At]]["duration?", int], + Args["user?", [str, At]], Option("-g|--group", Args["group_id", str]), + Option("-t|--time", Args["duration", int]), ), rule=admin_check("ban", "BAN_LEVEL"), priority=5, @@ -97,33 +106,27 @@ _unban_matcher = on_alconna( _status_matcher = on_alconna( Alconna( - "ban-status", - Option("-u|--user", action=store_true, help_text="过滤用户"), - Option("-g|--group", action=store_true, help_text="过滤群组"), + "ban列表", + Option("-u", Args["user_id", str], help_text="查找用户"), + Option("-g", Args["group_id", str], help_text="查找群组"), + Option("--user", action=store_true, help_text="过滤用户"), + Option("--group", action=store_true, help_text="过滤群组"), ), permission=SUPERUSER, priority=1, block=True, ) -_status_matcher.shortcut( - "ban列表", - command="ban-status", - arguments=[], - prefix=True, -) - - _status_matcher.shortcut( "用户ban列表", - command="ban-status", + command="ban列表", arguments=["--user"], prefix=True, ) _status_matcher.shortcut( "群组ban列表", - command="ban-status", + command="ban列表", arguments=["--group"], prefix=True, ) @@ -131,8 +134,6 @@ _status_matcher.shortcut( @_status_matcher.handle() async def _( - bot: Bot, - session: EventSession, arparma: Arparma, user_id: Match[str], group_id: Match[str], @@ -144,7 +145,7 @@ async def _( filter_type = "group" _user_id = user_id.result if user_id.available else None _group_id = group_id.result if group_id.available else None - if image := await BanManage.build_ban_image(filter_type): + if image := await BanManage.build_ban_image(filter_type, _user_id, _group_id): await Image(image.pic2bytes()).finish(reply=True) else: await Text("数据为空捏...").finish(reply=True) @@ -164,9 +165,11 @@ async def _( if isinstance(user.result, At): user_id = user.result.target else: + if session.id1 not in bot.config.superusers: + await Text("权限不足捏...").finish(reply=True) user_id = user.result _duration = duration.result * 60 if duration.available else -1 - if gid := session.id3 or session.id2: + if (gid := session.id3 or session.id2) and not group_id.available: if group_id.available: gid = group_id.result await BanManage.ban( @@ -181,7 +184,7 @@ async def _( await MessageFactory( [ Text("对 "), - Mention(user_id), # type: ignore + Mention(user_id) if isinstance(user.result, At) else Text(user_id), # type: ignore Text(f" 狠狠惩戒了一番,一脚踢进了小黑屋!"), ] ).finish(reply=True) @@ -211,34 +214,40 @@ async def _( if isinstance(user.result, At): user_id = user.result.target else: + if session.id1 not in bot.config.superusers: + await Text("权限不足捏...").finish(reply=True) user_id = user.result if gid := session.id3 or session.id2: if group_id.available: gid = group_id.result - await BanManage.unban( + if await BanManage.unban( user_id, gid, session, session.id1 in bot.config.superusers - ) - logger.info( - f"管理员UnBan", - arparma.header_result, - session=session, - target=f"{gid}:{user_id}", - ) - await MessageFactory( - [ - Text("将 "), - Mention(user_id), # type: ignore - Text(f" 从黑屋中拉了出来并急救了一下!"), - ] - ).finish(reply=True) + ): + logger.info( + f"管理员UnBan", + arparma.header_result, + session=session, + target=f"{gid}:{user_id}", + ) + await MessageFactory( + [ + Text("将 "), + Mention(user_id) if isinstance(user.result, At) else Text(user_id), # type: ignore + Text(f" 从黑屋中拉了出来并急救了一下!"), + ] + ).finish(reply=True) + else: + await Text(f"该用户不在黑名单中捏...").finish(reply=True) elif session.id1 in bot.config.superusers: _group_id = group_id.result if group_id.available else None - await BanManage.unban(user_id, _group_id, session, True) - logger.info( - f"超级用户UnBan", - arparma.header_result, - session=session, - target=f"{_group_id}:{user_id}", - ) - at_msg = user_id if user_id else f"群组:{_group_id}" - await Text(f"对 {at_msg} 从黑屋中拉了出来并急救了一下!").finish(reply=True) + if await BanManage.unban(user_id, _group_id, session, True): + logger.info( + f"超级用户UnBan", + arparma.header_result, + session=session, + target=f"{_group_id}:{user_id}", + ) + at_msg = user_id if user_id else f"群组:{_group_id}" + await Text(f"对 {at_msg} 从黑屋中拉了出来并急救了一下!").finish(reply=True) + else: + await Text(f"该用户不在黑名单中捏...").finish(reply=True) diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py index 4264258d..9c9df3be 100644 --- a/zhenxun/builtin_plugins/admin/ban/_data_source.py +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -14,22 +14,31 @@ class BanManage: async def build_ban_image( cls, filter_type: Literal["group", "user"] | None, + user_id: str | None = None, + group_id: str | None = None, ) -> BuildImage | None: """构造Ban列表图片 参数: filter_type: 过滤类型 + user_id: 用户id + group_id: 群组id 返回: BuildImage | None: Ban列表图片 """ data_list = None - if not filter_type: - data_list = await BanConsole.all() - elif filter_type == "user": - data_list = await BanConsole.filter(group_id__isnull=True).all() - elif filter_type == "group": - data_list = await BanConsole.filter(user_id__isnull=True).all() + query = BanConsole + if user_id: + query = query.filter(user_id=user_id) + elif group_id: + query = query.filter(group_id=group_id) + else: + if filter_type == "user": + query = query.filter(group_id__isnull=True) + elif filter_type == "group": + query = query.filter(user_id__isnull=True) + data_list = await query.all() if not data_list: return None column_name = [ diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index edc71a6e..661bff03 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -10,6 +10,7 @@ from nonebot_plugin_session import EventSession from pydantic import BaseModel from zhenxun.configs.config import Config +from zhenxun.models.ban_console import BanConsole from zhenxun.models.group_console import GroupConsole from zhenxun.models.level_user import LevelUser from zhenxun.models.plugin_info import PluginInfo diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py index 74d456f3..3d82e56f 100644 --- a/zhenxun/builtin_plugins/hooks/ban_hook.py +++ b/zhenxun/builtin_plugins/hooks/ban_hook.py @@ -34,6 +34,11 @@ async def _( return user_id = session.id1 group_id = session.id3 or session.id2 + if group_id: + if user_id in bot.config.superusers: + return + if await BanConsole.is_ban(None, group_id): + raise IgnoredException("群组处于黑名单中...") if user_id: ban_result = Config.get_config("hook", "BAN_RESULT") if user_id in bot.config.superusers: @@ -62,4 +67,4 @@ async def _( Text(f"{ban_result}\n在..在 {time_str} 后才会理你喔"), ] ).send() - raise IgnoredException("用户处于黑名单中") + raise IgnoredException("用户处于黑名单中...") diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index d15c5401..47fec6af 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -116,7 +116,7 @@ class BanConsole(Model): if await cls.check_ban_time(user_id, group_id): return True else: - if await cls.check_ban_time(user_id): + if await cls.check_ban_time(user_id, group_id): return True await cls.unban(user_id, group_id) return False @@ -140,7 +140,7 @@ class BanConsole(Model): operator: 操作者id """ logger.debug( - f"封禁用户,等级:{ban_level},时长: {duration}", + f"封禁用户/群组,等级:{ban_level},时长: {duration}", target=f"{group_id}:{user_id}", ) user = await cls._get_data(user_id, group_id) From 75fb71dc881e8fc02d9bd22f8b6dfcf9d0415ed5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 19:16:20 +0800 Subject: [PATCH 092/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BE=A4=E7=BB=84=E6=88=90=E5=91=98=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/group_member_update/__init__.py | 3 ++- .../builtin_plugins/admin/group_member_update/_data_source.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py index 1a7bfe4a..9286e750 100644 --- a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py +++ b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py @@ -6,6 +6,7 @@ from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession +from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType @@ -56,7 +57,7 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent): if str(event.user_id) == bot.self_id: await MemberUpdateManage.update(bot, str(event.group_id)) logger.info( - "{NICKNAME}加入群聊更新群组信息", + f"{NICKNAME}加入群聊更新群组信息", "更新群组成员列表", session=event.user_id, group_id=event.group_id, diff --git a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py index 442d337e..ad134625 100644 --- a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py +++ b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py @@ -91,7 +91,7 @@ class MemberUpdateManage: nickname = user_info["card"] or user_info["nickname"] role = user_info["role"] if default_auth: - if role in ["owner", "admin"] and not LevelUser.is_group_flag( + if role in ["owner", "admin"] and not await LevelUser.is_group_flag( str(user_id), group_id ): await LevelUser.set_level(user_id, group_id, default_auth) From 152661141390fe41afeec12216b1874e64471829 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 19:31:21 +0800 Subject: [PATCH 093/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=20=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=8E=92=E8=A1=8C=E6=B7=BB=E5=8A=A0shortcut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/admin_help.py | 1 - .../chat_history/chat_message_handle.py | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index 0b56da42..bdf48929 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -5,7 +5,6 @@ from nonebot_plugin_alconna.matcher import AlconnaMatcher from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from zhenxun.configs.config import Config from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.models.plugin_info import PluginInfo diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index 6e2cc8c9..c23d7ca9 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -1,11 +1,9 @@ from datetime import datetime, timedelta import pytz -from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import ( Alconna, - AlconnaMatch, Args, Arparma, Match, @@ -32,11 +30,14 @@ __plugin_meta__ = PluginMetadata( 消息排行 ?[type [日,周,月,年]] ?[--des] 快捷: - [日,周,月,年]消息排行 + [日,周,月,年]消息排行 ?[数量] 示例: - 消息排行 : 所有记录排行 - 日消息排行 : 今日记录排行 + 消息排行 : 所有记录排行 + 日消息排行 : 今日记录排行 + 周消息排行 : 今日记录排行 + 月消息排行 : 今日记录排行 + 年消息排行 : 今日记录排行 消息排行 周 --des : 逆序周记录排行 """.strip(), extra=PluginExtraData( @@ -60,16 +61,15 @@ _matcher = on_alconna( ) _matcher.shortcut( - r"(?P.+)?消息排行", + r"(?P['日', '周', '月', '年'])?消息(排行|统计)\s?(?P\d+)?", command="消息排行", - arguments=["{type}"], + arguments=["{type}", "{cnt}"], prefix=True, ) @_matcher.handle() async def _( - bot: Bot, session: EventSession, arparma: Arparma, type: Match[str], @@ -111,7 +111,7 @@ async def _( ).replace(microsecond=0) else: date_scope = time_now.replace(microsecond=0) - date_str = f"{date_scope} - 至今" + date_str = f"{str(date_scope).split('+')[0]} - 至今" else: date_str = f"{date_scope[0].replace(microsecond=0)} - {date_scope[1].replace(microsecond=0)}" A = await ImageTemplate.table_page( From 21b73b085eb4edbaffe6a143dd7d240f5ddc58fd Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 19:34:02 +0800 Subject: [PATCH 094/132] =?UTF-8?q?=F0=9F=8E=A8=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=A4=9A=E4=BD=99=E5=8C=85=E5=BC=95=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_history/chat_message.py | 1 - zhenxun/builtin_plugins/help/__init__.py | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py index 602a0199..8200ab5e 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message.py @@ -1,5 +1,4 @@ from nonebot import on_message -from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import UniMsg from nonebot_plugin_apscheduler import scheduler diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 3aa6021d..2c91490d 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -1,13 +1,20 @@ -from pathlib import Path from nonebot.adapters import Bot - from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import Alconna, AlconnaQuery, Args, Match, Option, Query, on_alconna, store_true +from nonebot_plugin_alconna import ( + Alconna, + AlconnaQuery, + Args, + Match, + Option, + Query, + on_alconna, + store_true, +) from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH +from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType @@ -44,7 +51,7 @@ _matcher = on_alconna( Alconna( "功能", Args["name?", str], - Option("-s|--superuser", action=store_true, help_text="超级用户帮助") + Option("-s|--superuser", action=store_true, help_text="超级用户帮助"), ), aliases={"help", "帮助"}, rule=to_me(), @@ -58,7 +65,7 @@ async def _( bot: Bot, name: Match[str], session: EventSession, - is_superuser: Query[bool] = AlconnaQuery("superuser.value", False) + is_superuser: Query[bool] = AlconnaQuery("superuser.value", False), ): _is_superuser = False if is_superuser.available: From 3a1cea09397359d7a939b36ca54a3e4b13cc249d Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 21:16:28 +0800 Subject: [PATCH 095/132] =?UTF-8?q?=E2=9C=A8=20=20=E6=B7=BB=E5=8A=A0exec?= =?UTF-8?q?=5Fsql=E6=9F=A5=E8=AF=A2=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/superuser/exec_sql.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py index 86118baa..2badb14c 100644 --- a/zhenxun/builtin_plugins/superuser/exec_sql.py +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -54,7 +54,9 @@ select a.tablename as name,d.description as desc from pg_tables a async def _(session: EventSession, message: UniMsg): sql_text = message.extract_plain_text().strip() if sql_text.startswith("exec"): - sql_text = sql_text[4:] + sql_text = sql_text[4:].strip() + if not sql_text: + await Text("需要执行的的SQL语句!").finish() logger.info(f"执行SQL语句: {sql_text}", "exec", session=session) try: if not sql_text.lower().startswith("select"): @@ -62,6 +64,20 @@ async def _(session: EventSession, message: UniMsg): else: db = Tortoise.get_connection("default") res = await db.execute_query_dict(sql_text) + _column = [] + for r in res: + if len(r) > len(_column): + _column = r.keys() + data_list = [] + for r in res: + data = [] + for c in _column: + data.append(r.get(c)) + data_list.append(data) + table = await ImageTemplate.table_page( + "EXEC", f"总共有 {len(data_list)} 条数据捏", list(_column), data_list + ) + await Image(table.pic2bytes()).send() except Exception as e: logger.error("执行 SQL 语句失败...", session=session, e=e) await Text(f"执行 SQL 语句失败... {type(e)}").finish() @@ -78,7 +94,9 @@ async def _(session: EventSession): for table in query: data_list.append([table["name"], table["desc"]]) logger.info("查看数据库所有表", "查看所有表", session=session) - table = await ImageTemplate.table_page("数据库表", "", column_name, data_list) + table = await ImageTemplate.table_page( + "数据库表", f"总共有 {len(data_list)} 张表捏", column_name, data_list + ) await Image(table.pic2bytes()).send() except Exception as e: logger.error("获取表数据失败...", session=session, e=e) From f09d4e1101188e2ed4b58582990e2358217c5a1c Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 23:54:28 +0800 Subject: [PATCH 096/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BE=A4=E6=9D=83=E9=99=90=E6=AD=A3=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/superuser/group_manage.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py index c63118c1..fe3d2ce9 100644 --- a/zhenxun/builtin_plugins/superuser/group_manage.py +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -41,10 +41,9 @@ __plugin_meta__ = PluginMetadata( group-manage super-handle : 添加/删除群白名单 group-manage auth-handle : 添加/删除群认证 group-manage del-group : 退群 - + 示例: 修改群权限 7 : 在群组中修改当前群组权限为7 - group-manage modify-level 7 : 在群组中修改当前群组权限为7 group-manage modify-level 7 1234556 : 修改 123456 群组的权限等级为7 添加/删除群白名单 1234567 : 添加/删除 1234567 为群白名单 添加/删除群认证 1234567 : 添加/删除 1234567 为群认证 @@ -84,9 +83,9 @@ _matcher = on_alconna( ) _matcher.shortcut( - "修改群权限", + r"修改群权限\s?(?P-?\d+)\s?(?P\d+)?", command="group-manage", - arguments=["modify-level", "{%0}"], + arguments=["modify-level", "{level}", "{group_id}"], prefix=True, ) From 2926a46ae87fc5e02777140e0c9ec1e7c4f94989 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 4 Aug 2024 23:54:54 +0800 Subject: [PATCH 097/132] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/scheduler/auto_update_group.py | 3 --- zhenxun/builtin_plugins/scheduler/morning.py | 1 - zhenxun/builtin_plugins/shop/_data_source.py | 6 +++--- zhenxun/builtin_plugins/sign_in/__init__.py | 1 - zhenxun/builtin_plugins/sign_in/_data_source.py | 1 - zhenxun/builtin_plugins/sign_in/goods_register.py | 1 - zhenxun/builtin_plugins/superuser/fg_manage.py | 5 +---- zhenxun/builtin_plugins/superuser/group_manage.py | 2 +- 8 files changed, 5 insertions(+), 15 deletions(-) diff --git a/zhenxun/builtin_plugins/scheduler/auto_update_group.py b/zhenxun/builtin_plugins/scheduler/auto_update_group.py index 2a8c5fe2..8e62bc69 100644 --- a/zhenxun/builtin_plugins/scheduler/auto_update_group.py +++ b/zhenxun/builtin_plugins/scheduler/auto_update_group.py @@ -1,8 +1,6 @@ import nonebot from nonebot_plugin_apscheduler import scheduler -from zhenxun.models.friend_user import FriendUser -from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.utils.platform import PlatformUtils @@ -15,7 +13,6 @@ from zhenxun.utils.platform import PlatformUtils ) async def _(): bots = nonebot.get_bots() - _used_group = [] for bot in bots.values(): try: await PlatformUtils.update_group(bot) diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index 1a7aac6a..c254d5e8 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -6,7 +6,6 @@ from nonebot_plugin_saa import Image from zhenxun.configs.config import NICKNAME from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import PluginExtraData, Task -from zhenxun.models.group_console import GroupConsole from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index e6e0d9cc..94f92433 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -2,7 +2,7 @@ import asyncio import inspect import time from types import MappingProxyType -from typing import Any, Callable, Dict, Literal +from typing import Any, Callable, Literal from nonebot.adapters import Bot, Event from nonebot_plugin_alconna import UniMsg @@ -70,7 +70,7 @@ class ShopParam(BaseModel): class ShopManage: - uuid2goods: Dict[str, Goods] = {} + uuid2goods: dict[str, Goods] = {} @classmethod def __build_params( @@ -82,7 +82,7 @@ class ShopManage: goods: Goods, num: int, text: str, - ) -> tuple[ShopParam, Dict[str, Any]]: + ) -> tuple[ShopParam, dict[str, Any]]: """构造参数 参数: diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index 5818852f..3eabb3f4 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -10,7 +10,6 @@ from nonebot_plugin_alconna import ( from nonebot_plugin_apscheduler import scheduler from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 0607619c..3fdf38f2 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -5,7 +5,6 @@ from pathlib import Path import pytz from nonebot_plugin_session import EventSession -from tortoise.functions import Count from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.models.friend_user import FriendUser diff --git a/zhenxun/builtin_plugins/sign_in/goods_register.py b/zhenxun/builtin_plugins/sign_in/goods_register.py index 80beafec..b73cffc0 100644 --- a/zhenxun/builtin_plugins/sign_in/goods_register.py +++ b/zhenxun/builtin_plugins/sign_in/goods_register.py @@ -4,7 +4,6 @@ import nonebot from nonebot.drivers import Driver from nonebot_plugin_session import EventSession -from zhenxun.configs.config import Config from zhenxun.models.sign_user import SignUser from zhenxun.models.user_console import UserConsole from zhenxun.utils.decorator.shop import NotMeetUseConditionsException, shop_register diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py index fe17f8f3..e2d30344 100644 --- a/zhenxun/builtin_plugins/superuser/fg_manage.py +++ b/zhenxun/builtin_plugins/superuser/fg_manage.py @@ -4,13 +4,10 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, on_alconna -from nonebot_plugin_alconna.matcher import AlconnaMatcher from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession -from zhenxun.configs.config import Config -from zhenxun.configs.path_config import DATA_PATH -from zhenxun.configs.utils import ConfigModel, PluginExtraData +from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.rules import admin_check, ensure_group diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py index fe3d2ce9..1b207c58 100644 --- a/zhenxun/builtin_plugins/superuser/group_manage.py +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -44,7 +44,7 @@ __plugin_meta__ = PluginMetadata( 示例: 修改群权限 7 : 在群组中修改当前群组权限为7 - group-manage modify-level 7 1234556 : 修改 123456 群组的权限等级为7 + 修改群权限 7 1234556 : 修改 123456 群组的权限等级为7 添加/删除群白名单 1234567 : 添加/删除 1234567 为群白名单 添加/删除群认证 1234567 : 添加/删除 1234567 为群认证 退群 12344566 : 退出指定群组 From 1dbed2beec2616ae46943e7354ee693084f72c46 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 00:20:27 +0800 Subject: [PATCH 098/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8DTaskIn?= =?UTF-8?q?fo=E7=8A=B6=E6=80=81=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/scheduler/morning.py | 4 ++-- zhenxun/models/task_info.py | 2 +- zhenxun/utils/platform.py | 14 +++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index c254d5e8..acda4b80 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -58,8 +58,8 @@ async def _(): # # 睡觉了 @scheduler.scheduled_job( "cron", - hour=23, - minute=59, + hour=0, + minute=19, ) async def _(): img = Image(IMAGE_PATH / "zhenxun" / "sleep.jpg") diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py index 7ed1c1c5..90aa3c08 100644 --- a/zhenxun/models/task_info.py +++ b/zhenxun/models/task_info.py @@ -35,7 +35,7 @@ class TaskInfo(Model): bool: 是否被禁用 """ if task := await cls.get_or_none(module=module): - if task.status: + if not task.status: return True if group_id: return await GroupConsole.is_block_task(group_id, module) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index d56beb99..a880c877 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -615,22 +615,18 @@ async def broadcast_group( ) ) or key in _used_group: continue - is_continue = False + is_run = False if check_func: if is_coroutine_callable(check_func): - is_continue = not await check_func( - group.group_id, group.channel_id - ) + is_run = await check_func(group.group_id) else: - is_continue = not check_func( - group.group_id, group.channel_id - ) - if is_continue: + is_run = check_func(group.group_id) + if not is_run: continue target = PlatformUtils.get_target( _bot, None, - group.group_id, + group.channel_id or group.group_id, # , group.channel_id ) if target: From f809f0f3b45df3651c68c8c3ff15c59286c9e352 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 00:22:04 +0800 Subject: [PATCH 099/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=99=9A=E5=AE=89=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E6=97=B6?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/scheduler/morning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index acda4b80..c254d5e8 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -58,8 +58,8 @@ async def _(): # # 睡觉了 @scheduler.scheduled_job( "cron", - hour=0, - minute=19, + hour=23, + minute=59, ) async def _(): img = Image(IMAGE_PATH / "zhenxun" / "sleep.jpg") From 2495c404234bc5c2e18cc847a3762b00812f1485 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 01:10:50 +0800 Subject: [PATCH 100/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=87=91=E5=B8=81=E6=B6=88=E8=80=97=E5=BC=82=E5=B8=B8=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtin_plugins/hooks/_auth_checker.py | 20 ++++++++++------ zhenxun/plugins/gold_redbag/__init__.py | 2 +- zhenxun/plugins/russian/data_source.py | 23 +++++++++++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 661bff03..738a7076 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -24,6 +24,7 @@ from zhenxun.utils.enum import ( PluginLimitType, PluginType, ) +from zhenxun.utils.exception import InsufficientGold from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter @@ -233,13 +234,18 @@ class AuthChecker: ) if cost_gold and user_id: """花费金币""" - await UserConsole.reduce_gold( - user_id, - cost_gold, - GoldHandle.PLUGIN, - matcher.plugin.name if matcher.plugin else "", - session.platform, - ) + try: + await UserConsole.reduce_gold( + user_id, + cost_gold, + GoldHandle.PLUGIN, + matcher.plugin.name if matcher.plugin else "", + session.platform, + ) + except InsufficientGold: + if u := await UserConsole.get_user(user_id): + u.gold = 0 + await u.save(update_fields=["gold"]) logger.debug(f"调用功能花费金币: {cost_gold}", "HOOK", session=session) if is_ignore: raise IgnoredException("权限检测 ignore") diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py index 6cc29f22..a24a5fb5 100644 --- a/zhenxun/plugins/gold_redbag/__init__.py +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -36,7 +36,7 @@ __plugin_meta__ = PluginMetadata( * 不同群组同一个节日红包用户只能开一次 - 示例: + 示例: 塞红包 1000 塞红包 1000 10 """.strip(), diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index d7d0c333..e7b5aed2 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -12,6 +12,7 @@ from zhenxun.configs.config import NICKNAME, Config from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.user_console import UserConsole from zhenxun.utils.enum import GoldHandle +from zhenxun.utils.exception import InsufficientGold from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType, text2image from zhenxun.utils.platform import PlatformUtils @@ -183,7 +184,9 @@ class RussianManage: self.__build_job(bot, group_id, True) return MessageFactory(message_list) - def accept(self, group_id: str, user_id: str, uname: str) -> Text | MessageFactory: + async def accept( + self, group_id: str, user_id: str, uname: str + ) -> Text | MessageFactory: """接受对决 参数: @@ -201,6 +204,9 @@ class RussianManage: return Text("当前决斗已被其他玩家接受!请等待下局对决!") if russian.player1[0] == user_id: return Text("你发起的对决,你接受什么啊!气!") + user = await UserConsole.get_user(user_id) + if user.gold < russian.money: + return Text("你没有足够的钱来接受这场挑战...") russian.player2 = (user_id, uname) russian.next_user = russian.player1[0] return MessageFactory( @@ -368,9 +374,18 @@ class RussianManage: await UserConsole.add_gold( win_user[0], russian.money - fee, "russian", platform ) - await UserConsole.reduce_gold( - lose_user[0], russian.money, GoldHandle.PLUGIN, "russian", platform - ) + try: + await UserConsole.reduce_gold( + lose_user[0], + russian.money, + GoldHandle.PLUGIN, + "russian", + platform, + ) + except InsufficientGold: + if u := await UserConsole.get_user(lose_user[0]): + u.gold = 0 + await u.save(update_fields=["gold"]) result = [Text("这场决斗是 "), Mention(win_user[0]), Text(" 胜利了!")] image = await text2image( f"结算:\n" From 76663a13034bae111f550c289df57fbb9250690a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 01:15:36 +0800 Subject: [PATCH 101/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BF=84=E7=BD=97=E6=96=AF=E8=BD=AE=E7=9B=98=E5=AF=B9=E5=86=B3?= =?UTF-8?q?=E6=8E=A5=E5=8F=97=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/russian/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index 2734b704..aa2559ef 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -108,7 +108,7 @@ async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): await Text("用户id为空...").finish() if not gid: await Text("群组id为空...").finish() - result = russian_manage.accept(gid, session.id1, uname) + result = await russian_manage.accept(gid, session.id1, uname) await result.send() logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session) From b20f748b4ff04b964259b9c63f656941e807f416 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 19:57:01 +0800 Subject: [PATCH 102/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?pix=E6=A0=87=E7=AD=BE=E4=B8=8E=E8=BD=AE=E7=9B=98=E6=A6=82?= =?UTF-8?q?=E7=8E=87=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/pix_gallery/pix.py | 6 +++--- zhenxun/plugins/russian/data_source.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py index 0502f8c2..d67e5ada 100644 --- a/zhenxun/plugins/pix_gallery/pix.py +++ b/zhenxun/plugins/pix_gallery/pix.py @@ -79,7 +79,7 @@ __plugin_meta__ = PluginMetadata( _matcher = on_alconna( Alconna( "pix", - Args["tags?", list[str]], + Args["tags?", str] / "\n", Option("-s", action=store_true, help_text="色图"), Option("-r", action=store_true, help_text="r18"), ), @@ -92,7 +92,7 @@ OMEGA_RATIO = None @_matcher.handle() -async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[list[str]]): +async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]): global PIX_RATIO, OMEGA_RATIO gid = session.id3 or session.id2 if not session.id1: @@ -106,7 +106,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[list[ num = 1 # keyword = arg.extract_plain_text().strip() keyword = "" - spt = tags.result if tags.available else [] + spt = tags.result.split() if tags.available else [] if arparma.find("s"): nsfw_tag = 1 elif arparma.find("r"): diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index e7b5aed2..b25b72cf 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -293,7 +293,7 @@ class RussianManage: return result, settle else: """存活""" - p = (russian.bullet_index + 1) / len(russian.bullet_arr) * 100 + p = (russian.bullet_index + russian.bullet_num + 1) / len(russian.bullet_arr) * 100 result = ( random.choice( [ From 4cb350a00385b5de8523bd03dcc0d19abff67ef0 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 20:25:02 +0800 Subject: [PATCH 103/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/models/ban_console.py | 2 -- zhenxun/models/task_info.py | 19 ++++++++++++++++--- zhenxun/plugins/parse_bilibili/__init__.py | 3 +-- zhenxun/plugins/russian/data_source.py | 6 +++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index 47fec6af..9f91438f 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -116,8 +116,6 @@ class BanConsole(Model): if await cls.check_ban_time(user_id, group_id): return True else: - if await cls.check_ban_time(user_id, group_id): - return True await cls.unban(user_id, group_id) return False diff --git a/zhenxun/models/task_info.py b/zhenxun/models/task_info.py index 90aa3c08..3ca1fb49 100644 --- a/zhenxun/models/task_info.py +++ b/zhenxun/models/task_info.py @@ -2,6 +2,7 @@ from tortoise import fields from zhenxun.services.db_context import Model +from .ban_console import BanConsole from .group_console import GroupConsole @@ -25,18 +26,30 @@ class TaskInfo(Model): @classmethod async def is_block(cls, module: str, group_id: str | None) -> bool: - """判断被动技能是否被禁用 + """判断被动技能是否可以发送 参数: module: 被动技能模块名 group_id: 群组id 返回: - bool: 是否被禁用 + bool: 是否可以发送 """ if task := await cls.get_or_none(module=module): + """被动全局状态""" if not task.status: return True if group_id: - return await GroupConsole.is_block_task(group_id, module) + if await GroupConsole.is_block_task(group_id, module): + """群组是否禁用被动""" + return True + if g := await GroupConsole.get_or_none( + group_id=group_id, channel_id__isnull=True + ): + """群组权限是否小于0""" + if g.level < 0: + return True + if await BanConsole.is_ban(None, group_id): + """群组是否被ban""" + return True return False diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index 9d5d4ba2..3fcd136e 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -10,7 +10,6 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task -from zhenxun.models.group_console import GroupConsole from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx @@ -45,7 +44,7 @@ __plugin_meta__ = PluginMetadata( async def _rule(session: EventSession) -> bool: - return not TaskInfo.is_block("bilibili_parse", session.id3 or session.id2) + return not await TaskInfo.is_block("bilibili_parse", session.id3 or session.id2) _matcher = on_message(priority=1, block=False, rule=_rule) diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index b25b72cf..6a6d96a4 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -293,7 +293,11 @@ class RussianManage: return result, settle else: """存活""" - p = (russian.bullet_index + russian.bullet_num + 1) / len(russian.bullet_arr) * 100 + p = ( + (russian.bullet_index + russian.bullet_num + 1) + / len(russian.bullet_arr) + * 100 + ) result = ( random.choice( [ From dfbb7f347eb7a6b07f67cebde0651601c77fdd24 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 21:29:29 +0800 Subject: [PATCH 104/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=87=91=E5=B8=81=E7=BA=A2=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/gold_redbag/__init__.py | 17 +++++++++-------- zhenxun/plugins/russian/__init__.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py index a24a5fb5..bb8feef7 100644 --- a/zhenxun/plugins/gold_redbag/__init__.py +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -1,7 +1,6 @@ import time import uuid from datetime import datetime, timedelta -from typing import List from apscheduler.jobstores.base import JobLookupError from nonebot.adapters import Bot @@ -44,7 +43,7 @@ __plugin_meta__ = PluginMetadata( author="HibiKier", version="0.1", superuser_help=""" - 节日红包 [金额] [红包数] ?[指定主题文字] ? -g [群id] + 节日红包 [金额] [红包数] ?[指定主题文字] ? -g [群id] [群id] ... * 不同群组同一个节日红包用户只能开一次 @@ -116,7 +115,7 @@ _festive_matcher = on_alconna( Alconna( "节日红包", Args["amount", int]["num", int]["text?", str], - Option("-g|--group", Args["group_list", str], help_text="指定群"), + Option("-g|--group", Args["groups", str] / "\n", help_text="指定群"), ), priority=1, block=True, @@ -275,25 +274,24 @@ async def _( async def _( bot: Bot, session: EventSession, - arparma: Arparma, amount: int, num: int, text: Match[str], - group_list: Match[str], - user_name: str = UserName(), + groups: Match[str], ): # TODO: 指定多个群 greetings = "恭喜发财 大吉大利" if text.available: greetings = text.result gl = [] - if group_list.available: - gl = [group_list.result] + if groups.available: + gl = groups.result.strip().split() else: g_l, platform = await PlatformUtils.get_group_list(bot) gl = [g.channel_id or g.group_id for g in g_l] _uuid = str(uuid.uuid1()) FestiveRedBagManage.add(_uuid) + _suc_cnt = 0 for g in gl: if target := PlatformUtils.get_target(bot, group_id=g): group_red_bag = RedBagManager.get_group_data(g) @@ -350,6 +348,9 @@ async def _( Image(image_result.pic2bytes()), ] ).send_to(target=target, bot=bot) + _suc_cnt += 1 logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) except ActionFailed: logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) + if gl: + await Text(f"节日红包发送成功,累计成功发送 {_suc_cnt} 个群组!").send() diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index aa2559ef..ea4a5ac0 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -35,7 +35,7 @@ __plugin_meta__ = PluginMetadata( 结算: 强行结束当前比赛 (仅当一方未开枪超过30秒时可使用) 我的战绩: 对,你的战绩 轮盘胜场排行/轮盘败场排行/轮盘欧洲人排行/轮盘慈善家排行/轮盘最高连胜排行/轮盘最高连败排行: 各种排行榜 - 示例:装弹 3 100 @sdd + 示例:装弹 100 3 @sdd * 注:同一时间群内只能有一场对决 * """.strip(), extra=PluginExtraData( From 1a8378f4337a89219e1cde40d9ca019ef8783e47 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 22:43:55 +0800 Subject: [PATCH 105/132] =?UTF-8?q?=E2=9C=A8=20=E9=92=89=E5=AE=AB=E9=AA=82?= =?UTF-8?q?=E6=88=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/gold_redbag/__init__.py | 1 - zhenxun/plugins/send_voice/__init__.py | 5 +++ zhenxun/plugins/send_voice/dinggong.py | 51 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 zhenxun/plugins/send_voice/__init__.py create mode 100644 zhenxun/plugins/send_voice/dinggong.py diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py index bb8feef7..1b047665 100644 --- a/zhenxun/plugins/gold_redbag/__init__.py +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -279,7 +279,6 @@ async def _( text: Match[str], groups: Match[str], ): - # TODO: 指定多个群 greetings = "恭喜发财 大吉大利" if text.available: greetings = text.result diff --git a/zhenxun/plugins/send_voice/__init__.py b/zhenxun/plugins/send_voice/__init__.py new file mode 100644 index 00000000..eb35e275 --- /dev/null +++ b/zhenxun/plugins/send_voice/__init__.py @@ -0,0 +1,5 @@ +from pathlib import Path + +import nonebot + +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/send_voice/dinggong.py b/zhenxun/plugins/send_voice/dinggong.py new file mode 100644 index 00000000..270e7dac --- /dev/null +++ b/zhenxun/plugins/send_voice/dinggong.py @@ -0,0 +1,51 @@ +import os +import random + +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me +from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, Voice, on_alconna +from nonebot_plugin_saa import Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.path_config import RECORD_PATH +from zhenxun.configs.utils import PluginCdBlock, PluginExtraData +from zhenxun.services.log import logger + +__plugin_meta__ = PluginMetadata( + name="钉宫骂我", + description="请狠狠的骂我一次!", + usage=""" + 多骂我一点,球球了 + 指令: + 骂老子 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + limits=[PluginCdBlock(cd=3, result="就...就算求我骂你也得慢慢来...")], + ).dict(), +) + +_matcher = on_alconna(Alconna("ma-wo"), rule=to_me(), priority=5, block=True) + +_matcher.shortcut( + r".*?骂.*?我.*?", + command="ma-wo", + arguments=[], + prefix=True, +) + +path = RECORD_PATH / "dinggong" + + +@_matcher.handle() +async def _(session: EventSession, arparma: Arparma): + if not path.exists(): + await Text("钉宫语音文件夹不存在...").finish() + files = os.listdir(path) + if not files: + await Text("钉宫语音文件夹为空...").finish() + voice = random.choice(files) + await UniMessage([Voice(path=path / voice)]).send() + await Text(voice.split("_")[1]).send() + logger.info(f"发送钉宫骂人: {voice}", arparma.header_result, session=session) From 409a4f867d1e7c171b1d5633ab4f4a70694bbf83 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Mon, 5 Aug 2024 23:07:17 +0800 Subject: [PATCH 106/132] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=90=8D=E7=A7=B0=E5=BD=93=E5=91=BD=E4=BB=A4=E6=A3=80?= =?UTF-8?q?=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/image/_base/laugh/0.jpg | Bin 0 -> 35486 bytes resources/image/_base/laugh/1.jpg | Bin 0 -> 22225 bytes zhenxun/builtin_plugins/help_help.py | 53 +++++++++++++++++++++++++++ zhenxun/plugins/alapi/cover.py | 3 +- zhenxun/plugins/russian/command.py | 2 +- 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 resources/image/_base/laugh/0.jpg create mode 100644 resources/image/_base/laugh/1.jpg create mode 100644 zhenxun/builtin_plugins/help_help.py diff --git a/resources/image/_base/laugh/0.jpg b/resources/image/_base/laugh/0.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ad37672274207844a9f720570ec08828e89482a GIT binary patch literal 35486 zcmbTe30M>N_CK5eL1j_FK_PB{5RTczy+H+K0A&dX z!39DR2#bmai(7w+Mg>ikwTjDCR9dy_RrI#Dz4!jl1hKup_kEss(I=TnGMO-Qw$J&T zbH4w%^Pfk!sccph3x~(!aCrES`_DaG7|wo@jjhc@ds`dZ$&>A;OmlUb=IA(W{;WAO zTzx72e!dhR@4&_EWr0Dgh2B1lxaF*v6?_3dV3{OI!cAoJ__5eY@RKJ`p5`!(!bW@scLEXCeW)rF9eMg|=OC;`jW+BQSZ> zu1Pl2XE?hMJxTM)6e^7#v@m$l;;?WgDwMt#D(drr+^-U*F8QM>O+i~V>=jAKLtJkh~b@%r54-Ecr z=kCzs;U`a@jr{o2^A~U5jlTcz&tHE1_#1Xzc%0?fWx?~&iY~*q_%mgtN26gYYaJa9`jWRDn^Zo{ICx*lwHPVt<#W9zn_P4~jFu&|v>GNp=`Qxk^cl3YGIV)T z7E4C1-D-Ye(FsTM*pJ}*L8!0gDVEG&ND$9f1#;Add3)2~vzSDC!apcf%1qqtJy&nc zmo)ZfMbQq+PpnrN!WLZlkTDnie$Rk*1LaOxmM?XZufGb73Z}DG#?o45ct>Ro*2+$a zzAclzEgJCfI$+h#nU$j2%tOC*#INY=C^3plgEa{@8#CFLGT-ODjald1)N4{?Ym@GO z`y>8g`S%kptnJ#eD_iID<5Fo`RL%0ySo@Wkt9L(ajR>Z-@KaA*J>o&P>2N=+63^~h zm{!zB@FAgLxeb=Q-Vx2#xhl#TZEYyrH+I-uRbY%-G4^b}xZM44dw7h!uj{u2p;k+1 z)f%c>$+ZL%nKndG{L!OIlVvfk!Jj0P;Wg zupcVAjR~Q;o`(CmccfYs$h^2yY6_&K6`D^fHQ^T0(u#O!oJSx_YIIgVfpht@$Q_r> z@A~4Ih+ry@>4Wq1LAdY&9zLFyHe|KJ5tXoUWHoAq@OvVYdC^RqCt~2bk+tTJs0=nm zT5F|Mqu4Px2^k?K6R%n5Z;=88-FK`{`Fgw*?_0k=u8Vf(drsWCk)zYZ1umzhtc_I8 zuTG*bQCn5>w9MW`mzp*?a#mbXj|L_cj2t8%^FC;Gq}{oyKfX(A8?Dsb;3bqk zJidI(o0T^0R7YwyOS74qu>0Y$Yp(8Ru9N0nkcMSmlO$-fX0kgE3*C<{ebF7Isw{Tz zYSh?As|sgiGJQx=lk4Ojl-V2fBN|cb)p{_(NF2gS%g#4c9s1=V8K96~E z3!*e6SfFI}BhU*R_V9F37jf|*O|t@ZfF4z=4{DR66z1)fD`*5xT<=PibZk} zzn5zPLJA0nAFu{VDeW-1e142cjf_*5jlWrzOF(!GTeHZ=MQ`2;i#~r#qdu%i=v#S$ zOt&gVWmE4W?>46UZtJ4Ux4m5XZdSR(=n@ohNL98t`4}gbIH-3y9k~}Ru?d${i(A7B zi)>XPXRC#Re-?@E#8IlAi>RBF1YOB7x{&`0`I!gieA7mz_vg4561q<!mHj}HTTd2 zZ9>^1KkCh_0Y5{taUJ=f3sD~=Pc1vij}hMZ;zHW+mx~%_5fcWSM@;ci)LEQ)?i-~h znjft_$jAR~Es;UF$Uo6p)H__nKVffj(Smd@Poo*&j-E&$*CXoONz5fkmBt@#O6YWS zgG`FkYP3#aAokom1dJPJFx>A^;kF=(UKHmag>Lb5or(Id&}iHY%0KwYRd%|*>l!02 zpz9wy=lnt|CS z{4kFJ(u?`u2Vt+5GZDi?vvGnZSiw2D{-11??5zA`$`+1gm8cA-@zA69u83G0hT9${mz4te|9yOBW(UuB?`i zZne#qTB8|$Gq#Xqd-@g%9F{R@rQA1~zcXyzh8s=a%Wt%ccS;>B_RF8*L|i|4{NIt5 zB`RKW1DCpoKaf{hIQglO$rAEiC`VGgCJ$E_1C-y%@Or6ipA1iX5`~(sG#`kX*z90> zBg$k&HfH(;(0UWU?tkt|FFIK&xR&{5Wryl&+KHrCJ1_c%Z=U}LC;LVJY(drkbnwoL z|2Fi^Prt`q@S0N9bDy%%<`(Re&-|iI99i&MYZ9zd$VEzGLfxhvJ*uK0S!YH57qql0 zj^B4wHtD*qjB4rQXW|nMcOKl>v~vr&QsMI&IhQA#EW6aCXzSq5aI%(>UHmhn&I@Ta zxI=WZe2*Z{l4O0zo~1rQ#`p7B6dW%arI_tgfUnZHYm6kaMF?N;HVVb1dVT@Q5GDm> z10k~|$omirnxI${ls9JGh%N{X|M2)6?HtraKJR zmV|Ut6Zpvr_j^&Nb@y09xINcY>&nHad-D*5I`N;nkHM`z>gp3*g6C?(xwm2$g19v2 zw!fXB>BGtb#Jj2?(Px9^?PDD%oF*%VEa2{nc zR#zUcxFydW{-?ezs>1%tu7j^ukSo1KQ2|tMNvZU~Y0A;`uOym;-}<)Am56;u6z9K^ zY(Fo*+4Vtd7m-s#Fzq{bt2H72=O04KN^%CO8kV&OQ35sSBjTYL|9HQIuLpk8-pZ*& zV@{7|@eF5lxn~c#o5Gl+hz^ygOJ8YI;bx&TCgiOpY2rB&Ax@yMs8F=HyB~fwu%ANN zxA%tDL8v}Nc9~ESaZu39w{Vyp;<+rBfs-WgQHk}ZXluy#L^?FJR&W!mCP7TV-4{eQ z^d{%p8O;h}JzS1}3sKCBW_tSBkToDal#B;Ql;{Hi1I!nMYL3)o%~ zB}<}7zVm`|uJM7eWG|n;LGvJnpfS10H@v`=p@rYfuVy$BWBR}T=>fxWH%so*U-OfN zoHHi9l|+W%CCtVgQG?{zjhK`YQE|JKrFH9MWj?570lT?e zoK#3E$G2UHmAB1qo->d$cz=$%-Su?LX8qa#w-erroEH33{I&S~M)FV8)ID*YQEqE6S8Sou= zj86~4SO~$KKoxfW<=_LH%^!lxAp{F#SrC{F$~=Aka*kWVH*Rznr7*+qLo<)`^mqNj z0fCR-4H2NTKQ6tDOr6sn-YwIG#VQkwZ)p5%wf-yGSG14Jo2y6lI(ftSo{qj}^A8!) zG^0(&I3&(8URLG#|KM_B7dJiaG0jqs_!Q_nbF}5`N!spN#=kEsxh2BWQhK^)UAeVX zl${lpR_8iEF1Wt+kG|bPhQKIQhm}&>BkdWKM+2JZlyiEmeotRibDLGo^M(DJJ8CDq z(L9JmdVj7B1ua`~m(^{}iqe(IGnAc5yz|;v0mq9mtnjp?oufE3C3Y&@-#rf!K0*_0 za4o`K0zEO8-PJUhyxKE3@s>hR5F+goZb1X8yjYix9{t#nFcx*f^*TZ2=UaotSPE%y5* z7w9Q#jr_%nf8)2!pJ~5)TJJHEDzo=&E3@ ztJH_l7{OE}f4B^e!;lVJVukRS;N`}m*Yq*?gZZ^~h9~GL25rC=1bE7%T2--16&@7a zPPk&~yPi``DHtE`n3vsTgWr|I1j_PR{cJmlbITL<+HH%XlNw4ZsX)?tyoai;87 z_2GgJli{4~y(m7;<@CXfWkrH#9%)0Ic3<*6rlK%orhK)bs-fFzl`OO$u=SbHCl)=Y zk1}V~Z3?JxVYhMI_2F?|Ki6k)0OMV`zt3lh6N!9}`07H|%|Mmd zR!Jo0sof3iw>;YBcv(i;1LAq()18bQF{3szxdMD4N@x za1(Ra(;B_4&^kt#CXv}Zy+xun--zr@tJ@&&b!&^M*1gvC4#ejN^C2l%t}C@G;SbkO ziT=(I-gbpA*w#C}Y3gr*ybZLPV!t-u7Yl_eX<0Ge(Px$Pu=1#KW~Wi{{@jaWPuR|V z9r1176%Sa;!g!hSM`>yGfFYNlmm75>N>`k4**(GvA3o zhHVrJl6+^~@IhL2Esm^rB1EW4Ik+!R6>t)Qy5$uGBkkN2ZZ~V`zd9ynCj4mPArkiYH%eATR>-=eahjwz&psI6<{7csyQ9_}M+Cm=x>TO)5-x9&DTt+-f zek-;p5-^arkg#rB9)(U}<17W|gp5(XrP1Hc%QREg-`PKLW3ZECm`_-$G4d`{5njfW zdx^`pB>OiX?0da5J><}B%RJv9xv*Yq=fF-X8X$!X?s?L>?)oBn4UN$n*J{sYcC~ti z54V-JM8DlHADT?HlALHSHgP+26?(GKt7wmQVQM@WzP(e?<~-{6LmWfC(2Fx9|FM#y zbBk0rxF4E@9x}%G-?j`_zV-CG#u;L|&UX}Csfa6OtYu99qkEw!@#>*g#&o>a$x#Jl z8M?(4!YbrXI5yyD2%%V*9)HAGY1{z>Tl;}`!+?Goh4O1XGVuVCd>B}t{Nb|0C>#6U z9*dr6l)7gZ&}EFy$|C-7RlZ6l7Y8@GJTJQzva9*L;aXYqxdHbGL%T~^{d}5&s41BH zT~RMpwLMH+vd7}k^a^f&Q-0H>WaGPxzqdRxsrh#T=@Rp@^42^1IZ;ma z&(N@`>Lx^)uh8|5C@f@WYE?3fl}f8o#h6K|Gl1qO@}wMMJxSpq9%%`;CAK=7WGk5+ zw*#IjBYN8(74EN?eaLdQ7vEWdRA(~h?{<9~pHqCoW%#UaMSHRS+NBnaNxh%?JgBXy z$xr^T--g44ZKjmFYG(6RSM8Lcg;*!ca4dkLW@?hnGK?8iK2%08iZ)&Qp*>(yp0K~u~5wtt=xcn z!~Lf98`-{|06WpPgHFQxyMpPq993eh$HNabe+!ChUA)qM-RkR+wXDtW9Obl;N@iD` zHF=r`FE{!evgeU7=KUo5s)6}01=l}J4oDA@On2&PT_qgW-@9g#uhJ2cNEwc)PVRjS ztyAtv55&;3LgS~-jb>cYH1PS($~25KNaj1NFnZ!xVimsu6dAuNbz!RHUerm2*Uz<{ zD*@=>3YcuQ(q{>R8;hr8Eo2lZ1_b|nXw_v^7}w)(W+-sLMjegRqx3x_c|% zqj;?;vQ2osVsV793VrFsZu?r4S0w1n6<49d#O*~kDs&t5%Fg)u8%~}`-|T!VWWAHC z`f%rE;M4k*3ErMAT8;W~B9NRK;Lp-3%T!+ij~Z$3E1wde5vXvPF1nu^&~1ogk7a~W zT)FtW2q&w0RFl?I^~Ac=KH6`jZgzTjN>{E{=My4QI|H~4U_w3%_!s^pz7Z=Gk1 z_I16n^5EN4PWo`uYMpR!Q&7Rh7n#X|l|>!&+b`O7zHPPM^}seod(U7ZA~$p*{!E_ev4Ws9v?XO!vy=g6I$2o<$68T2S}DQzINIx zDL2h&?^^X@@9zUz`(Kl82u3`w42_=APiLef4{QHTjeX@g*SO;8rW$@QZ}8Ny4V^!# zzV8^=9xT6pra!veBr>jLe|^8)B%a|NE9oA+F{fSLAK1=MF~~!I?rddELL{qF1LS{K zA_9;4fJemcju+{YXA0}KAJ@0l+O$wEP+5(4`p(Q--O==Vz)O|ew2By>^0fbiPKKVL zZ+%GZIu%}!c))3==fy_#0gD#0F^QCp_>wIubK_NYos}j0+kU`?)h1&J1iu$v0p|$> zHb_?%7)b!e!{h?-+RQtQ^Nwb}InGdwZ6)SpMXkW3hG{O4U>cnprK6#jYl$fe*L@a0 zmrY@EONK%-W9ifSTxK0Wn>{MLD?~;&hf^apD{Ra8-diHe==vFG2{ZaxUq|j-r^S)f z(>Hl#6rKA|nXwb-M?BrNQN{ek)f!`X*@$M!iYEww+kCCPFy9v_TB*)bM(NKlR1A@g z&f3g8PbkGP?AIDa71x`+gPTs-Sg}rKiPbr`=35^rD~(~*%2y98U-wHx_WLg18I37B zsnh7J-ZZ_-$wm9+4ueO2q@iBnDc2esazjyxA*~aYPzxNDeEw~VUVab5o~G0R@bzaC ze9dYJ_!PH+k{OMXErMnVJ+qtVYgrR$7PGCg-M=;b)J(dY?R5M3b0c9Mi_kgHaaPDX zenU_@T<+uOvwY(+fxw6P#RIys*nLw@(~g_zs-tg+FX_vFX|Cw_eFWknyZ!?5>`i2P z(E5CgyE9r;Ty-%9N6PYny!Q6Exn!W~&Fl!ttqI5k_d@h);g^wrLu>wdW~qGtC~H($ z9pruC#*q&Wr3Z2f3L01e@;*=XgIe$VKSVWL3=VE7UTl9QE_&VM9&yp#94DLeeD)nd z>T@&uPwGQES8Qj_D;!2%hAY8)11q5O4F^B$4s6Br~Zj1`%B_$&T*ro#v}Eot@0CB3VPe0yvN-pq33jwXkVzT zC@|LOtP5z?$)`E3@#5T};Cab<@dEiFa)W}hZ(lh1T^@&NA#J&h7F~|>k1}vMMbF*c z>lQgRzTHLDoERWYcLmyV5{W5HrOK|~X=bbu>+wwmB>9zf?v0a{HwYlr#>*gGcZS%> z^~3TdA~UWcP~65ld3#=v&?xp64l3J5^qko=QrL)!N6|R}Lb6k`WsgTi&wrAa){tJZ z)upmd11qLf)^BVWP&;$U>c$e_lE9GIR0bI%h|PR6mQ!KrSy*pXy(&|?++@kTM}Ha@ zmVDmklHr9{UEA*lq}b}YU_QGbNAQD|#GgZ@oDuK^vr;)}CWr6kyYr8&_-V)0bF+Cn z#m88$cCL-TKIrMjv`~t@AI{@gD0^+fY$!7OiB|Q9Wi(?V*=SLznp#1#^x@f@lAp;G z?@Rml!MS0p?QHW}z6lB;w{~2e-p=Ar$Y@G;|82t*dfld91@Q|{rl?Ml1QjQ*)+5tQ zZnU7tmixlVn`AfT%XD=o%dA}Jn~=u3n1P?xwmdaCdKSn#_fz(D=n2UtcVuWwP@T$E zizwBax=wVtnK)q>Ar1h{uY{5b=6^oLLI^hs8wrrD0K&$dP|05a+@I><WxsH zmIvub-0j~qS;r82*C#hc=f;>SY;IFo#(-m_^`)E;;vQm-YQW{><_5Hwx?H#~DLN`z z7>WJO`MJ@|NVv2ZE6hb>UKmhonSyw=bd@S1M3{6&Lz4~tkNa6$mu0+!3#WE;p0#z{ zirTWCW>o0ow998a(BIAsqfFK;wfCVPus)SKZ>I3eisl0s<_5;4>i>21*y8tpm_QLo znHuZT0}?5{eBr)f88E@i!}6@lif${|60*rZpIlF?))lP%q4mp?G1tA8E*?B~q+xq= zLCw18#44Ud@i={PU!Ie+D_2ra>OOcfk=WU>qpkUuzD=xj&wHenn=)Owk)rb-)}C={1{z?&d+Dq1k8)0GOVT;BOkYFtdb5Fm<>Xs$$06KUv2XC@AG+;eYrT&bal>*>4XSS!U6q&5Ql+iWp!L1w&@%&*1%uUSDM4Cv zH$b?nN_~XdV+ZiN$s?gB3X7#`sKQYwGR0q17jIQD6;Nn!qcST}dL}KP^DJlSPF}*l z%Kqp)yNd9UNl;}@sno7f_RsYR=C2KFT1b1#Ni~J}xDXHicKTS*FK^nn&3HRPIu`VA z?(%pOj>=={3=0j?v%;w+Rb#D^_4n~549w}2TruLX z3Kwd&ZHN|QGBuOpp9#>v2zy-UN`(YM%Kww(vD<1D#zaK>fHhwWUPXfu1{NSOvjYKi z4V1j{1v@d!{VB1*{X_H5RKpEnH7WaX(@h_4q@S!O$X;5opkDA|;*d^IDx~><(Eam1 zC6CtRBqO=@eVyaYm!(}zHQCL-*p|t072yFE=Y`>-t1GD1Ytkc2-Ce2SfYuwSJ%w`& zF9=T3d{45b!KRLYmF6Lu=&|CYncx4U&s;11W*vD%n`!j331hxoaNbLM;&=6#!mxa& zp7<{;cJ&yJZaqYZn3gHYsqT5hBY_E(^>ckz7k5A7WJ#3Yq`S-vAL-sC zjXxzm{~af4R_MvJw`48g0gk*-$n|w+1p1hhXj-r4T-$0_+_nID)@e1!nquj9D2cg#NOH$Wi%c28EI6czc8=#t0(J zu=3#-paw_QU?rDN=k$TZ>S_X7sfs}QoX}|z`v+FlwYto3*LTgam!#a8CqG#%yk_0N zYMtgFB}xh;nkM4(GVqoD(pnzF#@-6pfa+<-{Mj(4L_Qn`*NW!AFY=>;$s@p=3vh_Md_o5EbS>D~gGmpbeKCc|bjP zh#2N`uPeXx)#8LFs%OK=_8VknJ34pm*I5s@=$nFmL_6}!i6m=<%peT7-NkSnBsEMN zO5bu-d}_gy`}rr!f(mXpUnCtRKI$L6B2?|kTp75%&^=T5bpNq~R>GL-mX+t(%S4Nk zoYHS-g-&E0^J#Ir_%Tj$J1MUz&bCxobXWB#t--)pqe}7^xHmIdVu&=JTDyXQYks{I z-8Hnphv|8x<9e&ZZpyFcL!OSdm#v85q+%Z;jdBszZ3$FHot~w z;fgt9cwluBpxOS!j-d{q2<9Mr6_jB7u+K2U+*1K;WbMSeS=_EU%#}v}JeqOs;_yp4 z{iWtj5vr!=yCozq$9mz5yG_(P`qCGD4stZPJw)V&Qe-%-4oB8#NV$`ZG?cfzDZH&E zxG8S-i+kTXv9`Vx7d;hjX!l^LHyGYG`==`U>zcfKcXM1$CITE@VXb$BixhwpfOa5* zGOY+|OJn6@tTzNlLyx-;)blz_ZY8YtR^1`LmZwfTuRqQs%od$Wu{r0`Qn)C9AiHuG zsp?4~N)l<7e97#grthn*M2Fr+zU&av(x)9cxA(YH1@)Cj9B0eq<-={qTCXo^Q3VwS z_8uYMGxSB<7_SU@e={GsJ*(-?uvYuV05xU-q;})xPz2LP~5<)JW^RAm+Ta^p*Q%Ki}2ms$wD?HpzdU z#0#(3>T$89hK71B&*kGeyx5j|v`FyBWqSbkYC!w~%#Gt^0IkglslT}#0dI}fxISCz zxW)dt1t0(g^iKysCl7d!o=B+7LcU6o>dg1=uij%~K1yzK#UcR~03g3J^U4ei2Vm+# zK*o^cL^uMMPGX8Hm$h%IFWp4ZSqpUb?q$^brTzAEyuz<6oui9$ib)adE)`eJsK{GF zk4n`~muz!LD|aGZr##M^F>Mxs^$N`jA_yzjw9CrwgdCP@8fdMatQG0u>)diT$(a&F z*d5$zS3QTUpQuZ&uEEK=WgQJZG`IJB%Vh@K# zM!j%2w`Qv#@k48Kc7WHx_Jh{#tsB1i=|oS*UfNrc&$9>OYwVioRgYI#sHoxkHg!sX zv2^9=(9=0y>zrm@ov6?BiF~%<)#)ECPI(mPhQuXhO6Rt(Gd1kr-g2LVeE*e9GnBqr z_HilYxz2LvAbC-hKkMcLujKA~OPOm5V?;}-@29^wtZyuNx_p*f1Uu!mgZ#?RQ;FBU z>$n&);IVE~@gE=OtFD%wyxMT4#$!}?_f$+cDwqC1s@<0|xW{2|Q>uI9+U7_@{~9XS zKR#%ENj*wd*C}^_Yv%IJ4KHIQap>LwY}60Ap%LqX0>_bC$VC2v3&-){U$#CV$}yby zNv>eq;}Fpt1^$X11r|iIy1EK<=qd&)u>gU!)r}ty4IX201}k*oJgq@Zi>e#vEN+jC zqeta3F7pg51KTF4NR&_>tVit7vpqKZj4K{7W}&4iwBk9GDqXdA>C<6-_aCamzlj#* zK*CpZDpsqCjuSh_bpC-xQD;`x2_9*CKqWuiCFIP51kU>}{9f zHf@Vo@Z*K_D<36K-~WD~Dr>d<`7=DzbV^_pb-8GGNB^U@g5+Z%bQhh?M*WG`r2BnD z*RO@$GKTsx-)rKGUQN7mt3J}`UxkyekMrxS6?$JxtMR$$((>`xZ#~HyBL0?H^yr7B z*^AbXijN=LHqDd*C1baQd{&pP>1po=dPo%aZe-cevUE1>Vx!L!<7!^+$|w~R2lXD* zSmOpNT~-2j-h9)+PBByVu>g&SOb@#7k}>xOx82fA3}UI!=Lgn!^OyKP){^x(H^exp z@#YG%Yxz`ofipmWshSG-LEs8LMplG9jYkp%cE)kfgE=G&J&ZTxS$RTVV8ceDxaPE0 z)3DL&q`*d0MkxwzipfX$$DLR_)0Z8=rnH-Xs0=snd`}7FRcije%2<7J3!O@ADdsne z3#w0E3=ym}hNd*C*v%zfg?-zEZl_|o99opCe0BeA!#|W4F~OHyi+N|lx#pCn=(5vl zc9AW*FNDxV_GsxE#eD0^dT|2^U29|MPaoc3;BzgV1^#G_i>{(JrSfEk{9gOn&i&~} ztn|$e2~KYq-=DOtdl~2W$YJn|^P9GZ$C&!BZ!b9<8hzx0Yx(~6TTJ~FNtJxwV9D!@ z?8{4KJ8$D&#``oLkmPfsAC@HiH0M=Bj-LKv)RY*l|9XJ(Ro5H-lA2Qw^|$ENnVTnP zw{$I-^hR{#(UJFruGjOsLIwgpHa_Um@c36(?nxOeau`^WbiGxIB)64nB3K=Is5rN! z^-M2Q@>ZS-M|f-s0*!9UJs@GXaX<~0Dwqn*m6nk1n^PjQ#gBhF=A*~!Hkhv-Q;@L? z5rX>vQk4I~)DTn!4jAKf-LW#4>7NHU9{X8yjoPeO9VpANRAt)EKF40c*O6)U>qj+P>{kEwJ9-%o5?!& zYA9YsY9~YQBb4p+fs^0=2WL1cX^m1bRxY&rx<4au&WFiTliPi9Nq@|hzT$<*t|!^L zVJ$R#SyxY{Fr!bFXGHr1RN6j& zCQ_||%I@c48r;WExvTP1vkG<}31CM5JBUw&l;=-w2M)k&4IUJnVa$n-=hS8h_ZOMR zfYLAnFj*z{U?D#`YFv0aNa~^)oo4qSq2S;zz$anB^91xxVxa)p69g@|D2~S|*OF_C z(B!m?E7?u46P9|VJ~4$SI>}0U%3S}cSMQ$P^s6FAaK4t1K0U1`sst}enKkgd!^Gdf zkxftOnYuGrX!3*i~-RbwLY z+ol7lRRQg5C?(z(&O6-}Gso$(L4xYb9ndftxy=Te27IwdKHl!e#S(Yi;*!2B4yh_n z<6fiMSZifb7M9mATzBY?_Li8Z>+UZkHE+|;4^b8547T}~x&zB%c)KdR3lM|NkjEq@ zlnl5Eey@#$;if@4)Frko$Q6L{Dbu}#vai-%=#e<^OU%GJHck;ldG6_7rV6GhP9HZe z`6f;fYpw7YrGyL>^<^>~t7Tdf%_rCsw=lE5oNv`I@h2BYJ)NqJ)W_PQxIGbks^EDJ z7jg_EtsSqo+m0mXHK0@Njh-QW_WngXD^}*)OXi{Z%$AX2_bUbeNM}EI=jA$!{$;y- zqzsBvQ~MSD{+spZ#6B}g6!y8KDY{ zy&H9+_e7SxX{LTFF^ITza^eHliy)PSN<7yu?I7)(^d9surJ+H+!-a&-BIqz;?s}|Z z!_dNsgg}-I+kVV5Axb`MK=;IxlllcR>4`IWT3wH7>s-GVFvX$1JYol}lswdX)@7{9 zLV?fs&?S%l(6y8s`DZrR%N$n)riG|dHnX#}MdF@-(t!63Hq7VIk}F1(!gSR-0k$9O zlpKd|;8)7Z6iEO2GkVIzTAO1PmcI6DQiq$bpYOGxIR&(a2h&3~#y`64RtghHy zr9aVgVxU!BtS+2V)oSQD-SP^_V@{u(e2n5J|Nc_c3Bidhr|RuZ3&UFFi{GyNA*@x% zJycXZf%X*naW0*T4w3V(?N{=c(xN+RCEtc8akGtLLKF5UfC!u_8CP#k?SKuirn*em z>HJJMk=O1O0`zMi*7n*-15FJ1Svp%+G6kZW`Gz_k=4fHkN<-sHU2Fx+$j*9=RRJH( zvuEq7CTmQKRWe0dS%oZ<+Z~ge2Ypnj#<&gb%2LhB4Fk<9rj^DIzS=E0{mPi+kuhCj zQCS*`q@9xX_<=I2jS1i{F)znsdJ#G+p>H4yv(nCCUNnnroA(!w$G0tnwsC?H?8Raj zn#(Oa0Pk3Ifd6JBNWeWNWB4EQ0edsC1StU^lNlmoL3b*g{!iiz^AWGHeybUng2Yia zK+ETP+Isq6_T%%tAC(XC%adv+d{lmy$~5#Q-nRO8>Vj&~KcpiH?*i$l{5y^dZSu3D zK?hAuXJ{K}gd1xz=46QOOYci~wj`@)BXHqT7M(8r(rWzNRRLc)#7m#t5}u4!FawqfyL#+5TB88XAG?W0~?KY^r? z43aCa0gLVx?j~af9ip09on2-)@Tax#7}jIMH@e}YOkDU4Bjcs`7CWy};W|r*_75fq zmS3v1b?qIg6YH1PPO!lkNNDuNT&7kN5Y#AXkmtdilj^X2+-&*gCJV02-cswP!Py53 zD8uDMXCUir49w8ZI(7NX8TOK`54`+?f+3p=*$I4b@J8nbY>eh(esU4^6R^{>!u%z;8TClZtWKDa z@P}`(DVgvo=q^On>9z&f#*1(Tn|CTCT+!BJo2x%-?vO{xqh5`V?ROpU=8f=1Y!VlO z%V^>Z7Y~>0JKq#bZMaXqt|R=7dVmW4!aX;&1^%cF-hctbg%Oc=6HB>}B z6Pdt4D9*%o(~j_(&k<%4aQt_s`IYk5mspn=-ODg{@^xbU4Fn%?12p^V8_1FPTIOt> zLU-(A=m9d95JF_wC%ns>uOwR>r|qSG)s6H`RbLdl)RS7Wh56J(e(8RDUe%#NrGBt6 zrXthgtZ2`39<{%@x4mGV-mFx+pcMeEwISDjCg|`{kbpo+VZcEZ7o0ETEcOeLCzy6| z1BP2md5dpb;fA&1#+u!s?YNbKSSK4IgpOWt&Nzw`G35L8`%*7CO+%+n{YAf)KItX@ zn$T((onEP!iHfwE*)ogSXh9x3N4QQ)qJ;+2bM6c{Zj?(sprfiV=KgLgk>b4g8^d`~ z`$5!Lr{kPf`hbZB#bzOVc^;hSp!-4hLc;YR-pPjH{qX7TR8n^v zh{bDy|Ci8td$otv!`yo_4w8sA1JA%q9P#>DdspxZIfuFM@4ACTMK=D}0e7QMHkcJ@ z`N`t;WkW%|CqAOTOYez;de=kb_j;u<#=mt>VX*Lz! zNl@8BBw2em>Li2>*wfsR-jSZjau_ZW)?f%^WFF`U9NJj&h*^&wV)u4C6&Eg-) z|G@xY6d(`7;|^^TX3sJW#JXE zncDkOiyvzus;EbvM0eaK-16g{7jZ_4M-;waAzwKOZql-q_^j>q8=HOY}GRQ*tNy&l+5Rtz0~kbF1PcM}n_!4H3%a z%OzJr(Ty5J-8vDHWP%S3OI@+d;uGgFRvpKv40D;>0`qe!2qB8&?)|P=V&JbUF4azm z5Re-M=;1}R1mjG3YC^A4W93}CMeibOTBO58I9U$p=K2UT2NkYM<&n>IdqVmwx&``z z*jmeW*Ow3{6Sm2V&-Fdm!JOZ*jnarqnv$#-o+;IXpH?*O>wzn(Q;AMFv=)hNd3I~` zkMxB}^+~`VExASeF1zvksfbGHiG+iDGO$K1RaH`O6II zte2t_(h>0Mp?}Ny@{5N}SuBnh;l`0~=6oaQ(sn^4`0;4OQQf7(3a&|8`-)QQ<<>LY4OnT6n)&e#?>n zQTel%YR;B98(aQ^gBS!8%e0Wk5r@3KFx8uA@!M!S9ebULnydrhVN5^Jg27Gfv^0H)F4SQ+P(!GNBmA;eg6Bg6t*}`V4xKy}Wrk3Mm;mXB-wc{zmvW4>Cu1D-c%=QS zN&*WH`7q8jZ$9#&VU7~dEh3f$jg%VwSJ>mESC%{Z5(N3|)5WEc)&at=H_E$Q8oNgs z4yC1B!B}^-4$Gf)CK7eQa@@(ol>Y0_-$RAOz{@1i`t;-ur>9QD{qk%= z*zJD*%2(Ii4RtHLNE0r##(O*xuTL%BrPx)zy*9Yz;`V*KXEw`SwW9>w9IobF-hwL1 zHq<(>8#yA1KF{mmTCL&CFBiNf66TV7vsuS8koD#$l%00$WAXuSE@5_l zasKh_(DH7?PJ5!qFz|OW?-`fiqdO>TJ|dbU!U-?ctJ)#%(e{W&pqlcuH=BlEm8rQe zzg7M!>M57-4LF*XY~%=Yy35p(-U9nCx-{>Kr{hEVqDNG*@RMnUcH3&+L>&4?hM{U& zi{<$zFR@oguvhVutaQp~-g;UF?4BRu`NR!LFR3rCbnGo?I!?=C?!urY7t%nCF;$L} zp#;cM$hMQP0c!Cy=sN{M1)~28bK~3CGuHl=MmBx{^Uavky#snT20?IvNL2$gsa`aH zT2;$jbWNiES2`Yt@M zUL*j79m%$!S4zrUb6-RG+FZObRmW1VJgajk_6ON&k4a;-K5H{H6_oYC8K zo#Hp0bg9*OF511OednFQ7~2>9+9tZzy>{)lJ>ONMY)145zko`Vgpt6xQ~*rmjKJ0! zmI9cTt^6)yIgo|ZJ{q#yt)ay~eiTE{u(J|&sAD^n@b*=-G=OquZSNZze%nnXdzU_Y@8H3kG<=r%#mD?|*x$BY`;dB|N*1nDfKS@s z6m8OuN-bVLW-$e$7oP8UlY{%VG#Z=}zo60L2P9GS)#3w-72b*b45ulFBMwJS+KO^^ z)sM<{^f0E}A>W}KNd&v_n~_+b2r$9-En`9@C8TY|LrRzpZXN749o$&&u5iP!SdIIB zvPGV=1_s{9{w%CZ)Do$JP9crOa1n%s+gS1XA{M61bUbK|RXq)6)iBz|Jo*S6hFQI^ z+!RAjevT+b>cHRm;phTMU)7eDk9$dayd$xi6M8|O+Uo8$Y)AeNixh ztt1|x)%IF2WO6^*quZ8xnHi>kYMT4(h~UIHjXO|^o#X~08^&8N8#()_^ATKxoAvSK zr1ZeLX0gfSrh5C1az*I(t&{(AhSQI~>rOs%Cbg4(*PWXGGSZrrt!FNuD>(#st;ft| zF(yVhtywOny2$Ve8|BZMvxEmEZR_u&rrbv5AHqzPs&*|jzP`uvErWVr>HV2*rQq-3 z;v-cmn(|OO3gEP= zc2dY?wkI9_2z+vq&w?5*w7SW($o*4&zjfW0G*gHQq%{lR^UI@({93u{ilPmMLD2h z+;Vhm)r&uR;fx7=Q(^Us8aT7V&Fw88&klf1hOX^u3Uy!8zF0VEu*Fk0brv*4O6n$Y z8i*NOn6_x>i`4iV(spy;XATkTNQ!=ZDknrZaUej2<5MX8RT``s3`$rDqqzuxY)}tB z$6Qc6Vn#6Zl=pFb+%#5phM&H-Uz8WoTSA-~^(KyY(u=srPyXJq*CKmwce-z8S8K%j z)^!P!IhJ1623vgMR{wrt$rmJKM&(BBrF+b`Ry|S0chGBG8t~2O zy^Z%&XD?8lZQ0~nAr6voOB+~;0z9k1TuB<6^*o5c;t!|E+A8C(Lm(H+oNK~=J}En z6`F818IWyzsACQKjVCvMSjCJ&WB4wUpm*jQL$3j!fM&$p;Q7@~@DetChzr(EVa%vF zwZbIa&ps?EnL^{*BNruriT%vIv7Z=GjOZ}e4xw41xKgJ(p&{(d*TFp5dc(~}y_Vt8 z6-}eF`n`M!b{{ns3nbN$hJDbB6|>otC4AHxYPvdC%?9P;$Q*v6uq2DKrrl$=s%T5g zog1e|j@J#JYJKtB=Eh`FEkrA>zf1;w%+UA=wI*O;F-`@_u;AeS>{IbXa2Ve*Sq(ix z@ENGLEej*rQ^$0~ zS!4D>*rx(^e^o6ETtJi3&-~n&i78I3Ovz+w@x`W4uLU5%)j0!uo=d_fpc{4Q7Wp2~ z-U=wv6A%SOr|ev*c3V~LHG6hdFGu+51nUBotM-cI$^jjcRjxkCS$ObdV14} z`W4{THh^2hR{jC%8e92iA?*yPvajrnc}1QvC^8n(I1ue%DBA>o=9t7V-v+N;poJ_- zg-Px&46Fo`?qBZF8zfA&%ggFBInzF-DG0Em`6=+bv$UmGixe^Ve!h{N6N(>)vyg#|U_5OVgv5AJ2pr}X2?Vz%O`W1|G~#%hM^x7Kt2UcLyHEy|9@8pX zEZoxY1&7G z?^V~+=YVcRsTK;e;W*NZg#K);b?yO^j~|iy8K4?&_CUa6E`;gL2|WNMPSVZ#J=Rw+jaG+3Qg)z6r`$EGWE+GQx8EjeH?~z>M=%yzy zbUH!Igxu&zd(30P3O2BY0T<8sjM1R`p^v#(1z^dDJR)06n%=y4G_ER>3%nIMQ&2Co z^ABA}Q*=;d`|b-)G4ixXVKf8(oEL10ieyH+@UGE*T4C?KTb1&+oVudkJ#PhQ>!jh< z1?+r3Et0IXDY4_lT9hM-42)9b!l(}Be!AH^$@dt5Zh+DRwe4#I9fsj;2uPL`*&R4w z$ec^Sv&QI(t(=7Qa$$;2#ui?Ih*Sg#FH9IiWo$xPasLn?oW=(nSoRBO=x(y}zxvPV zm0YtrFmYdOnK}M~C7t@O{td7g*qDX{GJ5EqBx&sl?PprWj9WkV0}{q9WW0ooA+Arx z3Ojq$sJ&TSRrX6?RNTrrD?fZKM-vtPLGM_V(^qELS0L3O?M)aB?>8Ou{1`6&{D7ec zs5|BjHqfrRXb2mWbC1_w6 z+zec%-UKG&6GR49k8u@fF$*O({xN7h2K3C`g78=WrQA2xPJp%Ud^WN1eLhjbPR2~E zYGRIPNp76Iu;aOK4|jv%sqS0JCUL2QYlDCpEN)y5xA)>_S$&q9dC>!nWqev)YBO@x zMMv0}85^fNDaa96^d9PKvXWPOnU z@Y`>#;e;M?AFS7>`UT9%-^#vZ3pmizN1+JKky`nWW2!^!7~{FX#UaGTMF0T@Vhq67 zkZ?#wqYV@yP_#jlKqH5m-x`DmIvRmT(n%N8;= zJG~A&A?OP-cUCIc_TcdPUtXTUr|LbJ{=)mBW%48371I7jP z(t8s){GAX15pG>j1fOL>3$Kkf!9%JBSHTsA-TB7UtS?w=gdLeHSy`92Fg$u*Z`Sn{Wn$b-Inq1qY>-JH;t66F zjc(K#=Sa~-zysM!L6Pl9&>0~L1$O+%zXHsN@hZ@pZ}$VcsKIYh%})8;i(<=mLyyHg%yp_iK-GV?s>VOAn8SY2;}Bc+u>Zqd zk|0i^?*UT#K1q%1@^FWWI2_M4fGO0&+bc1h1ZBm1BzA1e=zGntdF-UEjV~1D3+jvx zoU@F7Uqq@kBu@$HPFmX?Q{6|YEnuB7(N>T@B}&=b8`C+8gg7m^s#5@vFe-=8QETL0 zoHs`VlUjc~MC)M+08;4{^uUTc>vk+KO7vq`4F^8`meos)9y4~_QIKL}0v(edBgX7X8q%4%W&%SJl?mMaN4AHPBL4bSnc$ znP|%)&3c)m>^j(F2jVr4^8(m;VudndOG?20)wmQa^ee()v{CMq0Uwiz?K8q> zSazftt8^!ZRSFN%4cVgErmClo`)doPSe|7X>~@L0Kco~Z7Of2vjYK|p z`t5LvIS^O}N|DoMv;_j3>{#j{e^VV|g(_n1V512rMx%*epfbW|1_;4wind7lZ_h}M zU;}ajg;=%!|qqhgu7B;AHq43T5+R2*`b=Qkhb^2@P=KC03Ru()9`oem|(2{oLDm( z@U>Vp^&Y2g$ZMbxkN@e9Y`zLgN-5n1Yd4G@bLx1H;Rj)5tVsUXw~USnSKE^)3EV8g zXPt^0hfP`W+na{-2TCH6OtGON{{bBs1K3?wA-p9mOSW?#e#KXa$(c-8h{st z9Ru=!4xuUBE8eV#C<;~4`$+&yD&-gft-(2*hZaDA8{{ldsH!+Pae0_RJ9qVlSVb}S zXZGGqfle;Fe`~8@?}9A1ANwE5+@w#ld@IXKDU8yIKm!O)L|(xuBCbIIxVF<^Wu-^v=SCF46=L27GYiyr`gnu*6?>WNu}Fq1P-5)S?c*@P|3 zk5VPa&GSu?949A9TFAJR{W^&vv_eYSt&oJrgolIo1ULa~&l(_6t;BaCAt#&;(wn z08bB$;fD5lT=r~CVO%A@sN5YrcEQey?wbnS6sk+88R?0DXQuom>5VVNS{&S3lVDnFy~OJ1Xeu?CnMF>dhfx{%TxC3gYU3OmQ6QYx^Ak z;n@=^32T&qV-~p-jl@BObW>i&xW1ZkBC+u--+ox2*yl+0 zt{2Qe?J**PbRkf{AH*D$P0vtL?UXTtqWjwPZcd`_8naXF{CvaLs&fkr`)Te|>CSyc z?|71&nxh?R4)CsWNj=mHFsYQ}Ng9!*#=8cKc>OD ztAAo*5OI)_=+!ZH*=4G;@x(bXU-!yqnET_J>3}S{)=&Qo5+2JrK)s)t1D?$gph6s zFVPEl#f6gkM$6BzrU@_)rCE-tHg=NMvnv2sL>w!Z(TV)m$0DGg1ZF{mu?fM7a6>Nb z_b=N%FkglllXX@XnC`c*oEtxVCZqa^X=Uld#0e@re)ZWN^KKJkpk%ob{Bwe2c^SNY zEoKhz+bDY;OX;j=fi*EF$;(5}OylXjRvpmd;>|B&*C-!_bGHK9n$I^wks`2(qaFbn zO4OjUZYRPUetKo}GI$bk>rX%KU!HhqnyR3P;Jvl6N;Firwu3-*bkIdMu8?d|*(W=c z?(KEnu6FWbOkI_DfiYW>nlwXH@^ZjKx1NO0S#zY?B9a{|mV2>b7d4*aqlM+Ru?{ex zIdyK|B_`g9Of~V2C6+uNc4_trxK|x8St|?Qw1n~9wza!?>{Qdy*5nv!6z@S?Sbkz* z3qMqlwcc-u(I+f1yPopw`?(Qz9qA8}$v23ZJ)Sa4P-~QL>~8IXMGT4iMvA0hO}#K7 z)mKwWfeDdfEJ`CL1x%92Q2<90`fsLXeI3R1@uGzERSp*Vhk^$598&~85CoQPpflVF zNdis6G!IY|({T)KvY{Ix<6C;s|ZltWTh1!a#K#`emCm(X%aYAqW(_ z1nz`7W7$hXj^)a0zWue{YPzwvnns5#iq4@H0Rjl4KljQ%Z3W#z^{_2QS`(JKoQ{}S z*t|i%!f3Ia;fQnCip#2e(O$+vcFA-TqxYCDPt7TU3F}*i44A}QMwhbHPAQD3lGEb4 zt!n3sy97p|r}R(XZn*j`&mOiub-ArfZ!{jiOv=+w4HI7IAx@_wE3SX@LY~a!P!Q(&7U0Cg0T$l@1nqY`IEH0^G{FSEy za}~ySi=$=yUmA~1$yB5=AHlY_00OW`p#-vRHy?*P`u*o5+zzxL0_1=pc_>}~w}4~I zxiN^|jHC@|Ff{(4s8Bti_jdx>X8n>4ONHI5t49ozb{IK!8;ee-tuN zcZw&G>g=aXVi=vzcn=za)yrKNvjnXicF7%4EF%{P2oY0t5eey}Z$9}Yd!wC*We^?T_;zpdMhy`|SqzK}Dz zOi|MaIuZ%)BT%^)R_y^+lZv!1kmFg;R8~!XmJs8%wzno&kGB^EdYr2CIX`%8UQ>$4 z!=#PhdWb)=G^TFXryai>mLYX7c_w*PxqS}dM8@9QFwvdmjd%5x`dMiWQTo*64nO81 zsf7yCz_3-DWxy#aM{N&-nE!}0+=Du@1I2sf0SQ>W=LeV(Rt1cHVS6JaQVgX)O%x2+ zC(k&-;3>YC{pcuwUT{AUa0uE8wkBM3j!itj>P6?Gg6GwVEYex z+y6g{9d#Z^rP@(J;hY^>UgaJ|@MHi7+sf%DwQStb>ku-px1^SJ(HL<~2}Edh2Uxmi~3I&XB5Y0p3GWAjUYlIjO`Ka~ni*3J#x(i9D6Y{sqw zj2MAGy1*f_Py7fO3*O!>au&088 z2SYfk_yEWV1o-}-#uCvh)Q4J(|yUkKOEh`TgBPt85 zv+UM9cQou>7?mrWH6~y5)0iZq!BSz70RajQI)JW*#g3-c<&CRmntMn$CHY%*@9U?u zVx0#~>nA=j9I_fdqitseHn|B-*;W*e!m9rx=sl%rkg;Xc>h1JapJ7dA5FsCfBM~qk zp3t^b^|D>Nz_2eVlN{IRThc?10}6CKBmx{<5bVGDhrqpq;7u)J{zO;jvai++HC-<3 z-*#2idurDlEsjm~&8v5-erc*OPtO-j=R4$Z>%$v#eXCuM`X~1^3Rg~jcS}KKM@SqK zWRiZq!8sN7!%tPv={U1lDFiGHAO=7N>_|er`HUI|Q&0k&TF~pxCDv!atf;1d)J(lU znOyYe_2<2l_+3thX?$>=aZZDodyN#?k=EmX-mtgSpUUx8RTjp*HPmD$s(olF-L7}1 zg=mLkg)v;B23w^yE?=;DKtwr4?h)A~8*_#K#q!WTHaHPfiHsI8$WajJ`IH;gmR}B# z{}M8!$2;cd3mqk8{>ddDk<4P)-lRdeHJXW?HeTr>LElIC?F2iNcp+!YbO&jPd@j)DeLYVI6IL= za;ZrBS{szLdAH)MAuytm#-WxK?VNd3a-Ec>IlV_Z+h}yl@;o!Wm`rQ>mA>%u22MUd zP$bl2g*rV za4!4di{!4k#YLD;k0>w%>=#XD} z99Ul=pJ0MM7ilrC4Ay1HVKcj1Z-aza?x)UduJ%{AG{uGFi+|Jf1l^gDpxE4i59i)I zd%$}jUB7DJ*UKK1Yv*>`_XJIkKH0YMA}ymiFGF(l-t~{NJWsd=CuTkFj<+t6fm)WP z#qtR`@KUYKLJ%#V&?5yGKAD%gDJ!?gP#jCaodr5Y;{G*^bsPKK=gW3-Zpc3F?^($9 zcr|di|HZ23-L;WnN$<@~)i-VUf`51L;Ifc3XI1&45#zK)9m&;+5kH~Y!!_Q)2%KIE z!B3-d4Mp*93_!O=7!+=Jbi}}fWdN{rere?)6+g=Z{$qU!Sb&IGN7Izc;pUFOxdjrK zAG&r_a%tT+R57?;VOFc6Rcx1fLt@mQuI3ub+v%KVrw*l*Pu6Ig->aT*h!6BjIs z7MQ4|FXD9yNyOC3!!hh0$aA4Xr-UWJ;5@8=9$YPo|MpDy3lV;zPfe18ZHWg>_NDHz zR4wc6e0xIK&1*4nIc03cuRlt^Tu-{Nn`wND5;1e%!KCcExibF^nRB$+DU!Uwg32au z?@9id51blJsrT9#k2ffAn;W*lY_Gb0LE_578?uL_&e%TBqOjx;@)RzAps8nMuw`Bv zZ42F{EqiH{sJj|ZJ zW>T_oVU&b~U9k9K8i}%5*Xr#?p%X)^N*aRq4ul=d&h6fC|D%hme!F{7#a>D75c2-pp7Xu#KJ1l_$KOmPb9ZakC@kz@cJkGe$2w(wJMYkj#a|ar zvUtY|yow{`K+qISqRk-@cOx|5p>*%&dZxL!pe)o*w5j$=n7AT5=o_iall2GIvPeSm zrq=wCP>X%Z_v(o~4^Ell=TFMFWMy9^&$)Y;iO=`1ww(To+EQDn_}uv+-@d)?wlNyCmdhNfjwHuv3I@t<&@+y(#4(P*v`BJEd+1w-oz?pt}%qVzcy$h1GFJu-)*iY(T4? zYYG-Q|6QNhBCu;D@fpi87!&gcW~I6P;C1a+iKhhLt26fXZ)#i&`wZq1=#h-m`3Zh@ zb917|j%dKw{xm67v~u|U9=jiD_vq)ooy?vDqZ#a%=ZbK%VTSoP*o}e9>{=u`q;_Y! ze^*Zz=5PT~`i6E_m=j6FMSzB`fLUsSzq0-d@d6;mDt?tZ8K={=G`pb3Tbpjb9cVI^ zydGF}wV0K)E=FKl&Om{xAKH>!a5kvxQV*%;9~NCA%hh}Y0_!M>WM$9P9O1K>R`N8Lw5mSQ&@!9O;EW(pmO1%^mZJPVYKMIQ^~=NYVxdOgwkEXhEofP4&^H=R{fkZ z?e;E574E41%YW3{t-AE{)Liio^CliQ*smKZotoW{v3B*MBU_c8tgZ`f&v*k{cA322 zaq@EswP-tX|CY4lYnE}wT`yVBv=#-CGefQ@6&{;RYNO-Tf>fW!qA3?VW->0*o%D9j zKV5g*>_24?xxWnGwFOP*uJOYMK_`W!(r-sHpME6`OPpD(RnPNZCo~wA#P3!d#=WL5 z1hXrb1Reggn3zdfe^_&L6G|YdYVE6z+;ybXRC4+?b&WF3J@$GAe zBl=;mVZ!^j{;C0X`toz%ew6@RdHXNE{fZCVcCNDU>ViAzYm>g z4prT%I6IJW_3=O%;p7gzP@8m<74{K?&T_zB5zoi~PR~j&LeUaEKotSw0}vKfJ<1CV z-ZC1&fz)-VXjwn*8y2;d?aI~3=e1k%XE$AuG)MLquYomKd>?&yec&$;)1AS?Luw8+ z7U=Tm0})j~xOv4mc|iTC7AIIc@RzJFB&wH}vUT#%JrjyHM9`Kz zD?4$vGynFDXd=hKI+l~@-Ll|58kcbs-E~Wm%M`Y$&M|HXG%{cJ=PB$`+n1*s-L;8#Hy*!BmN z^Cro<)|_~y-4fq1Uo@$ecte8=yFLFm_3y3IADr9O9oK&AT>pV!)~5kqFWq7I{`%#T zLEqlfh3B{FdtLTd_-=mq(#SYXY9?mv&eEL}n94dLsOu6oPu!r{ZQ5|}t0#7>n@aEg zG9G`#5IgDemyZ`bOR7^+zS;PLLlo;w_U0x>mW(YeZg6OvRXTHIc9hJ4JDq9nXs}~D z-0v4$@}L}}J9Xny*Pw9zmvg6Ror>@MN*1v8G^T8hhc3c3|I~;5!+wi~;HHbd!4LbH z#C92{QPh7;;FG0>D(n^;=G-7xqwYk*4r3JrcwIK>4gWxCgCVj05u#tPiKN2bOcjg_ z!u@*-`|#zI8*smi@Ajp%2{Ig@=kwdb=+(k-d#}> zYz$m=b|l15pU@o?*Z5x7fj04Af64ERp-YADH_UzfzIsyC)Aa+IkCvCcGXCY)@R7P3 z>0cJ84u194myK8RR^%IQ#6?|IyG~#KOHtmv+>gIYnzQj^@K4o$-bSM=n#BrLnWpiM zNJ%_16i*lG9i69Zy|qhdgsHHDBH)a-<%Tq;jGIXs*qGZPD8F?XAj&TLyv8+mlV{Qk zx`NK;_e~5LX;!Snb!wM0Z_04z7ksv>E%NmT6*v4F_P5WMmB;Y)z1o6{-J7nwv@DC; zAUj_8F)`EU7kv;hIl94g#^{!6kvlLvNKoarS^tF!?xKn- z8iYF}D0-GK7VV|fw`=zDE#J=H_I~5K5`5xaF053&b?<4!{tW{E_KT*Rgm>sB{|`<$ zN{j`!eGlpF#4eQLoNnp)EzKd!KF=M=edoKACcQ%`dv~B}O3@9O_I^l&xU48BQ|MXW zKG+)kw=+#mkBqtE&MHgs#i2%b*`$h${KRBgSHII%KS=rzFNEz5{;PPD4+bjsrm2L9 z6y}H#Yo&C;Xu5}CYkg=MRp6Fcx2?nfB*T0lF?C9mp}cyi`iF$TZox9@ELG*winsI2 zLd7MVf?fqIC=&sDB_R&EN>o`w31j8Sks!+$jjR9`A&3I*8U@MT+5~s8hbW$g+iQWz zxufJD3VzxnnR#Ez3zlS`fft}}=%>lpa61)Ec{SXN1O^dt2%bgl=FwucxWK8CVB}hu zyA^sL&_w|0qFCN-qHD|IVuAi^FZ1#VCF?0ZVZTJ;3) zM;&=*bAG$iocY$zx1$vlUcsNrf>g-mFKrFjpp;L zvA@odb)$>WB}=g{t)?8K)D~toILVS-;hvw;t_J4AR`1A2BGb95ma00gD)m$_Rx1w! z{UED5`kS6~UDcH<-H|;*4HFgHf^}6S%3R?N$!AwpMVf}q9tVS783Sc5yG^33D~%;c zwN}d3jj*(?cQ%b&p1XJVE^+N&Gky?NdF7nN&6JsMKcss$*?rjACaxSl+%~&i_4?0& zH(Bhxs%9(+ zi(kP!Eaikb*B7!m&N+gh{z#M4vE7P~BHZ>n=3902SC- zSx^fF2zH$=z>kJKpRD~vdT`_FMz^aJ1NW}pH^2FEfopo4c6NDHA@o!+O}<1zDv1F! zX%v{^Op`nPKJuQlp&&Uc#MB0BM0ru1>{qgO!@yVDntJ$;R**Ux6lDt9bGG(5+jR+} z;`{~G>HE$%+BW*sFAPfgJ-s_7o6f3!e#mu|`B|*%xtt2U;zhz2A9aS)Tei_ zCo_NAu20Q)>hB02XOa6(ASk-#>(q(OFN~^c!yrR4HW7)U)#rA6F&1APtw8;W{ zwQ#4Ra4CI|$L5j*-;q7at9|QzE(AtZ(!Pp(jgyuLc5Iyu>+NFG!;Z%-*d_R$v`I(Y zcFM6eG8_L~{EAO0Y`HQUI$E_1MiL(-?230#;bCQ{m%$-F zZEED8{!0JQhy8PNMTHBhd-3rd!2wxv4&3hq51uG+H`&hlglv%{%_m})nExX2fP;3Q zzJLW2xmYr1^H9i3{o#SA83-s>6G~zE3d{9O(E^X5-g&+HeyiH!~tDY$DD=<7Ap|;|HKI}?z*{szKmA2Y>apNM_)Im?-#)u@E z?ptm+H&n3dgC&u1i$gR8D3?Unk_xU<XpOzg?6}+Ri$(eT%NtONPUnz&AM%>OG`7*h7vf8thcX#V^? zT^tS54E-$uIR%u`dA*?D&Ira>C%Jz{iP> z84`*3_8Q*1LA4tBY}mTdbre7?&p#dF`8L8NF`@GiEFXexAbR}zZqg%`sC)W8!u$#P zFEeE&x$#pd?3G{A7sLK2%8CU&L=(ELea%|!&#pMhecFA-(L+&(;6|O1^e@ddW|vr0 z8{h*uq?s>qN|cYo!?TVF(@q82?!^O@a+umL?a9Kylrzj5oT;xHH~@2Bz0=y&uXKy^ z>+O8Y)l&M+9?Ld^o3}WQwKG1eRmX)*}X|- zXVkn#33->a^keQJS4QF#M#H-%hhJH09Uo$tlmryEwr|7vdk z>&MUeXMafg;(Jx?)-UDs;-RA}Pmybuj;uK~ENC}1&UD>!YJHIE>DHP1R%vg)z9_ij z_1-{pfbi6_BtflwWEenxxVaOQ)xZ$D_r|z*R<^Y|4HDa#FLzc=0&l9RDuTE%zP1m* J{rR7F{|{IGs$Bp8 literal 0 HcmV?d00001 diff --git a/resources/image/_base/laugh/1.jpg b/resources/image/_base/laugh/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8e3028176b15af7fd704b44f720ef36f23cd806a GIT binary patch literal 22225 zcmb5VhhG!R_ddRxMhbxd0YWt(y@p;y3Dr=gDJ@_?kRo72R1~jw6G*6PP^2hoKu{3S zsDM-rRZzsJh$zSnAOeaS6%@T*{PF&L|AOC{IcIlgU$Z;2ubr7a&vSOWm#4i$%rBr6 z_yGWa{`^5n;fd<%B$C=@Wd#bEBtxW-v@LXXbnW%DEewqeEe!1~%ybNF^(}0zZEUwW zIMXObq1)^fNnR@2!N#`R6}0vk*~D*grP^$#ySaIIdHaRaJiUXwcLr^kAorliT|0NB z1p7tr*?k~}?iIE#I)V`pcPK7yZ}PtVhm#Zc9o%>L;D$MvbTI44;Ul>|+gY)ZHG6iS zPEX-vWz`(ZVC9^s&dIX(%JT}%4%~k-;`oIl$GD7?>V(XOOb%~%&b57~ThcEKW)?g@ zSUY;Kc08+g>|oP)*8lQ!R?Bo=ZeD3&eo;wLc}Y=TL0L&rNkPfQ^0M-}LT*O^yRPJP zcS-KG%8I*{rPZ~Un`};eBxi?x-JY9KVs)`j}&&rMA~nws9XTwm(mDw$uNU50CrTM+g2JAALXm^!@bY z`tGg%_N27})FBitg7yef?z3_bc^YrxR!RfCH(+iImK2I-vc)0ZQ)u*4Y zfBackSopk=H}rpIBmGZ)EG%rC`0;t+&*u&M`Op7~KL7Xiiy0KE=?Xp=I%eh9z2wbm^+ zZPL;w&n(90fy||WLdi3S8l>@41){2Vi9_zKw*wgy_R|ri{!txe6yx(b4?gL39GvG@ zydJ)D?aAejcd?Wr@|6$yIOV0Ketr;KX z>ur;^_*4Xq9&i8l?%@%~v$lR0o>u$ZF;87;{WQ=FZ+9!X;Z|^ zAQv>N?);cp%b zj#n2^;W7*cOt!$ZZAhEm#T&4i^fl1R={^1w%GW`a(Hh4ZK_G+@E|zD#p{Zn!vaHj z>RiA4E8VgIvlow)4f$e((aZ9_5Szl3z(L(YCl(09QyCX8 zuoNE`6n~%!*lf^Y07wMsui)X*?T=0K(XNGuly=4?yB+H3BL-&%=yLyzC9)yJ=qluAu<-q zT+Q?qx5NETbjaDj*Y4HNlcEK`JyX_wI))z-hP3r_KQyryj5MsDGp;;#c)1hvZH}@( z+egVDhYDBi7FX_ed{F{=Xlr}{SeR(?N$d7}(f5IHc0^xV&gXlsa2RMHayS2x_Kkx= zcL}{8J)s00vnFaeG`_xcm3gg5i}tjzH1z7X*{DXd;FMy0pWmU!P03ihA3oH-B?2jdhx zLQvtD@G;w(?|}YFY-~3*r?&Y(YrDqhG~Ashz19K*U6i_Is87wLw93KckZAwkZrdHI zzjXK5=sW0dp2F8!T}*PH3te}Y+a-Nkzkjv%iNTTxr4~T#1PuVV>?03cVG!QZX(nP1 ztp{#P;CP^!JKhEbw|DW_x?M2WPi$$`r$I%OHYPN&ZSfMV30goY_u}!pSz1>rw zF);kZ^EIN()JY4WDsajF-g+`l{87G{5i`QDv8(-^r_?+Yb2f9pIH)8gcT2v!HR0OH z-4}PLFRE1&PPaYWrCF<9_x6%)wFO2wW#7gK18~nI(x--V6DNLwpSKY>fDEC?2Z9-k zEVZmr-T_aBV5mB7VS~kcB$^5uMeM@+Nbk4TA4BsVieW@*jIA03sYJaabWP36-xYEn zMfb`asT8OR))_gv?t%8b7aNJ^r*={g*=$R&hpRrL(r3NHWy1WIsus4?x&^kMJm|hs zb>FDg_PG7YK<~a?HRbMPI*%pA7vs8t2W7{KIr?OnQZ%IpPT+%@MPTHEBKBjwl^f&r z2Uv$fr~pZTOwB+m8zus{u$Z$MCYu?JRSD#8eQM>^d}QQAFV33G(p_O+R1U7v(Z_7P z8ek+>L_ivDbc}uMS=^Q`ErZngA_EocDzrxY_G?-vziRncK_3lVos(Z0fFAPBq@F&@ z+}czDOKJ!CrK}?WA`HmLNPfTa>8p9BG?rpQAR{jN<@0^k1S>5nGS~6!O@fIKrHojA zWlAX47W3=ik{*0OvnH!k59eb%0kjyf(}RKkWH%aYbNoV3&i01u67YyrJoA- zxtOqJ6Q+>%m6UE?RYFGG%JXY%iE{?63mo!e->XK5QcWxLQ8R4$F0%CQzrxL11?<&R zATn3QlB>yM8x-{v7D`~m3FbdTE3Qhn7Y9@?>TOcu?`na$JowaQQlBbu&2B1)IsAHw zqb!1?12b6{bOUTwW4v~1>^>>XAY#2aDaB;x6}yblN#e;oc^^bC zE?+FG?`~1BPq|~j?*37FXJh!6)V*9ky4MFApYvU-1fTr&GUmZyKMxPlPdSA}9^b3^+A36$)J zQmuD&(ZgD|MAz~6ByxJf8I-*E^*?j#aGiCL!zHfsZ`-vAPTt!A`66IoE;a4whxh8o z9ZWpl*~#W8WL}bT^2t%Ibl#C6cI^!kpyPQkGzHkXS#d89=E?z_1&By~G6BZSg5d~j z!Gg_)%d5Z?0O)4G-~d=BmT&cpY>LfH`U-#d_JAoLd5nQnhX|>5UQ}3cGtC7U zv}h5M&5Zl62YrTrg12zO;aci)s69^^dj>+dP{@wo5q6497KAo=nljEn8Sr3s14y^; zySo9|k5uVXU#U*aO|08yFUYw0SfxPJ7Tyd#g%6T$Bh(kcu&WWOJ=^mcThs4K?-9ZD z0;~-f@C-37RT8-#&cMGPjBzHw0s|7JAGpq3OPgon*dk;!GcJ>LBA1Eg@lRASb6#?f zo-I3C#Vp_hSe`J)kxJGQ!s&#AnOFWUV`!Xyj`L^00wGf~>Xz_96@Y;XwgLzfHtNUNr*L*!&mLaEFIPrxOf`!khncAePk&M01ToYNN>&R zMuXAh%;J#r#KkT6RV9uw6f+}9Ybu@-6RNnX!k;x}t z_DndZjowMiU%tvI6Cl$QAw2}}0)hqo0L+EfPgeGWclDzeg172A7>ReQXKE36>)4~E zXcmC8N82_NknwL1=t+=MLj1h|xV!{L(x;aQ;cYpv0-@ZI12{&hNy6>)-X3~^@SJc-Pi4ZS?>CYPaV?$x)siwywP3PaTh5fuNBQ_zF-hz^ZmnwvqeU5{lg8 zUHBlTe%7->w-zM6OlCKAOW1cLHY4_Xs0THfO61RRB9|+1| zi{;A_Py8xZE@Gmy1w3yEL0>xk2aR>9JHba6NL7@g>r4NvuT_pDp}lwrcc3g=tIR-z z*bCuW1?XYVtz{Dx|K(y0#d(xkrXH}bTL=rX1FZSU35ob66;I+?FeVN!7Gm|N6>W@) za)%Sq5W0(OdU8J3Oaj}(Ln?g7&QWCgC{j8ASloL)Kno|i3TX3TK|p@ymzbF{88K5c zK2WKhui3Sf^-7Sn=gK9c66G>pWkf!LCN6a@Kt%qmOc}oD*}>!RYN)C;kCPC8--5%> zfUUO>3l;cTfpokWSwb#OQ9wkCq{+YR#sx?<2JG^8gp7E%U|vQNa5Cs6=zGQUC7`o? zuDt1+?>|~dGypV`bJZ9&Z~7CYOAo9M3y`MN+74ah0IzDAFO^jWTYLKVBQjDI04)OG zMnWVnL-}xPwfKv4cerL74;LS!c~3>NnTO63S$$}~GMrzt?fsSQgqm#~HMfp6v_v#; z_y`WP@ia+}0Wicf5bcWETcs3=<_r%SQP5GT&;-MWNVag^x^K&lg9C#0$kTu@8a@MGIy zf&6@4NT${}Az{sI6m1@O3*T9WE+rRd?!lZCqK99d_9|$&`@YoY;VtK6bl3aR*>G46 z2xEfCeGqN{P@m{o$NCY%<&WZ;38^UK1Ro-d&Zj)@FAfdH8$wzGKYV#wta%vgieh}p z7in7nsN;h$!Q~}EMONMgFFAaH1SVsNG84iws5ey};t!Hxno#*e4qdehYaj$HM5qud ztfLLx@T`7$PWSKU{=aWE+74}6JaX&N@Hwy74YwZN^5P@B#20De8kWm#6(-q-hpdyt z>6XcamXjI_?20omQ3ABcTc@9l?2kYdiecp-xLc^T$U|QFaAYI{|Acua%aZw$d_%z% zwFF@&dznkZ?WI(wr<)a{MKD`FdN%~~;o(+~oxj}&?mq1F=@#*w2)6?OwHe6$0dVbg z0&E9xxAK=Xp~K-@hU)~V=J<B!&bvyJ`R^xB|F?M8Q__$h2`Wl})|ewn45ftL zlqG@uY6k;O>;1_F|1`uxT*)`H_SWm6z3*M#qYAm5#a7N4FtZi_B zNHZ3Wdqp@Qqm~jVMB%Q~J=h{M#YbxL%z9#EUQ(~Wu{YgAfN4{a`$ed%&|s#T&eGMb z4_xmey5t;x`Kz<03376Bp}ybdO#pRI7yHx0)!S586A0ZS}bJ*!Tw^%Y!Ae@F;H<5Y_ahsb1`UN z*=HdHv@6gL2s%#!GA8Bv+U~mJ1+qxc?#(KELbo=AeDs7NsgAIqyMQ%APt3%-cj_xSX}S8S zq|UIg0CKrX^&!t>LK0>xL=^<2g^TfTdA+?t#0@F+aR?W~L^1?4cguRM0QmlZr@JH% zrTH9Mmwd>-#38osY%{LAhrEMe7Add_A6Q2Q20ZhTz@5afFNSfl!VOnqzZL{@qB{RY z7)-Rd`Ir0E~oV4|Yq;rRGqtCES0O|t$<`jVHzu@qKhf851SFQRS`cOK8R>l8N$Miug z0E^1;@_*HxE2Xd-rYI=(Ac+RZ5|B)QX-Pmeu`@ve$npU+SsP8g&vbF#^CixxcAw`M z&QAh5QR&SP9@Cn6tUe9-8}{|i{?Jx%uP|&cIhRxo+6LoG(yfV$!3xzja53P+#0?V; z9(!!jCXtKkN0zLcqV`AKY@))g8K@f&HU&UEn+j_a!k_>9(t!?FXKt?#O&DgW zr1_C0GsmBRUAmx^mi4PnI?x4IUM}r*Pva#+OBOKu#@${4mz`vwG^tq?E?toX>Umw< zdhJ!JmL@QCQaN9IY4;g?-y=9+>T5uiG90PvWup8k^E=7N7zp?AA>qbPq#kAZEq=m^ z3{wjzYGA!>|2>cNg|Fa{^%Tvk$F6VvI4^xFVJFdOqA?|T&*^+g$+r-ExH!7bE|@r~ zVsrxv%GHvglf$-3Xel)SZXpD(|95IwkaEi`p-u=}=|qTTiM@dv0H?)8wXxif0&w~9^RLLf z5C`j3EcoCoeDsfl5G7}suEb!zZ@c2F^it2<|Y{5_$TtS49pLj`pYBBgHt zsAw(ZoD8ZCFfcod^A&)Vqn|=3aCL^-E43Xa|9*DJ##%$5tN<3rbBKbioZcf;qc*9( zA>1XTJ`{hxOUI)(?Z4O^@==TMfr)QqA`OJc6gDk>eCjyP(1P>9pLfxU6|>8>FmMRz zdKJ0fRJw_$So{g$BSxeINY|F3^n{>|WbR_x)A7jxGXc^#L?*=f?)m~kO$ca7CWSLk zy*F9CAfV2jy>tH-y^z;#F#Y$Vui|nR7*0`cKmw{P+-@pdi=xW3M*Vfllk2K_d9UGD zcazslRCP7^bv2k4C7R;nGWf_K%6upj>BE4>iKd>efBRdF03Yl8#M$&#UIJX-iW0wE zb7(W!ECKuy8Utf~u9}I@mU3SIO+S~Jk6X9L1>!Li?)mJ^m#gHuA?$7etoaJy1oWn> zssLZ&`yk>zRjuk4Qw>KhUZc99R2O;LqVQ7U1Td)PuBe7QgfoL`_UYoTo{DcLvY7!*0qV@uAG(rx$iV>7+UDTyf%A=c}1H1?@!|Migi zz$UK)E-GK#URX9xnjKn7|B(E$)a+FkgF*!g%)6g~!VAhkXKG`BG#YU6zM1l}G2E&$ zo`Ll!ReE##dUII`aR<@x*zKPFiUV`$to4}NIr$|f3DJuCR<-+;_igVq>mOjd6xkj- zzI5woaDtkVg}u`RFBWIadEa*1V^naHNL0118S1_M+O{D)O8@>P^*6R5)&a~{Ehl3- z;`2Mm+6{jl@3`kfmL@?g)Pe3}cP}mx$U>Cmc&pYnUo0MNxp*di+!br9pE<#n_c*ae zMmjn^n#bwah_vycIWv1JV{f2YDEjTmhv!WxJO*|v*Kn}RIsJLkwM%3M1FkvLHm`c+ zRIqtH=UB|1e1bK4hGWh&EHIYO3b}dSJb>0ylzck^RcxlzgL3&;^a~3Y68Mj9{E}Sv z#G+s%1~OzFPQaS0bmo*MH`CYiw}xB_i!Jhb$aH}#Ml3aMc8oacLE7i2_Ta*raZ7DQmzL<&iq0VU5Z2t06Va z1HA|}o=HO~wTqTG{Id0^E>3|!J(#V#*sx!N9u4@ibgx^EsH)8#t19wI8}@9-IG?PI z^q+x&OLBJ0c7IsB7^Sw_l zUIBPc#>K36a=A#(Gye@L@-+O8)~8O8K(-2- zbtKAnfA*J?wwdx;FeE|9Kp2dOU)Z0jnEIkhzn3^Oi%+yqcg)+O`MoJ#EuyZ=%-`(# z_A9MRLC%y99meLF(W;5}O)mZX%(=Wf;Cx+^s7JTZ^dU4aZ)Tgx$fsZ3s}Z7ch4Ff0zAPf99wgkL zq>p}St^ev&a%XS8G2IN70?5{o0?jkEd#TQ{HC^wJNR&8c3VwIizra+E2=PO?rJ!Qwl4)duq_%# zC@`pSy)f8RW8n4n_$OM8VZFHVjFOC!Ii+3R{iXsfO@(B}SMv9poj z;aCVj7cnq#f@sXXp;C={3QaGQdh$#{_bTy#&m;wot7Md|rpzft+sm4k7S}d164W{+ zy|-58nr)VJH5xNn@-Nz82onG+Xyg1ij;Jw5H)ZUA0K^Rj)ym(G6h!IKxwU?c7((MA zZ%91N@5dCyO7|&u-^qVfCsM!|K;Q#67XF@CFpf^=d0WUhLd^(wGXO>zfvJVD3KK%+ zRBcnd+%tLH>fhItJ8AAjc7R;?O056r+&^Z6JT}t)M&Jv9>`TC^j7S!Pk-Uu={eby* zF&kgN@7GOR0;0a&E|Vl9@zvIru0j6r&fj$%8FsDC;z7NHzAI=Q-XmE)gbJd-jgLJY z-A8zO0nk+sJ@!=U-Nw5hD}0BmpbBD)OVu7Bl7lmZ|Rex@Ts|{ z-+P;<03b#;Sa9(t$}Y#EHKBuu+gZoLZn!VRIx*6R{8)KJh@!9Exi!n%-6+CopY$}<~qTTKL4E|X?xxcw^llB$qHMrcd`t#;zdR9R-~c04G1gZDfc z%6_`fg@Z#7l5*FTFwG7r5x+D=?3#+Z&z~Ef3lfGF`lPs&z$b)^^L1fF12W<-=Bl^g z7OvwHTPbVnXH*2x5}Ui3ly=K!mq;3&qA#2?bNPM(eucjnxT z9_;IuEXM7$OP2_dSS5(PQ~I0M3)-MwR!nq+dnGd7q}7h@k}a!VdfYkPwW&w?E4-DB zIqQA&PL&3$^dCU_2AWIB`BDHpnZZQ>ed;aB?~&~h4Bj7fdAE9ZU(|ru z^Y`i_v$QVlZ}&ZVlrmS3Zc4o(ttygTO=@|*nVQMWO3j4t?1o1Kckuw3Q2B8QEv!mw1P&?#BkW#?YMfx}0`PvK{iW?d#w&hU=)%A(&#h zre&SWx|?G78t%Udh%&*2S-b6Hz?FE_YZI)Rqrztu-B~{BFWy@;&2@tm@K|r5GKFQl z$hnbeRt;Qkooq0eP>W&q)EA$3x85An2CIY2MGCxM9XDg=P>uo+Pi>b319ArU9r)Sb z4ssq9x(*k@O{fEgZLFPBd{Q}!1c5RH7D)(8BU{Dr;d;EDA7@VOm+wfPV3%Sb9b-2u z6Y7ygj)?^D-NMqhm#t-4qNf`C3%MVv&g5nz@g>?I4cygMsGP|vuQHIdBS&0%x;t_< z2$WT<+CG|IMwq+RL!>?gA)S0=39hOPwvh<- z@F0gy;2w4%*8i*pU!TiyJNNUtm!oLgcX_T7&4K0K{y_&|Ikm&Y{)vx#goquaHt=s1 z-!uu_kO!MQ4ve}XpIp#L!~lpyuVHJ?pSP>YYPaLVa?B*)raU&4mh&phJ}IX*zRq*1 zlS6z9TVg}znQCXQD~MjfHZX9G{R5u>#73)3#HhyKyykiz8J>qc8T>~l-^Gx4WQAa^ zV7q@r^}C<~NnAcKm0P+tVIp>gP$oUT2PGV^Ik%xPVrXfH!)z0cN`5ODhV>~g zB8_bZgJVIE3N*)Q`p6^x`ntjC6cr?Pf~&(AUy*%yj&5QFNIq9X$&I74DRnP!re(6C zys3)pnRcJ95C~xq+KmXouL1Iztg`2>G0*K^60_w*kb057Rgklz2!O3q&+a|z^J(Sj zrU9S(zcuo!O=Uc0B(Dg;<5GLDzEOXnN3%;Nd?Xwp%+zD@J z_m@Yvst)bFV2xs)l1(mg@FnPDSU2 zi%BzDi0&K@2~0T?VBk;b1Djos_q__vxlpWI!#uO@9OR2IHP@-;*9VfFkvs0Y1D}@1 z&faMT(cpHB{xGHVM>0p|XW5aTQ8$p&s|wtaJ#1q__O3)E(2yN?oU^dX3CwXdXv}sL zv2(g$rfo32%*e13po7CQl9Y8vg;~yV%}FdGlVu_*P9<>3O8`34JVaEidsJI(OTIySQ(=2zGYUP@+dW2K+VRBM^(wBk(#`7(|o+YC{o2?_b~0@ZE2K8c;y(5ozoJ zVzoY@v}|X|wzIJeq^A&($XiG4ZR70Vb9@j%WtW9fRh(S%K z3piVFMl}T&f(HLv1dQhYNzLPGPIBTFo3-Q_nPi62B>U;kQ&G@uOB(ACj>>Ui>63!P z$~hvFi-|7^u7+KI{zfJj7o$ds;c0^k_lAh4-n{RBnm7sVi%{y1%1>Bh6W0J$ezpuI zcYosiuIQa+(N{S(v3p3UNTu1x*7+FmeDu3{x_yqDJtv9JjkSH36u=E*a5i0<|LS2y zA%V*1oJ|nh%r`fdiqy)->aT^rWSll7Y(><)d;XzH)9N|8YY(U=PF1K_rsOVkZ3C_8p-vcIOb4ZZhOw<$twD;tCR1qXhaJRZr=Hi! zrR09#B0a=zl!;*X8I%R^wBRk|dJ%j|8!{qeC_I>|1d4AMm2G3&8=)LC3l`Scp|mrO z?ObarC}zN%4Nza>CvbL2CDBVy-brEzC?8TzfPLzPOQ`dDNHN$s6oyN zSYs)a>+HA1Uh!f{k!w=}q*4;uC^U3hQ7Y3wirV^q{>GFiZT;)HM=jAowVZ7P zWZxJ;&MwakJQ|QWmY9$Q72F#;AV7t-!H&IA3%k|r?cGNYV)e}tB9~|5XO|-fS7a8L zbV6!{=f@poT!l?JVYK^ZGAKh5sL4YFF%X(!7L97M7z4wTwpfu-=iJZAk&4r4T;2PK zx9f*$!SC0(x~T?d+3Aq<5U|tu*744OFv44s6>C=miXPBSpzX^pWcz(9FmHp|O6EQa zKGb@#bQ4|1XHcPH?|!Qiqs>xG0e6=m(RTg0Kc@<`sB*^ST*{ZzK&>->040_(K2ctj!Wuo5FXR*bd%$GDKN6}>now$dpI*2arj zmIjyGM%jJw`SIerrUBQzL!9@wagA^njEUL0G&S%@MB|$e^$sg0{gBiekQ&R0P}Di_ z3#POPEJ&buGMOq{5`PnpiH}5>aFeVmr{nR5yRP1?!0SMe2Rtsmm zNbZQ<9biCO=L0%J8IxfbW1izoUdOj6Z4GzLoow5GV9v5J@`$=!Ovd#(v5-#%VF&^- zKpZn%5j{O_ttb#%eOs4XZoL$q z`0pn&Wny`E+rj&vOr@2pT+Y;BbnNjFb#7Rdbokj`q*ldAZvmVc)+gg7d+toDZ z8KMV6P=VT)rsvC42Qr3F{Pk~H8L``biIH2Z^9!#lSA7K`n-X$I+SIRQ#Vu5=G4RFC zu#+wQC^UaH(7aK=Dzs7aYkO>Y*JL9)^@hoJKl{TK_NVN6*A5`@+U`6m0|8tG5Jn+0 zSiAMNPOWo$3_D<4yiSV11#KWAb5v6Jy;!qu`+S_y-jtX=oV9)61Im>2Ttv#v|Gw%{ z?*5gemd5x#e!(<{n4V|&JizU-^)sHfm+Leoq)5r|H`(jLBm@3PL5~)QBASP^BhFm- z>!lC&Z7O*%=*t^~f`3WCujtLYQ>f8M7Xk}W_e;xV4C9m&^T^cKVgR{AHtv>RHh%Bw z09s*JAJ(WX#7c{%KHiHahQ8G<%hx;BchWeb*`TKoN!Ue&5%Q>TQhU)5;qvt;iOL6y4d{1s6oTkl9HQ@puwU^6`nhU%Bewz>7N1Fx3&=Czb@H`MoG`{at{ zBa)NJ%*Nn3zS-7oYS6+%dPEP{%s^6vEbJGhVC7f+UumaKZHc%ttL!U)T+wRO4Xm8A zp8>;~m>fWA++(_#mqm{5x^PbzU^bZs>`(GFE0Ko1Cv&7NX}WjA`YJd1{HEk(%-4Kd zP#~#OH9x-9+Q9boB$8ejm_k6=E_o#f`@L_^7$#jnk%gG;km`DJ-IjkBdKC(Vy<;Q; z^hQu}x4lNg_D!`1@!`scxi;H}g=f04Nlw147JJl!r8N={3YQD_DJy{(-x0Zye@U*b z?aMS$ZZFI$VOXg;STc3EHtIskEv|7`*T~p!urV7iTs>XQKjvXp>Mjb~m;c9>`2}f+M*kH)Ibd=7F3TA02K|$F*`hRmcA~=keKHO3k-OLDa?} z=6Q9(t@$uTJ`^LD1)V*!9lZG(WqKm%tTyb9VLsEvTTXJ;;8R9IfUm1ohnVATqK#D} zL8#lJ<{EAw)^JS-qmVgw_~sgqCVCHEBEV#iyIkfqxP^ABf^z6FWrgk!;ol-?Ok$sE znoXi>kDP7pwlSNGk9bV6aqHSEWVnA=k1Xj6#(RPawpdYPyu6>G$gLZ$qHr0;fL*t~ zqHDFKIxBIoREoD9SSJ&Dzd3aVFRl`svkC8$f;%Hp(iD(+&jEO}ut4HRGpS>;Fqgy} zVhV(5oZ(v>fBNF4Hx;ptNb0eMfQ zXfe#3Q7vr{rhOEhHG%SF+#-^avhV?#hV(i0PDwl^+u^K_j%n==2Q~zk-a5*DNGLLB zLIwQGL1uly8A^mW`oDlec}(c+GDksXfg_twMIM}}v7jG-|Mns`2J_q~uI;f(3#*qY z+#3|3FvmzV14*CU_x7EmtEN>)*gu3pm6VN;Q{QT7J({7n3~vIZE(D|!)6eeK_y-?m zrMB_WJNwt)4mgjvTle)v73EcVz8Bfw2}OBPf%l=1l-p#4;Tp`C&u0-*Sgna(xi%vV zm{bj4`s4%*L*pZ3sLeJ#qzoqq3fyER@aYj`T(r+hBCU{x(jng~NE9^V3sTFaL91$W0W_j(Hv4k7T?_?%4i&ra2h&34iwGqsRAu#1~0F&op0J>55VfrmqojZvUa%aZ2hDNV} zB;OwCx6x{CJsK-9Y9kPOW!iXR;Bc|V>0*snt7mT!vyCfRN+v99rDx?yxgC7>7>)6U zz<&?{)c&Vr!Ya(^cm9`vQoo{0-@Bq>;;E<-A;w-V;o_8lwTnQMt*{`Gr7#BBMSz>Qn}g15 zeu;5&v^_tO$9ScPi(YfVHmiHPBd>Epj>X67rD^LAh91oj;+!;{QnDp+Di3z}XU;ktuRB3!1fLkdvr_VaMtJebxpfEEEhn9Yv#FmD*z+NPI0q>ZW6wEk4+ zH_m--s9M;4rRyWwUZ=!{DzCu+6#^KaRBAQI^ND)#8ub8a@xnE^>Mt?4TW###c3oK& z$oa6Ye)=pzGzu3%qySB)X1Gs^y8J7ItUd6N1mHjM;5oa0Zm?|xmP}j?P%fN1qyr?v zEkoFZN&sD_qpO37o3mj>OT{s-o`r$v$?N#neQC0 zQ6Jj2?oZd>R{2H)KqFi%d$d10RspxC^=*a*H?!T;;1$p#5#wCDG|yRL6}U? zv@&6b7K^|KFe5CqpNv7^TwKJQLHIl8qi$w-pc1`DF$6?BhOlu__{C^7U{ND)sT{kw z>5Pz=hrUHN*x`tBU}h5u!pVyz4W^|R&6Lu$_0xiY%MqKrB}%r5b^P~lWc1>Gu%-@t zT}t&AO2ae@uGS6z=PN%_ec;#V`EY$8u~&=al5rIIn4m8Gc&b_bOdCTUTO;_R)a4>NIWqDfJrp3&oz-3e5mK-}PVJ z7h0qZdPN8~VZGycIP!bZ3`piTAj|AIrp_MrU`dIn)u6B6tj;@%$?``XH~L-+2*d~> z#Kzhq1f+)`Zh8s%SF;U;pBW?(!UV3)5`We|K=|gKT zyh`Oto`I|UN};qOu$$6nSjD}4-l6>7rqr=WN5lP;gAeS@67 zM$&xsYdf;dz$QMrOHZ+m@fNk>VxNjI>!PA6h34KptCjke=TfmuKovHk?~vl}PgIwA z=6s+}eT4cZtr7XI31T-2bw52aO4IjO$4?vYeA~!9ulu;4=*MI| zL_kt0pek+B)T^%LR}#Dgkb{L;u9hsVr=Um1-|Ov^r!K%70kb-{Fgm=c5@2jZyVpF> zDl)A@uyKh(MJK&?%^&-En?f9ejHiTO*T;o7Va6+^^+Vgt6-R`4MB3^pH`Pqw=-Z@* z?(=z;W@+yXKQe?z1=(x8eJ-sC!&$_;^Zlr>kE5oncPLZNuO+}*X!M>ccLMwO+nZM; z=2xSU@;NmrJF~e%H}GVMOo=dTyjRy+B2Z!BsDMIE4DwMzc=zU3^)X|L%bWD&2k$>l zb1Bg49(+mT!CDF_uc5(3z5nqW+R@s}>-EX=0-4Z$bKSRD2Cl>!Dt4@by9;haIN*&j z)#sL6e_^(<5)p_V$gWzHHLJ+~)=qNI$uvj>I=e>CR^baV2$12*GMQpq%BabcKv^n) z<7*;iZytVy)Y{6WFu?UU5n>Hz^Q3nlAFRhHzIDZBxlQ7Ck?$`%mo_{;2B@aMwfnMd zp)V5B6K!(u1`B70OSW0*k+1uwZ9FCYdl|e@8yzQHK3;?}0e6;LD#AgO;PM(DZZ<*1 zEJAWb4<)|b=HoJEyU$^3J|23pv*cHp+7?fHV)lEB&r(~=hfWCu9WMAOXp=$6M~5DH zuR5EA*;Lz4kyoj2-!UEOD!!6sndn}#lI*t;aAO(^V^SPCY9i}l;9Z4)^C6;d`L^9i zIbbhAQKGzJP=Ip5LN2+4BuLkV{2P$s0a@Fun_N@apYq%cZmQ3@2-2-JF0Wda>7m8} zl9OBH{R;b~m4(ew241+n-fIoQ{=I3v3fgGOKutv~Wh6$E3^=Gn66%@1^LT*_Menp^WuZN1O7`CkdJW%Dk_ z)}14>=w*45+-9^`O5VLkcE!bZ%2k)-TCC^dvbi3eLsjP;PReH7lD0bi)>Ux>25Yd1 zFh3Y!W#uppL{>GLN~pWEZ%8YD1)LO>jvU$v5?DmlSu}7A=~K!Py34x|+f4u=O zPQN>%vCmo~%ep1tGyFO%B&Q_DGF<NF>$>Tg6-D%Sis=Dbq zX0%IPv~TWj{)OlJyk5`q{ygu`)48Y|PFe2U!6UQS*%jz@nfe6YDrrbG)M0LXd)5 zfA0mzK;emEQd-cl`S1L6&bu9XD<%pxke&8WI`$=8GJ?nXo?h8XU~Y<|ciHybWr1^n zHVv8Q$s=aR+|JyhFze|vW z&y%T^?1L=V^A?(HfbvUU%H{(O*v5!8(kz#o-oRQu>)GySiUhX9+?5~2G`H#%|2z(< z>vsNapYtrCao}UAY|%rMxqs`z)Q`ZlpH=%!O+JS#ppLIUQ1kKfqwqv4QlB|^^BKWK z)YYJs@mIt28+$ab4A28kOgWF+ z-S6EOv&_6xGg(|+bHK(8Y};AY_T|FyrSIYD37{Gc^P8u(;?ItH*}P8u`f1oQY`-GD zGeVR2iy&CjJ5EAn&x7; zZe?byb8bArr{ndH-r62LG6jd?ZR0l^HtYIe05X+9Mn!^5GFXxqsU@)V5WP%HOf>WbgHZdEW|5Ztq&T>*Yk-c!<;~@yV2` z%ck1Xk~K#SjDwqFDmWWo20gkkUQut%AZ%xfa2$k|uzbr9S^RyCYm>5#H$I=fel>37 z-Sy@e>&L4LX6__<9kn*lVyDT|UCX4w2_cS*Nf%iLkyhB3%Y1vaUff`oblJ1DO+->} z-~3}#z+?e z&aFFbdh(xtja<1IA^#&dGq+b?fF3CceM~&Rm zL5<|W@Rw|!Jpkqy2$xJ}P3ezTX_saU&2tqw7D+dj?y95F&8NzemKhd$ZMa_b1dE_4 zRhy_Ap)o?F9%Rk=G+i>N;8`$-y`!b6(g((W?~ux(Dq{AEUs&HLz`e&tKhR!PP~bB`*v(Oz#E<4cLo^DLSVyJ~~c zrg#L<411vJq=+(L^>5jwwkD5Ti@kTADRCP+=6D|O)VOeLN%m1?bX3N6m+*@TgiR#B=qhed_Iw(g_x|!G1~aI`g8>*oY73G2LH#`ldWBAGvL+ z+n;Mkd$>nBWZBl_iyDtz*5ZTu7o+l6K9tU!fSs;_uGG4DgG|VL?jN?NkX}WA6Z#KN zms{U?#0|&GB&3xI>(!oDt*P|L`=ii;#9%%@SKcJOZayLyTEyL4V(oYC-R*ay>3001 zU)F1l4wJ*uNN=KFA zXrN;YnkUOL|NGkuU{a{;1Vfl@i4YrFsZW|}Rt0H1W2~5a!X=n?ymqKz`qdHid>u7* zj)H=%&Ir`~L1W%~jpYZfGD(%x{8*hAefrtr{a4nC!zvVPD|BGUab`TnjBNqDjoUV% z3t#fwu>(qwj=~@tRjJCID*H^VNgqFh|f&kh?-`p!O6wi{^xq+VHiE z9hCZ;pWbCpha7L*d#^qaE;en90l}s*lvMU=mruT)gkX#QU0kpC0zb3(yt{3k*|a~a zsI#@b>{!h61Xap#E(pJ{zOX)OYhi|=fPjgYhe8@=$;JB4{kDuUIi%Ev%zNju)KNJ| zaoQf6Dio6^agAb!SL;oj4==MdJ@RbGQ~xrjO*|7uwi_VqIWsaKNUIM_O z;K|RscQm{7ok_GZi1tsz7XcxV_w|&Y-lNy6S4hJzh8zG14Ut-$rRt-buJdyH@}EH+ z!h(Z40E%wIrYRpe0LSoXQtxd0xXU?~?Fi&uy`IXE0-V+&-B%@&mAQf=PN>ZRm-a5# zZ*sbb;;H@6nYGfX{Dhi8vBkNpEkPz)K#ArAHYDG1>Ol5M%cKA(414gVdH%4Kv`RO& zQ<;j@APz2+y-i$Xi8Or7$pEL`1Rx5+&Qfh_HdtG98`PCTf+-I!b6KBtCz8psyaa1# zqXggW1y!8Q`={91HV5X(t7a7!F5dQx|AZg;4S=+#jcDAn1YGUh9H&9`4n~=Xz`w=g1FCzG~e9dTkRz zMz-9n%CUMDx8+7$<>V(vOuN5x7xlSM*g|1j-dDlOjYT;jzF$&rYU?qpS~Wu_ai1Mo z9O#^h>k_Gl6bQlrkI^GGS%6o*afMn2kj10$DS5Too=p|ZKyMk$VxtkHQL#cj6F}dMj7=$y_F_VN=-laP6&^mkddA>C@o+1x>7iZhUR=6Uf#caE@?yUEJ!3FxJQD6w*NzG;4cDuI4%Zrl1D-p9c zuk7aEq;$Q8N1p-*c=S%*?h@pDbXNhUFcf|yr5hYWpsC_7ZzOn z^IR4?CnO>AUfrRQ);~lqrW`7$4ZsnXyawUxR$&?Zbu zFw#1sWtZ(`>kt!0kpb+Y>0i8u&)(X3lHE~SVa-{s1Y7aAnS9ZuAJ}AJg0C(FrCIm* zD9NN2pcTxHjX(dPb|SIA?!VjF>Q3^UU)vDc9bx`1-T}q7zJZt-&k(T96_a2PR!12Z0F8pjKti{izzw#BL2jtcL&QX}8~%yF`T`9TD3!`_@*<9GAz+vOm$76(ZP%CzLG1>d^* zj|OX2^tFn)JV&e)IDSyvL=esF-#}~q(1D{n0B(^-tAEX7(*zM@E$`G$le(0ToeODh zC6?39u$E_uU`<`NbujutcWL}qPzdPE+5adz9|N5%mo}p|LbuZ_1cKG?< zv!|)10nlXcAY%Z+FqJG&H%aBooeQ;}d$p$Jq(e3&hayN#1n4WvC>S_leWH$(GI+Md zb2tQ0H z;A=m}X-Oi|tPtV@B^zQPU-Y|vF8r3!&>PXp+VNQ^CX%x}I{+dN0KKR$LJ`%}C{>^! zBqOTFvX2^oK%&KbWRy?GY}@fjbuC~DWdV)h^bO~OjHdS;$8N5%T9L(5E#(83g|47mW1EsZbEMf>Olz43JtCL%UKxRQo;#h>d(?=N+rQ_7IrY^K!{2d`azoznot22+vyn3 zeQxKo4hmG{0PBgI`4D3^@JL1g!3QI!Aor(ih#g9n_Ybr8Wv}+ZJb3iQVU^gaaPDXx zF~$~flvC9N(VBI;Fi%xfAQ*O*&-rZd|s3!u2rKaKR(S*&XCo48YJxNmtZ*(M*Zkj;4#$pC0>Vm>@v!%~Q$}$Oun6;63OL5W`SXshoZXb^im@ zN|AWvg>!pBy_XKA$$9^zUDOG%P+>a=o?^!DP?{G4rQl>;ke$L?nJ_`{LFwtTDgyy+ zja;RrYC~IoP@TKarz30*4nmt`I>U7%@z=?e0ffvz*?IGwwSu?|)>x1Q;)QtQkb*Ip+VX z*;s)wlMpyzj0`A*y8)`!GeR25ZBUkhX+?8YTqI? zqn6R?5;pe%>!sqk&$INv%lUPrZ-?Ia^+by@Z?i4#fi7|yvS;V9{4u^*sK9M`x3 zXf`W8jw#mulc5)Cxc=)!n`KuMIreF}a!O+|IV#h^9*bWNK>cjU7CEI4Ko0VR3nP#_ z3Qb;1bnf+EGGTl=12#gGyN=AdubK_OEM)nzNDF%rJwR)&lQ3xk{`&3@3>U<7Ige7CeEGQgz;(y1e_IZqB66xc z-5+IKXly4T`*bldq7XDd2oX3<{@EY8Ml$kSwBzu|0La!4r=;Uoe0j7`p0Cw2z?Vl) zm+-S>bhWNEzsCw6_N-&bXOM}!DFvsO2g3j;5TY&mpWMldWfjQc+~C_0dm2zbMogD9PX5BuiwcH0$mDiEc8WDC8e>F6?((r+^@I! zL4xRFHtd*_kdLm~{p_`7d_umQc59Dgt)H@F0j6N&bcD>YBY2BQNx+c>UQ$Jt$&Vp> zb6g09Q`~#(b>&LLhk3X?1Nfr@0O}eH@~*`9vvK-^$#cGe$}W(#21fIl1A9qL0hFOKp3o6*C-c_`8wI5 z`3xhqPjbH1{fyuv+IlZXD%h(5Ka@fP3%#>e8|~cpyV05qgdEkisj(Vnd=?$V2o!|i z#UsU`w1)uetGY=%3aO*Vz&|OvGI9zA{=ECCeHS}GPPdL@q;L85budCHCx6!Z?%t?* z{u0OoR{zC=y@XIG@^9Tcclj=E^`>J_rn~p@B zEEL=^BQ8bB&y&zW+50tQ&_js=LJ+ye$RJ%@sM0J|=ys35wmf)Y08rPm&$frPJ#ymT z)1rjo(KE!*jb}lT>R$~H91d&iPDgJQGlJwQb|Pvg?&`p!H--ZzHP2sJ2lNSI>lL)? zRALZdLBK#1aXpHbZ9EEYzX&!i-ydH1>-%M}^4r0uE6KRC#rX%>E`v)UUctrmR_#b} z*^M?mL*>Fn|8+rVxn4L7MgFFfaxa1#C(c-6_>Lvo{GWBSbceY@b&)IG0BsNz{<<#d zS!i#1`!rp_P&Tg2R-BkE_IOGGl;8Adh~P$4BVD2u1Zs5PBoF!Jr6_3>XZWHQ$I$p7D{aCxW5SsL zm6(RbdX$NO#!<7t!8P2(pzs=V&aGHXrAfj|K=UUbD&LrC<;wP-2y$rtmVl|Giy4%) zo|UYbR_W89x1CSF1>GC8!q9{M@{QfQ;K@ZDrthEE19ZunMwV8QKnm7#@KMh}91YlF zk$NNFT$Fg{UEg0n2@R^>I_moMx(G(yP$IT@>p+7wsOnOBap#AIInR)n*H1dC5`9V} z0`msgd{w32iMQ)czIgg4R7*yEDfiyS z$LrXCx=laWwtX+~xy9q7(ZxNaZ9EJ#xa|JXv$Qr*_5HD+ZE~s*x&M&6Fc!0wEhnc5 cxP#)k Date: Tue, 6 Aug 2024 00:00:55 +0800 Subject: [PATCH 107/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=B8=AE=E5=8A=A9=E5=91=BD=E4=BB=A4=E5=8C=B9=E9=85=8D=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help/__init__.py | 2 +- zhenxun/builtin_plugins/help_help.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 2c91490d..12021a46 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -53,7 +53,7 @@ _matcher = on_alconna( Args["name?", str], Option("-s|--superuser", action=store_true, help_text="超级用户帮助"), ), - aliases={"help", "帮助"}, + aliases={"help", "帮助", "菜单"}, rule=to_me(), priority=1, block=True, diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py index 697f5dc0..698603fb 100644 --- a/zhenxun/builtin_plugins/help_help.py +++ b/zhenxun/builtin_plugins/help_help.py @@ -25,7 +25,7 @@ __plugin_meta__ = PluginMetadata( ) -_matcher = on_message(rule=to_me(), priority=997) +_matcher = on_message(rule=to_me(), priority=996) _path = IMAGE_PATH / "_base" / "laugh" From 278a5319a4758cb0aaccf4ae05d44e1fb6c11063 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 6 Aug 2024 00:25:21 +0800 Subject: [PATCH 108/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=B8=AE=E5=8A=A9=E5=91=BD=E4=BB=A4=E5=8C=B9=E9=85=8D=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py index 698603fb..3a8a10ca 100644 --- a/zhenxun/builtin_plugins/help_help.py +++ b/zhenxun/builtin_plugins/help_help.py @@ -25,7 +25,7 @@ __plugin_meta__ = PluginMetadata( ) -_matcher = on_message(rule=to_me(), priority=996) +_matcher = on_message(rule=to_me(), priority=996, block=False) _path = IMAGE_PATH / "_base" / "laugh" From 50d8059e9f04517b2a88bf0a56c41912b19f3c80 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 6 Aug 2024 21:08:31 +0800 Subject: [PATCH 109/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=A7=81=E8=81=8Aban=E5=90=8E=E7=BE=A4=E7=BB=84=E4=B8=AD?= =?UTF-8?q?=E5=9B=9E=E5=A4=8D=E4=B8=8D=E6=AD=A3=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help_help.py | 6 ++++-- zhenxun/builtin_plugins/hooks/_auth_checker.py | 4 ++-- zhenxun/models/ban_console.py | 2 ++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py index 3a8a10ca..4071f203 100644 --- a/zhenxun/builtin_plugins/help_help.py +++ b/zhenxun/builtin_plugins/help_help.py @@ -2,6 +2,7 @@ import os import random from nonebot import on_message +from nonebot.matcher import Matcher from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Image, UniMessage, UniMsg @@ -32,7 +33,7 @@ _path = IMAGE_PATH / "_base" / "laugh" @_matcher.handle() -async def _(message: UniMsg, session: EventSession): +async def _(matcher: Matcher, message: UniMsg, session: EventSession): if text := message.extract_plain_text().strip(): if plugin := await PluginInfo.get_or_none( name=text, load_status=True, plugin_type=PluginType.NORMAL @@ -50,4 +51,5 @@ async def _(message: UniMsg, session: EventSession): logger.info( f"检测到功能名称当命令使用,已发送帮助信息", "功能帮助", session=session ) - await UniMessage(message_list).finish(reply_to=True) + await UniMessage(message_list).send(reply_to=True) + matcher.stop_propagation() diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 738a7076..4c92dfb3 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -206,9 +206,9 @@ class AuthChecker: if not group_id: group_id = channel_id channel_id = None - if user_id and matcher.plugin and (module := matcher.plugin.name): + if user_id and matcher.plugin and (module_path := matcher.plugin.module_name): user = await UserConsole.get_user(user_id, session.platform) - if plugin := await PluginInfo.get_or_none(module=module): + if plugin := await PluginInfo.get_or_none(module_path=module_path): if plugin.plugin_type == PluginType.HIDDEN: return try: diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index 9f91438f..607d2879 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -93,6 +93,8 @@ class BanConsole(Model): """ logger.debug(f"获取用户ban时长", target=f"{group_id}:{user_id}") user = await cls._get_data(user_id, group_id) + if not user and user_id: + user = await cls._get_data(user_id, None) if user: if user.duration == -1: return -1 From 45ad5d8e4e6925168aa0189bdbceeeda1329e83e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Tue, 6 Aug 2024 21:54:06 +0800 Subject: [PATCH 110/132] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E5=BC=80?= =?UTF-8?q?=E5=90=AF/=E5=85=B3=E9=97=AD=E6=89=80=E6=9C=89=E7=BE=A4?= =?UTF-8?q?=E8=A2=AB=E5=8A=A8=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 20 +++++- .../admin/plugin_switch/_data_source.py | 62 +++++++++++++++---- .../admin/plugin_switch/command.py | 16 ++++- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index ddc0e910..1c870b2b 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -24,6 +24,7 @@ __plugin_meta__ = PluginMetadata( 开启/关闭[功能名称] : 开关功能 开启/关闭群被动[被动名称] : 群被动开关 开启/关闭所有插件 : 开启/关闭当前群组所有插件状态 + 开启/关闭所有群被动 : 开启/关闭当前群组所有群被动 群被动状态 : 查看被动技能开关状态 醒来 : 结束休眠 休息吧 : 群组休眠, 不会再响应命令 @@ -107,8 +108,14 @@ async def _( if gid: """群组中使用命令""" if task.result: - result = await PluginManage.unblock_group_task(name, gid) - logger.info(f"开启群组被动 {name}", arparma.header_result, session=session) + if all.result: + result = await PluginManage.unblock_group_all_task(gid) + logger.info(f"开启所有群组被动", arparma.header_result, session=session) + else: + result = await PluginManage.unblock_group_task(name, gid) + logger.info( + f"开启群组被动 {name}", arparma.header_result, session=session + ) else: if session.id1 in bot.config.superusers and default_status.result: """单个插件的进群默认修改""" @@ -195,7 +202,14 @@ async def _( gid = session.id3 or session.id2 if gid: if task.result: - result = await PluginManage.block_group_task(name, gid) + if all.result: + result = await PluginManage.block_group_all_task(gid) + logger.info(f"开启所有群组被动", arparma.header_result, session=session) + else: + result = await PluginManage.block_group_task(name, gid) + logger.info( + f"关闭群组被动 {name}", arparma.header_result, session=session + ) else: if session.id1 in bot.config.superusers and default_status.result: """单个插件的进群默认修改""" diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index 1c96eed0..2565c463 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -286,6 +286,18 @@ class PluginManage: """ return await cls._change_group_task(task_name, group_id, False) + @classmethod + async def unblock_group_all_task(cls, group_id: str) -> str: + """启用被动技能 + + 参数: + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task("", group_id, False, True) + @classmethod async def block_group_task(cls, task_name: str, group_id: str) -> str: """禁用被动技能 @@ -299,6 +311,18 @@ class PluginManage: """ return await cls._change_group_task(task_name, group_id, True) + @classmethod + async def block_group_all_task(cls, group_id: str) -> str: + """禁用被动技能 + + 参数: + group_id: 群组id + + 返回: + str: 返回信息 + """ + return await cls._change_group_task("", group_id, True, True) + @classmethod async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: """启用群组插件 @@ -314,7 +338,7 @@ class PluginManage: @classmethod async def _change_group_task( - cls, task_name: str, group_id: str, status: bool + cls, task_name: str, group_id: str, status: bool, is_all: bool = False ) -> str: """改变群组被动技能状态 @@ -322,21 +346,35 @@ class PluginManage: task_name: 被动技能名称 group_id: 群组Id status: 状态 + is_all: 所有群被动 返回: str: 返回信息 """ - if task := await TaskInfo.get_or_none(name=task_name): - status_str = "关闭" if status else "开启" - group, _ = await GroupConsole.get_or_create( - group_id=group_id, channel_id__isnull=True - ) - if status: - group.block_task += f"{task.module}," - else: - group.block_task = group.block_task.replace(f"{task.module},", "") - await group.save(update_fields=["block_task"]) - return f"已成功{status_str} {task_name} 被动技能!" + status_str = "关闭" if status else "开启" + if is_all: + modules = await TaskInfo.annotate().values_list("module", flat=True) + if modules: + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task = ",".join(modules) + "," # type: ignore + else: + group.block_task = "" + await group.save(update_fields=["block_task"]) + return f"已成功{status_str}全部被动技能!" + else: + if task := await TaskInfo.get_or_none(name=task_name): + group, _ = await GroupConsole.get_or_create( + group_id=group_id, channel_id__isnull=True + ) + if status: + group.block_task += f"{task.module}," + else: + group.block_task = group.block_task.replace(f"{task.module},", "") + await group.save(update_fields=["block_task"]) + return f"已成功{status_str} {task_name} 被动技能!" return "没有找到这个被动技能喔..." @classmethod diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index ddbdb535..db89c596 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -15,7 +15,7 @@ _status_matcher = on_alconna( "switch", Option("-t|--task", action=store_true, help_text="被动技能"), Option("-df|--default", action=store_true, help_text="进群默认开关"), - Option("--all", action=store_true, help_text="全部插件"), + Option("--all", action=store_true, help_text="全部插件/被动"), Subcommand( "open", Args["plugin_name?", [str, int]], @@ -73,6 +73,20 @@ _status_matcher.shortcut( prefix=True, ) +_status_matcher.shortcut( + r"开启(所有|全部)群被动", + command="switch", + arguments=["open", "--task", "--all"], + prefix=True, +) + +_status_matcher.shortcut( + r"关闭(所有|全部)群被动", + command="switch", + arguments=["close", "--task", "--all"], + prefix=True, +) + _status_matcher.shortcut( r"开启所有(插件|功能)", From e07851aecf1e1846da8ff97a2a0fb75c44a7d1cb Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 7 Aug 2024 00:11:49 +0800 Subject: [PATCH 111/132] =?UTF-8?q?=E2=9C=A8=20=20=E6=9B=B4=E5=8A=A0?= =?UTF-8?q?=E7=BB=86=E8=87=B4=E7=9A=84=E7=BE=A4=E8=A2=AB=E5=8A=A8=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 118 +++++++++++++----- .../admin/plugin_switch/_data_source.py | 69 ++++++++-- .../admin/plugin_switch/command.py | 42 +++---- 3 files changed, 164 insertions(+), 65 deletions(-) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 1c870b2b..7d383046 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -51,6 +51,19 @@ __plugin_meta__ = PluginMetadata( 私聊中: 开启/关闭所有插件全局状态 群组中: 开启/关闭当前群组所有插件状态 + 开启/关闭群被动[name] ?[-g [group_id]] + 私聊中: 开启/关闭全局指定的被动状态 + 群组中: 开启/关闭当前群组指定的被动状态 + 示例: + 关闭群被动早晚安 + 关闭群被动早晚安 -g 12355555 + + 开启/关闭所有群被动 -[g ?[group_id]] + 私聊中: 开启/关闭全局或指定群组被动状态 + 示例: + 开启所有群被动: 开启全局所有被动 + 开启所有群被动 -g 12345678: 开启群组12345678所有被动 + 私聊下: 示例: 开启签到 : 全局开启签到 @@ -106,7 +119,7 @@ async def _( name = plugin_name.result gid = session.id3 or session.id2 if gid: - """群组中使用命令""" + """修改当前群组的数据""" if task.result: if all.result: result = await PluginManage.unblock_group_all_task(gid) @@ -137,7 +150,7 @@ async def _( session=session, ) else: - result = await PluginManage.block_group_plugin(name, gid) + result = await PluginManage.unblock_group_plugin(name, gid) logger.info( f"开启功能 {name}", arparma.header_result, session=session ) @@ -146,14 +159,21 @@ async def _( """私聊""" group_id = group.result if group.available else None if all.result: - result = await PluginManage.set_all_plugin_status( - True, default_status.result, group_id - ) - logger.info( - f"超级用户开启全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", - arparma.header_result, - session=session, - ) + if task.result: + """关闭全局或指定群全部被动""" + if group_id: + result = await PluginManage.unblock_group_all_task(group_id) + else: + result = await PluginManage.unblock_global_all_task() + else: + result = await PluginManage.set_all_plugin_status( + True, default_status.result, group_id + ) + logger.info( + f"超级用户开启全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) await Text(result).finish(reply=True) if default_status.result: result = await PluginManage.set_default_status(name, True) @@ -165,13 +185,25 @@ async def _( ) await Text(result).finish(reply=True) if task.result: - result = await PluginManage.superuser_task_handle(name, group_id, True) - logger.info( - f"超级用户开启被动技能 {name}", - arparma.header_result, - session=session, - target=group_id, - ) + split_list = name.split() + if len(split_list) > 1: + name = split_list[0] + group_id = split_list[1] + if group_id: + result = await PluginManage.superuser_task_handle(name, group_id, True) + logger.info( + f"超级用户开启被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + result = await PluginManage.unblock_global_task(name) + logger.info( + f"超级用户开启全局被动技能 {name}", + arparma.header_result, + session=session, + ) await Text(result).finish(reply=True) else: result = await PluginManage.superuser_block(name, None, group_id) @@ -201,6 +233,7 @@ async def _( name = plugin_name.result gid = session.id3 or session.id2 if gid: + """修改当前群组的数据""" if task.result: if all.result: result = await PluginManage.block_group_all_task(gid) @@ -231,7 +264,7 @@ async def _( session=session, ) else: - result = await PluginManage.unblock_group_plugin(name, gid) + result = await PluginManage.block_group_plugin(name, gid) logger.info( f"关闭功能 {name}", arparma.header_result, session=session ) @@ -239,14 +272,21 @@ async def _( elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None if all.result: - result = await PluginManage.set_all_plugin_status( - False, default_status.result, group_id - ) - logger.info( - f"超级用户关闭全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", - arparma.header_result, - session=session, - ) + if task.result: + """关闭全局或指定群全部被动""" + if group_id: + result = await PluginManage.block_group_all_task(group_id) + else: + result = await PluginManage.block_global_all_task() + else: + result = await PluginManage.set_all_plugin_status( + False, default_status.result, group_id + ) + logger.info( + f"超级用户关闭全部功能全局开关 {f'指定群组: {group_id}' if group_id else ''}", + arparma.header_result, + session=session, + ) await Text(result).finish(reply=True) if default_status.result: result = await PluginManage.set_default_status(name, False) @@ -258,13 +298,25 @@ async def _( ) await Text(result).finish(reply=True) if task.result: - result = await PluginManage.superuser_task_handle(name, group_id, False) - logger.info( - f"超级用户关闭被动技能 {name}", - arparma.header_result, - session=session, - target=group_id, - ) + split_list = name.split() + if len(split_list) > 1: + name = split_list[0] + group_id = split_list[1] + if group_id: + result = await PluginManage.superuser_task_handle(name, group_id, False) + logger.info( + f"超级用户关闭被动技能 {name}", + arparma.header_result, + session=session, + target=group_id, + ) + else: + result = await PluginManage.block_global_task(name) + logger.info( + f"超级用户关闭全局被动技能 {name}", + arparma.header_result, + session=session, + ) await Text(result).finish(reply=True) else: _type = BlockType.ALL diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py index 2565c463..b10a929e 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/_data_source.py @@ -189,8 +189,14 @@ class PluginManage: if group := await GroupConsole.get_or_none( group_id=group_id, channel_id__isnull=True ): + module_list = await PluginInfo.filter( + plugin_type=PluginType.NORMAL + ).values_list("module", flat=True) if status: - group.block_plugin = "" + for module in module_list: + group.block_plugin = group.block_plugin.replace( + f"{module},", "" + ) else: module_list = await PluginInfo.filter( plugin_type=PluginType.NORMAL @@ -271,7 +277,7 @@ class PluginManage: 返回: str: 返回信息 """ - return await cls._change_group_plugin(plugin_name, group_id, True) + return await cls._change_group_plugin(plugin_name, group_id, False) @classmethod async def unblock_group_task(cls, task_name: str, group_id: str) -> str: @@ -323,6 +329,52 @@ class PluginManage: """ return await cls._change_group_task("", group_id, True, True) + @classmethod + async def block_global_all_task(cls) -> str: + """禁用全局被动技能 + + 返回: + str: 返回信息 + """ + await TaskInfo.all().update(status=False) + return "已全局禁用所有被动状态" + + @classmethod + async def block_global_task(cls, name: str) -> str: + """禁用全局被动技能 + + 参数: + name: 被动技能名称 + + 返回: + str: 返回信息 + """ + await TaskInfo.filter(name=name).update(status=False) + return f"已全局禁用被动状态 {name}" + + @classmethod + async def unblock_global_all_task(cls) -> str: + """开启全局被动技能 + + 返回: + str: 返回信息 + """ + await TaskInfo.all().update(status=True) + return "已全局开启所有被动状态" + + @classmethod + async def unblock_global_task(cls, name: str) -> str: + """开启全局被动技能 + + 参数: + name: 被动技能名称 + + 返回: + str: 返回信息 + """ + await TaskInfo.filter(name=name).update(status=True) + return f"已全局开启被动状态 {name}" + @classmethod async def unblock_group_plugin(cls, plugin_name: str, group_id: str) -> str: """启用群组插件 @@ -334,7 +386,7 @@ class PluginManage: 返回: str: 返回信息 """ - return await cls._change_group_plugin(plugin_name, group_id, False) + return await cls._change_group_plugin(plugin_name, group_id, True) @classmethod async def _change_group_task( @@ -361,7 +413,8 @@ class PluginManage: if status: group.block_task = ",".join(modules) + "," # type: ignore else: - group.block_task = "" + for module in modules: + group.block_task = group.block_task.replace(f"{module},", "") await group.save(update_fields=["block_task"]) return f"已成功{status_str}全部被动技能!" else: @@ -372,6 +425,8 @@ class PluginManage: if status: group.block_task += f"{task.module}," else: + if f"super:{task.module}," in group.block_task: + return f"{status_str} {task_name} 被动技能失败,当前群组该被动已被管理员禁用" group.block_task = group.block_task.replace(f"{task.module},", "") await group.save(update_fields=["block_task"]) return f"已成功{status_str} {task_name} 被动技能!" @@ -443,10 +498,8 @@ class PluginManage: else: group.block_task += f"super:{task.module}," await group.save(update_fields=["block_task"]) - else: - task.status = status - await task.save(update_fields=["status"]) - return f"已成功将被动技能 {task_name} 全局{status_str}!" + return f"已成功将群组 {group_id} 被动技能 {task_name} {status_str}!" + return "没有找到这个群组喔..." return "没有找到这个功能喔..." @classmethod diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/command.py b/zhenxun/builtin_plugins/admin/plugin_switch/command.py index db89c596..6d33b4df 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/command.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/command.py @@ -16,24 +16,17 @@ _status_matcher = on_alconna( Option("-t|--task", action=store_true, help_text="被动技能"), Option("-df|--default", action=store_true, help_text="进群默认开关"), Option("--all", action=store_true, help_text="全部插件/被动"), + Option("-g|--group", Args["group?", str], help_text="指定群组"), Subcommand( "open", Args["plugin_name?", [str, int]], - Option( - "-g|--group", - Args["group", str], - ), ), Subcommand( "close", Args["plugin_name?", [str, int]], Option( "-t|--type", - Args["block_type", ["all", "a", "private", "p", "group", "g"]], - ), - Option( - "-g|--group", - Args["group", str], + Args["block_type?", ["all", "a", "private", "p", "group", "g"]], ), ), ), @@ -67,12 +60,20 @@ _status_matcher.shortcut( _status_matcher.shortcut( - r"开启群被动(?P.+)", + r"开启群被动\s*(?P.+)", command="switch", arguments=["open", "{name}", "--task"], prefix=True, ) +_status_matcher.shortcut( + r"关闭群被动\s*(?P.+)", + command="switch", + arguments=["close", "{name}", "--task"], + prefix=True, +) + + _status_matcher.shortcut( r"开启(所有|全部)群被动", command="switch", @@ -117,14 +118,6 @@ _status_matcher.shortcut( ) -_status_matcher.shortcut( - r"关闭群被动(?P.+)", - command="switch", - arguments=["close", "{name}", "--task"], - prefix=True, -) - - _status_matcher.shortcut( r"关闭所有(插件|功能)", command="switch", @@ -139,6 +132,13 @@ _status_matcher.shortcut( prefix=True, ) +_status_matcher.shortcut( + r"关闭(?P.+)", + command="switch", + arguments=["close", "{name}"], + prefix=True, +) + _status_matcher.shortcut( r"关闭(插件|功能)df(?P.+)", command="switch", @@ -146,12 +146,6 @@ _status_matcher.shortcut( prefix=True, ) -_status_matcher.shortcut( - r"关闭(?P.+)", - command="switch", - arguments=["close", "{name}"], - prefix=True, -) _group_status_matcher.shortcut( r"醒来", From 968fb7b157aaba74961b46d324b2fd7e24603954 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 7 Aug 2024 18:59:43 +0800 Subject: [PATCH 112/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=94=B9bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/plugin_switch/__init__.py | 3 +- .../builtin_plugins/hooks/_auth_checker.py | 19 +- zhenxun/builtin_plugins/init/init_plugin.py | 6 +- zhenxun/plugins/black_word/__init__.py | 282 +----------------- zhenxun/plugins/black_word/black_watch.py | 61 ++++ zhenxun/plugins/black_word/black_word.py | 235 +++++++++++++++ zhenxun/plugins/mute/mute_message.py | 15 + 7 files changed, 326 insertions(+), 295 deletions(-) create mode 100644 zhenxun/plugins/black_word/black_watch.py create mode 100644 zhenxun/plugins/black_word/black_word.py diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 7d383046..63c7484f 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -337,7 +337,6 @@ async def _( @_group_status_matcher.handle() async def _( - bot: Bot, session: EventSession, arparma: Arparma, status: str, @@ -348,7 +347,7 @@ async def _( logger.info("进行休眠", arparma.header_result, session=session) await Text("那我先睡觉了...").finish() else: - if PluginManage.is_wake(gid): + if await PluginManage.is_wake(gid): await Text("我还醒着呢!").finish() await PluginManage.wake(gid) logger.info("醒来", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 4c92dfb3..773867c8 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -1,7 +1,4 @@ -from typing import Dict -from unittest import result - -from nonebot.adapters import Bot, Event +from nonebot.adapters import Bot from nonebot.exception import IgnoredException from nonebot.matcher import Matcher from nonebot_plugin_alconna import UniMsg @@ -10,7 +7,6 @@ from nonebot_plugin_session import EventSession from pydantic import BaseModel from zhenxun.configs.config import Config -from zhenxun.models.ban_console import BanConsole from zhenxun.models.group_console import GroupConsole from zhenxun.models.level_user import LevelUser from zhenxun.models.plugin_info import PluginInfo @@ -41,9 +37,9 @@ class LimitManage: add_module = [] - cd_limit: Dict[str, Limit] = {} - block_limit: Dict[str, Limit] = {} - count_limit: Dict[str, Limit] = {} + cd_limit: dict[str, Limit] = {} + block_limit: dict[str, Limit] = {} + count_limit: dict[str, Limit] = {} @classmethod def add_limit(cls, limit: PluginLimit): @@ -209,7 +205,8 @@ class AuthChecker: if user_id and matcher.plugin and (module_path := matcher.plugin.module_name): user = await UserConsole.get_user(user_id, session.platform) if plugin := await PluginInfo.get_or_none(module_path=module_path): - if plugin.plugin_type == PluginType.HIDDEN: + if plugin.plugin_type == PluginType.HIDDEN and plugin.name != "帮助": + logger.debug("插件为HIDDEN且不是帮助功能,已跳过...") return try: cost_gold = await self.auth_cost(user, plugin, session) @@ -433,7 +430,7 @@ class AuthChecker: if group.level < 0: """群权限小于0""" logger.debug( - f"{plugin.name}({plugin.module}) 群黑名单, 群权限-1...", + f"群黑名单, 群权限-1...", "HOOK", session=session, ) @@ -442,7 +439,7 @@ class AuthChecker: """群休眠""" if text.strip() != "醒来": logger.debug( - f"{plugin.name}({plugin.module}) 功能总开关关闭状态...", + f"功能总开关关闭状态...", "HOOK", session=session, ) diff --git a/zhenxun/builtin_plugins/init/init_plugin.py b/zhenxun/builtin_plugins/init/init_plugin.py index a2380d5c..49ec8dfb 100644 --- a/zhenxun/builtin_plugins/init/init_plugin.py +++ b/zhenxun/builtin_plugins/init/init_plugin.py @@ -105,7 +105,7 @@ async def _(): if module_list := await PluginInfo.all().values("id", "module_path"): module2id = {m["module_path"]: m["id"] for m in module_list} for plugin in get_loaded_plugins(): - load_plugin.append(plugin.name) + load_plugin.append(plugin.module_name) if plugin.metadata: await _handle_setting(plugin, plugin_list, limit_list, task_list) create_list = [] @@ -162,8 +162,8 @@ async def _(): 10, ) await data_migration() - await PluginInfo.filter(module__in=load_plugin).update(load_status=True) - await PluginInfo.filter(module__not_in=load_plugin).update(load_status=False) + await PluginInfo.filter(module_path__in=load_plugin).update(load_status=True) + await PluginInfo.filter(module_path__not_in=load_plugin).update(load_status=False) async def data_migration(): diff --git a/zhenxun/plugins/black_word/__init__.py b/zhenxun/plugins/black_word/__init__.py index 79cf4747..eb35e275 100644 --- a/zhenxun/plugins/black_word/__init__.py +++ b/zhenxun/plugins/black_word/__init__.py @@ -1,281 +1,5 @@ -from datetime import datetime -from typing import Any, List +from pathlib import Path -from nonebot import on_message -from nonebot.adapters import Bot, Event -from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor -from nonebot.permission import SUPERUSER -from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import ( - Alconna, - Args, - Arparma, - Match, - Option, - UniMsg, - on_alconna, -) -from nonebot_plugin_saa import Image, Text -from nonebot_plugin_session import EventSession +import nonebot -from zhenxun.configs.config import NICKNAME, Config -from zhenxun.configs.utils import PluginExtraData, RegisterConfig -from zhenxun.models.ban_console import BanConsole -from zhenxun.models.group_console import GroupConsole -from zhenxun.services.log import logger -from zhenxun.utils.enum import PluginType -from zhenxun.utils.image_utils import BuildImage - -from .data_source import set_user_punish, show_black_text_image -from .utils import black_word_manager - -__plugin_meta__ = PluginMetadata( - name="敏感词检测", - description="请注意你的发言!", - usage=""" - 惩罚机制: 检测内容提示 - 设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取 - 记录名单: 查看检测记录名单 - 记录名单: - -u [uid] 指定用户记录名单 - -g [gid] 指定群组记录名单 - -d [date] 指定日期 - -dt ['=', '>', '<'] 大于小于等于指定日期 - - 示例: - 设置惩罚 123123123 0 1 - 记录名单 -u 123123123 - 记录名单 -g 333333 - 记录名单 -d 2022-11-11 - 记录名单 -d 2022-11-11 -dt > - """.strip(), - extra=PluginExtraData( - author="HibiKier", - version="0.1", - plugin_type=PluginType.SUPERUSER, - menu_type="其他", - configs=[ - RegisterConfig( - key="CYCLE_DAYS", - value=30, - help="黑名单词汇记录周期", - default_value=30, - type=int, - ), - RegisterConfig( - key="TOLERATE_COUNT", - value=[5, 1, 1, 1, 1], - help="各个级别惩罚的容忍次数, 依次为: 1, 2, 3, 4, 5", - default_value=[5, 1, 1, 1, 1], - type=List[int], - ), - RegisterConfig( - key="AUTO_PUNISH", - value=True, - help="是否启动自动惩罚机制", - default_value=True, - type=bool, - ), - RegisterConfig( - key="BAN_4_DURATION", - value=360, - help="Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", - default_value=360, - type=int, - ), - RegisterConfig( - key="BAN_3_DURATION", - value=7, - help="Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", - default_value=7, - type=int, - ), - RegisterConfig( - key="WARNING_RESULT", - value=f"请注意对{NICKNAME}的发言内容", - help="口头警告内容", - default_value=None, - ), - RegisterConfig( - key="AUTO_ADD_PUNISH_LEVEL", - value=360, - help="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", - default_value=360, - type=int, - ), - RegisterConfig( - key="ADD_PUNISH_LEVEL_TO_COUNT", - value=3, - help="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", - default_value=3, - type=int, - ), - RegisterConfig( - key="ALAPI_CHECK_FLAG", - value=False, - help="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", - default_value=False, - type=bool, - ), - RegisterConfig( - key="CONTAIN_BLACK_STOP_PROPAGATION", - value=True, - help="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", - default_value=True, - type=bool, - ), - ], - ).dict(), -) - - -_message_matcher = on_message(priority=1, block=False) - -_punish_matcher = on_alconna( - Alconna("设置惩罚", Args["uid", str]["id", int]["punish_level", int]), - priority=1, - permission=SUPERUSER, - block=True, -) - - -_show_matcher = on_alconna( - Alconna( - "记录名单", - Option("-u|--uid", Args["uid", str]), - Option("-g|--group", Args["gid", str]), - Option("-d|--date", Args["date", str]), - Option("-dt|--type", Args["date_type", ["=", ">", "<"]], default="="), - ), - priority=1, - permission=SUPERUSER, - block=True, -) - -_show_punish_matcher = on_alconna( - Alconna("惩罚机制"), aliases={"敏感词检测"}, priority=1, block=True -) - - -# 黑名单词汇检测 -@run_preprocessor -async def _( - bot: Bot, message: UniMsg, matcher: Matcher, event: Event, session: EventSession -): - gid = session.id3 or session.id2 - if session.id1: - if ( - event.is_tome() - and matcher.plugin_name == "black_word" - and not await BanConsole.is_ban(session.id1, gid) - ): - msg = message.extract_plain_text() - if session.id1 in bot.config.superusers: - return logger.debug( - f"超级用户跳过黑名单词汇检查 Message: {msg}", target=session.id1 - ) - if gid: - """屏蔽群权限-1的群""" - group, _ = await GroupConsole.get_or_create( - group_id=gid, channel_id__isnull=True - ) - if group.level < 0: - return - if await black_word_manager.check(bot, session, msg) and Config.get_config( - "black_word", "CONTAIN_BLACK_STOP_PROPAGATION" - ): - matcher.stop_propagation() - - -@_show_matcher.handle() -async def _( - bot: Bot, uid: Match[str], gid: Match[str], date: Match[str], date_type: Match[str] -): - user_id = None - group_id = None - date_ = None - date_str = None - date_type_ = "=" - if uid.available: - user_id = uid.result - if gid.available: - group_id = gid.result - if date.available: - date_str = date.result - if date_type.available: - date_type_ = date_type.result - if date_str: - try: - date_ = datetime.strptime(date_str, "%Y-%m-%d") - except ValueError: - await Text("日期格式错误,需要:年-月-日").finish() - result = await show_black_text_image( - user_id, - group_id, - date_, - date_type_, - ) - await Image(result.pic2bytes()).send() - - -@_show_punish_matcher.handle() -async def _(): - text = f""" - ** 惩罚机制 ** - - 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 - - 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 - - 目前公开的惩罚等级: - - 1级:永久ban - - 2级:删除好友 - - 3级:ban指定/随机天数 - - 4级:ban指定/随机时长 - - 5级:警告 - - 备注: - - 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 - - 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 - - 关于敏感词: - - 记住不要骂{NICKNAME}就对了! - """.strip() - max_width = 0 - for m in text.split("\n"): - max_width = len(m) * 20 if len(m) * 20 > max_width else max_width - max_height = len(text.split("\n")) * 24 - A = BuildImage( - max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" - ) - await A.text((10, 10), text) - await Image(A.pic2bytes()).send() - - -@_punish_matcher.handle() -async def _( - bot: Bot, - session: EventSession, - arparma: Arparma, - uid: str, - id: int, - punish_level: int, -): - result = await set_user_punish( - bot, uid, session.id2 or session.id3, id, punish_level - ) - await Text(result).send(reply=True) - logger.info( - f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}", - arparma.header_result, - session=session, - ) +nonebot.load_plugins(str(Path(__file__).parent.resolve())) diff --git a/zhenxun/plugins/black_word/black_watch.py b/zhenxun/plugins/black_word/black_watch.py new file mode 100644 index 00000000..be081a12 --- /dev/null +++ b/zhenxun/plugins/black_word/black_watch.py @@ -0,0 +1,61 @@ +from nonebot.adapters import Bot, Event +from nonebot.matcher import Matcher +from nonebot.message import run_preprocessor +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import Config +from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType + +from .utils import black_word_manager + +__plugin_meta__ = PluginMetadata( + name="敏感词文本监听", + description="敏感词文本监听", + usage="".strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.HIDDEN, + ).dict(), +) + +base_config = Config.get("black_word") + + +# 黑名单词汇检测 +@run_preprocessor +async def _( + bot: Bot, message: UniMsg, matcher: Matcher, event: Event, session: EventSession +): + gid = session.id3 or session.id2 + if session.id1: + if ( + event.is_tome() + and matcher.plugin_name == "black_word" + and not await BanConsole.is_ban(session.id1, gid) + ): + msg = message.extract_plain_text() + if session.id1 in bot.config.superusers: + return logger.debug( + f"超级用户跳过黑名单词汇检查 Message: {msg}", target=session.id1 + ) + if gid: + """屏蔽群权限-1的群""" + group, _ = await GroupConsole.get_or_create( + group_id=gid, channel_id__isnull=True + ) + if group.level < 0: + return + if await BanConsole.is_ban(None, gid): + """屏蔽群被ban的群""" + return + if await black_word_manager.check(bot, session, msg) and base_config.get( + "CONTAIN_BLACK_STOP_PROPAGATION" + ): + matcher.stop_propagation() diff --git a/zhenxun/plugins/black_word/black_word.py b/zhenxun/plugins/black_word/black_word.py new file mode 100644 index 00000000..1cad209f --- /dev/null +++ b/zhenxun/plugins/black_word/black_word.py @@ -0,0 +1,235 @@ +from datetime import datetime +from typing import List + +from nonebot.adapters import Bot +from nonebot.permission import SUPERUSER +from nonebot.plugin import PluginMetadata +from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna +from nonebot_plugin_saa import Image, Text +from nonebot_plugin_session import EventSession + +from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType +from zhenxun.utils.image_utils import BuildImage + +from .data_source import set_user_punish, show_black_text_image + +__plugin_meta__ = PluginMetadata( + name="敏感词检测", + description="请注意你的发言!", + usage=""" + 惩罚机制: 检测内容提示 + 设置惩罚 [uid] [id] [level]: 设置惩罚内容, 此id需要通过`记录名单 -u:uid`来获取 + 记录名单: 查看检测记录名单 + 记录名单: + -u [uid] 指定用户记录名单 + -g [gid] 指定群组记录名单 + -d [date] 指定日期 + -dt ['=', '>', '<'] 大于小于等于指定日期 + + 示例: + 设置惩罚 123123123 0 1 + 记录名单 -u 123123123 + 记录名单 -g 333333 + 记录名单 -d 2022-11-11 + 记录名单 -d 2022-11-11 -dt > + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + plugin_type=PluginType.SUPERUSER, + menu_type="其他", + configs=[ + RegisterConfig( + key="CYCLE_DAYS", + value=30, + help="黑名单词汇记录周期", + default_value=30, + type=int, + ), + RegisterConfig( + key="TOLERATE_COUNT", + value=[5, 1, 1, 1, 1], + help="各个级别惩罚的容忍次数, 依次为: 1, 2, 3, 4, 5", + default_value=[5, 1, 1, 1, 1], + type=List[int], + ), + RegisterConfig( + key="AUTO_PUNISH", + value=True, + help="是否启动自动惩罚机制", + default_value=True, + type=bool, + ), + RegisterConfig( + key="BAN_4_DURATION", + value=360, + help="Ban时长(分钟),四级惩罚,可以为指定数字或指定列表区间(随机),例如 [30, 360]", + default_value=360, + type=int, + ), + RegisterConfig( + key="BAN_3_DURATION", + value=7, + help="Ban时长(天),三级惩罚,可以为指定数字或指定列表区间(随机),例如 [7, 30]", + default_value=7, + type=int, + ), + RegisterConfig( + key="WARNING_RESULT", + value=f"请注意对{NICKNAME}的发言内容", + help="口头警告内容", + default_value=None, + ), + RegisterConfig( + key="AUTO_ADD_PUNISH_LEVEL", + value=360, + help="自动提级机制,当周期内处罚次数大于某一特定值就提升惩罚等级", + default_value=360, + type=int, + ), + RegisterConfig( + key="ADD_PUNISH_LEVEL_TO_COUNT", + value=3, + help="在CYCLE_DAYS周期内触发指定惩罚次数后提升惩罚等级", + default_value=3, + type=int, + ), + RegisterConfig( + key="ALAPI_CHECK_FLAG", + value=False, + help="当未检测到已收录的敏感词时,开启ALAPI文本检测并将疑似文本发送给超级用户", + default_value=False, + type=bool, + ), + RegisterConfig( + key="CONTAIN_BLACK_STOP_PROPAGATION", + value=True, + help="当文本包含任意敏感词时,停止向下级插件传递,即不触发ai", + default_value=True, + type=bool, + ), + ], + ).dict(), +) + + +_punish_matcher = on_alconna( + Alconna("设置惩罚", Args["uid", str]["id", int]["punish_level", int]), + priority=1, + permission=SUPERUSER, + block=True, +) + + +_show_matcher = on_alconna( + Alconna( + "记录名单", + Option("-u|--uid", Args["uid", str]), + Option("-g|--group", Args["gid", str]), + Option("-d|--date", Args["date", str]), + Option("-dt|--type", Args["date_type", ["=", ">", "<"]], default="="), + ), + priority=1, + permission=SUPERUSER, + block=True, +) + +_show_punish_matcher = on_alconna( + Alconna("惩罚机制"), aliases={"敏感词检测"}, priority=1, block=True +) + + +@_show_matcher.handle() +async def _( + bot: Bot, uid: Match[str], gid: Match[str], date: Match[str], date_type: Match[str] +): + user_id = None + group_id = None + date_ = None + date_str = None + date_type_ = "=" + if uid.available: + user_id = uid.result + if gid.available: + group_id = gid.result + if date.available: + date_str = date.result + if date_type.available: + date_type_ = date_type.result + if date_str: + try: + date_ = datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + await Text("日期格式错误,需要:年-月-日").finish() + result = await show_black_text_image( + user_id, + group_id, + date_, + date_type_, + ) + await Image(result.pic2bytes()).send() + + +@_show_punish_matcher.handle() +async def _(): + text = f""" + ** 惩罚机制 ** + + 惩罚前包含容忍机制,在指定周期内会容忍偶尔少次数的敏感词只会进行警告提醒 + + 多次触发同级惩罚会使惩罚等级提高,即惩罚自动提级机制 + + 目前公开的惩罚等级: + + 1级:永久ban + + 2级:删除好友 + + 3级:ban指定/随机天数 + + 4级:ban指定/随机时长 + + 5级:警告 + + 备注: + + 该功能为测试阶段,如果你有被误封情况,请联系管理员,会从数据库中提取出你的数据进行审核后判断 + + 目前该功能暂不完善,部分情况会由管理员鉴定,请注意对真寻的发言 + + 关于敏感词: + + 记住不要骂{NICKNAME}就对了! + """.strip() + max_width = 0 + for m in text.split("\n"): + max_width = len(m) * 20 if len(m) * 20 > max_width else max_width + max_height = len(text.split("\n")) * 24 + A = BuildImage( + max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" + ) + await A.text((10, 10), text) + await Image(A.pic2bytes()).send() + + +@_punish_matcher.handle() +async def _( + bot: Bot, + session: EventSession, + arparma: Arparma, + uid: str, + id: int, + punish_level: int, +): + result = await set_user_punish( + bot, uid, session.id2 or session.id3, id, punish_level + ) + await Text(result).send(reply=True) + logger.info( + f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}", + arparma.header_result, + session=session, + ) diff --git a/zhenxun/plugins/mute/mute_message.py b/zhenxun/plugins/mute/mute_message.py index 401b2fc5..a9e6d42a 100644 --- a/zhenxun/plugins/mute/mute_message.py +++ b/zhenxun/plugins/mute/mute_message.py @@ -1,17 +1,32 @@ from nonebot import on_message from nonebot.adapters import Bot +from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import UniMsg from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME +from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import get_download_image_hash from zhenxun.utils.platform import PlatformUtils from ._data_source import mute_manage +__plugin_meta__ = PluginMetadata( + name="刷屏监听", + description="", + usage="", + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="其他", + plugin_type=PluginType.HIDDEN, + ).dict(), +) + _matcher = on_message(priority=1, block=False) From 8b9fe6e2bc049ec3a773f3ba51009cf66f650ced Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 7 Aug 2024 19:03:07 +0800 Subject: [PATCH 113/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BF=84=E7=BD=97=E6=96=AF=E8=BD=AE=E7=9B=98=E6=8E=A5=E5=8F=97?= =?UTF-8?q?=E5=AF=B9=E5=86=B3=E6=97=B6=E6=9C=AA=E9=87=8D=E7=BD=AE30s?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/russian/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index ea4a5ac0..e561085f 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -102,13 +102,13 @@ async def _( @_accept_matcher.handle() -async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): +async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): gid = session.id2 if not session.id1: await Text("用户id为空...").finish() if not gid: await Text("群组id为空...").finish() - result = await russian_manage.accept(gid, session.id1, uname) + result = await russian_manage.accept(bot, gid, session.id1, uname) await result.send() logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session) From 0b3698438bdbad4e1168c2c522dfe790f45be13f Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Wed, 7 Aug 2024 23:31:25 +0800 Subject: [PATCH 114/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8E=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- poetry.lock | 128 +++++++++--------- pyproject.toml | 2 +- .../builtin_plugins/admin/ban/_data_source.py | 4 +- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 14 +- zhenxun/models/ban_console.py | 2 +- zhenxun/plugins/draw_card/__init__.py | 2 +- zhenxun/plugins/pid_search.py | 16 ++- zhenxun/plugins/russian/data_source.py | 4 +- zhenxun/plugins/web_ui/auth/__init__.py | 2 +- 9 files changed, 90 insertions(+), 84 deletions(-) diff --git a/poetry.lock b/poetry.lock index 53a0678d..8d552dfd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -216,18 +216,18 @@ reference = "ali" [[package]] name = "arclet-alconna" -version = "1.8.19" +version = "1.8.23" description = "A High-performance, Generality, Humane Command Line Arguments Parser Library." optional = false python-versions = ">=3.8" files = [ - {file = "arclet_alconna-1.8.19-py3-none-any.whl", hash = "sha256:c78d5527d8ea13990e96f996a3480bf236ad63b81114f53ce2c010bc2a0ee1d8"}, - {file = "arclet_alconna-1.8.19.tar.gz", hash = "sha256:12064caad6854a4b00dc5b7376d86e15911072acd9278531bf86e4fb97568288"}, + {file = "arclet_alconna-1.8.23-py3-none-any.whl", hash = "sha256:d4d8a427715408399e46530ec6bdefff4de72ff5d51183fa50ce5ea56a4e2a2a"}, + {file = "arclet_alconna-1.8.23.tar.gz", hash = "sha256:f811caf60dc4231b70a6885fe1af35aa95ae93bad46566e9086b623f449c9a09"}, ] [package.dependencies] -nepattern = ">=0.7.3,<1.0.0" -tarina = ">=0.5.0" +nepattern = ">=0.7.6,<1.0.0" +tarina = ">=0.5.5" typing-extensions = ">=4.5.0" [package.extras] @@ -240,17 +240,17 @@ reference = "ali" [[package]] name = "arclet-alconna-tools" -version = "0.7.6" +version = "0.7.9" description = "Builtin Tools for Alconna" optional = false python-versions = ">=3.8" files = [ - {file = "arclet_alconna_tools-0.7.6-py3-none-any.whl", hash = "sha256:fdd1cb900603ce6bb00295bf7bf7f60dfdb764f0614abe248cdcb754e5149edd"}, - {file = "arclet_alconna_tools-0.7.6.tar.gz", hash = "sha256:7cb7dc54c1c2198529c63227739423401051b8489374f1a7a3efa0c4e70b2a22"}, + {file = "arclet_alconna_tools-0.7.9-py3-none-any.whl", hash = "sha256:01a3462bb9f8dbe55010b394f7a0ac11e331799d463e326738870dce191aa608"}, + {file = "arclet_alconna_tools-0.7.9.tar.gz", hash = "sha256:bded24c4157e13e2d803fe7b77ee246fda456206451337015513f150d1e4449c"}, ] [package.dependencies] -arclet-alconna = ">=1.8.15" +arclet-alconna = ">=1.8.21" nepattern = ">=0.7.3,<1.0.0" [package.source] @@ -2103,23 +2103,23 @@ reference = "ali" [[package]] name = "nonebot-plugin-alconna" -version = "0.50.2" +version = "0.51.1" description = "Alconna Adapter for Nonebot" optional = false python-versions = ">=3.9" files = [ - {file = "nonebot_plugin_alconna-0.50.2-py3-none-any.whl", hash = "sha256:be641eaf539f6f9dfb2398be80e994fa27814064eeed89e7a46a03754756dfc1"}, - {file = "nonebot_plugin_alconna-0.50.2.tar.gz", hash = "sha256:ebae23723cee5cbbc350aa864d9e3d95cb1ab8324ba8674130df3302066277b1"}, + {file = "nonebot_plugin_alconna-0.51.1-py3-none-any.whl", hash = "sha256:450a27afa9dcaedb6c82f649d57d42c4ca81596bf6accdf2e163f2dc9befc2c4"}, + {file = "nonebot_plugin_alconna-0.51.1.tar.gz", hash = "sha256:aaec8206adc9892e284d7ad12c8bb03b43586bbc145d439f0a40a055146ed176"}, ] [package.dependencies] -arclet-alconna = ">=1.8.19" -arclet-alconna-tools = ">=0.7.6" +arclet-alconna = ">=1.8.23" +arclet-alconna-tools = ">=0.7.9" importlib-metadata = ">=4.13.0" nepattern = ">=0.7.4" nonebot-plugin-waiter = ">=0.6.0" nonebot2 = ">=2.3.0" -tarina = ">=0.5.4" +tarina = ">=0.5.5" [package.source] type = "legacy" @@ -3582,58 +3582,58 @@ reference = "ali" [[package]] name = "tarina" -version = "0.5.4" +version = "0.5.5" description = "A collection of common utils for Arclet" optional = false python-versions = ">=3.8" files = [ - {file = "tarina-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:49f20a447866ecc831acc82f09dec01f77a0ca1f89b12fa27268bccd29378449"}, - {file = "tarina-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5b24b5c07dc02c006d80930028e1c5f46945bf55effbeeaa426d5ac8f46eff88"}, - {file = "tarina-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed8fe5a1df3b32e69f99f5ae6615dc8c2e34459c7e7f828bbeadefb4ecd4fe4f"}, - {file = "tarina-0.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab6fac674c408bff3161a27473951df8994b54fff406680814079c9c0b82f804"}, - {file = "tarina-0.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfabcce37425aaf5db604ad916c9b69350174afcdb98192c6dbf1fc0cda2183f"}, - {file = "tarina-0.5.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:18900dc94388da4d322c56292cdab6a62da46d27ab5db30ed8809caab57c3502"}, - {file = "tarina-0.5.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b3f8b69949c85bb3cf5b27985961ba0c26e4359a42352f7d5870f6d455f4890"}, - {file = "tarina-0.5.4-cp310-cp310-win32.whl", hash = "sha256:8e4389a6147460b6ea6a795f21a6348190ca2fe0eb95faafb3120bb0d4de7033"}, - {file = "tarina-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:042bdbaac389334ab9c0851a5f1972dc9ed5c0387b4bcdee3ba1b2223aadb39f"}, - {file = "tarina-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:08964a6daa02d992be4b4bf2ace99c94549350195a749198f2d422221e93cc9f"}, - {file = "tarina-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f81635455a307d65440c20645923041c8815c50dfeac046b64b64fd7840b7c30"}, - {file = "tarina-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f20ce1ecc06362bbfd7ca30b1dc19c3a049f69b7dc6061df95a0bf93ce627055"}, - {file = "tarina-0.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:539d239b35af0052be9cc7eeb3675c84b02a4b98c3d8ec51dbe7db2e9e5da92a"}, - {file = "tarina-0.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d8e9da2d450cdd93ac9a11af1ff02b6c9a305aa477cbada0d397c5b0b64e3"}, - {file = "tarina-0.5.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:15a2ac416e972b0318c53f20c3478d77fb770dfa9ab25ab43aa8975886ecb160"}, - {file = "tarina-0.5.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af522dc1ad30d7bcbbf9384f4f3aede3bebd7cecfc7127148ae0d12bd69b65d9"}, - {file = "tarina-0.5.4-cp311-cp311-win32.whl", hash = "sha256:781b1df4250e8f8f0b7902f3b7952135cbf43284e2cf490f57b738160d74b56f"}, - {file = "tarina-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:9d32bab544e7c74e56958b0ebcd430a80194492ca6e98ed2f6217708fabc4027"}, - {file = "tarina-0.5.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:95b1504e4241a28fe75fa0995ebfed1dad140381ad72541e5b69428c84d16735"}, - {file = "tarina-0.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9bbaefb3a627fcefc868d455cdc5d42297ba48369651821b04d8c8836307c39f"}, - {file = "tarina-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7cfec7c6a725bebb46b4e4a8ed64523c6deeae94dba1d3102b866c0247a32cdb"}, - {file = "tarina-0.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fccfd98ca925ec3597ca88f359f608f7762ad13a14dffcb17742b1e78e071306"}, - {file = "tarina-0.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bef0dfa5007f5138f48cbb9c2ef9564579def00b75caf47ebf53d32db7bf4044"}, - {file = "tarina-0.5.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8e6e2f0580d8dd956f92313ff51760df6893cd16fc009cdc2607130463d08bbb"}, - {file = "tarina-0.5.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:82f09edcf58b2e02622b173822c31c0ad5685f3e36667bd9de751f8c16b5305f"}, - {file = "tarina-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b56956862d70f0383973d8413ed0fca9623e930acea0d7bf11a67c79714b869f"}, - {file = "tarina-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:3ee6dafc31cceae46634314db0b547052790015abaec433ff39fef5bf5b3f0f6"}, - {file = "tarina-0.5.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2db7a3c9061ff6b8ba4ad3536850ac39ecc15b01bc41d6ee50468c8a8f06519c"}, - {file = "tarina-0.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a62297950d1448adaa3cc8ffb9ef1d076e1f51da07862f0205d660914cbee15"}, - {file = "tarina-0.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3b52205781e8b7dfc94ac90f6433a55e8025872b8ceb3bc0498ae2ba3e8b8cfb"}, - {file = "tarina-0.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17d95f0eb66785ef845b0f9567c738e2323f3e6ed56cf82b7c28ab9314dd7896"}, - {file = "tarina-0.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff02d7ae7718e48290dea13287c554928c09ea7859e3e0cf5bff91d031ad5b2"}, - {file = "tarina-0.5.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:13bc48018b78f2fa2707ae5dda3c38e482fdb38e911c38ac1c7208593b58c8a2"}, - {file = "tarina-0.5.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0dbf855e6f31397422cccd3816c2ffdc613fa746c0ff064730676eb8c59eb5a"}, - {file = "tarina-0.5.4-cp38-cp38-win32.whl", hash = "sha256:99767cdc271e35edb401c772c87e2dba9b24f93803a51d0979ef0c113aafb0e0"}, - {file = "tarina-0.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:64abd0da7146430c9dbce9a659861f09f03a0eecb4c65f42a6ac1c347961c534"}, - {file = "tarina-0.5.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:aa01c6032226f996286d60bd7b3bfb95565e9288e89b64208649b584386cfd9e"}, - {file = "tarina-0.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4c93781dfcf0c95c7e12c29fa788a32898aa090ba26bef9b1c970412b8cb7f59"}, - {file = "tarina-0.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b61ab72169c2289001a047694dbf6e0e73ed0b1c5405f65651b2500190928d43"}, - {file = "tarina-0.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3b9ee386d0a8558c9270ae2f4fd33ff2394482705a2849646aad3df870cf754"}, - {file = "tarina-0.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d6937a4911e5b7bf1f5a4bcc466e2cce3b1576eb6462459e568668f63a073f"}, - {file = "tarina-0.5.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d4d332b30374b2d8fec2852d6af77f121c0fb026c48593cebdfbed6d49c2b260"}, - {file = "tarina-0.5.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:077b99101ee19699c8791f2630ed7c40c592e5d75ab309a042f5303d89f382c6"}, - {file = "tarina-0.5.4-cp39-cp39-win32.whl", hash = "sha256:a553a8790215ecd6f1af2616769012f16e28eaae0b805ddc780fe543ec2a6a4b"}, - {file = "tarina-0.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:5c75b66d011cb7dd78149bf3911a78eaa96885dab4477fd4a96613349411f378"}, - {file = "tarina-0.5.4-py3-none-any.whl", hash = "sha256:1aa7d5c00e4bb6a35c5fd21bcbc536670df755922cd49bd9076a024fea191ade"}, - {file = "tarina-0.5.4.tar.gz", hash = "sha256:5d192a50d47b22ae8ca79e50ee760f171e563135eb04dc834a9b254211dbf32e"}, + {file = "tarina-0.5.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fda200701a81ed48e4303ccff10b5d680a7ad3d1772a6830f32995fe04459d6e"}, + {file = "tarina-0.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ffe373da5f9e35179b96e233731e8a7bb83fe6bf8866753f468db53b3ed22e"}, + {file = "tarina-0.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb7474ba9f9d55dc29df9d317c12fdc870ba10582b0c5ce36550e237881c9ea6"}, + {file = "tarina-0.5.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a392ac4d4b94a9a51b7540d8194605be621a129147dc874933a524911a09c94e"}, + {file = "tarina-0.5.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc131ecab68d7ec31a12dfb8f0ab0638729a9b866043a79b66dcf7022000652"}, + {file = "tarina-0.5.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:724a3d33ed7c48f68af7fc583aa21abff2cd1b60d0c51d3ba043683d715717f8"}, + {file = "tarina-0.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b04897665d96ebd55461c0876407c3e569008ba8efee4d4342bad47c32b64b0f"}, + {file = "tarina-0.5.5-cp310-cp310-win32.whl", hash = "sha256:f58c9eaa087af597cfd7e2885073c9dc93a3f93ba3f6957d55a9dacbcc1270ee"}, + {file = "tarina-0.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:b7dc4a5e0779fd4ee023abf445c2f801069a5861133c3ad04a5e055d5d5071fb"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ffb4ed6bd241809fd76b82bc7df857413cbc4a73a2ac8397374b79cb6e85e9b"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f5551815a970cd22d6d609a8769eac3e8b499e54ac5283e01169727f9ce0edd0"}, + {file = "tarina-0.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e2c18bcb1a3c59e45dc0fe39880b41d7e4fb5d742ef98a88fb4621aea9da02f"}, + {file = "tarina-0.5.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2db5c4bc285d73bec00b159dde6ec41b74d14371eb6da29d8b14a382e370567e"}, + {file = "tarina-0.5.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74923bc3d6884639e102a6a35bffda9578d934a23c4eb3f2d835e718ac75cee"}, + {file = "tarina-0.5.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e55686cff98c91ed4982226163ac5daeaf85510b4acab0c3d75331e255fbdce0"}, + {file = "tarina-0.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:50572901cd69983cfdc9d5a5823d17c49755f9e071eb287e091df014beaf6e73"}, + {file = "tarina-0.5.5-cp311-cp311-win32.whl", hash = "sha256:9d0a20f8b084af361fab7b070917edad611ede38014bab2cfc4024599586ade0"}, + {file = "tarina-0.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:8e740532d5a9346079c55613adfb77895f596a9c57e46c06d7d6c03640bd4f38"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1bab4762a24d9fcd8eacae4376c8fa2d4a96e1a3c5aadbeaad9e113cd679ee7d"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:05149d5aef6947fcf11a5b6cbbab788202077a734b7a2d184a574283de311725"}, + {file = "tarina-0.5.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b4ae866721d7b906fb327f847d9f8522f46bbea3b0df61b74d6bcc22dad1a33c"}, + {file = "tarina-0.5.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c687aa0cfef24b1df2c8f044a72d8993d68b4e13ea8967b79105be7a2e4097dd"}, + {file = "tarina-0.5.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e609199df957cd35cee6a942028f4caded21f1db8ac4c300c1dba94d61f0080"}, + {file = "tarina-0.5.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d57033ce9fa1c6c0a3a4851503c7320e7f7eba5dfc77e4e2f98932f1b329ba85"}, + {file = "tarina-0.5.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:986c5c59e30041e2a223c04b429777d3848c40e70b449f395b4b40290b6ff1ef"}, + {file = "tarina-0.5.5-cp312-cp312-win32.whl", hash = "sha256:256cf6a4f6a395b90aa4c1305f69a36c5fa6155124b30157a4c7e7af7c6be9ca"}, + {file = "tarina-0.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:ada4a85937cb7f0c5968ffc1b4914779d35525bff14e451113da94028d6a7a23"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4dc78ecae28f9422cb211268e7741058838d24dbf0714ae68ee3c00da278519d"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a85e14f1006c4f1cab21535c47819c3aceedd909e9b34c3044cfec584deee9ea"}, + {file = "tarina-0.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0689be4febdb9ba442b44c79d9dd861f6269f3dd62a33d258db6f6f1c40454c7"}, + {file = "tarina-0.5.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:046e441e9598c03d3013693688aa1825ba9f78538f81ba15ab3a0dc31cffb74c"}, + {file = "tarina-0.5.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6af01b231f724aef7233ce85ad99619e0bda81bf7d29863ba624117b5e3a82f9"}, + {file = "tarina-0.5.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:028f156980c0e89bc739d3875bafee82bfb198523a0199dd80b10931b50cda8f"}, + {file = "tarina-0.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7f4738381cb9291918c0f83928a13720879e0cfdcc679389bfa1bef985beed93"}, + {file = "tarina-0.5.5-cp38-cp38-win32.whl", hash = "sha256:30b30d0e3c21d2ab04f11f079d2205faa7320b595d1252c6728e8705781f6171"}, + {file = "tarina-0.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:e1f36c9972fa2e0cf3c1ca3842660531008fa4b6b1b89b31cdf06c56254cc902"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d819c4fa630c78e1d3c1b5fbc72158a84da6404009dc040e675e664fa38c030a"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a81375dab4b02eacedd2364e2394d0c3d76ac064fb0a9d3af1f0c0ea7740e296"}, + {file = "tarina-0.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:926bf0cd6901091c60460c6ac90ef5ea53ebb5a24d865ab1b9381117e4ba2825"}, + {file = "tarina-0.5.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee3dd8ebe04370915e7b763d39f8faee1bd4e9d2600acc8005da5104a698d9e8"}, + {file = "tarina-0.5.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bccac5a9b5af0c4c4b545d7e37eca55abab0abd779f4554cf69bbe29635e3c5c"}, + {file = "tarina-0.5.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dda57675b259a8b0db6647832c4f6a734ce3acf63b2392b7a45e34bace681230"}, + {file = "tarina-0.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:aeba9af50fba8d270abdcffb9f7ca3390223e7e7b4cf1a6a52c8adb2c98b8726"}, + {file = "tarina-0.5.5-cp39-cp39-win32.whl", hash = "sha256:fb1e3130cb6e35495f5867c54d8f049f06a1d915644afce2138ab915ff78291a"}, + {file = "tarina-0.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:da9ababc95b38037280eaeedbbb80c45179bda08578e2a4254e44ee1ef794ac9"}, + {file = "tarina-0.5.5-py3-none-any.whl", hash = "sha256:4828ace26e49037b2dab624e62ca13a473909b2f535f1b4fd5169dd01e16f6c5"}, + {file = "tarina-0.5.5.tar.gz", hash = "sha256:762a3871906e3dd79fc82d13ff99f14f1af977c4b8e2ce860209b8fa97a8b321"}, ] [package.dependencies] @@ -4345,4 +4345,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "1069f396df7f09336b9ea7737997061e4dfea458a561995a2afee74fd9cf36ad" +content-hash = "92e7c882369238f6e25599c854fb715013d2d51323a8ef930e8fc03db6b4715b" diff --git a/pyproject.toml b/pyproject.toml index 8faaa329..6d56c5c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,10 +43,10 @@ black = "^24.4.2" cn2an = "^0.5.22" aiohttp = "^3.9.5" dateparser = "^1.2.0" -nonebot-plugin-alconna = "^0.50.2" bilireq = "0.2.3post0" python-jose = {extras = ["cryptography"], version = "^3.3.0"} python-multipart = "^0.0.9" +nonebot-plugin-alconna = "^0.51.1" [tool.poetry.dev-dependencies] diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py index 9c9df3be..cc35b9c3 100644 --- a/zhenxun/builtin_plugins/admin/ban/_data_source.py +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -88,7 +88,7 @@ class BanManage: session: EventSession, is_superuser: bool = False, ) -> bool: - """ban掉目标用户 + """unban目标用户 参数: user_id: 用户id @@ -102,7 +102,7 @@ class BanManage: user_level = 9999 if not is_superuser and user_id and session.id1: user_level = await LevelUser.get_user_level(session.id1, group_id) - if await BanConsole.check_ban_level(user_id, group_id, user_level): + if not await BanConsole.check_ban_level(user_id, group_id, user_level): await BanConsole.unban(user_id, group_id) return True return False diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index 610d90e2..a279d9d0 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -65,7 +65,9 @@ _blmt = BanCheckLimiter( # 恶意触发命令检测 @run_preprocessor async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): + module = None if plugin := matcher.plugin: + module = plugin.module_name if metadata := plugin.metadata: extra = metadata.extra if extra.get("plugin_type") == PluginType.HIDDEN: @@ -76,14 +78,8 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): if not malicious_ban_time: raise ValueError("模块: [hook], 配置项: [MALICIOUS_BAN_TIME] 为空或小于0") if user_id: - command = state["_prefix"]["raw_command"] - if state.get("_alc_result"): - try: - command = state["_alc_result"].source.command - except AttributeError: - pass - if command: - if _blmt.check(f"{user_id}__{command}"): + if module: + if _blmt.check(f"{user_id}__{module}"): await BanConsole.ban( user_id, group_id, 9, malicious_ban_time * 60, bot.self_id ) @@ -104,4 +100,4 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): session=session, ) raise IgnoredException("检测到恶意触发命令") - _blmt.add(f"{user_id}__{command}") + _blmt.add(f"{user_id}__{module}") diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index 607d2879..b93d2fcb 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -76,7 +76,7 @@ class BanConsole(Model): f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", target=f"{group_id}:{user_id}", ) - return bool(user and user.ban_level >= level) + return user.ban_level >= level return False @classmethod diff --git a/zhenxun/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py index 36e56ad7..e8e68aad 100644 --- a/zhenxun/plugins/draw_card/__init__.py +++ b/zhenxun/plugins/draw_card/__init__.py @@ -50,7 +50,7 @@ __plugin_meta__ = PluginMetadata( 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池 fgo[1-300]抽: fgo卡池 (已失效) 阴阳师[1-300]抽: 阴阳师卡池 - ba/碧蓝档案[1-200]抽:碧蓝档案卡池 (已失效) + ba/碧蓝档案[1-200]抽:碧蓝档案卡池 * 以上指令可以通过 XX一井 来指定最大抽取数量 * * 示例:原神一井 * """.strip(), diff --git a/zhenxun/plugins/pid_search.py b/zhenxun/plugins/pid_search.py index a8916585..2d1b7c05 100644 --- a/zhenxun/plugins/pid_search.py +++ b/zhenxun/plugins/pid_search.py @@ -7,7 +7,7 @@ from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config -from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH +from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx @@ -34,7 +34,7 @@ headers = { } _matcher = on_alconna( - Alconna("p搜", Args["pid", int]), aliases={"P搜"}, priority=5, block=True + Alconna("p搜", Args["pid", str]), aliases={"P搜"}, priority=5, block=True ) @@ -44,11 +44,13 @@ async def _(pid: Match[int]): _matcher.set_path_arg("pid", pid.result) -@_matcher.got_path("pid", prompt="需要查询的图片PID是?") -async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: int): +@_matcher.got_path("pid", prompt="需要查询的图片PID是?或发送'取消'结束搜索") +async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): url = Config.get_config("hibiapi", "HIBIAPI") + "/api/pixiv/illust" if pid in ["取消", "算了"]: await Text("已取消操作...").finish() + if not pid.isdigit(): + await Text("pid必须为数字...").finish() for _ in range(3): try: data = ( @@ -61,6 +63,12 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: int): except TimeoutError: pass except Exception as e: + logger.error( + f"pixiv pid 搜索发生了一些错误...", + arparma.header_result, + session=session, + e=e, + ) await Text(f"发生了一些错误..{type(e)}:{e}").finish() else: if data.get("error"): diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index 6a6d96a4..e89f428f 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -185,11 +185,12 @@ class RussianManage: return MessageFactory(message_list) async def accept( - self, group_id: str, user_id: str, uname: str + self, bot: Bot, group_id: str, user_id: str, uname: str ) -> Text | MessageFactory: """接受对决 参数: + bot: Bot group_id: 群组id user_id: 用户id uname: 用户名称 @@ -209,6 +210,7 @@ class RussianManage: return Text("你没有足够的钱来接受这场挑战...") russian.player2 = (user_id, uname) russian.next_user = russian.player1[0] + self.__build_job(bot, group_id, True) return MessageFactory( [ Text("决斗已经开始!请"), diff --git a/zhenxun/plugins/web_ui/auth/__init__.py b/zhenxun/plugins/web_ui/auth/__init__.py index 6551d1ad..d5a4ead7 100644 --- a/zhenxun/plugins/web_ui/auth/__init__.py +++ b/zhenxun/plugins/web_ui/auth/__init__.py @@ -28,7 +28,7 @@ async def login_get_token(form_data: OAuth2PasswordRequestForm = Depends()): password = Config.get_config("web-ui", "password") if not username or not password: return Result.fail("你滴配置文件里用户名密码配置项为空", 998) - if username != form_data.username or password != form_data.password: + if username != form_data.username or str(password) != form_data.password: return Result.fail("真笨, 账号密码都能记错!", 999) user = get_user(form_data.username) if not user: From 96d08858a46f254fc3a2f0a77017abb4150e85b9 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 00:37:14 +0800 Subject: [PATCH 115/132] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BF=84=E7=BD=97?= =?UTF-8?q?=E6=96=AF=E8=BD=AE=E7=9B=98=E6=8B=92=E7=BB=9D=E6=97=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=AE=9A=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/russian/data_source.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index e89f428f..589f7a3a 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -236,6 +236,7 @@ class RussianManage: if russian.at_user != user_id: return Text("又不是找你决斗,你拒绝什么啊!气!") del self._data[group_id] + self.__remove_job(group_id) return MessageFactory( [Mention(russian.player1[0]), Text(f"{uname}拒绝了你的对决!")] ) From b4b5b4a6372c209aa01d3ac030e33bc3dfd6038a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 02:51:11 +0800 Subject: [PATCH 116/132] =?UTF-8?q?=F0=9F=90=9B=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E8=BF=87=E6=BB=A4b=E7=AB=99=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 5 ++++- zhenxun/plugins/fudu.py | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index a279d9d0..df23e48e 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -61,6 +61,9 @@ _blmt = BanCheckLimiter( malicious_ban_count, ) +# TODO: 恶意出发命令检测过滤 +_ignore = ["parse_bilibili"] + # 恶意触发命令检测 @run_preprocessor @@ -70,7 +73,7 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): module = plugin.module_name if metadata := plugin.metadata: extra = metadata.extra - if extra.get("plugin_type") == PluginType.HIDDEN: + if extra.get("plugin_type") == PluginType.HIDDEN or module not in _ignore: return user_id = session.id1 group_id = session.id3 or session.id2 diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py index 1b05b983..ffb1057d 100644 --- a/zhenxun/plugins/fudu.py +++ b/zhenxun/plugins/fudu.py @@ -91,6 +91,9 @@ class Fudu: _manage = Fudu() +base_config = Config.get("fudu") + + _matcher = on_message(rule=ensure_group, priority=999) @@ -124,11 +127,11 @@ async def _(message: UniMsg, event: Event, session: EventSession): _manage.clear(group_id) _manage.append(group_id, add_msg) if _manage.size(group_id) > 2: - if random.random() < Config.get_config( - "fudu", "FUDU_PROBABILITY" + if random.random() < base_config.get( + "FUDU_PROBABILITY" ) and not _manage.is_repeater(group_id): if random.random() < 0.2: - if plain_text.endswith("打断施法!"): + if plain_text.startswith("打断施法"): await Text("打断" + plain_text).finish() else: await Text("打断施法!").finish() From fb171e2d4baa1d4a1df9fb7b9a40927ca4d0474e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 04:41:35 +0800 Subject: [PATCH 117/132] =?UTF-8?q?=E2=9C=A8=20=E4=B8=A4=E6=97=A5=E5=86=85?= =?UTF-8?q?=E6=9C=AA=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF=E7=9A=84=E7=BE=A4?= =?UTF-8?q?=E7=BB=84=E5=B0=86=E8=A2=AB=E5=85=B3=E9=97=AD=E6=89=80=E6=9C=89?= =?UTF-8?q?=E8=A2=AB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat_history/chat_message.py | 3 +- .../builtin_plugins/scheduler/chat_check.py | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 zhenxun/builtin_plugins/scheduler/chat_check.py diff --git a/zhenxun/builtin_plugins/chat_history/chat_message.py b/zhenxun/builtin_plugins/chat_history/chat_message.py index 8200ab5e..2cfdd607 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message.py @@ -44,7 +44,8 @@ TEMP_LIST = [] @chat_history.handle() async def _(message: UniMsg, session: EventSession): - group_id = session.id3 or session.id2 + # group_id = session.id3 or session.id2 + group_id = session.id2 TEMP_LIST.append( ChatHistory( user_id=session.id1, diff --git a/zhenxun/builtin_plugins/scheduler/chat_check.py b/zhenxun/builtin_plugins/scheduler/chat_check.py new file mode 100644 index 00000000..599dff35 --- /dev/null +++ b/zhenxun/builtin_plugins/scheduler/chat_check.py @@ -0,0 +1,54 @@ +from datetime import datetime, timedelta + +import nonebot +import pytz +from nonebot_plugin_apscheduler import scheduler + +from zhenxun.models.chat_history import ChatHistory +from zhenxun.models.group_console import GroupConsole +from zhenxun.models.task_info import TaskInfo +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils + + +@scheduler.scheduled_job( + "cron", + hour=4, + minute=40, +) +async def _(): + """检测群组发言时间并禁用全部被动""" + update_list = [] + for bot in nonebot.get_bots().values(): + group_list, _ = await PlatformUtils.get_group_list(bot) + group_list = [g for g in group_list if g.channel_id == None] + for group in group_list: + try: + last_message = ( + await ChatHistory.filter(group_id=group.group_id) + .annotate() + .order_by("-create_time") + .first() + ) + if last_message: + now = datetime.now(pytz.timezone("Asia/Shanghai")) + if modules := await TaskInfo.annotate().values_list( + "module", flat=True + ): + if now - timedelta(days=2) > last_message.create_time: + _group, _ = await GroupConsole.get_or_create( + group_id=group.group_id, channel_id__isnull=True + ) + _group.block_task = ",".join(modules) + "," # type: ignore + update_list.append(_group) + logger.info( + "群组两日内未发送任何消息,关闭该群全部被动", + "Chat检测", + target=_group.group_id, + ) + except Exception as e: + logger.error( + "检测群组发言时间失败...", "Chat检测", target=group.group_id + ) + if update_list: + await GroupConsole.bulk_update(update_list, ["block_task"], 10) From 3e29a5e3a3b73b84f70a6abf40ac491e468896b8 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 13:09:49 +0800 Subject: [PATCH 118/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AD=BE=E5=88=B0=E5=A4=A9=E6=95=B0=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/sign_in/utils.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index 68b2da11..ecf7ad7f 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -172,9 +172,19 @@ async def _generate_card( uid_img = await BuildImage.build_text_image( f"UID: {uid}", size=30, font_color=(255, 255, 255) ) + bk.getsize("Accumulative check-in for") + image1 = await bk.build_text_image("Accumulative check-in for", size=25) + image2 = await bk.build_text_image("days", size=25) sign_day_img = await BuildImage.build_text_image( f"{user.sign_count}", size=40, font_color=(211, 64, 33) ) + tip_width = image1.width + image2.width + sign_day_img.width + 60 + tip_height = max([image1.height, image2.height, sign_day_img.height]) + tip_image = BuildImage(tip_width, tip_height, (255, 255, 255, 0)) + await tip_image.paste(image1, (0, 7)) + await tip_image.paste(sign_day_img, (image1.width + 15, 0)) + await tip_image.paste(image2, (image1.width + sign_day_img.width + 30, 7)) + lik_text1_img = await BuildImage.build_text_image("当前", size=20) lik_text2_img = await BuildImage.build_text_image( f"好感度:{user.impression:.2f}", size=30 @@ -235,10 +245,11 @@ async def _generate_card( await bk.paste(nickname_img, (30, 15)) await bk.paste(uid_img, (30, 85)) await bk.paste(A, (0, 150)) - await bk.text((30, 167), "Accumulative check-in for") - _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45 - await bk.paste(sign_day_img, (398, 158)) - await bk.text((_x, 167), "days") + # await bk.text((30, 167), "Accumulative check-in for") + # _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.width + 45 + # await bk.paste(sign_day_img, (398, 158)) + # await bk.text((_x, 167), "days") + await bk.paste(tip_image, (10, 167)) await bk.paste(data_img, (220, 370)) await bk.paste(lik_text1_img, (220, 240)) await bk.paste(lik_text2_img, (262, 234)) From 8470777f6cef6d4d0dce69da807250a6ade3051a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 18:57:30 +0800 Subject: [PATCH 119/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?b=E7=AB=99=E8=BD=AC=E5=8F=91=E8=A7=A3=E6=9E=90=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 5 +---- zhenxun/builtin_plugins/sign_in/utils.py | 5 ++--- zhenxun/plugins/parse_bilibili/__init__.py | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index df23e48e..a279d9d0 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -61,9 +61,6 @@ _blmt = BanCheckLimiter( malicious_ban_count, ) -# TODO: 恶意出发命令检测过滤 -_ignore = ["parse_bilibili"] - # 恶意触发命令检测 @run_preprocessor @@ -73,7 +70,7 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): module = plugin.module_name if metadata := plugin.metadata: extra = metadata.extra - if extra.get("plugin_type") == PluginType.HIDDEN or module not in _ignore: + if extra.get("plugin_type") == PluginType.HIDDEN: return user_id = session.id1 group_id = session.id3 or session.id2 diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index ecf7ad7f..cd9380a8 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -172,9 +172,8 @@ async def _generate_card( uid_img = await BuildImage.build_text_image( f"UID: {uid}", size=30, font_color=(255, 255, 255) ) - bk.getsize("Accumulative check-in for") - image1 = await bk.build_text_image("Accumulative check-in for", size=25) - image2 = await bk.build_text_image("days", size=25) + image1 = await bk.build_text_image("Accumulative check-in for", bk.font) + image2 = await bk.build_text_image("days", bk.font) sign_day_img = await BuildImage.build_text_image( f"{user.sign_count}", size=40, font_color=(211, 64, 33) ) diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index 3fcd136e..f42f78ae 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -12,6 +12,7 @@ from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger +from zhenxun.utils.enum import PluginType from zhenxun.utils.http_utils import AsyncHttpx from .information_container import InformationContainer @@ -27,7 +28,7 @@ __plugin_meta__ = PluginMetadata( extra=PluginExtraData( author="leekooyo", version="0.1", - menu_type="其他", + plugin_type=PluginType.HIDDEN, configs=[ RegisterConfig( module="_task", From f02c276310d622238366a1fc180b1558bfd51dbf Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Thu, 8 Aug 2024 21:43:40 +0800 Subject: [PATCH 120/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/help_help.py | 11 ++++ zhenxun/models/group_console.py | 16 +++++ zhenxun/plugins/russian/__init__.py | 15 +++-- zhenxun/plugins/russian/command.py | 2 +- zhenxun/plugins/search_image/__init__.py | 17 ++++- .../plugins/send_setu_/send_setu/__init__.py | 42 +++++++++---- .../send_setu_/send_setu/_data_source.py | 1 - zhenxun/utils/utils.py | 63 ++++++++++++++++++- 8 files changed, 145 insertions(+), 22 deletions(-) diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py index 4071f203..56556e70 100644 --- a/zhenxun/builtin_plugins/help_help.py +++ b/zhenxun/builtin_plugins/help_help.py @@ -10,6 +10,8 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import PluginExtraData +from zhenxun.models.ban_console import BanConsole +from zhenxun.models.group_console import GroupConsole from zhenxun.models.plugin_info import PluginInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType @@ -34,6 +36,15 @@ _path = IMAGE_PATH / "_base" / "laugh" @_matcher.handle() async def _(matcher: Matcher, message: UniMsg, session: EventSession): + gid = session.id3 or session.id2 + if await BanConsole.is_ban(session.id1, gid): + return + if gid: + if await BanConsole.is_ban(None, gid): + return + if g := await GroupConsole.get_group(gid): + if g.level < 0: + return if text := message.extract_plain_text().strip(): if plugin := await PluginInfo.get_or_none( name=text, load_status=True, plugin_type=PluginType.NORMAL diff --git a/zhenxun/models/group_console.py b/zhenxun/models/group_console.py index d5946396..88959e8b 100644 --- a/zhenxun/models/group_console.py +++ b/zhenxun/models/group_console.py @@ -1,4 +1,5 @@ from tortoise import fields +from typing_extensions import Self from zhenxun.services.db_context import Model @@ -39,6 +40,21 @@ class GroupConsole(Model): table_description = "群组信息表" unique_together = ("group_id", "channel_id") + @classmethod + async def get_group(cls, group_id: str, channel_id: str | None = None) -> Self: + """获取群组 + + 参数: + group_id: 群组id + channel_id: 频道id. + + 返回: + Self: GroupConsole + """ + if channel_id: + return await cls.get(group_id=group_id, channel_id=channel_id) + return await cls.get(group_id=group_id, channel_id__isnull=True) + @classmethod async def is_super_group(cls, group_id: str, channel_id: str | None = None) -> bool: """是否超级用户指定群 diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index e561085f..db797e64 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -56,7 +56,7 @@ __plugin_meta__ = PluginMetadata( @_russian_matcher.handle() -async def _(money: int, num: Match[int], at_user: Match[alcAt]): +async def _(money: int, num: Match[str], at_user: Match[alcAt]): _russian_matcher.set_path_arg("money", money) if num.available: _russian_matcher.set_path_arg("num", num.result) @@ -73,7 +73,7 @@ async def _( message: UniMsg, arparma: Arparma, money: int, - num: int, + num: str, at_user: Match[alcAt], uname: str = UserName(), ): @@ -86,16 +86,21 @@ async def _( await Text("群组id为空...").finish() if money <= 0: await Text("赌注金额必须大于0!").finish(reply=True) - if num < 0 or num > 6: + if num in ["取消", "算了"]: + await Text("已取消装弹...").finish() + if not num.isdigit(): + await Text("输入的子弹数必须是数字!").finish(reply=True) + b_num = int(num) + if b_num < 0 or b_num > 6: await Text("子弹数量必须在1-6之间!").finish(reply=True) _at_user = at_user.result.target if at_user.available else None rus = Russian( - at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=num + at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=b_num ) result = await russian_manage.add_russian(bot, gid, rus) await result.send() logger.info( - f"添加俄罗斯轮盘 装弹: {num}, 金额: {money}", + f"添加俄罗斯轮盘 装弹: {b_num}, 金额: {money}", arparma.header_result, session=session, ) diff --git a/zhenxun/plugins/russian/command.py b/zhenxun/plugins/russian/command.py index e3beaca3..a20dd2af 100644 --- a/zhenxun/plugins/russian/command.py +++ b/zhenxun/plugins/russian/command.py @@ -7,7 +7,7 @@ from zhenxun.utils.rules import ensure_group _russian_matcher = on_alconna( Alconna( "俄罗斯轮盘", - Args["money", int]["num?", int]["at_user?", alcAt], + Args["money", int]["num?", str]["at_user?", alcAt], ), aliases={"装弹", "俄罗斯转盘"}, rule=ensure_group, diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py index f3eb1102..d98e9ca1 100644 --- a/zhenxun/plugins/search_image/__init__.py +++ b/zhenxun/plugins/search_image/__init__.py @@ -1,3 +1,4 @@ +from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma from nonebot_plugin_alconna import Image as alcImg @@ -7,6 +8,8 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import template2forward from .saucenao import get_saucenao_image @@ -62,11 +65,13 @@ async def _(mode: Match[str], image: Match[alcImg]): @_matcher.got_path("image", prompt="图来!") async def _( + bot: Bot, session: EventSession, arparma: Arparma, mode: str, image: alcImg, ): + gid = session.id3 or session.id2 if not image.url: await Text("图片url为空...").finish() await Text("开始处理图片...").send() @@ -75,6 +80,14 @@ async def _( await Text(info_list).finish(at_sender=True) if not info_list: await Text("未查询到...").finish() - for info in info_list[1:]: - await info.send() + platform = PlatformUtils.get_platform(bot) + if "qq" == platform and gid: + forward = template2forward(info_list, bot.self_id) # type: ignore + await bot.send_group_forward_msg( + group_id=int(gid), + messages=forward, # type: ignore + ) + else: + for info in info_list[1:]: + await info.send() logger.info(f" 识图: {image.url}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py index c38cf222..fd341730 100644 --- a/zhenxun/plugins/send_setu_/send_setu/__init__.py +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -2,6 +2,7 @@ import random from typing import Tuple from nonebot.adapters import Bot +from nonebot.adapters.onebot.v11 import MessageSegment from nonebot.matcher import Matcher from nonebot.message import run_postprocessor from nonebot.plugin import PluginMetadata @@ -22,9 +23,11 @@ from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.models.sign_user import SignUser from zhenxun.models.user_console import UserConsole from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils +from zhenxun.utils.utils import template2forward from zhenxun.utils.withdraw_manage import WithdrawManager -from ._data_source import SetuManage, base_config +from ._data_source import Image, SetuManage, base_config __plugin_meta__ = PluginMetadata( name="色图", @@ -211,17 +214,32 @@ async def _( result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18) if isinstance(result_list, str): await Text(result_list).finish(reply=True) - for result in result_list: - logger.info(f"发送色图 {result}", arparma.header_result, session=session) - receipt = await result.send() - if receipt: - message_id = receipt.extract_message_id().message_id # type: ignore - await WithdrawManager.withdraw_message( - bot, - message_id, - base_config.get("WITHDRAW_SETU_MESSAGE"), - session, - ) + max_once_num2forward = base_config.get("MAX_ONCE_NUM2FORWARD") + platform = PlatformUtils.get_platform(bot) + if ( + "qq" == platform + and gid + and max_once_num2forward + and len(result_list) >= max_once_num2forward + ): + logger.debug("使用合并转发转发色图数据", arparma.header_result, session=session) + forward = template2forward(result_list, bot.self_id) # type: ignore + await bot.send_group_forward_msg( + group_id=int(gid), + messages=forward, # type: ignore + ) + else: + for result in result_list: + logger.info(f"发送色图 {result}", arparma.header_result, session=session) + receipt = await result.send() + if receipt: + message_id = receipt.extract_message_id().message_id # type: ignore + await WithdrawManager.withdraw_message( + bot, + message_id, + base_config.get("WITHDRAW_SETU_MESSAGE"), + session, + ) logger.info( f"调用发送 {num}张 色图 tags: {_tags}", arparma.header_result, session=session ) diff --git a/zhenxun/plugins/send_setu_/send_setu/_data_source.py b/zhenxun/plugins/send_setu_/send_setu/_data_source.py index 796578b9..e54099d0 100644 --- a/zhenxun/plugins/send_setu_/send_setu/_data_source.py +++ b/zhenxun/plugins/send_setu_/send_setu/_data_source.py @@ -4,7 +4,6 @@ from pathlib import Path from asyncpg import UniqueViolationError from nonebot_plugin_saa import Image, MessageFactory, Text -from pydantic import BaseModel from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 7ea698a9..50bc2b69 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -3,13 +3,16 @@ import time from collections import defaultdict from datetime import datetime from pathlib import Path +from re import L from typing import Any import httpx import pypinyin import pytz +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot_plugin_saa import Image, MessageFactory, Text -from zhenxun.configs.config import Config +from zhenxun.configs.config import NICKNAME, Config from zhenxun.services.log import logger @@ -230,3 +233,61 @@ def is_valid_date(date_text: str, separator: str = "-") -> bool: return True except ValueError: return False + + +def custom_forward_msg( + msg_list: list[str | Message], + uin: str, + name: str = f"这里是{NICKNAME}", +) -> list[dict]: + """生成自定义合并消息 + + 参数: + msg_list: 消息列表 + uin: 发送者 QQ + name: 自定义名称 + + 返回: + list[dict]: 转发消息 + """ + mes_list = [] + for _message in msg_list: + data = { + "type": "node", + "data": { + "name": name, + "uin": f"{uin}", + "content": _message, + }, + } + mes_list.append(data) + return mes_list + + +def template2forward( + msg_list: list[MessageFactory | Text | Image], uni: str +) -> list[dict]: + """模板转转发消息 + + 参数: + msg_list: 消息列表 + uni: 发送者qq + + 返回: + list[dict]: 转发消息 + """ + forward_data = [] + for r_list in msg_list: + s = "" + if isinstance(r_list, MessageFactory): + for r in r_list: + if isinstance(r, Text): + s += str(r) + elif isinstance(r, Image): + s += MessageSegment.image(r.data["image"]) + elif isinstance(r_list, Image): + s = MessageSegment.image(r_list.data["image"]) + else: + s = str(r_list) + forward_data.append(s) + return custom_forward_msg(forward_data, uni) From 2251f5faface771b4b640969e25989e0bd8550b1 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 9 Aug 2024 19:39:58 +0800 Subject: [PATCH 121/132] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98alc=E7=89=88=E6=9C=AC=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/dialogue/__init__.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py index 1ef9cd81..527cffad 100644 --- a/zhenxun/plugins/dialogue/__init__.py +++ b/zhenxun/plugins/dialogue/__init__.py @@ -22,11 +22,15 @@ __plugin_meta__ = PluginMetadata( name="联系管理员", description="跨越空间与时间跟管理员对话", usage=""" - [滴滴滴]/滴滴滴- ?[文本] ?[图片] + 滴滴滴- ?[文本] ?[图片] 示例:滴滴滴- 我喜欢你 - - 超级管理员额外命令 - /t: 查看当前存储的消息 + """.strip(), + extra=PluginExtraData( + author="HibiKier", + version="0.1", + menu_type="联系管理员", + superuser_help=""" + /t: 查看当前存储的消息 /t [user_id] [group_id] [文本]: 在group回复指定用户 /t [user_id] [文本]: 私聊用户 /t -1 [group_id] [文本]: 在group内发送消息 @@ -35,9 +39,7 @@ __plugin_meta__ = PluginMetadata( 示例:/t 73747222 你好不好 示例:/t -1 32848432 我不太好 示例:/t 0 我收到你的话了 - """.strip(), - extra=PluginExtraData( - author="HibiKier", version="0.1", menu_type="联系管理员" + """.strip(), ).dict(), ) @@ -79,12 +81,12 @@ async def _( logger.info( f"发送消息至{platform}管理员: {message}", "滴滴滴-", session=session ) - message.insert(0, "消息:\n") + message.insert(0, alcText("消息:\n")) if gid: - message.insert(0, f"群组: {group_name}({gid})\n") - message.insert(0, f"昵称: {uname}({session.id1})\n") - message.insert(0, f"Id: {DialogueManage._index}\n") - message.insert(0, "*****一份交流报告*****\n") + message.insert(0, alcText(f"群组: {group_name}({gid})\n")) + message.insert(0, alcText(f"昵称: {uname}({session.id1})\n")) + message.insert(0, alcText(f"Id: {DialogueManage._index}\n")) + message.insert(0, alcText("*****一份交流报告*****\n")) DialogueManage.add(uname, session.id1, gid, group_name, message, platform) await message.send(bot=bot, target=Target(superuser_id, private=True)) await Text("已成功发送给管理员啦!").send(reply=True) From 5e4de123b0442ed22139b5be3764414bd9e58352 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Fri, 9 Aug 2024 22:27:32 +0800 Subject: [PATCH 122/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=94=B9webui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web_ui/api/tabs/manage/__init__.py | 71 ++++++++++++------- .../plugins/web_ui/api/tabs/manage/model.py | 2 - .../web_ui/api/tabs/plugin_manage/__init__.py | 21 +++--- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py index 82a34d56..1067cfb8 100644 --- a/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py +++ b/zhenxun/plugins/web_ui/api/tabs/manage/__init__.py @@ -50,13 +50,13 @@ router = APIRouter(prefix="/manage") SUB_PATTERN = r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))" -GROUP_PATTERN = r'.*?Message (-?\d*) from (\d*)@\[群:(\d*)] "(.*)"' +GROUP_PATTERN = r".*?Message (-?\d*) from (\d*)@\[群:(\d*)] '(.*)'" -PRIVATE_PATTERN = r'.*?Message (-?\d*) from (\d*) "(.*)"' +PRIVATE_PATTERN = r".*?Message (-?\d*) from (\d*) '(.*)'" AT_PATTERN = r"\[CQ:at,qq=(.*)\]" -IMAGE_PATTERN = r"\[CQ:image,.*,url=(.*);.*?\]" +IMAGE_PATTERN = r"\[image:file=.*,url=(.*);.*?\]" @router.get( @@ -90,13 +90,22 @@ async def _(bot_id: str) -> Result: async def _(group: UpdateGroup) -> Result: try: group_id = group.group_id - if db_group := await GroupConsole.get_or_none(group_id=group_id): + if db_group := await GroupConsole.get_group(group_id): + task_list = await TaskInfo.all().values_list("module", flat=True) db_group.level = group.level db_group.status = group.status if group.close_plugins: db_group.block_plugin = ",".join(group.close_plugins) + "," - # TODO: 关闭task - await db_group.save(update_fields=["level", "status", "block_plugin"]) + if group.task: + block_task = [] + for t in task_list: + if t not in group.task: + block_task.append(t) + if block_task: + db_group.block_task = ",".join(block_task) + "," + await db_group.save( + update_fields=["level", "status", "block_plugin", "block_task"] + ) except Exception as e: logger.error("调用API错误", "/get_group", e=e) return Result.fail(f"{type(e)}: {e}") @@ -149,7 +158,7 @@ async def _() -> Result: async def _() -> Result: try: req_result = ReqResult() - data_list = await FgRequest.filter(handle_type__not_isnull=True).all() + data_list = await FgRequest.filter(handle_type__isnull=True).all() for req in data_list: if req.request_type == RequestType.FRIEND: req_result.friend.append( @@ -220,7 +229,7 @@ async def _(parma: HandleRequest) -> Result: @router.post("/delete_request", dependencies=[authentication()], description="忽略请求") async def _(parma: HandleRequest) -> Result: - await FgRequest.expire(parma.id) + await FgRequest.ignore(parma.id) return Result.ok(info="成功处理了请求!") @@ -233,25 +242,25 @@ async def _(parma: HandleRequest) -> Result: bot_id = parma.bot_id if bot_id not in nonebot.get_bots(): return Result.warning_("指定Bot未连接...") - if parma.request_type == "group": - if req := await FgRequest.get_or_none(id=parma.id): - if group := await GroupConsole.get_or_none(group_id=req.group_id): - await group.update_or_create(group_flag=1) - else: - group_info = await bots[bot_id].get_group_info( - group_id=req.group_id - ) - await GroupConsole.update_or_create( - group_id=str(group_info["group_id"]), - defaults={ - "group_name": group_info["group_name"], - "max_member_count": group_info["max_member_count"], - "member_count": group_info["member_count"], - "group_flag": 1, - }, - ) + if req := await FgRequest.get_or_none(id=parma.id): + if group := await GroupConsole.get_group(group_id=req.group_id): + group.group_flag = 1 + await group.save(update_fields=["group_flag"]) else: - return Result.warning_("未找到此Id请求...") + group_info = await bots[bot_id].get_group_info( + group_id=req.group_id + ) + await GroupConsole.update_or_create( + group_id=str(group_info["group_id"]), + defaults={ + "group_name": group_info["group_name"], + "max_member_count": group_info["max_member_count"], + "member_count": group_info["member_count"], + "group_flag": 1, + }, + ) + else: + return Result.warning_("未找到此Id请求...") try: await FgRequest.approve(bots[bot_id], parma.id) return Result.ok(info="成功处理了请求!") @@ -393,6 +402,15 @@ async def _(bot_id: str, group_id: str) -> Result: status=task[0] not in split_task, ) ) + else: + for task in all_task: + task_list.append( + Task( + name=task[0], + zh_name=task_module2name.get(task[0]) or task[0], + status=True, + ) + ) group_detail = GroupDetail( group_id=group_id, ava_url=GROUP_AVA_URL.format(group_id, group_id), @@ -507,7 +525,6 @@ async def _(websocket: WebSocket): async def log_listener(log: str): global MSG_LIST, ID2NAME sub_log = re.sub(SUB_PATTERN, "", log) - img_list = re.findall(IMAGE_PATTERN, sub_log) if "message.private.friend" in log: if message := await message_handle(sub_log, "private"): await websocket.send_json(message.dict()) diff --git a/zhenxun/plugins/web_ui/api/tabs/manage/model.py b/zhenxun/plugins/web_ui/api/tabs/manage/model.py index a1e16bf4..8df5a6cd 100644 --- a/zhenxun/plugins/web_ui/api/tabs/manage/model.py +++ b/zhenxun/plugins/web_ui/api/tabs/manage/model.py @@ -132,8 +132,6 @@ class HandleRequest(BaseModel): """bot_id""" id: int """数据id""" - request_type: Literal["private", "group"] - """类型""" class LeaveGroup(BaseModel): diff --git a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py index f9aea30b..b2a90577 100644 --- a/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py +++ b/zhenxun/plugins/web_ui/api/tabs/plugin_manage/__init__.py @@ -32,7 +32,7 @@ async def _( plugin_list: list[PluginInfo] = [] query = DbPluginInfo if plugin_type: - query = query.filter(plugin_type__in=plugin_type) + query = query.filter(plugin_type__in=plugin_type, load_status=True) if menu_type: query = query.filter(menu_type=menu_type) plugins = await query.all() @@ -62,16 +62,17 @@ async def _( async def _() -> Result: plugin_count = PluginCount() plugin_count.normal = await DbPluginInfo.filter( - plugin_type=PluginType.NORMAL + plugin_type=PluginType.NORMAL, load_status=True ).count() plugin_count.admin = await DbPluginInfo.filter( - plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN] + plugin_type__in=[PluginType.ADMIN, PluginType.SUPER_AND_ADMIN], load_status=True ).count() plugin_count.superuser = await DbPluginInfo.filter( - plugin_type=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN] + plugin_type__in=[PluginType.SUPERUSER, PluginType.SUPER_AND_ADMIN], + load_status=True, ).count() plugin_count.other = await DbPluginInfo.filter( - plugin_type=PluginType.HIDDEN + plugin_type=PluginType.HIDDEN, load_status=True ).count() return Result.ok(plugin_count) @@ -81,7 +82,9 @@ async def _() -> Result: ) async def _(plugin: UpdatePlugin) -> Result: try: - db_plugin = await DbPluginInfo.get_or_none(module=plugin.module) + db_plugin = await DbPluginInfo.get_or_none( + module=plugin.module, load_status=True + ) if not db_plugin: return Result.fail("插件不存在...") db_plugin.default_status = plugin.default_status @@ -111,7 +114,7 @@ async def _(plugin: UpdatePlugin) -> Result: @router.post("/change_switch", dependencies=[authentication()], description="开关插件") async def _(param: PluginSwitch) -> Result: - db_plugin = await DbPluginInfo.get_or_none(module=param.module) + db_plugin = await DbPluginInfo.get_or_none(module=param.module, load_status=True) if not db_plugin: return Result.fail("插件不存在...") if not param.status: @@ -131,14 +134,14 @@ async def _() -> Result: menu_type_list = [] result = await DbPluginInfo.annotate().values_list("menu_type", flat=True) for r in result: - if r not in menu_type_list: + if r not in menu_type_list and r: menu_type_list.append(r) return Result.ok(menu_type_list) @router.get("/get_plugin", dependencies=[authentication()], description="获取插件详情") async def _(module: str) -> Result: - db_plugin = await DbPluginInfo.get_or_none(module=module) + db_plugin = await DbPluginInfo.get_or_none(module=module, load_status=True) if not db_plugin: return Result.fail("插件不存在...") config_list = [] From e0a3fe526ea3ec699b3b825e06ca5a539f1b58df Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 02:25:04 +0800 Subject: [PATCH 123/132] =?UTF-8?q?=F0=9F=8E=A8=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=8F=91=E9=80=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/builtin_plugins/admin/admin_help.py | 7 +- zhenxun/builtin_plugins/admin/ban/__init__.py | 44 ++++--- .../builtin_plugins/admin/ban/_data_source.py | 2 +- .../admin/group_member_update/__init__.py | 8 +- .../admin/plugin_switch/__init__.py | 42 +++--- .../chat_history/chat_message_handle.py | 6 +- zhenxun/builtin_plugins/help/__init__.py | 14 +- zhenxun/builtin_plugins/help_help.py | 9 +- zhenxun/builtin_plugins/nickname.py | 58 +++++--- .../platform/qq/group_handle.py | 17 +-- zhenxun/builtin_plugins/scheduler/morning.py | 16 ++- zhenxun/builtin_plugins/shop/__init__.py | 27 ++-- zhenxun/builtin_plugins/shop/_data_source.py | 6 +- zhenxun/builtin_plugins/sign_in/__init__.py | 18 +-- zhenxun/builtin_plugins/superuser/exec_sql.py | 14 +- .../superuser/power/__ini__.py | 0 .../superuser/request_manage.py | 20 +-- .../builtin_plugins/superuser/super_help.py | 12 +- zhenxun/models/ban_console.py | 4 +- zhenxun/plugins/ai/__init__.py | 4 +- zhenxun/plugins/ai/data_source.py | 44 +++---- zhenxun/plugins/alapi/cover.py | 10 +- zhenxun/plugins/black_word/black_word.py | 10 +- zhenxun/plugins/check/__init__.py | 3 +- zhenxun/plugins/coser.py | 9 +- .../plugins/draw_card/handles/base_handle.py | 1 - zhenxun/plugins/epic/data_source.py | 54 +++----- zhenxun/plugins/fudu.py | 18 +-- zhenxun/plugins/gold_redbag/__init__.py | 73 +++++------ zhenxun/plugins/group_welcome_msg.py | 12 +- zhenxun/plugins/luxun.py | 8 +- zhenxun/plugins/one_friend/__init__.py | 10 +- zhenxun/plugins/open_cases/__init__.py | 80 +++++------ zhenxun/plugins/open_cases/open_cases_c.py | 76 ++++------- zhenxun/plugins/parse_bilibili/__init__.py | 24 ++-- zhenxun/plugins/parse_bilibili/get_image.py | 7 +- zhenxun/plugins/pid_search.py | 36 ++--- zhenxun/plugins/pix_gallery/_data_source.py | 4 +- zhenxun/plugins/pix_gallery/pix.py | 42 +++--- zhenxun/plugins/pix_gallery/pix_show_info.py | 18 ++- zhenxun/plugins/pixiv_rank_search/__init__.py | 42 +++--- .../plugins/pixiv_rank_search/data_source.py | 6 +- zhenxun/plugins/poke/__init__.py | 11 +- zhenxun/plugins/russian/__init__.py | 50 +++---- zhenxun/plugins/search_image/__init__.py | 19 +-- zhenxun/plugins/search_image/saucenao.py | 9 +- .../plugins/send_setu_/send_setu/__init__.py | 15 +-- .../send_setu_/send_setu/_data_source.py | 47 ++++--- zhenxun/plugins/send_voice/dinggong.py | 8 +- zhenxun/plugins/statistics/_data_source.py | 1 - .../plugins/statistics/statistics_handle.py | 5 +- zhenxun/plugins/statistics/statistics_hook.py | 9 +- zhenxun/plugins/translate/__init__.py | 12 +- zhenxun/plugins/wbtop/__init__.py | 10 +- zhenxun/plugins/wbtop/data_source.py | 8 +- zhenxun/plugins/word_bank/_data_source.py | 30 +++-- zhenxun/plugins/word_bank/_model.py | 23 ++-- zhenxun/plugins/word_bank/word_handle.py | 59 +++++---- zhenxun/utils/http_utils.py | 10 +- zhenxun/utils/message.py | 124 ++++++++++++++++++ zhenxun/utils/utils.py | 58 -------- 61 files changed, 758 insertions(+), 665 deletions(-) create mode 100644 zhenxun/builtin_plugins/superuser/power/__ini__.py create mode 100644 zhenxun/utils/message.py diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index bdf48929..a0cb4166 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -2,7 +2,6 @@ import nonebot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_alconna.matcher import AlconnaMatcher -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH @@ -18,6 +17,7 @@ from zhenxun.utils.image_utils import ( group_image, text2image, ) +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import admin_check, ensure_group __plugin_meta__ = PluginMetadata( @@ -149,13 +149,12 @@ async def build_help() -> BuildImage: @_matcher.handle() async def _( session: EventSession, - matcher: AlconnaMatcher, arparma: Arparma, ): if not ADMIN_HELP_IMAGE.exists(): try: await build_help() except EmptyError: - await Text("管理员帮助为空").finish(reply=True) - await Image(ADMIN_HELP_IMAGE).send() + await MessageUtils.build_message("管理员帮助为空").finish(reply=True) + await MessageUtils.build_message(ADMIN_HELP_IMAGE).send() logger.info("查看管理员帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index d21641c6..19e8f455 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -11,13 +11,13 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import admin_check from ._data_source import BanManage @@ -146,9 +146,9 @@ async def _( _user_id = user_id.result if user_id.available else None _group_id = group_id.result if group_id.available else None if image := await BanManage.build_ban_image(filter_type, _user_id, _group_id): - await Image(image.pic2bytes()).finish(reply=True) + await MessageUtils.build_message(image).finish(reply_to=True) else: - await Text("数据为空捏...").finish(reply=True) + await MessageUtils.build_message("数据为空捏...").finish(reply_to=True) @_ban_matcher.handle() @@ -166,7 +166,7 @@ async def _( user_id = user.result.target else: if session.id1 not in bot.config.superusers: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) user_id = user.result _duration = duration.result * 60 if duration.available else -1 if (gid := session.id3 or session.id2) and not group_id.available: @@ -181,13 +181,13 @@ async def _( session=session, target=f"{gid}:{user_id}", ) - await MessageFactory( + await MessageUtils.build_message( [ - Text("对 "), - Mention(user_id) if isinstance(user.result, At) else Text(user_id), # type: ignore - Text(f" 狠狠惩戒了一番,一脚踢进了小黑屋!"), + "对 ", + At(flag="user", target=user_id) if isinstance(user.result, At) else user_id, # type: ignore + f" 狠狠惩戒了一番,一脚踢进了小黑屋!", ] - ).finish(reply=True) + ).finish(reply_to=True) elif session.id1 in bot.config.superusers: _group_id = group_id.result if group_id.available else None await BanManage.ban(user_id, _group_id, _duration, session, True) @@ -198,7 +198,9 @@ async def _( target=f"{_group_id}:{user_id}", ) at_msg = user_id if user_id else f"群组:{_group_id}" - await Text(f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!").finish(reply=True) + await MessageUtils.build_message( + f"对 {at_msg} 狠狠惩戒了一番,一脚踢进了小黑屋!" + ).finish(reply_to=True) @_unban_matcher.handle() @@ -215,7 +217,7 @@ async def _( user_id = user.result.target else: if session.id1 not in bot.config.superusers: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) user_id = user.result if gid := session.id3 or session.id2: if group_id.available: @@ -229,15 +231,17 @@ async def _( session=session, target=f"{gid}:{user_id}", ) - await MessageFactory( + await MessageUtils.build_message( [ - Text("将 "), - Mention(user_id) if isinstance(user.result, At) else Text(user_id), # type: ignore - Text(f" 从黑屋中拉了出来并急救了一下!"), + "将 ", + At(flag="user", target=user_id) if isinstance(user.result, At) else user_id, # type: ignore + f" 从黑屋中拉了出来并急救了一下!", ] ).finish(reply=True) else: - await Text(f"该用户不在黑名单中捏...").finish(reply=True) + await MessageUtils.build_message(f"该用户不在黑名单中捏...").finish( + reply_to=True + ) elif session.id1 in bot.config.superusers: _group_id = group_id.result if group_id.available else None if await BanManage.unban(user_id, _group_id, session, True): @@ -248,6 +252,10 @@ async def _( target=f"{_group_id}:{user_id}", ) at_msg = user_id if user_id else f"群组:{_group_id}" - await Text(f"对 {at_msg} 从黑屋中拉了出来并急救了一下!").finish(reply=True) + await MessageUtils.build_message( + f"对 {at_msg} 从黑屋中拉了出来并急救了一下!" + ).finish(reply_to=True) else: - await Text(f"该用户不在黑名单中捏...").finish(reply=True) + await MessageUtils.build_message(f"该用户不在黑名单中捏...").finish( + reply_to=True + ) diff --git a/zhenxun/builtin_plugins/admin/ban/_data_source.py b/zhenxun/builtin_plugins/admin/ban/_data_source.py index cc35b9c3..3d8f704d 100644 --- a/zhenxun/builtin_plugins/admin/ban/_data_source.py +++ b/zhenxun/builtin_plugins/admin/ban/_data_source.py @@ -102,7 +102,7 @@ class BanManage: user_level = 9999 if not is_superuser and user_id and session.id1: user_level = await LevelUser.get_user_level(session.id1, group_id) - if not await BanConsole.check_ban_level(user_id, group_id, user_level): + if await BanConsole.check_ban_level(user_id, group_id, user_level): await BanConsole.unban(user_id, group_id) return True return False diff --git a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py index 9286e750..c7036cc8 100644 --- a/zhenxun/builtin_plugins/admin/group_member_update/__init__.py +++ b/zhenxun/builtin_plugins/admin/group_member_update/__init__.py @@ -3,13 +3,13 @@ from nonebot.adapters import Bot from nonebot.adapters.onebot.v11 import GroupIncreaseNoticeEvent from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import admin_check, ensure_group from ._data_source import MemberUpdateManage @@ -44,8 +44,10 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma): if gid := session.id3 or session.id2: logger.info("更新群组成员信息", arparma.header_result, session=session) await MemberUpdateManage.update(bot, gid) - await Text("已经成功更新了群组成员信息!").finish(reply=True) - await Text("群组id为空...").send() + await MessageUtils.build_message("已经成功更新了群组成员信息!").finish( + reply_to=True + ) + await MessageUtils.build_message("群组id为空...").send() _notice = on_notice(priority=1, block=False) diff --git a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py index 63c7484f..76b10259 100644 --- a/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py +++ b/zhenxun/builtin_plugins/admin/plugin_switch/__init__.py @@ -1,13 +1,13 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import AlconnaQuery, Arparma, Match, Query -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import PluginManage, build_plugin, build_task from .command import _group_status_matcher, _status_matcher @@ -98,9 +98,9 @@ async def _( arparma.header_result, session=session, ) - await Image(image.pic2bytes()).finish(reply=True) + await MessageUtils.build_message(image).finish(reply_to=True) else: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) @_status_matcher.assign("open") @@ -115,7 +115,7 @@ async def _( all: Query[bool] = AlconnaQuery("all.value", False), ): if not all.result and not plugin_name.available: - await Text("请输入功能名称").finish(reply=True) + await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) name = plugin_name.result gid = session.id3 or session.id2 if gid: @@ -154,7 +154,7 @@ async def _( logger.info( f"开启功能 {name}", arparma.header_result, session=session ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) elif session.id1 in bot.config.superusers: """私聊""" group_id = group.result if group.available else None @@ -174,7 +174,7 @@ async def _( arparma.header_result, session=session, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) if default_status.result: result = await PluginManage.set_default_status(name, True) logger.info( @@ -183,7 +183,7 @@ async def _( session=session, target=group_id, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) if task.result: split_list = name.split() if len(split_list) > 1: @@ -204,7 +204,7 @@ async def _( arparma.header_result, session=session, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) else: result = await PluginManage.superuser_block(name, None, group_id) logger.info( @@ -213,7 +213,7 @@ async def _( session=session, target=group_id, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) @_status_matcher.assign("close") @@ -229,7 +229,7 @@ async def _( all: Query[bool] = AlconnaQuery("all.value", False), ): if not all.result and not plugin_name.available: - await Text("请输入功能名称").finish(reply=True) + await MessageUtils.build_message("请输入功能名称").finish(reply_to=True) name = plugin_name.result gid = session.id3 or session.id2 if gid: @@ -268,7 +268,7 @@ async def _( logger.info( f"关闭功能 {name}", arparma.header_result, session=session ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) elif session.id1 in bot.config.superusers: group_id = group.result if group.available else None if all.result: @@ -287,7 +287,7 @@ async def _( arparma.header_result, session=session, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) if default_status.result: result = await PluginManage.set_default_status(name, False) logger.info( @@ -296,7 +296,7 @@ async def _( session=session, target=group_id, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) if task.result: split_list = name.split() if len(split_list) > 1: @@ -317,7 +317,7 @@ async def _( arparma.header_result, session=session, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) else: _type = BlockType.ALL if block_type.available: @@ -332,7 +332,7 @@ async def _( session=session, target=group_id, ) - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) @_group_status_matcher.handle() @@ -345,14 +345,14 @@ async def _( if status == "sleep": await PluginManage.sleep(gid) logger.info("进行休眠", arparma.header_result, session=session) - await Text("那我先睡觉了...").finish() + await MessageUtils.build_message("那我先睡觉了...").finish() else: if await PluginManage.is_wake(gid): - await Text("我还醒着呢!").finish() + await MessageUtils.build_message("我还醒着呢!").finish() await PluginManage.wake(gid) logger.info("醒来", arparma.header_result, session=session) - await Text("呜..醒来了...").finish() - return Text("群组id为空...").send() + await MessageUtils.build_message("呜..醒来了...").finish() + return MessageUtils.build_message("群组id为空...").send() @_status_matcher.assign("task") @@ -367,6 +367,6 @@ async def _( arparma.header_result, session=session, ) - await Image(image.pic2bytes()).finish(reply=True) + await MessageUtils.build_message(image).finish(reply_to=True) else: - await Text("获取群被动任务失败...").finish(reply=True) + await MessageUtils.build_message("获取群被动任务失败...").finish(reply_to=True) diff --git a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py index c23d7ca9..1287e1b0 100644 --- a/zhenxun/builtin_plugins/chat_history/chat_message_handle.py +++ b/zhenxun/builtin_plugins/chat_history/chat_message_handle.py @@ -12,7 +12,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData @@ -21,6 +20,7 @@ from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="消息统计", @@ -120,8 +120,8 @@ async def _( logger.info( f"查看消息排行 数量={count.result}", arparma.header_result, session=session ) - await Image(A.pic2bytes()).finish(reply=True) - await Text("群组消息记录为空...").finish() + await MessageUtils.build_message(A).finish(reply_to=True) + await MessageUtils.build_message("群组消息记录为空...").finish() # # @test.handle() diff --git a/zhenxun/builtin_plugins/help/__init__.py b/zhenxun/builtin_plugins/help/__init__.py index 12021a46..f274c484 100644 --- a/zhenxun/builtin_plugins/help/__init__.py +++ b/zhenxun/builtin_plugins/help/__init__.py @@ -11,7 +11,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH @@ -19,6 +18,7 @@ from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ._data_source import create_help_img, get_plugin_help from ._utils import GROUP_HELP_PATH @@ -75,11 +75,13 @@ async def _( _is_superuser = False if result := await get_plugin_help(name.result, _is_superuser): if isinstance(result, BuildImage): - await Image(result.pic2bytes()).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) else: - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) else: - await Text("没有此功能的帮助信息...").send(reply=True) + await MessageUtils.build_message("没有此功能的帮助信息...").send( + reply_to=True + ) logger.info( f"查看帮助详情: {name.result}", "帮助", @@ -90,10 +92,10 @@ async def _( _image_path = GROUP_HELP_PATH / f"{gid}.png" if not _image_path.exists(): await create_help_img(gid) - await Image(_image_path).finish() + await MessageUtils.build_message(_image_path).finish() else: if not SIMPLE_HELP_IMAGE.exists(): if SIMPLE_HELP_IMAGE.exists(): SIMPLE_HELP_IMAGE.unlink() await create_help_img(None) - await Image(SIMPLE_HELP_IMAGE).finish() + await MessageUtils.build_message(SIMPLE_HELP_IMAGE).finish() diff --git a/zhenxun/builtin_plugins/help_help.py b/zhenxun/builtin_plugins/help_help.py index 56556e70..b2682aa4 100644 --- a/zhenxun/builtin_plugins/help_help.py +++ b/zhenxun/builtin_plugins/help_help.py @@ -5,7 +5,7 @@ from nonebot import on_message from nonebot.matcher import Matcher from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import Image, UniMessage, UniMsg +from nonebot_plugin_alconna import UniMsg from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH @@ -14,7 +14,9 @@ from zhenxun.models.ban_console import BanConsole from zhenxun.models.group_console import GroupConsole from zhenxun.models.plugin_info import PluginInfo from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="功能名称当命令检测", @@ -27,7 +29,6 @@ __plugin_meta__ = PluginMetadata( ).dict(), ) - _matcher = on_message(rule=to_me(), priority=996, block=False) @@ -52,7 +53,7 @@ async def _(matcher: Matcher, message: UniMsg, session: EventSession): image = None if _path.exists(): if files := os.listdir(_path): - image = Image(path=_path / random.choice(files)) + image = _path / random.choice(files) message_list = [] if image: message_list.append(image) @@ -62,5 +63,5 @@ async def _(matcher: Matcher, message: UniMsg, session: EventSession): logger.info( f"检测到功能名称当命令使用,已发送帮助信息", "功能帮助", session=session ) - await UniMessage(message_list).send(reply_to=True) + await MessageUtils.build_message(message_list).send(reply_to=True) matcher.stop_propagation() diff --git a/zhenxun/builtin_plugins/nickname.py b/zhenxun/builtin_plugins/nickname.py index 4eda8187..f5cce1fb 100644 --- a/zhenxun/builtin_plugins/nickname.py +++ b/zhenxun/builtin_plugins/nickname.py @@ -7,7 +7,6 @@ from nonebot.params import Depends, RegexGroup from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Option, on_alconna, store_true -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo @@ -19,6 +18,7 @@ from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.services.log import logger from zhenxun.utils.depends import UserName from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="昵称系统", @@ -124,27 +124,37 @@ def CheckNickname(): (name,) = reg_group logger.debug(f"昵称检查: {name}", "昵称设置", session=session) if not name: - await Text("叫你空白?叫你虚空?叫你无名??").finish(at_sender=True) + await MessageUtils.build_message("叫你空白?叫你虚空?叫你无名??").finish( + at_sender=True + ) if session.id1 in bot.config.superusers: logger.debug( f"超级用户设置昵称, 跳过合法检测: {name}", "昵称设置", session=session ) return if len(name) > 20: - await Text("昵称可不能超过20个字!").finish(at_sender=True) + await MessageUtils.build_message("昵称可不能超过20个字!").finish( + at_sender=True + ) if name in bot.config.nickname: - await Text("笨蛋!休想占用我的名字! #").finish(at_sender=True) + await MessageUtils.build_message("笨蛋!休想占用我的名字! #").finish( + at_sender=True + ) if black_word: for x in name: if x in black_word: logger.debug("昵称设置禁止字符: [{x}]", "昵称设置", session=session) - await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True) + await MessageUtils.build_message(f"字符 [{x}] 为禁止字符!").finish( + at_sender=True + ) for word in black_word: if word in name: logger.debug( "昵称设置禁止字符: [{word}]", "昵称设置", session=session ) - await Text(f"字符 [{x}] 为禁止字符!").finish(at_sender=True) + await MessageUtils.build_message(f"字符 [{x}] 为禁止字符!").finish( + at_sender=True + ) return Depends(dependency) @@ -171,7 +181,9 @@ async def _( session.platform, ) logger.info(f"设置群昵称成功: {name}", "昵称设置", session=session) - await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) + await MessageUtils.build_message( + random.choice(CALL_NAME).format(name) + ).finish(reply_to=True) else: await FriendUser.set_user_nickname( session.id1, @@ -182,8 +194,10 @@ async def _( session.platform, ) logger.info(f"设置私聊昵称成功: {name}", "昵称设置", session=session) - await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) - await Text("用户id为空...").send() + await MessageUtils.build_message( + random.choice(CALL_NAME).format(name) + ).finish(reply_to=True) + await MessageUtils.build_message("用户id为空...").send() @_global_nickname_matcher.handle(parameterless=[CheckNickname()]) @@ -202,8 +216,10 @@ async def _( ) await GroupInfoUser.filter(user_id=session.id1).update(nickname=name) logger.info(f"设置全局昵称成功: {name}", "设置全局昵称", session=session) - await Text(random.choice(CALL_NAME).format(name)).finish(reply=True) - await Text("用户id为空...").send() + await MessageUtils.build_message(random.choice(CALL_NAME).format(name)).finish( + reply_to=True + ) + await MessageUtils.build_message("用户id为空...").send() @_matcher.assign("name") @@ -216,9 +232,11 @@ async def _(session: EventSession, user_info: UserInfo = EventUserInfo()): nickname = await FriendUser.get_user_nickname(session.id1) card = user_info.user_name if nickname: - await Text(random.choice(REMIND).format(nickname)).finish(reply=True) + await MessageUtils.build_message( + random.choice(REMIND).format(nickname) + ).finish(reply_to=True) else: - await Text( + await MessageUtils.build_message( random.choice( [ "没..没有昵称嘛,{}", @@ -227,8 +245,8 @@ async def _(session: EventSession, user_info: UserInfo = EventUserInfo()): "你是{}?", ] ).format(card) - ).finish(reply=True) - await Text("用户id为空...").send() + ).finish(reply_to=True) + await MessageUtils.build_message("用户id为空...").send() @_matcher.assign("cancel") @@ -240,7 +258,9 @@ async def _(bot: Bot, session: EventSession, user_info: UserInfo = EventUserInfo else: nickname = await FriendUser.get_user_nickname(session.id1) if nickname: - await Text(random.choice(CANCEL).format(nickname)).send(reply=True) + await MessageUtils.build_message( + random.choice(CANCEL).format(nickname) + ).send(reply_to=True) if gid: await GroupInfoUser.set_user_nickname(session.id1, gid, "") else: @@ -248,5 +268,7 @@ async def _(bot: Bot, session: EventSession, user_info: UserInfo = EventUserInfo await BanConsole.ban(session.id1, gid, 9, 60, bot.self_id) return else: - await Text("你在做梦吗?你没有昵称啊").finish(reply=True) - await Text("用户id为空...").send() + await MessageUtils.build_message("你在做梦吗?你没有昵称啊").finish( + reply_to=True + ) + await MessageUtils.build_message("用户id为空...").send() diff --git a/zhenxun/builtin_plugins/platform/qq/group_handle.py b/zhenxun/builtin_plugins/platform/qq/group_handle.py index bb6aee49..e04f8e5a 100644 --- a/zhenxun/builtin_plugins/platform/qq/group_handle.py +++ b/zhenxun/builtin_plugins/platform/qq/group_handle.py @@ -16,7 +16,7 @@ from nonebot.adapters.onebot.v12 import ( GroupMemberIncreaseEvent, ) from nonebot.plugin import PluginMetadata -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from nonebot_plugin_alconna import At from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH @@ -29,6 +29,7 @@ from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType, RequestHandleType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import FreqLimiter __plugin_meta__ = PluginMetadata( @@ -235,26 +236,26 @@ async def _(bot: Bot, event: GroupIncreaseNoticeEvent | GroupMemberIncreaseEvent msg_split = re.split(r"\[image:\d+\]", message) msg_list = [] if data["at"]: - msg_list.append(Mention(user_id)) + msg_list.append(At(flag="user", target=user_id)) for i, text in enumerate(msg_split): - msg_list.append(Text(text)) + msg_list.append(text) img_file = path / f"{i}.png" if img_file.exists(): - msg_list.append(Image(img_file)) + msg_list.append(img_file) if not TaskInfo.is_block("group_welcome", group_id): logger.info(f"发送群欢迎消息...", "入群检测", group_id=group_id) if msg_list: - await MessageFactory(msg_list).send() + await MessageUtils.build_message(msg_list).send() else: image = ( IMAGE_PATH / "qxz" / random.choice(os.listdir(IMAGE_PATH / "qxz")) ) - await MessageFactory( + await MessageUtils.build_message( [ - Text("新人快跑啊!!本群现状↓(快使用自定义!)"), - Image(image), + "新人快跑啊!!本群现状↓(快使用自定义!)", + image, ] ).send() diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index c254d5e8..edbc959f 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -1,7 +1,6 @@ import nonebot from nonebot.plugin import PluginMetadata from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image from zhenxun.configs.config import NICKNAME from zhenxun.configs.path_config import IMAGE_PATH @@ -9,6 +8,7 @@ from zhenxun.configs.utils import PluginExtraData, Task from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import broadcast_group __plugin_meta__ = PluginMetadata( @@ -50,21 +50,23 @@ async def check(group_id: str) -> bool: minute=1, ) async def _(): - img = Image(IMAGE_PATH / "zhenxun" / "zao.jpg") - await broadcast_group("早上好" + img, log_cmd="被动早晚安", check_func=check) + message = MessageUtils.build_message(["早上好", IMAGE_PATH / "zhenxun" / "zao.jpg"]) + await broadcast_group(message, log_cmd="被动早晚安", check_func=check) logger.info("每日早安发送...") # # 睡觉了 @scheduler.scheduled_job( "cron", - hour=23, - minute=59, + hour=1, + minute=16, ) async def _(): - img = Image(IMAGE_PATH / "zhenxun" / "sleep.jpg") + message = MessageUtils.build_message( + [f"{NICKNAME}要睡觉了,你们也要早点睡呀", IMAGE_PATH / "zhenxun" / "sleep.jpg"] + ) await broadcast_group( - f"{NICKNAME}要睡觉了,你们也要早点睡呀" + img, + message, log_cmd="被动早晚安", check_func=check, ) diff --git a/zhenxun/builtin_plugins/shop/__init__.py b/zhenxun/builtin_plugins/shop/__init__.py index 041ca6e8..9ce4c51c 100644 --- a/zhenxun/builtin_plugins/shop/__init__.py +++ b/zhenxun/builtin_plugins/shop/__init__.py @@ -5,16 +5,17 @@ from nonebot_plugin_alconna import ( Args, Arparma, Subcommand, + UniMessage, UniMsg, on_alconna, ) -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.utils import BaseBlock, PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import BlockType, PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import ShopManage @@ -84,7 +85,7 @@ _matcher.shortcut( async def _(session: EventSession, arparma: Arparma): image = await ShopManage.build_shop_image() logger.info("查看商店", arparma.header_result, session=session) - await Image(image.pic2bytes()).send() + await MessageUtils.build_message(image).send() @_matcher.assign("my-cost") @@ -92,9 +93,9 @@ async def _(session: EventSession, arparma: Arparma): if session.id1: logger.info("查看金币", arparma.header_result, session=session) gold = await ShopManage.my_cost(session.id1, session.platform) - await Text(f"你的当前余额: {gold}").send(reply=True) + await MessageUtils.build_message(f"你的当前余额: {gold}").send(reply_to=True) else: - await Text(f"用户id为空...").send(reply=True) + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) @_matcher.assign("my-props") @@ -108,10 +109,12 @@ async def _( user_info.user_displayname or user_info.user_name, session.platform, ): - await Image(image.pic2bytes()).finish(reply=True) - return await Text(f"你的道具为空捏...").send(reply=True) + await MessageUtils.build_message(image.pic2bytes()).finish(reply_to=True) + return await MessageUtils.build_message(f"你的道具为空捏...").send( + reply_to=True + ) else: - await Text(f"用户id为空...").send(reply=True) + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) @_matcher.assign("buy") @@ -123,9 +126,9 @@ async def _(session: EventSession, arparma: Arparma, name: str, num: int): session=session, ) result = await ShopManage.buy_prop(session.id1, name, num, session.platform) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) else: - await Text(f"用户id为空...").send(reply=True) + await MessageUtils.build_message(f"用户id为空...").send(reply_to=True) @_matcher.assign("use") @@ -141,6 +144,6 @@ async def _( result = await ShopManage.use(bot, event, session, message, name, num, "") logger.info(f"使用道具 {name}, 数量: {num}", arparma.header_result, session=session) if isinstance(result, str): - await Text(result).send(reply=True) - elif isinstance(result, MessageFactory): - await result.finish(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) + elif isinstance(result, UniMessage): + await result.finish(reply_to=True) diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 94f92433..8f509631 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -5,7 +5,7 @@ from types import MappingProxyType from typing import Any, Callable, Literal from nonebot.adapters import Bot, Event -from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_alconna import UniMessage, UniMsg from nonebot_plugin_saa import MessageFactory from nonebot_plugin_session import EventSession from pydantic import BaseModel, create_model @@ -190,7 +190,7 @@ class ShopManage: session: EventSession, message: UniMsg, **kwargs, - ) -> str | MessageFactory | None: + ) -> str | UniMessage | None: """运行道具函数 参数: @@ -229,7 +229,7 @@ class ShopManage: goods_name: str, num: int, text: str, - ) -> str | MessageFactory | None: + ) -> str | UniMessage | None: """使用道具 参数: diff --git a/zhenxun/builtin_plugins/sign_in/__init__.py b/zhenxun/builtin_plugins/sign_in/__init__.py index 3eabb3f4..ac09256a 100644 --- a/zhenxun/builtin_plugins/sign_in/__init__.py +++ b/zhenxun/builtin_plugins/sign_in/__init__.py @@ -8,12 +8,12 @@ from nonebot_plugin_alconna import ( store_true, ) from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils from ._data_source import SignManage from .goods_register import driver @@ -121,8 +121,8 @@ async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()) if session.id1: if path := await SignManage.sign(session, nickname): logger.info("签到成功", arparma.header_result, session=session) - await Image(path).finish() - return Text("用户id为空...").send() + await MessageUtils.build_message(path).finish() + return MessageUtils.build_message("用户id为空...").send() @_sign_matcher.assign("my") @@ -130,22 +130,24 @@ async def _(session: EventSession, arparma: Arparma, nickname: str = UserName()) if session.id1: if image := await SignManage.sign(session, nickname, True): logger.info("查看我的签到", arparma.header_result, session=session) - await Image(image).finish() - return Text("用户id为空...").send() + await MessageUtils.build_message(image).finish() + return MessageUtils.build_message("用户id为空...").send() @_sign_matcher.assign("list") async def _(session: EventSession, arparma: Arparma, num: int): gid = session.id3 or session.id2 if not arparma.find("global") and not gid: - await Text("私聊中无法查看 '好感度排行',请发送 '好感度总排行'").finish() + await MessageUtils.build_message( + "私聊中无法查看 '好感度排行',请发送 '好感度总排行'" + ).finish() if session.id1: if arparma.find("global"): gid = None if image := await SignManage.rank(session.id1, num, gid): logger.info("查看签到排行", arparma.header_result, session=session) - await Image(image.pic2bytes()).finish() - return Text("用户id为空...").send() + await MessageUtils.build_message(image).finish() + return MessageUtils.build_message("用户id为空...").send() @scheduler.scheduled_job( diff --git a/zhenxun/builtin_plugins/superuser/exec_sql.py b/zhenxun/builtin_plugins/superuser/exec_sql.py index 2badb14c..e99cd8f6 100644 --- a/zhenxun/builtin_plugins/superuser/exec_sql.py +++ b/zhenxun/builtin_plugins/superuser/exec_sql.py @@ -3,7 +3,6 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from tortoise import Tortoise @@ -12,6 +11,7 @@ from zhenxun.services.db_context import TestSQL from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="数据库操作", @@ -56,7 +56,7 @@ async def _(session: EventSession, message: UniMsg): if sql_text.startswith("exec"): sql_text = sql_text[4:].strip() if not sql_text: - await Text("需要执行的的SQL语句!").finish() + await MessageUtils.build_message("需要执行的的SQL语句!").finish() logger.info(f"执行SQL语句: {sql_text}", "exec", session=session) try: if not sql_text.lower().startswith("select"): @@ -77,11 +77,11 @@ async def _(session: EventSession, message: UniMsg): table = await ImageTemplate.table_page( "EXEC", f"总共有 {len(data_list)} 条数据捏", list(_column), data_list ) - await Image(table.pic2bytes()).send() + await MessageUtils.build_message(table).send() except Exception as e: logger.error("执行 SQL 语句失败...", session=session, e=e) - await Text(f"执行 SQL 语句失败... {type(e)}").finish() - await Text("执行 SQL 语句成功!").finish() + await MessageUtils.build_message(f"执行 SQL 语句失败... {type(e)}").finish() + await MessageUtils.build_message("执行 SQL 语句成功!").finish() @_table_matcher.handle() @@ -97,7 +97,7 @@ async def _(session: EventSession): table = await ImageTemplate.table_page( "数据库表", f"总共有 {len(data_list)} 张表捏", column_name, data_list ) - await Image(table.pic2bytes()).send() + await MessageUtils.build_message(table).send() except Exception as e: logger.error("获取表数据失败...", session=session, e=e) - await Text(f"获取表数据失败... {type(e)}").send() + await MessageUtils.build_message(f"获取表数据失败... {type(e)}").send() diff --git a/zhenxun/builtin_plugins/superuser/power/__ini__.py b/zhenxun/builtin_plugins/superuser/power/__ini__.py new file mode 100644 index 00000000..e69de29b diff --git a/zhenxun/builtin_plugins/superuser/request_manage.py b/zhenxun/builtin_plugins/superuser/request_manage.py index aa4a1e34..7f7c4497 100644 --- a/zhenxun/builtin_plugins/superuser/request_manage.py +++ b/zhenxun/builtin_plugins/superuser/request_manage.py @@ -14,7 +14,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH @@ -24,6 +23,7 @@ from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType from zhenxun.utils.exception import NotFoundError from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import get_user_avatar usage = """ @@ -143,11 +143,13 @@ async def _( if handle_type == RequestHandleType.IGNORE: await FgRequest.ignore(id) except NotFoundError: - await Text("未发现此id的请求...").finish(reply=True) + await MessageUtils.build_message("未发现此id的请求...").finish(reply_to=True) except Exception: - await Text("其他错误, 可能flag已失效...").finish(reply=True) + await MessageUtils.build_message("其他错误, 可能flag已失效...").finish( + reply_to=True + ) logger.info("处理请求", arparma.header_result, session=session) - await Text("成功处理请求!").finish(reply=True) + await MessageUtils.build_message("成功处理请求!").finish(reply_to=True) @_read_matcher.handle() @@ -232,9 +234,9 @@ async def _( await result_image.text((15, 13), _type_text, fill=(140, 140, 143)) req_image_list.append(result_image) if not req_image_list: - await Text("没有任何请求喔...").finish(reply=True) + await MessageUtils.build_message("没有任何请求喔...").finish(reply_to=True) if len(req_image_list) == 1: - await Image(req_image_list[0].pic2bytes()).finish() + await MessageUtils.build_message(req_image_list[0]).finish() width = sum([img.width for img in req_image_list]) height = max([img.height for img in req_image_list]) background = BuildImage(width, height) @@ -242,8 +244,8 @@ async def _( await req_image_list[1].line((0, 10, 1, req_image_list[1].height - 10), width=1) await background.paste(req_image_list[1], (req_image_list[1].width, 0)) logger.info("查看请求", arparma.header_result, session=session) - await Image(background.pic2bytes()).finish() - await Text("没有任何请求喔...").finish(reply=True) + await MessageUtils.build_message(background).finish() + await MessageUtils.build_message("没有任何请求喔...").finish(reply_to=True) @_clear_matcher.handle() @@ -270,4 +272,4 @@ async def _( handle_type=RequestHandleType.IGNORE ) logger.info(f"清空{_type}请求", arparma.header_result, session=session) - await Text(f"已清空{_type}请求!").finish() + await MessageUtils.build_message(f"已清空{_type}请求!").finish() diff --git a/zhenxun/builtin_plugins/superuser/super_help.py b/zhenxun/builtin_plugins/superuser/super_help.py index 5136bb5c..5fa1e09e 100644 --- a/zhenxun/builtin_plugins/superuser/super_help.py +++ b/zhenxun/builtin_plugins/superuser/super_help.py @@ -1,15 +1,12 @@ import nonebot -from arclet.alconna import Args, Option from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_alconna.matcher import AlconnaMatcher -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession -from zhenxun.configs.config import Config -from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH -from zhenxun.configs.utils import PluginExtraData, RegisterConfig +from zhenxun.configs.path_config import IMAGE_PATH +from zhenxun.configs.utils import PluginExtraData from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger @@ -21,6 +18,7 @@ from zhenxun.utils.image_utils import ( group_image, text2image, ) +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import admin_check, ensure_group __plugin_meta__ = PluginMetadata( @@ -156,6 +154,6 @@ async def _( try: await build_help() except EmptyError: - await Text("超级用户帮助为空").finish(reply=True) - await Image(SUPERUSER_HELP_IMAGE).send() + await MessageUtils.build_message("超级用户帮助为空").finish(reply_to=True) + await MessageUtils.build_message(SUPERUSER_HELP_IMAGE).send() logger.info("查看超级用户帮助", arparma.header_result, session=session) diff --git a/zhenxun/models/ban_console.py b/zhenxun/models/ban_console.py index b93d2fcb..0ab0fc69 100644 --- a/zhenxun/models/ban_console.py +++ b/zhenxun/models/ban_console.py @@ -68,7 +68,7 @@ class BanConsole(Model): level: 权限等级 返回: - bool: 权限判断 + bool: 权限判断,能否unban """ user = await cls._get_data(user_id, group_id) if user: @@ -76,7 +76,7 @@ class BanConsole(Model): f"检测用户被ban等级,user_level: {user.ban_level},level: {level}", target=f"{group_id}:{user_id}", ) - return user.ban_level >= level + return user.ban_level <= level return False @classmethod diff --git a/zhenxun/plugins/ai/__init__.py b/zhenxun/plugins/ai/__init__.py index 52862676..28e15354 100644 --- a/zhenxun/plugins/ai/__init__.py +++ b/zhenxun/plugins/ai/__init__.py @@ -4,7 +4,6 @@ from nonebot import on_message from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME, Config @@ -13,6 +12,7 @@ from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.services.log import logger from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils from .data_source import get_chat_result, hello, no_result @@ -82,6 +82,6 @@ async def _(message: UniMsg, session: EventSession, uname: str = UserName()): result = str(result) for t in Config.get_config("ai", "TEXT_FILTER"): result = result.replace(t, "*") - await Text(result).finish() + await MessageUtils.build_message(result).finish() else: await no_result().finish() diff --git a/zhenxun/plugins/ai/data_source.py b/zhenxun/plugins/ai/data_source.py index dbef7731..555f3885 100644 --- a/zhenxun/plugins/ai/data_source.py +++ b/zhenxun/plugins/ai/data_source.py @@ -3,13 +3,13 @@ import random import re import ujson as json -from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage, UniMsg from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils from .utils import ai_message_manager @@ -24,7 +24,7 @@ anime_data = json.load(open(DATA_PATH / "anime.json", "r", encoding="utf8")) async def get_chat_result( message: UniMsg, user_id: str, nickname: str -) -> Text | MessageFactory | None: +) -> UniMessage | None: """获取 AI 返回值,顺序: 特殊回复 -> 图灵 -> 青云客 参数: @@ -42,7 +42,7 @@ async def get_chat_result( special_rst = await ai_message_manager.get_result(user_id, nickname) if special_rst: ai_message_manager.add_result(user_id, special_rst) - return Text(special_rst) + return MessageUtils.build_message(special_rst) if index == 5: index = 0 if len(text) < 6 and random.random() < 0.6: @@ -66,7 +66,7 @@ async def get_chat_result( ai_message_manager.add_result(user_id, rst) for t in Config.get_config("ai", "TEXT_FILTER"): rst = rst.replace(t, "*") - return Text(rst) + return MessageUtils.build_message(rst) # 图灵接口 @@ -182,7 +182,7 @@ async def xie_ai(text: str) -> str: return "" -def hello() -> MessageFactory: +def hello() -> UniMessage: """一些打招呼的内容""" result = random.choice( ( @@ -194,31 +194,27 @@ def hello() -> MessageFactory: ) ) img = random.choice(os.listdir(IMAGE_PATH / "zai")) - return MessageFactory([Image(IMAGE_PATH / "zai" / img), Text(result)]) + return MessageUtils.build_message([IMAGE_PATH / "zai" / img, result]) -def no_result() -> MessageFactory: +def no_result() -> UniMessage: """ 没有回答时的回复 """ - return MessageFactory( + return MessageUtils.build_message( [ - Text( - random.choice( - [ - "你在说啥子?", - f"纯洁的{NICKNAME}没听懂", - "下次再告诉你(下次一定)", - "你觉得我听懂了吗?嗯?", - "我!不!知!道!", - ] - ) - ), - Image( - IMAGE_PATH - / "noresult" - / random.choice(os.listdir(IMAGE_PATH / "noresult")) + random.choice( + [ + "你在说啥子?", + f"纯洁的{NICKNAME}没听懂", + "下次再告诉你(下次一定)", + "你觉得我听懂了吗?嗯?", + "我!不!知!道!", + ] ), + IMAGE_PATH + / "noresult" + / random.choice(os.listdir(IMAGE_PATH / "noresult")), ] ) diff --git a/zhenxun/plugins/alapi/cover.py b/zhenxun/plugins/alapi/cover.py index f6f5fd99..e26bf79c 100644 --- a/zhenxun/plugins/alapi/cover.py +++ b/zhenxun/plugins/alapi/cover.py @@ -1,10 +1,10 @@ from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import Alconna, Args, Arparma, Image, on_alconna from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import get_data @@ -34,11 +34,13 @@ async def _(session: EventSession, arparma: Arparma, url: str): params = {"c": url} data, code = await get_data(cover_url, params) if code != 200 and isinstance(data, str): - await Text(data).finish(reply=True) + await MessageUtils.build_message(data).finish(reply_to=True) data = data["data"] # type: ignore title = data["title"] # type: ignore img = data["cover"] # type: ignore - await MessageFactory([Text(f"title:{title}\n"), Image(img)]).send(reply=True) + await MessageUtils.build_message([f"title:{title}\n", Image(url=img)]).send( + reply_to=True + ) logger.info( f" 获取b站封面: {title} url:{img}", arparma.header_result, session=session ) diff --git a/zhenxun/plugins/black_word/black_word.py b/zhenxun/plugins/black_word/black_word.py index 1cad209f..8b819c6a 100644 --- a/zhenxun/plugins/black_word/black_word.py +++ b/zhenxun/plugins/black_word/black_word.py @@ -5,7 +5,6 @@ from nonebot.adapters import Bot from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME @@ -13,6 +12,7 @@ from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from .data_source import set_user_punish, show_black_text_image @@ -163,14 +163,14 @@ async def _( try: date_ = datetime.strptime(date_str, "%Y-%m-%d") except ValueError: - await Text("日期格式错误,需要:年-月-日").finish() + await MessageUtils.build_message("日期格式错误,需要:年-月-日").finish() result = await show_black_text_image( user_id, group_id, date_, date_type_, ) - await Image(result.pic2bytes()).send() + await MessageUtils.build_message(result).send() @_show_punish_matcher.handle() @@ -212,7 +212,7 @@ async def _(): max_width, max_height, font="CJGaoDeGuo.otf", font_size=24, color="#E3DBD1" ) await A.text((10, 10), text) - await Image(A.pic2bytes()).send() + await MessageUtils.build_message(A).send() @_punish_matcher.handle() @@ -227,7 +227,7 @@ async def _( result = await set_user_punish( bot, uid, session.id2 or session.id3, id, punish_level ) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info( f"设置惩罚 uid:{uid} id_:{id} punish_level:{punish_level} --> {result}", arparma.header_result, diff --git a/zhenxun/plugins/check/__init__.py b/zhenxun/plugins/check/__init__.py index 8e8b91d7..198f9f60 100644 --- a/zhenxun/plugins/check/__init__.py +++ b/zhenxun/plugins/check/__init__.py @@ -8,6 +8,7 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from .data_source import Check @@ -36,5 +37,5 @@ _matcher = on_alconna( @_matcher.handle() async def _(session: EventSession, arparma: Arparma): image = await check.show() - await Image(image.pic2bytes()).send() + await MessageUtils.build_message(image).send() logger.info("自检", arparma.header_result, session=session) diff --git a/zhenxun/plugins/coser.py b/zhenxun/plugins/coser.py index a3194c11..90062d51 100644 --- a/zhenxun/plugins/coser.py +++ b/zhenxun/plugins/coser.py @@ -2,10 +2,8 @@ import time from typing import Tuple from nonebot.adapters import Bot -from nonebot.params import RegexGroup from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config @@ -13,6 +11,7 @@ from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils from zhenxun.utils.withdraw_manage import WithdrawManager __plugin_meta__ = PluginMetadata( @@ -72,8 +71,8 @@ async def _( path = TEMP_PATH / f"cos_cc{int(time.time())}.jpeg" try: await AsyncHttpx.download_file(url, path) - receipt = await Image(path).send() - message_id = receipt.extract_message_id().dict().get("message_id") + receipt = await MessageUtils.build_message(path).send() + message_id = receipt.msg_ids[0]["message_id"] if message_id and WithdrawManager.check(session, withdraw_time): WithdrawManager.append( bot, @@ -82,7 +81,7 @@ async def _( ) logger.info(f"发送cos", arparma.header_result, session=session) except Exception as e: - await Text("你cos给我看!").send() + await MessageUtils.build_message("你cos给我看!").send() logger.error( f"cos错误", arparma.header_result, diff --git a/zhenxun/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py index 1e48482a..28c39807 100644 --- a/zhenxun/plugins/draw_card/handles/base_handle.py +++ b/zhenxun/plugins/draw_card/handles/base_handle.py @@ -7,7 +7,6 @@ from typing import Generic, TypeVar import aiohttp import anyio import ujson as json -from nonebot_plugin_saa import Image from nonebot_plugin_saa import Image as SaaImage from nonebot_plugin_saa import MessageFactory, Text from PIL import Image diff --git a/zhenxun/plugins/epic/data_source.py b/zhenxun/plugins/epic/data_source.py index e0666a2b..87bc5a63 100644 --- a/zhenxun/plugins/epic/data_source.py +++ b/zhenxun/plugins/epic/data_source.py @@ -3,11 +3,13 @@ from datetime import datetime from nonebot.adapters import Bot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import Image, UniMessage from zhenxun.configs.config import NICKNAME from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils # 获取所有 Epic Game Store 促销游戏 @@ -56,7 +58,7 @@ async def get_epic_game_desp(name) -> dict | None: # https://github.com/SD4RK/epicstore_api/blob/master/examples/free_games_example.py async def get_epic_free( bot: Bot, type_event: str -) -> tuple[MessageFactory | list | str, int]: +) -> tuple[UniMessage | list | str, int]: games = await get_epic_game() if not games: return "Epic 可能又抽风啦,请稍后再试(", 404 @@ -86,18 +88,8 @@ async def get_epic_free( "%b.%d %H:%M" ) if type_event == "Group": - _message = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( - game_corp, game_name, game_price, start_date, end_date - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) + _message = f"\n由 {game_corp} 公司发行的游戏 {game_name} ({game_price}) 在 UTC 时间 {start_date} 即将推出免费游玩,预计截至 {end_date}。" + msg_list.append(_message) else: msg = "\n由 {} 公司发行的游戏 {} ({}) 在 UTC 时间 {} 即将推出免费游玩,预计截至 {}。".format( game_corp, game_name, game_price, start_date, end_date @@ -171,36 +163,20 @@ async def get_epic_free( f"/p/{slugs[0]}" if len(slugs) else "" ) if isinstance(bot, (v11Bot, v12Bot)) and type_event == "Group": - _message = "[CQ:image,file={}]\n\nFREE now :: {} ({})\n{}\n此游戏由 {} 开发、{} 发行,将在 UTC 时间 {} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{}\n".format( - game_thumbnail, - game_name, - game_price, - game_desp, - game_dev, - game_pub, - end_date, - game_url, - ) - data = { - "type": "node", - "data": { - "name": f"这里是{NICKNAME}酱", - "uin": f"{bot.self_id}", - "content": _message, - }, - } - msg_list.append(data) + _message = [ + Image(url=game_thumbnail), + f"\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n", + ] + msg_list.append(_message) else: _message = [] if game_thumbnail: - _message.append(Image(game_thumbnail)) + _message.append(Image(url=game_thumbnail)) _message.append( - Text( - f"\n\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n" - ) + f"\n\nFREE now :: {game_name} ({game_price})\n{game_desp}\n此游戏由 {game_dev} 开发、{game_pub} 发行,将在 UTC 时间 {end_date} 结束免费游玩,戳链接速度加入你的游戏库吧~\n{game_url}\n" ) - return MessageFactory(_message), 200 + return MessageUtils.build_message(_message), 200 except TypeError as e: # logger.info(str(e)) pass - return msg_list, 200 + return MessageUtils.template2forward(msg_list, bot.self_id), 200 diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py index ffb1057d..338b0c3a 100644 --- a/zhenxun/plugins/fudu.py +++ b/zhenxun/plugins/fudu.py @@ -5,7 +5,6 @@ from nonebot.adapters import Event from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Image as alcImg from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME, Config @@ -14,6 +13,7 @@ from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.models.task_info import TaskInfo from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import get_download_image_hash +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import ensure_group __plugin_meta__ = PluginMetadata( @@ -113,7 +113,7 @@ async def _(message: UniMsg, event: Event, session: EventSession): if not plain_text and not image_list: return if plain_text and plain_text.startswith(f"@可爱的{NICKNAME}"): - await Text("复制粘贴的虚空艾特?").send(reply=True) + await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply=True) if image_list: img_hash = await get_download_image_hash(image_list[0], group_id) else: @@ -132,18 +132,20 @@ async def _(message: UniMsg, event: Event, session: EventSession): ) and not _manage.is_repeater(group_id): if random.random() < 0.2: if plain_text.startswith("打断施法"): - await Text("打断" + plain_text).finish() + await MessageUtils.build_message("打断" + plain_text).finish() else: - await Text("打断施法!").finish() + await MessageUtils.build_message("打断施法!").finish() _manage.set_repeater(group_id) rst = None if image_list and plain_text: - rst = MessageFactory( - [Text(plain_text), Image(TEMP_PATH / f"compare_{group_id}_img.jpg")] + rst = MessageUtils.build_message( + [plain_text, TEMP_PATH / f"compare_download_{group_id}_img.jpg"] ) elif image_list: - rst = Image(TEMP_PATH / f"compare_{group_id}_img.jpg") + rst = MessageUtils.build_message( + TEMP_PATH / f"compare_download_{group_id}_img.jpg" + ) elif plain_text: - rst = Text(plain_text) + rst = MessageUtils.build_message(plain_text) if rst: await rst.finish() diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py index 1b047665..e3c851af 100644 --- a/zhenxun/plugins/gold_redbag/__init__.py +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -8,17 +8,16 @@ from nonebot.exception import ActionFailed from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me -from nonebot_plugin_alconna import Alconna, Args, Arparma -from nonebot_plugin_alconna import At as alcAt -from nonebot_plugin_alconna import Match, Option, on_alconna +from nonebot_plugin_alconna import Alconna, Args, Arparma, At, Match, Option, on_alconna from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.depends import GetConfig, UserName +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.rules import ensure_group @@ -92,7 +91,7 @@ __plugin_meta__ = PluginMetadata( _red_bag_matcher = on_alconna( - Alconna("塞红包", Args["amount", int]["num", int, 5]["user?", alcAt]), + Alconna("塞红包", Args["amount", int]["num", int, 5]["user?", At]), aliases={"金币红包"}, priority=5, block=True, @@ -130,7 +129,7 @@ async def _( arparma: Arparma, amount: int, num: int, - user: Match[alcAt], + user: Match[At], default_interval: int = GetConfig(config="DEFAULT_INTERVAL"), user_name: str = UserName(), ): @@ -142,9 +141,9 @@ async def _( """以频道id为键""" user_id = session.id1 if not user_id: - await Text("用户id为空").finish() + await MessageUtils.build_message("用户id为空").finish() if not group_id: - await Text("群组id为空").finish() + await MessageUtils.build_message("群组id为空").finish() group_red_bag = RedBagManager.get_group_data(group_id) # 剩余过期时间 time_remaining = group_red_bag.check_timeout(user_id) @@ -153,13 +152,13 @@ async def _( if user_red_bag := group_red_bag.get_user_red_bag(user_id): now = time.time() if now < user_red_bag.start_time + default_interval: - await Text( + await MessageUtils.build_message( f"你的红包还没消化完捏...还剩下 {user_red_bag.num - len(user_red_bag.open_user)} 个! 请等待红包领取完毕..." f"(或等待{time_remaining}秒红包cd)" ).finish() result = await RedBagManager.check_gold(user_id, amount, session.platform) if result: - await Text(result).finish(at_sender=True) + await MessageUtils.build_message(result).finish(at_sender=True) await group_red_bag.add_red_bag( f"{user_name}的红包", int(amount), @@ -172,15 +171,13 @@ async def _( image = await RedBagManager.random_red_bag_background( user_id, platform=session.platform ) - message_list: list = [ - Text(f"{user_name}发起了金币红包\n金额: {amount}\n数量: {num}\n") - ] + message_list: list = [f"{user_name}发起了金币红包\n金额: {amount}\n数量: {num}\n"] if at_user: - message_list.append(Text("指定人: ")) - message_list.append(Mention(at_user)) - message_list.append(Text("\n")) - message_list.append(Image(image.pic2bytes())) - await MessageFactory(message_list).send() + message_list.append("指定人: ") + message_list.append(At(flag="user", target=at_user)) + message_list.append("\n") + message_list.append(image) + await MessageUtils.build_message(message_list).send() logger.info( f"塞入 {num} 个红包,共 {amount} 金币", arparma.header_result, session=session @@ -197,9 +194,9 @@ async def _( """以频道id为键""" user_id = session.id1 if not user_id: - await Text("用户id为空").finish() + await MessageUtils.build_message("用户id为空").finish() if not group_id: - await Text("群组id为空").finish() + await MessageUtils.build_message("群组id为空").finish() if group_red_bag := RedBagManager.get_group_data(group_id): open_data, settlement_list = await group_red_bag.open(user_id, session.platform) # send_msg = Text("没有红包给你开!") @@ -209,18 +206,18 @@ async def _( result_image = await RedBagManager.build_open_result_image( red_bag, user_id, amount, session.platform ) - send_msg.append( - Text(f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n") - ) - send_msg.append(Image(result_image.pic2bytes())) - send_msg.append(Text("\n")) + send_msg.append(f"开启了 {red_bag.promoter} 的红包, 获取 {amount} 个金币\n") + send_msg.append(result_image) + send_msg.append("\n") logger.info( f"抢到了 {red_bag.promoter}({red_bag.promoter_id}) 的红包,获取了{amount}个金币", "开红包", session=session, ) send_msg = ( - MessageFactory(send_msg[:-1]) if send_msg else Text("没有红包给你开!") + MessageUtils.build_message(send_msg[:-1]) + if send_msg + else MessageUtils.build_message("没有红包给你开!") ) await send_msg.send(reply=True) if settlement_list: @@ -228,8 +225,8 @@ async def _( result_image = await red_bag.build_amount_rank( rank_num, session.platform ) - await MessageFactory( - [Text(f"{red_bag.name}已结算\n"), Image(result_image.pic2bytes())] + await MessageUtils.build_message( + [f"{red_bag.name}已结算\n", result_image] ).send() @@ -242,17 +239,17 @@ async def _( group_id = session.id3 or session.id2 user_id = session.id1 if not user_id: - await Text("用户id为空").finish() + await MessageUtils.build_message("用户id为空").finish() if not group_id: - await Text("群组id为空").finish() + await MessageUtils.build_message("群组id为空").finish() if group_red_bag := RedBagManager.get_group_data(group_id): if user_red_bag := group_red_bag.get_user_red_bag(user_id): now = time.time() if now - user_red_bag.start_time < default_interval: - await Text( + await MessageUtils.build_message( f"你的红包还没有过时, 在 {int(default_interval - now + user_red_bag.start_time)} " f"秒后可以退回..." - ).finish(reply=True) + ).finish(reply_to=True) user_red_bag = group_red_bag.get_user_red_bag(user_id) if user_red_bag and ( data := await group_red_bag.settlement(user_id, session.platform) @@ -261,13 +258,13 @@ async def _( rank_num, session.platform ) logger.info(f"退回了红包 {data[0]} 金币", "红包退回", session=session) - await MessageFactory( + await MessageUtils.build_message( [ - Text(f"已成功退还了 " f"{data[0]} 金币\n"), - Image(image_result.pic2bytes()), + f"已成功退还了 " f"{data[0]} 金币\n", + image_result, ] ).finish(reply=True) - await Text("目前没有红包可以退回...").finish(reply=True) + await MessageUtils.build_message("目前没有红包可以退回...").finish(reply_to=True) @_festive_matcher.handle() @@ -352,4 +349,6 @@ async def _( except ActionFailed: logger.warning(f"节日红包图片信息发送失败...", "节日红包", group_id=g) if gl: - await Text(f"节日红包发送成功,累计成功发送 {_suc_cnt} 个群组!").send() + await MessageUtils.build_message( + f"节日红包发送成功,累计成功发送 {_suc_cnt} 个群组!" + ).send() diff --git a/zhenxun/plugins/group_welcome_msg.py b/zhenxun/plugins/group_welcome_msg.py index 064987e6..7148e8e9 100644 --- a/zhenxun/plugins/group_welcome_msg.py +++ b/zhenxun/plugins/group_welcome_msg.py @@ -3,12 +3,12 @@ import re import ujson as json from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import DATA_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import ensure_group __plugin_meta__ = PluginMetadata( @@ -46,17 +46,17 @@ async def _( ) file = path / "text.json" if not file.exists(): - await Text("未设置群欢迎消息...").finish(reply=True) + await MessageUtils.build_message("未设置群欢迎消息...").finish(reply_to=True) message = json.load(open(file, encoding="utf8"))["message"] message_split = re.split(r"\[image:\d+\]", message) if len(message_split) == 1: - await Text(message_split[0]).finish(reply=True) + await MessageUtils.build_message(message_split[0]).finish(reply_to=True) idx = 0 data_list = [] for msg in message_split[:-1]: - data_list.append(Text(msg)) - data_list.append(Image(path / f"{idx}.png")) + data_list.append(msg) + data_list.append(path / f"{idx}.png") idx += 1 data_list.append(message_split[-1]) - await MessageFactory(data_list).send(reply=True) + await MessageUtils.build_message(data_list).send(reply_to=True) logger.info("查看群欢迎消息", arparma.header_result, session=session) diff --git a/zhenxun/plugins/luxun.py b/zhenxun/plugins/luxun.py index e407cb3a..1c1ab09c 100644 --- a/zhenxun/plugins/luxun.py +++ b/zhenxun/plugins/luxun.py @@ -1,12 +1,12 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import BaseBlock, PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="鲁迅说", @@ -54,14 +54,14 @@ async def _(content: str, session: EventSession, arparma: Arparma): ) text = "" if len(content) > 40: - await Text("太长了,鲁迅说不完...").finish() + await MessageUtils.build_message("太长了,鲁迅说不完...").finish() while A.getsize(content)[0] > A.width - 50: n = int(len(content) / 2) text += content[:n] + "\n" content = content[n:] text += content if len(text.split("\n")) > 2: - await Text("太长了,鲁迅说不完...").finish() + await MessageUtils.build_message("太长了,鲁迅说不完...").finish() await A.text( (int((480 - A.getsize(text.split("\n")[0])[0]) / 2), 300), text, (255, 255, 255) ) @@ -70,5 +70,5 @@ async def _(content: str, session: EventSession, arparma: Arparma): "--鲁迅", "msyh.ttf", 30, (255, 255, 255) ) await A.paste(_sign, (320, 400)) - await Image(A.pic2bytes()).send() + await MessageUtils.build_message(A).send() logger.info(f"鲁迅说: {content}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/one_friend/__init__.py b/zhenxun/plugins/one_friend/__init__.py index 67c5082e..0191084f 100644 --- a/zhenxun/plugins/one_friend/__init__.py +++ b/zhenxun/plugins/one_friend/__init__.py @@ -6,13 +6,13 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args from nonebot_plugin_alconna import At as alcAt from nonebot_plugin_alconna import Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils __plugin_meta__ = PluginMetadata( @@ -44,9 +44,9 @@ _matcher.shortcut( async def _(bot: Bot, text: str, at: Match[alcAt], session: EventSession): gid = session.id3 or session.id2 if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() at_user = None if at.available: at_user = at.result.target @@ -75,5 +75,5 @@ async def _(bot: Bot, text: str, at: Match[alcAt], session: EventSession): await A.paste(content, (150, 38)) await A.text((150, 85), text, (125, 125, 125)) logger.info(f"发送有一个朋友: {text}", "我有一个朋友", session=session) - await Image(A.pic2bytes()).finish() - await Text("获取用户信息失败...").send() + await MessageUtils.build_message(A).finish() + await MessageUtils.build_message("获取用户信息失败...").send() diff --git a/zhenxun/plugins/open_cases/__init__.py b/zhenxun/plugins/open_cases/__init__.py index bac0bcc2..2798942e 100644 --- a/zhenxun/plugins/open_cases/__init__.py +++ b/zhenxun/plugins/open_cases/__init__.py @@ -6,12 +6,12 @@ from typing import List from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Arparma, Match from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig, Task from zhenxun.services.log import logger from zhenxun.utils.image_utils import text2image +from zhenxun.utils.message import MessageUtils from .command import ( _group_open_matcher, @@ -126,11 +126,11 @@ async def _( if day.available: _day = day.result if _day > 180: - await Text("天数必须大于0且小于180").finish() + await MessageUtils.build_message("天数必须大于0且小于180").finish() result = await init_skin_trends(name, skin, abrasion, _day) if not result: - await Text("未查询到数据...").finish(reply=True) - await Image(result.pic2bytes()).send() + await MessageUtils.build_message("未查询到数据...").finish(reply_to=True) + await MessageUtils.build_message(result).send() logger.info( f"查看 [{name}:{skin}({abrasion})] 价格趋势", arparma.header_result, @@ -148,26 +148,26 @@ async def _(session: EventSession, arparma: Arparma): async def _(session: EventSession, arparma: Arparma, name: Match[str]): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() case_name = None if name.available: case_name = name.result.replace("武器箱", "").strip() result = await open_case(session.id1, gid, case_name, session) - await result.finish(reply=True) + await result.finish(reply_to=True) @_my_open_matcher.handle() async def _(session: EventSession, arparma: Arparma): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() - await Text( + await MessageUtils.build_message("群组id为空...").finish() + await MessageUtils.build_message( await total_open_statistics(session.id1, gid), - ).send(reply=True) + ).send(reply_to=True) logger.info("查询我的开箱", arparma.header_result, session=session) @@ -175,9 +175,9 @@ async def _(session: EventSession, arparma: Arparma): async def _(session: EventSession, arparma: Arparma): gid = session.id3 or session.id2 if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result = await group_statistics(gid) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info("查询群开箱统计", arparma.header_result, session=session) @@ -185,11 +185,11 @@ async def _(session: EventSession, arparma: Arparma): async def _(session: EventSession, arparma: Arparma): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result = await get_my_knifes(session.id1, gid) - await result.send(reply=True) + await result.send(reply_to=True) logger.info("查询我的金色", arparma.header_result, session=session) @@ -197,18 +197,18 @@ async def _(session: EventSession, arparma: Arparma): async def _(session: EventSession, arparma: Arparma, num: int, name: Match[str]): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() if num > 30: - await Text("开箱次数不要超过30啊笨蛋!").finish() + await MessageUtils.build_message("开箱次数不要超过30啊笨蛋!").finish() if num < 0: - await Text("再负开箱就扣你明天开箱数了!").finish() + await MessageUtils.build_message("再负开箱就扣你明天开箱数了!").finish() case_name = None if name.available: case_name = name.result.replace("武器箱", "").strip() result = await open_multiple_case(session.id1, gid, case_name, num, session) - await result.send(reply=True) + await result.send(reply_to=True) logger.info(f"{num}连开箱", arparma.header_result, session=session) @@ -229,8 +229,8 @@ async def _(session: EventSession, arparma: Arparma, name: Match[str]): skin_list.append(f"{skin_name}") text = "武器箱:\n" + "\n".join(case_list) + "\n皮肤:\n" + ", ".join(skin_list) img = await text2image(text, padding=20, color="#f9f6f2") - await MessageFactory( - [Text("未指定武器箱, 当前已包含武器箱/皮肤\n"), Image(img.pic2bytes())] + await MessageUtils.build_message( + ["未指定武器箱, 当前已包含武器箱/皮肤\n", img] ).finish() if case_name in ["ALL", "ALL1"]: if case_name == "ALL": @@ -239,34 +239,40 @@ async def _(session: EventSession, arparma: Arparma, name: Match[str]): else: case_list = list(KNIFE2ID.keys()) type_ = "罕见皮肤" - await Text(f"即将更新所有{type_}, 请稍等").send() + await MessageUtils.build_message(f"即将更新所有{type_}, 请稍等").send() for i, case_name in enumerate(case_list): try: info = await update_skin_data(case_name, arparma.find("s")) if "请先登录" in info: - await Text(f"未登录, 已停止更新, 请配置BUFF token...").send() + await MessageUtils.build_message( + f"未登录, 已停止更新, 请配置BUFF token..." + ).send() return rand = random.randint(300, 500) result = f"更新全部{type_}完成" if i < len(case_list) - 1: next_case = case_list[i + 1] result = f"将在 {rand} 秒后更新下一{type_}: {next_case}" - await Text(f"{info}, {result}").send() + await MessageUtils.build_message(f"{info}, {result}").send() logger.info(f"info, {result}", "更新武器箱", session=session) await asyncio.sleep(rand) except Exception as e: logger.error(f"更新{type_}: {case_name}", session=session, e=e) - await Text(f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}").send() - await Text(f"更新全部{type_}完成").send() + await MessageUtils.build_message( + f"更新{type_}: {case_name} 发生错误: {type(e)}: {e}" + ).send() + await MessageUtils.build_message(f"更新全部{type_}完成").send() else: - await Text(f"开始{arparma.header_result}: {case_name}, 请稍等").send() + await MessageUtils.build_message( + f"开始{arparma.header_result}: {case_name}, 请稍等" + ).send() try: - await Text(await update_skin_data(case_name, arparma.find("s"))).send( - at_sender=True - ) + await MessageUtils.build_message( + await update_skin_data(case_name, arparma.find("s")) + ).send(at_sender=True) except Exception as e: logger.error(f"{arparma.header_result}: {case_name}", session=session, e=e) - await Text( + await MessageUtils.build_message( f"成功{arparma.header_result}: {case_name} 发生错误: {type(e)}: {e}" ).send() @@ -278,9 +284,9 @@ async def _(session: EventSession, arparma: Arparma, name: Match[str]): case_name = name.result.strip() result = await build_case_image(case_name) if isinstance(result, str): - await Text(result).send() + await MessageUtils.build_message(result).send() else: - await Image(result.pic2bytes()).send() + await MessageUtils.build_message(result).send() logger.info("查看武器箱", arparma.header_result, session=session) @@ -289,9 +295,9 @@ async def _(session: EventSession, arparma: Arparma, name: Match[str]): case_name = None if name.available: case_name = name.result.strip() - await Text("开始更新图片...").send(reply=True) + await MessageUtils.build_message("开始更新图片...").send(reply_to=True) await download_image(case_name) - await Text("更新图片完成...").send(at_sender=True) + await MessageUtils.build_message("更新图片完成...").send(at_sender=True) logger.info("更新武器箱图片", arparma.header_result, session=session) diff --git a/zhenxun/plugins/open_cases/open_cases_c.py b/zhenxun/plugins/open_cases/open_cases_c.py index 74f642aa..f56aa9fe 100644 --- a/zhenxun/plugins/open_cases/open_cases_c.py +++ b/zhenxun/plugins/open_cases/open_cases_c.py @@ -3,7 +3,7 @@ import random import re from datetime import datetime -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config @@ -11,6 +11,7 @@ from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.models.sign_user import SignUser from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import cn2py from .build_image import draw_card @@ -96,7 +97,7 @@ async def get_user_max_count(user_id: str) -> int: async def open_case( user_id: str, group_id: str, case_name: str | None, session: EventSession -) -> MessageFactory: +) -> UniMessage: """开箱 参数: @@ -111,7 +112,7 @@ async def open_case( user_id = str(user_id) group_id = str(group_id) if not CaseManager.CURRENT_CASES: - return MessageFactory([Text("未收录任何武器箱")]) + return MessageUtils.build_message("未收录任何武器箱") if not case_name: case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore if case_name not in CaseManager.CURRENT_CASES: @@ -128,16 +129,12 @@ async def open_case( max_count = await get_user_max_count(user_id) # 一天次数上限 if user.today_open_total >= max_count: - return MessageFactory( - [ - Text( - f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" - ) - ] + return MessageUtils.build_message( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" ) skin_list = await random_skin(1, case_name) # type: ignore if not skin_list: - return MessageFactory(Text("未抽取到任何皮肤")) + return MessageUtils.build_message("未抽取到任何皮肤") skin, rand = skin_list[0] rand = str(rand)[:11] case_price = 0 @@ -176,13 +173,11 @@ async def open_case( logger.debug(f"添加 1 条开箱日志", "开箱", session=session) over_count = max_count - user.today_open_total img = await draw_card(skin, rand) - return MessageFactory( + return MessageUtils.build_message( [ - Text(f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n"), - Image(img.pic2bytes()), - Text( - f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}" - ), + f"开启{case_name}武器箱.\n剩余开箱次数:{over_count}.\n", + img, + f"\n箱子单价:{case_price}\n花费:{17 + case_price:.2f}\n:{ridicule_result}", ] ) @@ -193,7 +188,7 @@ async def open_multiple_case( case_name: str | None, num: int = 10, session: EventSession | None = None, -) -> MessageFactory: +) -> UniMessage: """多连开箱 参数: @@ -209,17 +204,12 @@ async def open_multiple_case( user_id = str(user_id) group_id = str(group_id) if not CaseManager.CURRENT_CASES: - return MessageFactory([Text("未收录任何武器箱")]) + return MessageUtils.build_message("未收录任何武器箱") if not case_name: case_name = random.choice(CaseManager.CURRENT_CASES) # type: ignore if case_name not in CaseManager.CURRENT_CASES: - return MessageFactory( - [ - Text( - "武器箱未收录, 当前可用武器箱:\n" - + ", ".join(CaseManager.CURRENT_CASES) - ) - ] + return MessageUtils.build_message( + "武器箱未收录, 当前可用武器箱:\n" + ", ".join(CaseManager.CURRENT_CASES) ) user, _ = await OpenCasesUser.get_or_create( user_id=user_id, @@ -228,21 +218,13 @@ async def open_multiple_case( ) max_count = await get_user_max_count(user_id) if user.today_open_total >= max_count: - return MessageFactory( - [ - Text( - f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" - ) - ] + return MessageUtils.build_message( + f"今天已达开箱上限了喔,明天再来吧\n(提升好感度可以增加每日开箱数 #疯狂暗示)" ) if max_count - user.today_open_total < num: - return MessageFactory( - [ - Text( - f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" - f"\n剩余开箱次数:{max_count - user.today_open_total}" - ) - ] + return MessageUtils.build_message( + f"今天开箱次数不足{num}次噢,请单抽试试看(也许单抽运气更好?)" + f"\n剩余开箱次数:{max_count - user.today_open_total}" ) logger.debug(f"尝试开启武器箱: {case_name}", "开箱", session=session) case = cn2py(case_name) # type: ignore @@ -250,7 +232,7 @@ async def open_multiple_case( img_list = [] skin_list = await random_skin(num, case_name) # type: ignore if not skin_list: - return MessageFactory([Text("未抽取到任何皮肤...")]) + return MessageUtils.build_message("未抽取到任何皮肤...") total_price = 0 log_list = [] now = datetime.now() @@ -314,13 +296,11 @@ async def open_multiple_case( result = "" for color_name in skin_count: result += f"[{color_name}:{skin_count[color_name]}] " - return MessageFactory( + return MessageUtils.build_message( [ - Text(f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n"), - Image(mark_image.pic2bytes()), - Text( - f"\n{result[:-1]}\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}" - ), + f"开启{case_name}武器箱\n剩余开箱次数:{over_count}\n", + mark_image, + f"\n{result[:-1]}\n箱子单价:{case_price}\n总获取金额:{total_price:.2f}\n总花费:{(17 + case_price) * num:.2f}", ] ) @@ -382,7 +362,7 @@ async def group_statistics(group_id: str): ) -async def get_my_knifes(user_id: str, group_id: str) -> MessageFactory: +async def get_my_knifes(user_id: str, group_id: str) -> UniMessage: """获取我的金色 参数: @@ -397,7 +377,7 @@ async def get_my_knifes(user_id: str, group_id: str) -> MessageFactory: user_id=user_id, group_id=group_id, color="KNIFE" ).all() if not data_list: - return MessageFactory([Text("您木有开出金色级别的皮肤喔...")]) + return MessageUtils.build_message("您木有开出金色级别的皮肤喔...") length = len(data_list) if length < 5: h = 600 @@ -427,7 +407,7 @@ async def get_my_knifes(user_id: str, group_id: str) -> MessageFactory: await knife_img.text((5, 560), f"\t价格:{skin.price}") image_list.append(knife_img) A = await A.auto_paste(image_list, 5) - return MessageFactory([Image(A.pic2bytes())]) + return MessageUtils.build_message(A) async def get_old_knife(user_id: str, group_id: str) -> list[OpenCasesLog]: diff --git a/zhenxun/plugins/parse_bilibili/__init__.py b/zhenxun/plugins/parse_bilibili/__init__.py index f42f78ae..1d319093 100644 --- a/zhenxun/plugins/parse_bilibili/__init__.py +++ b/zhenxun/plugins/parse_bilibili/__init__.py @@ -4,8 +4,7 @@ import time import ujson as json from nonebot import on_message from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Hyper, UniMsg -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import Hyper, Image, UniMsg from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import TEMP_PATH @@ -14,6 +13,7 @@ from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils from .information_container import InformationContainer from .parse_url import parse_bili_url @@ -138,12 +138,10 @@ async def _(session: EventSession, message: UniMsg): _tmp[vd_url] = time.time() _path = TEMP_PATH / f"{aid}.jpg" await AsyncHttpx.download_file(pic, _path) - await MessageFactory( + await MessageUtils.build_message( [ - Image(_path), - Text( - f"av{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n点赞:{like},弹幕:{danmuku}\n{vd_url}" - ), + _path, + f"av{aid}\n标题:{title}\nUP:{author}\n上传日期:{date}\n回复:{reply},收藏:{favorite},投币:{coin}\n点赞:{like},弹幕:{danmuku}\n{vd_url}", ] ).send() @@ -161,14 +159,12 @@ async def _(session: EventSession, message: UniMsg): parent_area_name = live_info.get("parent_area_name", "") # 父分区 logger.info(f"解析bilibili转发 {live_url}", "b站解析", session=session) _tmp[live_url] = time.time() - await MessageFactory( + await MessageUtils.build_message( [ - Image(user_cover), - Text( - f"开播用户:https://space.bilibili.com/{uid}\n开播时间:{live_time}\n直播分区:{parent_area_name}——>{area_name}\n标题:{title}\n简介:{description}\n直播截图:\n" - ), - Image(keyframe), - Text(f"{live_url}"), + Image(url=user_cover), + f"开播用户:https://space.bilibili.com/{uid}\n开播时间:{live_time}\n直播分区:{parent_area_name}——>{area_name}\n标题:{title}\n简介:{description}\n直播截图:\n", + Image(url=keyframe), + f"{live_url}", ] ).send() elif image_info: diff --git a/zhenxun/plugins/parse_bilibili/get_image.py b/zhenxun/plugins/parse_bilibili/get_image.py index e2f4ddcb..bbc005c5 100644 --- a/zhenxun/plugins/parse_bilibili/get_image.py +++ b/zhenxun/plugins/parse_bilibili/get_image.py @@ -1,12 +1,13 @@ import os import re -from nonebot_plugin_saa import Image +from nonebot_plugin_alconna import UniMessage from zhenxun.configs.path_config import TEMP_PATH from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncPlaywright from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from zhenxun.utils.user_agent import get_user_agent_str @@ -21,7 +22,7 @@ async def resize(path: str): await A.save(path) -async def get_image(url) -> Image | None: +async def get_image(url) -> UniMessage | None: """获取Bilibili链接的截图,并返回base64格式的图片 参数: @@ -101,7 +102,7 @@ async def get_image(url) -> Image | None: except Exception as e: logger.warning(f"尝试解析bilibili转发失败", e=e) return None - return Image(screenshot_path) + return MessageUtils.build_message(screenshot_path) except Exception as e: logger.error(f"尝试解析bilibili转发失败", e=e) return None diff --git a/zhenxun/plugins/pid_search.py b/zhenxun/plugins/pid_search.py index 2d1b7c05..97fc4d40 100644 --- a/zhenxun/plugins/pid_search.py +++ b/zhenxun/plugins/pid_search.py @@ -3,7 +3,6 @@ from asyncio.exceptions import TimeoutError from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config @@ -11,6 +10,7 @@ from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import change_pixiv_image_links from zhenxun.utils.withdraw_manage import WithdrawManager @@ -69,13 +69,17 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): session=session, e=e, ) - await Text(f"发生了一些错误..{type(e)}:{e}").finish() + await MessageUtils.build_message(f"发生了一些错误..{type(e)}:{e}").finish() else: if data.get("error"): - await Text(data["error"]["user_message"]).finish(reply=True) + await MessageUtils.build_message(data["error"]["user_message"]).finish( + reply_to=True + ) data = data["illust"] if not data["width"] and not data["height"]: - await Text(f"没有搜索到 PID:{pid} 的图片").finish(reply=True) + await MessageUtils.build_message( + f"没有搜索到 PID:{pid} 的图片" + ).finish(reply_to=True) pid = data["id"] title = data["title"] author = data["user"]["name"] @@ -93,20 +97,20 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): TEMP_PATH / f"pid_search_{session.id1}_{i}.png", headers=headers, ): - await Text("图片下载失败了...").finish(reply=True) + await MessageUtils.build_message("图片下载失败了...").finish( + reply_to=True + ) tmp = "" if session.id3 or session.id2: tmp = "\n【注】将在30后撤回......" - receipt = await MessageFactory( + receipt = await MessageUtils.build_message( [ - Text( - f"title:{title}\n" - f"pid:{pid}\n" - f"author:{author}\n" - f"author_id:{author_id}\n" - ), - Image(TEMP_PATH / f"pid_search_{session.id1}_{i}.png"), - Text(f"{tmp}"), + f"title:{title}\n" + f"pid:{pid}\n" + f"author:{author}\n" + f"author_id:{author_id}\n", + TEMP_PATH / f"pid_search_{session.id1}_{i}.png", + f"{tmp}", ] ).send() logger.info( @@ -114,8 +118,8 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): ) if session.id3 or session.id2: await WithdrawManager.withdraw_message( - bot, receipt.extract_message_id().message_id, 30 # type: ignore + bot, receipt.msg_ids[0]["message_id"], 30 # type: ignore ) break else: - await Text("图片下载失败了...").send(reply=True) + await Text("图片下载失败了...").send(reply_to=True) diff --git a/zhenxun/plugins/pix_gallery/_data_source.py b/zhenxun/plugins/pix_gallery/_data_source.py index a15eec28..7e9db221 100644 --- a/zhenxun/plugins/pix_gallery/_data_source.py +++ b/zhenxun/plugins/pix_gallery/_data_source.py @@ -192,7 +192,7 @@ async def search_image( return pid_count, pic_count -async def get_image(img_url: str, user_id: str) -> str | Path | None: +async def get_image(img_url: str, user_id: str) -> Path | None: """下载图片 参数: @@ -200,7 +200,7 @@ async def get_image(img_url: str, user_id: str) -> str | Path | None: user_id: 用户id 返回: - str | Path | None: 图片名称 + Path | None: 图片名称 """ if "https://www.pixiv.net/artworks" in img_url: pid = img_url.rsplit("/", maxsplit=1)[-1] diff --git a/zhenxun/plugins/pix_gallery/pix.py b/zhenxun/plugins/pix_gallery/pix.py index d67e5ada..2f8d25c3 100644 --- a/zhenxun/plugins/pix_gallery/pix.py +++ b/zhenxun/plugins/pix_gallery/pix.py @@ -11,12 +11,12 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from zhenxun.utils.withdraw_manage import WithdrawManager @@ -96,7 +96,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) global PIX_RATIO, OMEGA_RATIO gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if PIX_RATIO is None: pix_omega_pixiv_ratio = Config.get_config("pix", "PIX_OMEGA_PIXIV_RATIO") PIX_RATIO = pix_omega_pixiv_ratio[0] / ( @@ -117,7 +117,9 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) if (nsfw_tag == 1 and not Config.get_config("pix", "ALLOW_GROUP_SETU")) or ( nsfw_tag == 2 and not Config.get_config("pix", "ALLOW_GROUP_R18") ): - await Text("你不能看这些噢,这些都是是留给管理员看的...").finish() + await MessageUtils.build_message( + "你不能看这些噢,这些都是是留给管理员看的..." + ).finish() if (n := len(spt)) == 1: if str(spt[0]).isdigit() and int(spt[0]) < 100: num = int(spt[0]) @@ -132,7 +134,9 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) session.id1 in bot.config.superusers and num > 30 ): num = random.randint(1, 10) - await Text(f"太贪心了,就给你发 {num}张 好了").send() + await MessageUtils.build_message( + f"太贪心了,就给你发 {num}张 好了" + ).send() spt = spt[:-1] keyword = " ".join(spt) pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 @@ -149,7 +153,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) elif keyword.lower().startswith("pid"): pid = keyword.replace("pid", "").replace(":", "").replace(":", "") if not str(pid).isdigit(): - await Text("PID必须是数字...").finish(reply=True) + await MessageUtils.build_message("PID必须是数字...").finish(reply_to=True) all_image = await Pixiv.query_images( pid=int(pid), r18=1 if nsfw_tag == 2 else 0 ) @@ -169,15 +173,15 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) all_image.append(x) tmp_.append(x.pid) if not all_image: - await Text(f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片...").finish( - reply=True - ) + await MessageUtils.build_message( + f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片..." + ).finish(reply_to=True) msg_list = [] for _ in range(num): img_url = None author = None if not all_image: - await Text("坏了...发完了,没图了...").finish() + await MessageUtils.build_message("坏了...发完了,没图了...").finish() img = random.choice(all_image) all_image.remove(img) # type: ignore if isinstance(img, OmegaPixivIllusts): @@ -194,24 +198,22 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) if _img: if Config.get_config("pix", "SHOW_INFO"): msg_list.append( - MessageFactory( + MessageUtils.build_message( [ - Text( - f"title:{title}\n" - f"author:{author}\n" - f"PID:{pid}\nUID:{uid}\n" - ), - Image(_img), + f"title:{title}\n" + f"author:{author}\n" + f"PID:{pid}\nUID:{uid}\n", + _img, ] ) ) else: - msg_list.append(Image(_img)) + msg_list.append(_img) logger.info( f" 查看PIX图库PID: {pid}", arparma.header_result, session=session ) else: - msg_list.append(Text("这张图似乎下载失败了")) + msg_list.append(MessageUtils.build_message("这张图似乎下载失败了")) logger.info( f" 查看PIX图库PID: {pid},下载图片出错", arparma.header_result, @@ -225,7 +227,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) for msg in msg_list: receipt = await msg.send() if receipt: - message_id = receipt.extract_message_id().message_id + message_id = receipt.msg_ids[0]["message_id"] await WithdrawManager.withdraw_message( bot, str(message_id), @@ -236,7 +238,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, tags: Match[str]) for msg in msg_list: receipt = await msg.send() if receipt: - message_id = receipt.extract_message_id().message_id + message_id = receipt.msg_ids[0]["message_id"] await WithdrawManager.withdraw_message( bot, message_id, diff --git a/zhenxun/plugins/pix_gallery/pix_show_info.py b/zhenxun/plugins/pix_gallery/pix_show_info.py index 9dcf1af9..cb1cbf2a 100644 --- a/zhenxun/plugins/pix_gallery/pix_show_info.py +++ b/zhenxun/plugins/pix_gallery/pix_show_info.py @@ -1,11 +1,11 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import gen_keyword_pic, get_keyword_num from ._model.pixiv_keyword_user import PixivKeywordUser @@ -40,8 +40,10 @@ async def _(arparma: Arparma, session: EventSession): "keyword", flat=True ) if not data: - await Text("您目前没有提供任何Pixiv搜图关键字...").finish(reply=True) - await Text(f"您目前提供的如下关键字:\n\t" + ",".join(data)).send() # type: ignore + await MessageUtils.build_message("您目前没有提供任何Pixiv搜图关键字...").finish( + reply_to=True + ) + await MessageUtils.build_message(f"您目前提供的如下关键字:\n\t" + ",".join(data)).send() # type: ignore logger.info("查看我的pix关键词", arparma.header_result, session=session) @@ -52,12 +54,14 @@ async def _(bot: Bot, arparma: Arparma, session: EventSession): image = await gen_keyword_pic( _pass_keyword, not_pass_keyword, session.id1 in bot.config.superusers ) - await Image(image.pic2bytes()).send() # type: ignore + await MessageUtils.build_message(image).send() # type: ignore else: if session.id1 in bot.config.superusers: - await Text(f"目前没有已收录或待收录的搜索关键词...").send() + await MessageUtils.build_message( + f"目前没有已收录或待收录的搜索关键词..." + ).send() else: - await Text(f"目前没有已收录的搜索关键词...").send() + await MessageUtils.build_message(f"目前没有已收录的搜索关键词...").send() @_pix_matcher.handle() @@ -66,7 +70,7 @@ async def _(bot: Bot, arparma: Arparma, session: EventSession, keyword: Match[st if keyword.available: _keyword = keyword.result count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(_keyword) - await Text( + await MessageUtils.build_message( f"PIX图库:{_keyword}\n" f"总数:{count + r18_count}\n" f"美图:{count}\n" diff --git a/zhenxun/plugins/pixiv_rank_search/__init__.py b/zhenxun/plugins/pixiv_rank_search/__init__.py index a82a269e..01945cd8 100644 --- a/zhenxun/plugins/pixiv_rank_search/__init__.py +++ b/zhenxun/plugins/pixiv_rank_search/__init__.py @@ -13,12 +13,12 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import is_valid_date from .data_source import download_pixiv_imgs, get_pixiv_urls, search_pixiv_urls @@ -148,34 +148,40 @@ async def _( ): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() code = 0 info_list = [] _datetime = None if datetime.available: _datetime = datetime.result if not is_valid_date(_datetime): - await Text("日期不合法,示例: 2018-4-25").finish(reply=True) + await MessageUtils.build_message("日期不合法,示例: 2018-4-25").finish( + reply_to=True + ) if rank_type in [6, 7, 8, 9]: if gid: - await Text("羞羞脸!私聊里自己看!").finish(at_sender=True) + await MessageUtils.build_message("羞羞脸!私聊里自己看!").finish( + at_sender=True + ) info_list, code = await get_pixiv_urls( rank_dict[str(rank_type)], num, date=_datetime ) if code != 200 and info_list: if isinstance(info_list[0], str): - await Text(info_list[0]).finish() + await MessageUtils.build_message(info_list[0]).finish() if not info_list: - await Text("没有找到啊,等等再试试吧~V").send(at_sender=True) + await MessageUtils.build_message("没有找到啊,等等再试试吧~V").send( + at_sender=True + ) for title, author, urls in info_list: try: images = await download_pixiv_imgs(urls, session.id1) # type: ignore - await MessageFactory( - [Text(f"title: {title}\n"), Text(f"author: {author}\n")] + images + await MessageUtils.build_message( + [f"title: {title}\nauthor: {author}\n"] + images # type: ignore ).send() except (NetworkError, TimeoutError): - await Text("这张图网络直接炸掉了!").send() + await MessageUtils.build_message("这张图网络直接炸掉了!").send() logger.info( f" 查看了P站排行榜 rank_type{rank_type}", arparma.header_result, session=session ) @@ -187,29 +193,33 @@ async def _( ): gid = session.id3 or session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if gid: if arparma.find("r") and not Config.get_config( "pixiv_rank_search", "ALLOW_GROUP_R18" ): - await Text("(脸红#) 你不会害羞的 八嘎!").finish(at_sender=True) + await MessageUtils.build_message("(脸红#) 你不会害羞的 八嘎!").finish( + at_sender=True + ) r18 = 0 if arparma.find("r") else 1 info_list = None keyword = keyword.replace("#", " ") info_list, code = await search_pixiv_urls(keyword, num, page, r18) if code != 200 and isinstance(info_list[0], str): - await Text(info_list[0]).finish() + await MessageUtils.build_message(info_list[0]).finish() if not info_list: - await Text("没有找到啊,等等再试试吧~V").finish(at_sender=True) + await MessageUtils.build_message("没有找到啊,等等再试试吧~V").finish( + at_sender=True + ) for title, author, urls in info_list: try: images = await download_pixiv_imgs(urls, session.id1) # type: ignore - await MessageFactory( - [Text(f"title: {title}\n"), Text(f"author: {author}\n")] + images + await MessageUtils.build_message( + [f"title: {title}\nauthor: {author}\n"] + images # type: ignore ).send() except (NetworkError, TimeoutError): - await Text("这张图网络直接炸掉了!").send() + await MessageUtils.build_message("这张图网络直接炸掉了!").send() logger.info( f" 查看了搜索 {keyword} R18:{r18}", arparma.header_result, session=session ) diff --git a/zhenxun/plugins/pixiv_rank_search/data_source.py b/zhenxun/plugins/pixiv_rank_search/data_source.py index 28b31b53..d3f54dfe 100644 --- a/zhenxun/plugins/pixiv_rank_search/data_source.py +++ b/zhenxun/plugins/pixiv_rank_search/data_source.py @@ -121,7 +121,7 @@ async def parser_data( async def download_pixiv_imgs( urls: list[str], user_id: str, forward_msg_index: int | None = None -) -> list[Image]: +) -> list[Path]: """下载图片 参数: @@ -156,12 +156,12 @@ async def download_pixiv_imgs( change_img_md5(file) image = None if forward_msg_index is not None: - image = Image( + image = ( TEMP_PATH / f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg" ) else: - image = Image(TEMP_PATH / f"{user_id}_{index}_pixiv.jpg") + image = TEMP_PATH / f"{user_id}_{index}_pixiv.jpg" if image: result_list.append(image) index += 1 diff --git a/zhenxun/plugins/poke/__init__.py b/zhenxun/plugins/poke/__init__.py index 555ebf15..7f46be96 100644 --- a/zhenxun/plugins/poke/__init__.py +++ b/zhenxun/plugins/poke/__init__.py @@ -5,12 +5,12 @@ from nonebot import on_notice from nonebot.adapters.onebot.v11 import PokeNotifyEvent from nonebot.adapters.onebot.v11.message import MessageSegment from nonebot.plugin import PluginMetadata -from nonebot_plugin_saa import Image, MessageFactory, Text from zhenxun.configs.path_config import IMAGE_PATH, RECORD_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.models.ban_console import BanConsole from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import CountLimiter __plugin_meta__ = PluginMetadata( @@ -66,10 +66,10 @@ async def _(event: PokeNotifyEvent): index = random.randint( 0, len(os.listdir(IMAGE_PATH / "image_management" / path)) - 1 ) - await MessageFactory( + await MessageUtils.build_message( [ - Text(f"id: {index}"), - Image(IMAGE_PATH / "image_management" / path / f"{index}.jpg"), + f"id: {index}", + IMAGE_PATH / "image_management" / path / f"{index}.jpg", ] ).send() logger.info(f"USER {event.user_id} 戳了戳我") @@ -79,7 +79,8 @@ async def _(event: PokeNotifyEvent): await poke_.send(result) await poke_.send(voice.split("_")[1]) logger.info( - f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}' + f'USER {event.user_id} 戳了戳我 回复: {result} \n {voice.split("_")[1]}', + "戳一戳", ) else: await poke_.send(MessageSegment("poke", {"qq": event.user_id})) diff --git a/zhenxun/plugins/russian/__init__.py b/zhenxun/plugins/russian/__init__.py index db797e64..ee25fdfd 100644 --- a/zhenxun/plugins/russian/__init__.py +++ b/zhenxun/plugins/russian/__init__.py @@ -3,12 +3,12 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Arparma from nonebot_plugin_alconna import At as alcAt from nonebot_plugin_alconna import Match, UniMsg -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils from .command import ( _accept_matcher, @@ -79,20 +79,22 @@ async def _( ): gid = session.id2 if message.extract_plain_text() == "取消": - await Text("已取消装弹...").finish() + await MessageUtils.build_message("已取消装弹...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() if money <= 0: - await Text("赌注金额必须大于0!").finish(reply=True) + await MessageUtils.build_message("赌注金额必须大于0!").finish(reply_to=True) if num in ["取消", "算了"]: - await Text("已取消装弹...").finish() + await MessageUtils.build_message("已取消装弹...").finish() if not num.isdigit(): - await Text("输入的子弹数必须是数字!").finish(reply=True) + await MessageUtils.build_message("输入的子弹数必须是数字!").finish( + reply_to=True + ) b_num = int(num) if b_num < 0 or b_num > 6: - await Text("子弹数量必须在1-6之间!").finish(reply=True) + await MessageUtils.build_message("子弹数量必须在1-6之间!").finish(reply_to=True) _at_user = at_user.result.target if at_user.available else None rus = Russian( at_user=_at_user, player1=(session.id1, uname), money=money, bullet_num=b_num @@ -110,9 +112,9 @@ async def _( async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result = await russian_manage.accept(bot, gid, session.id1, uname) await result.send() logger.info(f"俄罗斯轮盘接受对决", arparma.header_result, session=session) @@ -122,9 +124,9 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = User async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result = russian_manage.refuse(gid, session.id1, uname) await result.send() logger.info(f"俄罗斯轮盘拒绝对决", arparma.header_result, session=session) @@ -134,9 +136,9 @@ async def _(session: EventSession, arparma: Arparma, uname: str = UserName()): async def _(session: EventSession, arparma: Arparma): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result = await russian_manage.settlement(gid, session.id1, session.platform) await result.send() logger.info(f"俄罗斯轮盘结算", arparma.header_result, session=session) @@ -146,9 +148,9 @@ async def _(session: EventSession, arparma: Arparma): async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = UserName()): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() result, settle = await russian_manage.shoot( bot, gid, session.id1, uname, session.platform ) @@ -162,11 +164,11 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, uname: str = User async def _(session: EventSession, arparma: Arparma): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() user, _ = await RussianUser.get_or_create(user_id=session.id1, group_id=gid) - await Text( + await MessageUtils.build_message( f"俄罗斯轮盘\n" f"总胜利场次:{user.win_count}\n" f"当前连胜:{user.winning_streak}\n" @@ -176,7 +178,7 @@ async def _(session: EventSession, arparma: Arparma): f"最高连败:{user.max_losing_streak}\n" f"赚取金币:{user.make_money}\n" f"输掉金币:{user.lose_money}", - ).send(reply=True) + ).send(reply_to=True) logger.info(f"俄罗斯轮盘查看战绩", arparma.header_result, session=session) @@ -184,16 +186,16 @@ async def _(session: EventSession, arparma: Arparma): async def _(session: EventSession, arparma: Arparma, rank_type: str, num: int): gid = session.id2 if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not gid: - await Text("群组id为空...").finish() + await MessageUtils.build_message("群组id为空...").finish() if 51 < num or num < 10: num = 10 result = await russian_manage.rank(session.id1, gid, rank_type, num) if isinstance(result, str): - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) result.show() - await Image(result.pic2bytes()).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info( f"查看轮盘排行: {rank_type} 数量: {num}", arparma.header_result, session=session ) diff --git a/zhenxun/plugins/search_image/__init__.py b/zhenxun/plugins/search_image/__init__.py index d98e9ca1..38e86de0 100644 --- a/zhenxun/plugins/search_image/__init__.py +++ b/zhenxun/plugins/search_image/__init__.py @@ -1,15 +1,16 @@ +from pathlib import Path + from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma from nonebot_plugin_alconna import Image as alcImg from nonebot_plugin_alconna import Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils -from zhenxun.utils.utils import template2forward from .saucenao import get_saucenao_image @@ -48,7 +49,7 @@ _matcher = on_alconna( ) -async def get_image_info(mod: str, url: str) -> str | list[Image | Text] | None: +async def get_image_info(mod: str, url: str) -> str | list[str | Path] | None: if mod == "saucenao": return await get_saucenao_image(url) @@ -73,21 +74,21 @@ async def _( ): gid = session.id3 or session.id2 if not image.url: - await Text("图片url为空...").finish() - await Text("开始处理图片...").send() + await MessageUtils.build_message("图片url为空...").finish() + await MessageUtils.build_message("开始处理图片...").send() info_list = await get_image_info(mode, image.url) if isinstance(info_list, str): - await Text(info_list).finish(at_sender=True) + await MessageUtils.build_message(info_list).finish(at_sender=True) if not info_list: - await Text("未查询到...").finish() + await MessageUtils.build_message("未查询到...").finish() platform = PlatformUtils.get_platform(bot) if "qq" == platform and gid: - forward = template2forward(info_list, bot.self_id) # type: ignore + forward = MessageUtils.template2forward(info_list[1:], bot.self_id) # type: ignore await bot.send_group_forward_msg( group_id=int(gid), messages=forward, # type: ignore ) else: for info in info_list[1:]: - await info.send() + await MessageUtils.build_message(info).send() logger.info(f" 识图: {image.url}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/search_image/saucenao.py b/zhenxun/plugins/search_image/saucenao.py index c76c96b2..eab44fab 100644 --- a/zhenxun/plugins/search_image/saucenao.py +++ b/zhenxun/plugins/search_image/saucenao.py @@ -1,6 +1,5 @@ import random - -from nonebot_plugin_saa import Image, Text +from pathlib import Path from zhenxun.configs.config import Config from zhenxun.configs.path_config import TEMP_PATH @@ -12,7 +11,7 @@ API_URL_ASCII2D = "https://ascii2d.net/search/url/" API_URL_IQDB = "https://iqdb.org/" -async def get_saucenao_image(url: str) -> str | list[Image | Text]: +async def get_saucenao_image(url: str) -> str | list[str | Path]: """获取图片源 参数: @@ -44,7 +43,7 @@ async def get_saucenao_image(url: str) -> str | list[Image | Text]: msg_list = [] index = random.randint(0, 10000) if await AsyncHttpx.download_file(url, TEMP_PATH / f"saucenao_search_{index}.jpg"): - msg_list.append(Image(TEMP_PATH / f"saucenao_search_{index}.jpg")) + msg_list.append(TEMP_PATH / f"saucenao_search_{index}.jpg") for info in data: try: similarity = info["header"]["similarity"] @@ -57,7 +56,7 @@ async def get_saucenao_image(url: str) -> str | list[Image | Text]: tmp += f'source:{info["data"]["ext_urls"][0]}\n' except KeyError: tmp += f'source:{info["header"]["thumbnail"]}\n' - msg_list.append(Text(tmp[:-1])) + msg_list.append(tmp[:-1]) except Exception as e: logger.warning(f"识图获取图片信息发生错误", e=e) return msg_list diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py index fd341730..8f9bd2ff 100644 --- a/zhenxun/plugins/send_setu_/send_setu/__init__.py +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -2,7 +2,6 @@ import random from typing import Tuple from nonebot.adapters import Bot -from nonebot.adapters.onebot.v11 import MessageSegment from nonebot.matcher import Matcher from nonebot.message import run_postprocessor from nonebot.plugin import PluginMetadata @@ -23,11 +22,11 @@ from zhenxun.configs.utils import PluginCdBlock, PluginExtraData, RegisterConfig from zhenxun.models.sign_user import SignUser from zhenxun.models.user_console import UserConsole from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils -from zhenxun.utils.utils import template2forward from zhenxun.utils.withdraw_manage import WithdrawManager -from ._data_source import Image, SetuManage, base_config +from ._data_source import SetuManage, base_config __plugin_meta__ = PluginMetadata( name="色图", @@ -196,7 +195,7 @@ async def _( if is_r18 and gid: """群聊中禁止查看r18""" if not base_config.get("ALLOW_GROUP_R18"): - await Text( + await MessageUtils.build_message( random.choice( [ "这种不好意思的东西怎么可能给这么多人看啦", @@ -209,11 +208,11 @@ async def _( """指定id""" result = await SetuManage.get_setu(local_id=local_id.result) if isinstance(result, str): - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply=True) await result[0].finish() result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18) if isinstance(result_list, str): - await Text(result_list).finish(reply=True) + await MessageUtils.build_message(result_list).finish(reply=True) max_once_num2forward = base_config.get("MAX_ONCE_NUM2FORWARD") platform = PlatformUtils.get_platform(bot) if ( @@ -223,7 +222,7 @@ async def _( and len(result_list) >= max_once_num2forward ): logger.debug("使用合并转发转发色图数据", arparma.header_result, session=session) - forward = template2forward(result_list, bot.self_id) # type: ignore + forward = MessageUtils.template2forward(result_list, bot.self_id) # type: ignore await bot.send_group_forward_msg( group_id=int(gid), messages=forward, # type: ignore @@ -233,7 +232,7 @@ async def _( logger.info(f"发送色图 {result}", arparma.header_result, session=session) receipt = await result.send() if receipt: - message_id = receipt.extract_message_id().message_id # type: ignore + message_id = receipt.msg_ids[0]["message_id"] await WithdrawManager.withdraw_message( bot, message_id, diff --git a/zhenxun/plugins/send_setu_/send_setu/_data_source.py b/zhenxun/plugins/send_setu_/send_setu/_data_source.py index e54099d0..6bac3d22 100644 --- a/zhenxun/plugins/send_setu_/send_setu/_data_source.py +++ b/zhenxun/plugins/send_setu_/send_setu/_data_source.py @@ -3,13 +3,14 @@ import random from pathlib import Path from asyncpg import UniqueViolationError -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import IMAGE_PATH, TEMP_PATH from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.image_utils import compressed_image +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import change_img_md5, change_pixiv_image_links from .._model import Setu @@ -36,7 +37,7 @@ class SetuManage: num: int = 10, tags: list[str] | None = None, is_r18: bool = False, - ) -> list[MessageFactory] | str: + ) -> list[UniMessage] | str: """获取色图 参数: @@ -82,7 +83,7 @@ class SetuManage: result_list.append(cls.init_image_message(file_path, setu)) if flag: result_list.append( - MessageFactory([Text("坏了,已经没图了,被榨干了!")]) + MessageUtils.build_message("坏了,已经没图了,被榨干了!") ) return result_list data_list = await cls.search_lolicon(tags, num, is_r18) @@ -101,17 +102,19 @@ class SetuManage: for setu in data_list: file = await cls.get_image(setu) if isinstance(file, str): - result_list.append(MessageFactory([Text(file)])) + result_list.append(MessageUtils.build_message(file)) continue result_list.append(cls.init_image_message(file, setu)) if not result_list: return "没找到符合条件的色图..." if flag: - result_list.append(MessageFactory([Text("坏了,已经没图了,被榨干了!")])) + result_list.append( + MessageUtils.build_message("坏了,已经没图了,被榨干了!") + ) return result_list @classmethod - def init_image_message(cls, file: Path, setu: Setu) -> MessageFactory: + def init_image_message(cls, file: Path, setu: Setu) -> UniMessage: """初始化图片发送消息 参数: @@ -119,20 +122,18 @@ class SetuManage: setu: Setu 返回: - MessageFactory: 发送消息内容 + UniMessage: 发送消息内容 """ data_list = [] if base_config.get("SHOW_INFO"): data_list.append( - Text( - f"id:{setu.local_id or ''}\n" - f"title:{setu.title}\n" - f"author:{setu.author}\n" - f"PID:{setu.pid}\n" - ) + f"id:{setu.local_id or ''}\n" + f"title:{setu.title}\n" + f"author:{setu.author}\n" + f"PID:{setu.pid}\n" ) - data_list.append(Image(file)) - return MessageFactory(data_list) + data_list.append(file) + return MessageUtils.build_message(data_list) @classmethod async def get_setu_list( @@ -167,7 +168,7 @@ class SetuManage: return image_list @classmethod - def get_luo(cls, impression: float) -> MessageFactory | None: + def get_luo(cls, impression: float) -> UniMessage | None: """罗翔 参数: @@ -179,15 +180,13 @@ class SetuManage: if initial_setu_probability := base_config.get("INITIAL_SETU_PROBABILITY"): probability = float(impression) + initial_setu_probability * 100 if probability < random.randint(1, 101): - return MessageFactory( + return MessageUtils.build_message( [ - Text("我为什么要给你发这个?"), - Image( - IMAGE_PATH - / "luoxiang" - / random.choice(os.listdir(IMAGE_PATH / "luoxiang")) - ), - Text(f"\n(快向{NICKNAME}签到提升好感度吧!)"), + "我为什么要给你发这个?", + IMAGE_PATH + / "luoxiang" + / random.choice(os.listdir(IMAGE_PATH / "luoxiang")), + f"\n(快向{NICKNAME}签到提升好感度吧!)", ] ) return None diff --git a/zhenxun/plugins/send_voice/dinggong.py b/zhenxun/plugins/send_voice/dinggong.py index 270e7dac..a01129ca 100644 --- a/zhenxun/plugins/send_voice/dinggong.py +++ b/zhenxun/plugins/send_voice/dinggong.py @@ -4,12 +4,12 @@ import random from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, Voice, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import RECORD_PATH from zhenxun.configs.utils import PluginCdBlock, PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="钉宫骂我", @@ -41,11 +41,11 @@ path = RECORD_PATH / "dinggong" @_matcher.handle() async def _(session: EventSession, arparma: Arparma): if not path.exists(): - await Text("钉宫语音文件夹不存在...").finish() + await MessageUtils.build_message("钉宫语音文件夹不存在...").finish() files = os.listdir(path) if not files: - await Text("钉宫语音文件夹为空...").finish() + await MessageUtils.build_message("钉宫语音文件夹为空...").finish() voice = random.choice(files) await UniMessage([Voice(path=path / voice)]).send() - await Text(voice.split("_")[1]).send() + await MessageUtils.build_message(voice.split("_")[1]).send() logger.info(f"发送钉宫骂人: {voice}", arparma.header_result, session=session) diff --git a/zhenxun/plugins/statistics/_data_source.py b/zhenxun/plugins/statistics/_data_source.py index 526d4713..e83707b1 100644 --- a/zhenxun/plugins/statistics/_data_source.py +++ b/zhenxun/plugins/statistics/_data_source.py @@ -6,7 +6,6 @@ from zhenxun.models.group_console import GroupConsole from zhenxun.models.group_member_info import GroupInfoUser from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.statistics import Statistics -from zhenxun.models.user_console import UserConsole from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py index f756ad2a..bea1b2d8 100644 --- a/zhenxun/plugins/statistics/statistics_handle.py +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -13,6 +13,7 @@ from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import StatisticsManage @@ -155,8 +156,8 @@ async def _( plugin_name, arparma.find("global"), st, uid, gid ): if isinstance(result, str): - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) else: - await Image(result.pic2bytes()).send() + await MessageUtils.build_message(result).send() else: await Text("获取数据失败...").send() diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/plugins/statistics/statistics_hook.py index 27062a4a..bf8f959c 100644 --- a/zhenxun/plugins/statistics/statistics_hook.py +++ b/zhenxun/plugins/statistics/statistics_hook.py @@ -27,11 +27,10 @@ async def _( ): plugin = await PluginInfo.get_or_none(module=matcher.plugin_name) plugin_type = plugin.plugin_type if plugin else None - if ( - plugin_type == PluginType.NORMAL - and matcher.priority not in [1, 999] - and matcher.plugin_name not in ["update_info", "statistics_handle"] - ): + if plugin_type == PluginType.NORMAL and matcher.plugin_name not in [ + "update_info", + "statistics_handle", + ]: await Statistics.create( user_id=session.id1, group_id=session.id3 or session.id2, diff --git a/zhenxun/plugins/translate/__init__.py b/zhenxun/plugins/translate/__init__.py index 62dc15c9..146705f0 100644 --- a/zhenxun/plugins/translate/__init__.py +++ b/zhenxun/plugins/translate/__init__.py @@ -1,12 +1,12 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.depends import CheckConfig from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils from .data_source import language, translate_message @@ -19,7 +19,7 @@ __plugin_meta__ = PluginMetadata( 示例: 翻译 你好: 将中文翻译为英文 翻译 Hello: 将英文翻译为中文 - + 翻译 你好 -to 希腊语: 将"你好"翻译为希腊语 翻译 你好: 允许form和to使用中文 翻译 你好 -form:中文 to:日语 你好: 指定原语种并将"你好"翻译为日文 @@ -57,7 +57,7 @@ async def _(session: EventSession, arparma: Arparma): for key, value in language.items(): data_list.append([key, value]) image = await ImageTemplate.table_page("翻译语种", "", column_list, data_list) - await Image(image.pic2bytes()).send() + await MessageUtils.build_message(image).send() logger.info(f"查看翻译语种", arparma.header_result, session=session) @@ -79,11 +79,11 @@ async def _( values = language.values() keys = language.keys() if source not in values and source not in keys: - await Text("源语种不支持...").finish() + await MessageUtils.build_message("源语种不支持...").finish() if to not in values and to not in keys: - await Text("目标语种不支持...").finish() + await MessageUtils.build_message("目标语种不支持...").finish() result = await translate_message(text, source, to) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply=True) logger.info( f"source: {source}, to: {to}, 翻译: {text}", arparma.header_result, diff --git a/zhenxun/plugins/wbtop/__init__.py b/zhenxun/plugins/wbtop/__init__.py index 60ab1e37..9b760c9a 100644 --- a/zhenxun/plugins/wbtop/__init__.py +++ b/zhenxun/plugins/wbtop/__init__.py @@ -1,12 +1,12 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncPlaywright +from zhenxun.utils.message import MessageUtils from .data_source import get_hot_image @@ -33,7 +33,7 @@ _matcher = on_alconna(Alconna("微博热搜", Args["idx?", int]), priority=5, bl async def _(session: EventSession, arparma: Arparma, idx: Match[int]): result, data_list = await get_hot_image() if isinstance(result, str): - await Text(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply=True) if idx.available: _idx = idx.result url = data_list[_idx - 1]["url"] @@ -45,12 +45,12 @@ async def _(session: EventSession, arparma: Arparma, idx: Match[int]): wait_time=12, ) if img: - await Image(file).send() + await MessageUtils.build_message(file).send() logger.info( f"查询微博热搜 Id: {_idx}", arparma.header_result, session=session ) else: - await Text("获取图片失败...").send() + await MessageUtils.build_message("获取图片失败...").send() else: - await Image(result.pic2bytes()).send() + await MessageUtils.build_message(result).send() logger.info(f"查询微博热搜", arparma.header_result, session=session) diff --git a/zhenxun/plugins/wbtop/data_source.py b/zhenxun/plugins/wbtop/data_source.py index c734b1ec..e9c20627 100644 --- a/zhenxun/plugins/wbtop/data_source.py +++ b/zhenxun/plugins/wbtop/data_source.py @@ -50,11 +50,11 @@ async def get_hot_image() -> tuple[BuildImage | str, list]: await bk.paste(wbtop_bk) text_bk = BuildImage(700, 32 * 50, color="#797979") image_list = [] - for i, data in enumerate(data): - title = f"{i + 1}. {data['hot_word']}" - hot = str(data["hot_word_num"]) + for i, _data in enumerate(data): + title = f"{i + 1}. {_data['hot_word']}" + hot = str(_data["hot_word_num"]) img = BuildImage(700, 30, font_size=20) - w, h = img.getsize(title) + _, h = img.getsize(title) await img.text((10, int((30 - h) / 2)), title) await img.text((580, int((30 - h) / 2)), hot) image_list.append(img) diff --git a/zhenxun/plugins/word_bank/_data_source.py b/zhenxun/plugins/word_bank/_data_source.py index 467490f3..c9a53a1b 100644 --- a/zhenxun/plugins/word_bank/_data_source.py +++ b/zhenxun/plugins/word_bank/_data_source.py @@ -1,10 +1,12 @@ +from nonebot_plugin_alconna import At from nonebot_plugin_alconna import At as alcAt +from nonebot_plugin_alconna import Image from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import Text as alcText from nonebot_plugin_alconna import UniMessage, UniMsg -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text from zhenxun.utils.image_utils import ImageTemplate +from zhenxun.utils.message import MessageUtils from ._model import WordBank @@ -210,7 +212,7 @@ class WordBankManage: index: int | None = None, group_id: str | None = None, word_scope: int | None = 1, - ) -> Text | MessageFactory | Image: + ) -> UniMessage: """获取群词条 参数: @@ -228,25 +230,25 @@ class WordBankManage: word_scope, ) if not _problem_list: - return Text(problem) + return MessageUtils.build_message(problem) for msg in _problem_list: _text = str(msg) - if isinstance(msg, Mention): - _text = f"[at:{msg.data}]" + if isinstance(msg, At): + _text = f"[at:{msg.target}]" elif isinstance(msg, Image): - _text = msg.data + _text = msg.url or msg.path elif isinstance(msg, list): _text = [] for m in msg: __text = str(m) - if isinstance(m, Mention): - __text = f"[at:{m.data['user_id']}]" + if isinstance(m, At): + __text = f"[at:{m.target}]" elif isinstance(m, Image): # TODO: 显示词条回答图片 # __text = (m.data["image"], 30, 30) __text = "[图片]" _text.append(__text) - msg_list.append("".join(_text)) + msg_list.append("".join(str(_text))) column_name = ["序号", "回答内容"] data_list = [] for index, msg in enumerate(msg_list): @@ -254,7 +256,7 @@ class WordBankManage: template_image = await ImageTemplate.table_page( f"词条 {problem} 的回答", None, column_name, data_list ) - return Image(template_image.pic2bytes()) + return MessageUtils.build_message(template_image) else: result = [] if group_id: @@ -265,7 +267,7 @@ class WordBankManage: raise Exception("群组id和词条范围不能都为空") global_problem_list = await WordBank.get_problem_by_scope(0) if not _problem_list and not global_problem_list: - return Text("未收录任何词条...") + return MessageUtils.build_message("未收录任何词条...") column_name = ["序号", "关键词", "匹配类型", "收录用户"] data_list = [list(s) for s in _problem_list] for i in range(len(data_list)): @@ -273,7 +275,7 @@ class WordBankManage: group_image = await ImageTemplate.table_page( "群组内词条" if group_id else "私聊词条", None, column_name, data_list ) - result.append(Image(group_image.pic2bytes())) + result.append(group_image) if global_problem_list: data_list = [list(s) for s in global_problem_list] for i in range(len(data_list)): @@ -281,5 +283,5 @@ class WordBankManage: global_image = await ImageTemplate.table_page( "全局词条", None, column_name, data_list ) - result.append(Image(global_image.pic2bytes())) - return MessageFactory(result) + result.append(global_image) + return MessageUtils.build_message(result) diff --git a/zhenxun/plugins/word_bank/_model.py b/zhenxun/plugins/word_bank/_model.py index eef86941..abf03779 100644 --- a/zhenxun/plugins/word_bank/_model.py +++ b/zhenxun/plugins/word_bank/_model.py @@ -8,7 +8,7 @@ from typing import Any from nonebot_plugin_alconna import At as alcAt from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import Text as alcText -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from tortoise import Tortoise, fields from tortoise.expressions import Q from typing_extensions import Self @@ -17,6 +17,7 @@ from zhenxun.configs.path_config import DATA_PATH from zhenxun.services.db_context import Model from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.image_utils import get_img_hash +from zhenxun.utils.message import MessageUtils from ._config import int2type @@ -208,7 +209,7 @@ class WordBank(Model): user_id: int, group_id: int, query: Self | None = None, - ) -> MessageFactory | Text: + ) -> UniMessage: """将占位符转换为实际内容 参数: @@ -232,16 +233,16 @@ class WordBank(Model): answer_split = re.split(rf"\[.*:placeholder_.*?]", answer) placeholder_split = query.placeholder.split(",") for index, ans in enumerate(answer_split): - result_list.append(Text(ans)) + result_list.append(ans) if index < len(type_list): t = type_list[index] p = placeholder_split[index] if t == "image": - result_list.append(Image(path / p)) + result_list.append(path / p) elif t == "at": - result_list.append(Mention(p)) - return MessageFactory(result_list) - return Text(answer) + result_list.append(alcAt(flag="user", target=p)) + return MessageUtils.build_message(result_list) + return MessageUtils.build_message(answer) @classmethod async def check_problem( @@ -296,7 +297,7 @@ class WordBank(Model): problem: str, word_scope: int | None = None, word_type: int | None = None, - ) -> Text | MessageFactory | None: + ) -> UniMessage | None: """根据问题内容获取随机回答 参数: @@ -324,7 +325,7 @@ class WordBank(Model): random_answer, ) if random_answer.placeholder - else Text(random_answer.answer) + else MessageUtils.build_message(random_answer.answer) ) @classmethod @@ -334,7 +335,7 @@ class WordBank(Model): index: int | None = None, group_id: str | None = None, word_scope: int | None = 0, - ) -> tuple[str, list[Text | MessageFactory]]: + ) -> tuple[str, list[UniMessage]]: """获取指定问题所有回答 参数: @@ -344,7 +345,7 @@ class WordBank(Model): word_scope: 词条范围 返回: - tuple[str, list[Text | MessageFactory]]: 问题和所有回答 + tuple[str, list[UniMessage]]: 问题和所有回答 """ if index is not None: # TODO: group_by和order_by不能同时使用 diff --git a/zhenxun/plugins/word_bank/word_handle.py b/zhenxun/plugins/word_bank/word_handle.py index 820ae244..0f70a44a 100644 --- a/zhenxun/plugins/word_bank/word_handle.py +++ b/zhenxun/plugins/word_bank/word_handle.py @@ -9,14 +9,15 @@ from nonebot.params import RegexGroup from nonebot.plugin import PluginMetadata from nonebot.typing import T_State from nonebot_plugin_alconna import AlconnaQuery, Arparma +from nonebot_plugin_alconna import Image from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import Match, Query, UniMsg -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._config import scope2int, type2int from ._data_source import WordBankManage, get_answer, get_img_and_at_list, get_problem @@ -96,7 +97,7 @@ async def _( user_id = session.id1 group_id = session.id3 or session.id2 if not group_id and user_id not in bot.config.superusers: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) word_scope, word_type, problem, answer = reg_group if not word_scope and not group_id: word_scope = "私聊" @@ -105,11 +106,13 @@ async def _( and word_scope in ["全局", "私聊"] and user_id not in bot.config.superusers ): - await Text("权限不足,无法添加该范围词条...").finish(reply=True) + await MessageUtils.build_message("权限不足,无法添加该范围词条...").finish( + reply_to=True + ) if (not problem or not problem.strip()) and word_type != "图片": - await Text("词条问题不能为空!").finish(reply=True) + await MessageUtils.build_message("词条问题不能为空!").finish(reply_to=True) if (not answer or not answer.strip()) and not len(img_list) and not len(at_list): - await Text("词条回答不能为空!").finish(reply=True) + await MessageUtils.build_message("词条回答不能为空!").finish(reply_to=True) if word_type != "图片": state["problem_image"] = "YES" temp_problem = message.copy() @@ -118,7 +121,7 @@ async def _( # if at_list: answer = get_answer(message.copy()) # text = str(message.pop(0)).split("答", maxsplit=1)[-1].strip() - # temp_problem.insert(0, alcText(text)) + # temp_problem.insert(0, alcMessageUtils.build_message(text)) state["word_scope"] = word_scope state["word_type"] = word_type state["problem"] = get_problem(temp_problem) @@ -141,7 +144,7 @@ async def _( answer: Any = Arg("answer"), ): if not session.id1: - await Text("用户id不存在...").finish() + await MessageUtils.build_message("用户id不存在...").finish() user_id = session.id1 group_id = session.id3 or session.id2 try: @@ -152,16 +155,16 @@ async def _( try: re.compile(problem) except re.error: - await Text(f"添加词条失败,正则表达式 {problem} 非法!").finish( - reply=True - ) + await MessageUtils.build_message( + f"添加词条失败,正则表达式 {problem} 非法!" + ).finish(reply_to=True) # if str(event.user_id) in bot.config.superusers and isinstance(event, PrivateMessageEvent): # word_scope = "私聊" nickname = None if problem and bot.config.nickname: nickname = [nk for nk in bot.config.nickname if problem.startswith(nk)] if not problem: - await Text("获取问题失败...").finish(reply=True) + await MessageUtils.build_message("获取问题失败...").finish(reply_to=True) await WordBank.add_problem_answer( user_id, ( @@ -186,13 +189,15 @@ async def _( session=session, e=e, ) - await Text( + await MessageUtils.build_message( f"添加词条 {problem if word_type != '图片' else '图片'} 发生错误!" - ).finish(reply=True) + ).finish(reply_to=True) if word_type == "图片": - result = MessageFactory([Text("添加词条 "), Image(problem), Text(" 成功!")]) + result = MessageUtils.build_message( + ["添加词条 ", Image(url=problem), " 成功!"] + ) else: - result = Text(f"添加词条 {problem} 成功!") + result = MessageUtils.build_message(f"添加词条 {problem} 成功!") await result.send() logger.info( f"添加词条 {problem} 成功!", @@ -212,9 +217,9 @@ async def _( all: Query[bool] = AlconnaQuery("all.value", False), ): if not problem.available and not index.available: - await Text("此命令之后需要跟随指定词条或id,通过“显示词条“查看").finish( - reply=True - ) + await MessageUtils.build_message( + "此命令之后需要跟随指定词条或id,通过“显示词条“查看" + ).finish(reply_to=True) word_scope = 1 if session.id3 or session.id2 else 2 if all.result: word_scope = 0 @@ -228,7 +233,7 @@ async def _( ) else: if session.id1 not in bot.config.superusers: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) result, _ = await WordBankManage.delete_word( problem.result, index.result if index.available else None, @@ -236,7 +241,7 @@ async def _( None, word_scope, ) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info(f"删除词条: {problem.result}", arparma.header_result, session=session) @@ -251,9 +256,9 @@ async def _( all: Query[bool] = AlconnaQuery("all.value", False), ): if not problem.available and not index.available: - await Text("此命令之后需要跟随指定词条或id,通过“显示词条“查看").finish( - reply=True - ) + await MessageUtils.build_message( + "此命令之后需要跟随指定词条或id,通过“显示词条“查看" + ).finish(reply_to=True) word_scope = 1 if session.id3 or session.id2 else 2 if all.result: word_scope = 0 @@ -267,7 +272,7 @@ async def _( ) else: if session.id1 not in bot.config.superusers: - await Text("权限不足捏...").finish(reply=True) + await MessageUtils.build_message("权限不足捏...").finish(reply_to=True) result, old_problem = await WordBankManage.update_word( replace, problem.result if problem.available else "", @@ -275,7 +280,7 @@ async def _( session.id3 or session.id2, word_scope, ) - await Text(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info( f"更新词条词条: {old_problem} -> {replace}", arparma.header_result, @@ -303,7 +308,9 @@ async def _( if index.result < 0 or index.result > len( await WordBank.get_problem_by_scope(2) ): - await Text("id必须在范围内...").finish(reply=True) + await MessageUtils.build_message("id必须在范围内...").finish( + reply_to=True + ) result = await WordBankManage.show_word( problem.result, index.result if index.available else None, diff --git a/zhenxun/utils/http_utils.py b/zhenxun/utils/http_utils.py index 142d2ceb..b751f9cd 100644 --- a/zhenxun/utils/http_utils.py +++ b/zhenxun/utils/http_utils.py @@ -9,19 +9,17 @@ import httpx import rich from httpx import ConnectTimeout, Response from nonebot import require +from nonebot_plugin_alconna import UniMessage from playwright.async_api import Page from retrying import retry from zhenxun.configs.config import SYSTEM_PROXY from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.user_agent import get_user_agent from .browser import get_browser -require("nonebot_plugin_saa") - -from nonebot_plugin_saa import Image - class AsyncHttpx: @@ -332,7 +330,7 @@ class AsyncPlaywright: type_: Literal["jpeg", "png"] | None = None, user_agent: str | None = None, **kwargs, - ) -> Image | None: + ) -> UniMessage | None: """截图,该方法仅用于简单快捷截图,复杂截图请操作 page 参数: @@ -367,7 +365,7 @@ class AsyncPlaywright: card = await card.wait_for_selector(e, timeout=wait_time) if card: await card.screenshot(path=path, timeout=timeout, type=type_) - return Image(path) + return MessageUtils.build_message(path) return None diff --git a/zhenxun/utils/message.py b/zhenxun/utils/message.py new file mode 100644 index 00000000..22e6b744 --- /dev/null +++ b/zhenxun/utils/message.py @@ -0,0 +1,124 @@ +from io import BytesIO +from pathlib import Path + +from nonebot.adapters.onebot.v11 import Message, MessageSegment +from nonebot_plugin_alconna import At, Image, Text, UniMessage + +from zhenxun.configs.config import NICKNAME +from zhenxun.services.log import logger +from zhenxun.utils._build_image import BuildImage + +MESSAGE_TYPE = ( + str | int | float | Path | bytes | BytesIO | BuildImage | At | Image | Text +) + + +class MessageUtils: + + @classmethod + def __build_message(cls, msg_list: list[MESSAGE_TYPE]) -> list[Text | Image]: + """构造消息 + + 参数: + msg_list: 消息列表 + + 返回: + list[Text | Text]: 构造完成的消息列表 + """ + message_list = [] + for msg in msg_list: + if isinstance(msg, (Image, Text, At)): + message_list.append(msg) + elif isinstance(msg, (str, int, float)): + message_list.append(Text(str(msg))) + elif isinstance(msg, Path): + if msg.exists(): + message_list.append(Image(path=msg)) + else: + logger.warning(f"图片路径不存在: {msg}") + elif isinstance(msg, bytes): + message_list.append(Image(raw=msg)) + elif isinstance(msg, BytesIO): + message_list.append(Image(raw=msg)) + elif isinstance(msg, BuildImage): + message_list.append(Image(raw=msg.pic2bytes())) + return message_list + + @classmethod + def build_message( + cls, msg_list: MESSAGE_TYPE | list[MESSAGE_TYPE | list[MESSAGE_TYPE]] + ) -> UniMessage: + """构造消息 + + 参数: + msg_list: 消息列表 + + 返回: + UniMessage: 构造完成的消息列表 + """ + message_list = [] + if not isinstance(msg_list, list): + msg_list = [msg_list] + for m in msg_list: + _data = m if isinstance(m, list) else [m] + message_list += cls.__build_message(_data) # type: ignore + return UniMessage(message_list) + + @classmethod + def custom_forward_msg( + cls, + msg_list: list[str | Message], + uin: str, + name: str = f"这里是{NICKNAME}", + ) -> list[dict]: + """生成自定义合并消息 + + 参数: + msg_list: 消息列表 + uin: 发送者 QQ + name: 自定义名称 + + 返回: + list[dict]: 转发消息 + """ + mes_list = [] + for _message in msg_list: + data = { + "type": "node", + "data": { + "name": name, + "uin": f"{uin}", + "content": _message, + }, + } + mes_list.append(data) + return mes_list + + @classmethod + def template2forward(cls, msg_list: list[UniMessage], uni: str) -> list[dict]: + """模板转转发消息 + + 参数: + msg_list: 消息列表 + uni: 发送者qq + + 返回: + list[dict]: 转发消息 + """ + forward_data = [] + for r_list in msg_list: + s = "" + if isinstance(r_list, (UniMessage, list)): + for r in r_list: + if isinstance(r, Text): + s += str(r) + elif isinstance(r, Image): + if v := r.url or r.path: + s += MessageSegment.image(v) + elif isinstance(r_list, Image): + if v := r_list.url or r_list.path: + s = MessageSegment.image(v) + else: + s = str(r_list) + forward_data.append(s) + return cls.custom_forward_msg(forward_data, uni) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 50bc2b69..9e3caca2 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -233,61 +233,3 @@ def is_valid_date(date_text: str, separator: str = "-") -> bool: return True except ValueError: return False - - -def custom_forward_msg( - msg_list: list[str | Message], - uin: str, - name: str = f"这里是{NICKNAME}", -) -> list[dict]: - """生成自定义合并消息 - - 参数: - msg_list: 消息列表 - uin: 发送者 QQ - name: 自定义名称 - - 返回: - list[dict]: 转发消息 - """ - mes_list = [] - for _message in msg_list: - data = { - "type": "node", - "data": { - "name": name, - "uin": f"{uin}", - "content": _message, - }, - } - mes_list.append(data) - return mes_list - - -def template2forward( - msg_list: list[MessageFactory | Text | Image], uni: str -) -> list[dict]: - """模板转转发消息 - - 参数: - msg_list: 消息列表 - uni: 发送者qq - - 返回: - list[dict]: 转发消息 - """ - forward_data = [] - for r_list in msg_list: - s = "" - if isinstance(r_list, MessageFactory): - for r in r_list: - if isinstance(r, Text): - s += str(r) - elif isinstance(r, Image): - s += MessageSegment.image(r.data["image"]) - elif isinstance(r_list, Image): - s = MessageSegment.image(r_list.data["image"]) - else: - s = str(r_list) - forward_data.append(s) - return custom_forward_msg(forward_data, uni) From d05b1fb9b2d9e940dcf38d12528d93446930859a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 02:38:41 +0800 Subject: [PATCH 124/132] =?UTF-8?q?=E2=9C=A8=20=E6=8F=90=E4=BE=9B=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=9B=BE=E7=89=87=E7=BB=9F=E4=B8=80bytes=E5=8F=91?= =?UTF-8?q?=E9=80=81=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 6 ++++-- zhenxun/utils/message.py | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.env.dev b/.env.dev index 115bcb61..127b4891 100644 --- a/.env.dev +++ b/.env.dev @@ -10,6 +10,9 @@ NICKNAME=["真寻", "小真寻", "绪山真寻", "小寻子"] SESSION_EXPIRE_TIMEOUT=30 +# 全局图片统一使用bytes发送,当真寻与协议端不在同一服务器上时为True +IMAGE_TO_BYTES = False + PLATFORM_SUPERUSERS = ' { "qq": [""], @@ -17,7 +20,6 @@ PLATFORM_SUPERUSERS = ' } ' -# DRIVER=~fastapi DRIVER=~fastapi+~httpx+~websockets # kook adapter toekn @@ -51,7 +53,7 @@ DRIVER=~fastapi+~httpx+~websockets # application_commands的{"*": ["*"]}代表将全部应用命令注册为全局应用命令 # {"admin": ["123", "456"]}则代表将admin命令注册为id是123、456服务器的局部命令,其余命令不注册 -LOG_LEVEL=DEBUG +# LOG_LEVEL=DEBUG # 服务器和端口 HOST = 127.0.0.1 PORT = 8080 diff --git a/zhenxun/utils/message.py b/zhenxun/utils/message.py index 22e6b744..234c2a37 100644 --- a/zhenxun/utils/message.py +++ b/zhenxun/utils/message.py @@ -1,6 +1,7 @@ from io import BytesIO from pathlib import Path +import nonebot from nonebot.adapters.onebot.v11 import Message, MessageSegment from nonebot_plugin_alconna import At, Image, Text, UniMessage @@ -8,6 +9,8 @@ from zhenxun.configs.config import NICKNAME from zhenxun.services.log import logger from zhenxun.utils._build_image import BuildImage +driver = nonebot.get_driver() + MESSAGE_TYPE = ( str | int | float | Path | bytes | BytesIO | BuildImage | At | Image | Text ) @@ -25,6 +28,7 @@ class MessageUtils: 返回: list[Text | Text]: 构造完成的消息列表 """ + is_bytes = driver.config.image_to_bytes == "True" message_list = [] for msg in msg_list: if isinstance(msg, (Image, Text, At)): @@ -33,7 +37,11 @@ class MessageUtils: message_list.append(Text(str(msg))) elif isinstance(msg, Path): if msg.exists(): - message_list.append(Image(path=msg)) + if is_bytes: + image = BuildImage.open(msg) + message_list.append(Image(raw=image.pic2bytes())) + else: + message_list.append(Image(path=msg)) else: logger.warning(f"图片路径不存在: {msg}") elif isinstance(msg, bytes): From ab05d8a1b521e2732b642b8d3cb0cfcaf357e8a5 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 12:10:53 +0800 Subject: [PATCH 125/132] =?UTF-8?q?=E2=9C=A8=20=E6=8F=90=E4=BE=9Bdatabase.?= =?UTF-8?q?json=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../builtin_plugins/hooks/_auth_checker.py | 9 ++++- zhenxun/plugins/statistics/statistics_hook.py | 25 +++++++------- zhenxun/services/db_context.py | 34 +++++++++++++++++-- zhenxun/utils/message.py | 6 +++- 4 files changed, 57 insertions(+), 17 deletions(-) diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index 773867c8..d480a88f 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -5,6 +5,7 @@ from nonebot_plugin_alconna import UniMsg from nonebot_plugin_saa import Mention, MessageFactory, Text from nonebot_plugin_session import EventSession from pydantic import BaseModel +from tortoise.exceptions import IntegrityError from zhenxun.configs.config import Config from zhenxun.models.group_console import GroupConsole @@ -203,7 +204,13 @@ class AuthChecker: group_id = channel_id channel_id = None if user_id and matcher.plugin and (module_path := matcher.plugin.module_name): - user = await UserConsole.get_user(user_id, session.platform) + try: + user = await UserConsole.get_user(user_id, session.platform) + except IntegrityError as e: + logger.debug( + "重复创建用户,已跳过全选该次权限...", "HOOK", session=session, e=e + ) + return if plugin := await PluginInfo.get_or_none(module_path=module_path): if plugin.plugin_type == PluginType.HIDDEN and plugin.name != "帮助": logger.debug("插件为HIDDEN且不是帮助功能,已跳过...") diff --git a/zhenxun/plugins/statistics/statistics_hook.py b/zhenxun/plugins/statistics/statistics_hook.py index bf8f959c..cb1f4b1f 100644 --- a/zhenxun/plugins/statistics/statistics_hook.py +++ b/zhenxun/plugins/statistics/statistics_hook.py @@ -25,15 +25,16 @@ __plugin_meta__ = PluginMetadata( async def _( matcher: Matcher, exception: Exception | None, bot: Bot, session: EventSession ): - plugin = await PluginInfo.get_or_none(module=matcher.plugin_name) - plugin_type = plugin.plugin_type if plugin else None - if plugin_type == PluginType.NORMAL and matcher.plugin_name not in [ - "update_info", - "statistics_handle", - ]: - await Statistics.create( - user_id=session.id1, - group_id=session.id3 or session.id2, - plugin_name=matcher.plugin_name, - create_time=datetime.now(), - ) + if session.id1: + plugin = await PluginInfo.get_or_none(module=matcher.plugin_name) + plugin_type = plugin.plugin_type if plugin else None + if plugin_type == PluginType.NORMAL and matcher.plugin_name not in [ + "update_info", + "statistics_handle", + ]: + await Statistics.create( + user_id=session.id1, + group_id=session.id3 or session.id2, + plugin_name=matcher.plugin_name, + create_time=datetime.now(), + ) diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 33b612ad..9342a6ef 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -1,3 +1,4 @@ +import ujson as json from nonebot.utils import is_coroutine_callable from tortoise import Tortoise, fields from tortoise.connection import connections @@ -12,6 +13,7 @@ from zhenxun.configs.config import ( sql_name, user, ) +from zhenxun.configs.path_config import DATA_PATH from .log import logger @@ -19,6 +21,8 @@ MODELS: list[str] = [] SCRIPT_METHOD = [] +DATABASE_SETTING_FILE = DATA_PATH / "database.json" + class Model(Model_): """ @@ -46,11 +50,35 @@ class TestSQL(Model): async def init(): - if not bind and not any([user, password, address, port, database]): + if DATABASE_SETTING_FILE.exists(): + with open(DATABASE_SETTING_FILE, "r", encoding="utf-8") as f: + setting_data = json.load(f) + else: + i_bind = bind + if not i_bind and any([user, password, address, port, database]): + i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" + setting_data = { + "bind": i_bind, + "sql_name": sql_name, + "user": user, + "password": password, + "address": address, + "port": port, + "database": database, + } + with open(DATABASE_SETTING_FILE, "w", encoding="utf-8") as f: + json.dump(setting_data, f, ensure_ascii=False, indent=4) + i_bind = setting_data.get("bind") + _sql_name = setting_data.get("sql_name") + _user = setting_data.get("user") + _password = setting_data.get("password") + _address = setting_data.get("address") + _port = setting_data.get("port") + _database = setting_data.get("database") + if not i_bind and not any([_user, _password, _address, _port, _database]): raise ValueError("\n数据库配置未填写...") - i_bind = bind if not i_bind: - i_bind = f"{sql_name}://{user}:{password}@{address}:{port}/{database}" + i_bind = f"{_sql_name}://{_user}:{_password}@{_address}:{_port}/{_database}" try: await Tortoise.init( db_url=i_bind, modules={"models": MODELS}, timezone="Asia/Shanghai" diff --git a/zhenxun/utils/message.py b/zhenxun/utils/message.py index 234c2a37..54e7f908 100644 --- a/zhenxun/utils/message.py +++ b/zhenxun/utils/message.py @@ -28,7 +28,11 @@ class MessageUtils: 返回: list[Text | Text]: 构造完成的消息列表 """ - is_bytes = driver.config.image_to_bytes == "True" + is_bytes = False + try: + is_bytes = driver.config.image_to_bytes in ["True", "true"] + except AttributeError: + pass message_list = [] for msg in msg_list: if isinstance(msg, (Image, Text, At)): From 59b32f8b25fc0f2f258aa5e6b4e7a61a65b55c8e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 13:20:06 +0800 Subject: [PATCH 126/132] =?UTF-8?q?=F0=9F=90=9B=20=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/open_cases/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhenxun/plugins/open_cases/utils.py b/zhenxun/plugins/open_cases/utils.py index 212ef69e..6fda2265 100644 --- a/zhenxun/plugins/open_cases/utils.py +++ b/zhenxun/plugins/open_cases/utils.py @@ -13,6 +13,7 @@ from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType +from zhenxun.utils.utils import cn2py from .build_image import generate_skin from .config import ( From 5a5c0be51a4d8de5ff905c8a12252b78cb801e3e Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 14:18:05 +0800 Subject: [PATCH 127/132] =?UTF-8?q?=E2=9C=A8=20bot=E5=B7=B2=E6=9C=89?= =?UTF-8?q?=E7=BE=A4=E7=BB=84=E6=B7=BB=E5=8A=A0=E7=BE=A4=E8=AE=A4=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + zhenxun/builtin_plugins/init/__init__.py | 36 ++++++++++++++++++++++++ zhenxun/plugins/statistics/__init__.py | 1 + zhenxun/services/db_context.py | 2 +- 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ac493193..dc0ad84c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "pixiv", "Setu", "tobytes", + "ujson", "unban", "userinfo", "zhenxun" diff --git a/zhenxun/builtin_plugins/init/__init__.py b/zhenxun/builtin_plugins/init/__init__.py index eb35e275..fa10c4d7 100644 --- a/zhenxun/builtin_plugins/init/__init__.py +++ b/zhenxun/builtin_plugins/init/__init__.py @@ -1,5 +1,41 @@ from pathlib import Path import nonebot +from nonebot.adapters import Bot + +from zhenxun.models.group_console import GroupConsole +from zhenxun.services.log import logger +from zhenxun.utils.platform import PlatformUtils nonebot.load_plugins(str(Path(__file__).parent.resolve())) + + +driver = nonebot.get_driver() + + +@driver.on_bot_connect +async def _(bot: Bot): + """将bot已存在的群组添加群认证 + + 参数: + bot: Bot + """ + if PlatformUtils.get_platform(bot) == "qq": + logger.debug(f"更新Bot: {bot.self_id} 的群认证...") + group_list, _ = await PlatformUtils.get_group_list(bot) + gid_list = [g.group_id for g in group_list] + db_group_list = await GroupConsole.all().values_list("group_id", flat=True) + create_list = [] + update_id = [] + for gid in gid_list: + if gid not in db_group_list: + create_list.append(GroupConsole(group_id=gid, group_flag=1)) + else: + update_id.append(gid) + if create_list: + await GroupConsole.bulk_create(create_list, 10) + else: + await GroupConsole.filter(group_id__in=update_id).update(group_flag=1) + logger.debug( + f"更新Bot: {bot.self_id} 的群认证完成,共创建 {len(create_list)} 条数据,共修改 {len(update_id)} 条数据..." + ) diff --git a/zhenxun/plugins/statistics/__init__.py b/zhenxun/plugins/statistics/__init__.py index bd99a86f..5cf30279 100644 --- a/zhenxun/plugins/statistics/__init__.py +++ b/zhenxun/plugins/statistics/__init__.py @@ -63,6 +63,7 @@ for file in [statistics_group_file, statistics_user_file]: data[x][key]["商店"] = num for x in ["week_statistics", "month_statistics"]: for key in data[x].keys(): + num = 0 if key == "total": if data[x][key].get("ai") is not None: if data[x][key].get("Ai") is not None: diff --git a/zhenxun/services/db_context.py b/zhenxun/services/db_context.py index 9342a6ef..2bf9c109 100644 --- a/zhenxun/services/db_context.py +++ b/zhenxun/services/db_context.py @@ -111,7 +111,7 @@ async def init(): await Tortoise.generate_schemas() logger.info(f"Database loaded successfully!") except Exception as e: - raise Exception(f"数据库连接错误...") + raise Exception(f"数据库连接错误... e:{e}") async def disconnect(): From 71f3b031d454fd92bb2992b056ea2245ec948fac Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sat, 10 Aug 2024 17:56:49 +0800 Subject: [PATCH 128/132] =?UTF-8?q?=F0=9F=8E=A8=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=96=87=E5=AD=97getsize=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/utils/_build_image.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index deeb3255..4138fae6 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -219,7 +219,13 @@ class BuildImage: _font = font if font and type(font) == str: _font = cls.load_font(font, font_size) - return _font.getsize(str(text)) # type: ignore + temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) + draw = ImageDraw.Draw(temp_image) + text_box = draw.textbbox((0, 0), str(text), font=_font) # type: ignore + text_width = text_box[2] - text_box[0] + text_height = text_box[3] - text_box[1] + return text_width, text_height + 10 + # return _font.getsize(str(text)) # type: ignore def getsize(self, msg: str) -> Tuple[int, int]: """ @@ -231,7 +237,13 @@ class BuildImage: 返回: Tuple[int, int]: 长宽 """ - return self.font.getsize(msg) # type: ignore + temp_image = Image.new("RGB", (1, 1), (255, 255, 255)) + draw = ImageDraw.Draw(temp_image) + text_box = draw.textbbox((0, 0), str(msg), font=self.font) + text_width = text_box[2] - text_box[0] + text_height = text_box[3] - text_box[1] + return text_width, text_height + 10 + # return self.font.getsize(msg) # type: ignore def __center_xy( self, @@ -379,7 +391,7 @@ class BuildImage: else: font = self.font if center_type: - ttf_w, ttf_h = font.getsize(max_length_text) # type: ignore + ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore ttf_h = ttf_h * len(sentence) pos = self.__center_xy(pos, ttf_w, ttf_h, center_type) self.draw.text(pos, text, fill=fill, font=font) From d0792e0a1c63f3be8e1564ea0e235808369afc3a Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 11 Aug 2024 15:57:33 +0800 Subject: [PATCH 129/132] =?UTF-8?q?=E2=9C=A8=20=E7=A7=BB=E9=99=A4saa?= =?UTF-8?q?=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- poetry.lock | 2060 +++++++++-------- pyproject.toml | 1 - zhenxun/builtin_plugins/__init__.py | 4 - zhenxun/builtin_plugins/admin/admin_help.py | 2 +- zhenxun/builtin_plugins/admin/ban/__init__.py | 2 +- .../admin/group_member_update/_data_source.py | 116 +- .../builtin_plugins/hooks/_auth_checker.py | 44 +- zhenxun/builtin_plugins/hooks/ban_hook.py | 10 +- zhenxun/builtin_plugins/hooks/chkdsk_hook.py | 13 +- zhenxun/builtin_plugins/record_request.py | 20 +- zhenxun/builtin_plugins/scheduler/morning.py | 4 +- zhenxun/builtin_plugins/shop/_data_source.py | 4 +- .../builtin_plugins/sign_in/_data_source.py | 1 - zhenxun/builtin_plugins/sign_in/utils.py | 8 +- .../superuser/broadcast/__init__.py | 7 +- .../superuser/broadcast/_data_source.py | 24 +- .../builtin_plugins/superuser/clear_data.py | 26 +- .../builtin_plugins/superuser/fg_manage.py | 14 +- .../builtin_plugins/superuser/group_manage.py | 20 +- .../superuser/reload_setting.py | 4 +- .../builtin_plugins/superuser/set_admin.py | 42 +- .../superuser/update_fg_info.py | 12 +- zhenxun/plugins/about.py | 5 +- zhenxun/plugins/alapi/comments_163.py | 8 +- zhenxun/plugins/alapi/jitang.py | 8 +- zhenxun/plugins/alapi/poetry.py | 8 +- zhenxun/plugins/bt/__init__.py | 10 +- zhenxun/plugins/check/__init__.py | 1 - zhenxun/plugins/dialogue/__init__.py | 29 +- zhenxun/plugins/draw_card/__init__.py | 18 +- .../plugins/draw_card/handles/azur_handle.py | 13 +- .../plugins/draw_card/handles/ba_handle.py | 6 +- .../plugins/draw_card/handles/base_handle.py | 12 +- .../draw_card/handles/genshin_handle.py | 34 +- .../draw_card/handles/guardian_handle.py | 13 +- .../draw_card/handles/pretty_handle.py | 17 +- .../plugins/draw_card/handles/prts_handle.py | 15 +- zhenxun/plugins/epic/__init__.py | 9 +- zhenxun/plugins/fudu.py | 2 +- zhenxun/plugins/gold_redbag/__init__.py | 29 +- zhenxun/plugins/gold_redbag/data_source.py | 40 +- .../plugins/image_management/delete_image.py | 22 +- .../plugins/image_management/move_image.py | 23 +- .../plugins/image_management/upload_image.py | 37 +- zhenxun/plugins/mute/mute_message.py | 8 +- zhenxun/plugins/mute/mute_setting.py | 8 +- zhenxun/plugins/nbnhhsh.py | 8 +- .../plugins/pix_gallery/pix_add_keyword.py | 26 +- .../pix_gallery/pix_pass_del_keyword.py | 55 +- zhenxun/plugins/pix_gallery/pix_update.py | 20 +- .../plugins/pixiv_rank_search/data_source.py | 2 - zhenxun/plugins/quotations.py | 4 +- zhenxun/plugins/roll.py | 13 +- zhenxun/plugins/russian/data_source.py | 131 +- zhenxun/plugins/search_anime/__init__.py | 12 +- .../search_buff_skin_price/__init__.py | 17 +- .../plugins/send_setu_/send_setu/__init__.py | 9 +- .../send_setu_/update_setu/__init__.py | 8 +- .../plugins/statistics/statistics_handle.py | 3 +- zhenxun/plugins/translate/__init__.py | 2 +- zhenxun/plugins/wbtop/__init__.py | 2 +- zhenxun/plugins/what_anime/__init__.py | 12 +- zhenxun/utils/_build_image.py | 9 +- zhenxun/utils/depends/__init__.py | 6 +- zhenxun/utils/platform.py | 71 +- zhenxun/utils/utils.py | 5 +- zhenxun/utils/withdraw_manage.py | 19 +- 68 files changed, 1722 insertions(+), 1528 deletions(-) diff --git a/.gitignore b/.gitignore index 94fc3aad..64932223 100644 --- a/.gitignore +++ b/.gitignore @@ -180,4 +180,5 @@ plugins/csgo_server/ plugins/activity/ !/resources/image/genshin/alc/back.png !/data/genshin_alc/ -.vscode/launch.json \ No newline at end of file +.vscode/launch.json +/resources/template/my_info \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8d552dfd..9d4e8ff1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,92 +16,109 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" +[[package]] +name = "aiohappyeyeballs" +version = "2.3.5" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, + {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, +] + +[package.source] +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" +reference = "ali" + [[package]] name = "aiohttp" -version = "3.9.5" +version = "3.10.3" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"}, - {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"}, - {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"}, - {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"}, - {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"}, - {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"}, - {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"}, - {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"}, - {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"}, - {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"}, - {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"}, - {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"}, - {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"}, - {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"}, - {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"}, - {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"}, - {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"}, - {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"}, - {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"}, - {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"}, - {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"}, - {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"}, - {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"}, - {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"}, - {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"}, - {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"}, - {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc36cbdedf6f259371dbbbcaae5bb0e95b879bc501668ab6306af867577eb5db"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85466b5a695c2a7db13eb2c200af552d13e6a9313d7fa92e4ffe04a2c0ea74c1"}, + {file = "aiohttp-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:71bb1d97bfe7e6726267cea169fdf5df7658831bb68ec02c9c6b9f3511e108bb"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baec1eb274f78b2de54471fc4c69ecbea4275965eab4b556ef7a7698dee18bf2"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13031e7ec1188274bad243255c328cc3019e36a5a907978501256000d57a7201"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bbc55a964b8eecb341e492ae91c3bd0848324d313e1e71a27e3d96e6ee7e8e8"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8cc0564b286b625e673a2615ede60a1704d0cbbf1b24604e28c31ed37dc62aa"}, + {file = "aiohttp-3.10.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f817a54059a4cfbc385a7f51696359c642088710e731e8df80d0607193ed2b73"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8542c9e5bcb2bd3115acdf5adc41cda394e7360916197805e7e32b93d821ef93"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:671efce3a4a0281060edf9a07a2f7e6230dca3a1cbc61d110eee7753d28405f7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0974f3b5b0132edcec92c3306f858ad4356a63d26b18021d859c9927616ebf27"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:44bb159b55926b57812dca1b21c34528e800963ffe130d08b049b2d6b994ada7"}, + {file = "aiohttp-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ae9ae382d1c9617a91647575255ad55a48bfdde34cc2185dd558ce476bf16e9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win32.whl", hash = "sha256:aed12a54d4e1ee647376fa541e1b7621505001f9f939debf51397b9329fd88b9"}, + {file = "aiohttp-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b51aef59370baf7444de1572f7830f59ddbabd04e5292fa4218d02f085f8d299"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e021c4c778644e8cdc09487d65564265e6b149896a17d7c0f52e9a088cc44e1b"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24fade6dae446b183e2410a8628b80df9b7a42205c6bfc2eff783cbeedc224a2"}, + {file = "aiohttp-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bc8e9f15939dacb0e1f2d15f9c41b786051c10472c7a926f5771e99b49a5957f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5a9ec959b5381271c8ec9310aae1713b2aec29efa32e232e5ef7dcca0df0279"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a5d0ea8a6467b15d53b00c4e8ea8811e47c3cc1bdbc62b1aceb3076403d551f"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9ed607dbbdd0d4d39b597e5bf6b0d40d844dfb0ac6a123ed79042ef08c1f87e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3e66d5b506832e56add66af88c288c1d5ba0c38b535a1a59e436b300b57b23e"}, + {file = "aiohttp-3.10.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fda91ad797e4914cca0afa8b6cccd5d2b3569ccc88731be202f6adce39503189"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:61ccb867b2f2f53df6598eb2a93329b5eee0b00646ee79ea67d68844747a418e"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d881353264e6156f215b3cb778c9ac3184f5465c2ece5e6fce82e68946868ef"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b031ce229114825f49cec4434fa844ccb5225e266c3e146cb4bdd025a6da52f1"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5337cc742a03f9e3213b097abff8781f79de7190bbfaa987bd2b7ceb5bb0bdec"}, + {file = "aiohttp-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab3361159fd3dcd0e48bbe804006d5cfb074b382666e6c064112056eb234f1a9"}, + {file = "aiohttp-3.10.3-cp311-cp311-win32.whl", hash = "sha256:05d66203a530209cbe40f102ebaac0b2214aba2a33c075d0bf825987c36f1f0b"}, + {file = "aiohttp-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:70b4a4984a70a2322b70e088d654528129783ac1ebbf7dd76627b3bd22db2f17"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:166de65e2e4e63357cfa8417cf952a519ac42f1654cb2d43ed76899e2319b1ee"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7084876352ba3833d5d214e02b32d794e3fd9cf21fdba99cff5acabeb90d9806"}, + {file = "aiohttp-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d98c604c93403288591d7d6d7d6cc8a63459168f8846aeffd5b3a7f3b3e5e09"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d73b073a25a0bb8bf014345374fe2d0f63681ab5da4c22f9d2025ca3e3ea54fc"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8da6b48c20ce78f5721068f383e0e113dde034e868f1b2f5ee7cb1e95f91db57"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a9dcdccf50284b1b0dc72bc57e5bbd3cc9bf019060dfa0668f63241ccc16aa7"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56fb94bae2be58f68d000d046172d8b8e6b1b571eb02ceee5535e9633dcd559c"}, + {file = "aiohttp-3.10.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf75716377aad2c718cdf66451c5cf02042085d84522aec1f9246d3e4b8641a6"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6c51ed03e19c885c8e91f574e4bbe7381793f56f93229731597e4a499ffef2a5"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b84857b66fa6510a163bb083c1199d1ee091a40163cfcbbd0642495fed096204"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c124b9206b1befe0491f48185fd30a0dd51b0f4e0e7e43ac1236066215aff272"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3461d9294941937f07bbbaa6227ba799bc71cc3b22c40222568dc1cca5118f68"}, + {file = "aiohttp-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08bd0754d257b2db27d6bab208c74601df6f21bfe4cb2ec7b258ba691aac64b3"}, + {file = "aiohttp-3.10.3-cp312-cp312-win32.whl", hash = "sha256:7f9159ae530297f61a00116771e57516f89a3de6ba33f314402e41560872b50a"}, + {file = "aiohttp-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:e1128c5d3a466279cb23c4aa32a0f6cb0e7d2961e74e9e421f90e74f75ec1edf"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d1100e68e70eb72eadba2b932b185ebf0f28fd2f0dbfe576cfa9d9894ef49752"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a541414578ff47c0a9b0b8b77381ea86b0c8531ab37fc587572cb662ccd80b88"}, + {file = "aiohttp-3.10.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5548444ef60bf4c7b19ace21f032fa42d822e516a6940d36579f7bfa8513f9c"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba2e838b5e6a8755ac8297275c9460e729dc1522b6454aee1766c6de6d56e5e"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48665433bb59144aaf502c324694bec25867eb6630fcd831f7a893ca473fcde4"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bac352fceed158620ce2d701ad39d4c1c76d114255a7c530e057e2b9f55bdf9f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0f670502100cdc567188c49415bebba947eb3edaa2028e1a50dd81bd13363f"}, + {file = "aiohttp-3.10.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43b09f38a67679e32d380fe512189ccb0b25e15afc79b23fbd5b5e48e4fc8fd9"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:cd788602e239ace64f257d1c9d39898ca65525583f0fbf0988bcba19418fe93f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:214277dcb07ab3875f17ee1c777d446dcce75bea85846849cc9d139ab8f5081f"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:32007fdcaab789689c2ecaaf4b71f8e37bf012a15cd02c0a9db8c4d0e7989fa8"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:123e5819bfe1b87204575515cf448ab3bf1489cdeb3b61012bde716cda5853e7"}, + {file = "aiohttp-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:812121a201f0c02491a5db335a737b4113151926a79ae9ed1a9f41ea225c0e3f"}, + {file = "aiohttp-3.10.3-cp38-cp38-win32.whl", hash = "sha256:b97dc9a17a59f350c0caa453a3cb35671a2ffa3a29a6ef3568b523b9113d84e5"}, + {file = "aiohttp-3.10.3-cp38-cp38-win_amd64.whl", hash = "sha256:3731a73ddc26969d65f90471c635abd4e1546a25299b687e654ea6d2fc052394"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38d91b98b4320ffe66efa56cb0f614a05af53b675ce1b8607cdb2ac826a8d58e"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9743fa34a10a36ddd448bba8a3adc2a66a1c575c3c2940301bacd6cc896c6bf1"}, + {file = "aiohttp-3.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7c126f532caf238031c19d169cfae3c6a59129452c990a6e84d6e7b198a001dc"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926e68438f05703e500b06fe7148ef3013dd6f276de65c68558fa9974eeb59ad"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:434b3ab75833accd0b931d11874e206e816f6e6626fd69f643d6a8269cd9166a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d35235a44ec38109b811c3600d15d8383297a8fab8e3dec6147477ec8636712a"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59c489661edbd863edb30a8bd69ecb044bd381d1818022bc698ba1b6f80e5dd1"}, + {file = "aiohttp-3.10.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50544fe498c81cb98912afabfc4e4d9d85e89f86238348e3712f7ca6a2f01dab"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09bc79275737d4dc066e0ae2951866bb36d9c6b460cb7564f111cc0427f14844"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:af4dbec58e37f5afff4f91cdf235e8e4b0bd0127a2a4fd1040e2cad3369d2f06"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b22cae3c9dd55a6b4c48c63081d31c00fc11fa9db1a20c8a50ee38c1a29539d2"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ba562736d3fbfe9241dad46c1a8994478d4a0e50796d80e29d50cabe8fbfcc3f"}, + {file = "aiohttp-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f25d6c4e82d7489be84f2b1c8212fafc021b3731abdb61a563c90e37cced3a21"}, + {file = "aiohttp-3.10.3-cp39-cp39-win32.whl", hash = "sha256:b69d832e5f5fa15b1b6b2c8eb6a9fd2c0ec1fd7729cb4322ed27771afc9fc2ac"}, + {file = "aiohttp-3.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:673bb6e3249dc8825df1105f6ef74e2eab779b7ff78e96c15cadb78b04a83752"}, + {file = "aiohttp-3.10.3.tar.gz", hash = "sha256:21650e7032cc2d31fc23d353d7123e771354f2a3d5b05a5647fc30fea214e696"}, ] [package.dependencies] +aiohappyeyeballs = ">=2.3.0" aiosignal = ">=1.1.2" async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" @@ -110,7 +127,7 @@ multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "brotlicffi"] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [package.source] type = "legacy" @@ -362,22 +379,22 @@ reference = "ali" [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [package.source] type = "legacy" @@ -456,33 +473,33 @@ reference = "ali" [[package]] name = "black" -version = "24.4.2" +version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] @@ -507,13 +524,13 @@ reference = "ali" [[package]] name = "cachetools" -version = "5.3.2" +version = "5.4.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, - {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, + {file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"}, + {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] [package.source] @@ -523,13 +540,13 @@ reference = "ali" [[package]] name = "cashews" -version = "6.4.0" +version = "7.1.0" description = "cache tools with async power" optional = false python-versions = ">=3.8" files = [ - {file = "cashews-6.4.0-py3-none-any.whl", hash = "sha256:6b7121a0629a17aa72d22bf4007462a9fbcdcd418b8ec1083f2806950c265e58"}, - {file = "cashews-6.4.0.tar.gz", hash = "sha256:0f5ec89b4e8d2944e9403c5fc24fb2947003d279e338de40f2fd3ebc9145c4e3"}, + {file = "cashews-7.1.0-py3-none-any.whl", hash = "sha256:b7c1ae4d49df6fdbff88e5025d3c1156515f58724c5b96fc9a9d081afada82a8"}, + {file = "cashews-7.1.0.tar.gz", hash = "sha256:058df55a39cb15697d331e7e41c2882b58d0d323f5671316105cc78668af7705"}, ] [package.extras] @@ -538,7 +555,7 @@ diskcache = ["diskcache (>=5.0.0)"] lint = ["mypy (>=1.5.0)", "types-redis"] redis = ["redis (>=4.3.1,!=5.0.1)"] speedup = ["bitarray (<3.0.0)", "hiredis", "xxhash (<4.0.0)"] -tests = ["hypothesis", "pytest", "pytest-asyncio (==0.23.3)"] +tests = ["hypothesis (==6.100.2)", "pytest (==8.2.0)", "pytest-asyncio (==0.23.6)", "pytest-cov (==5.0.0)", "pytest-rerunfailures (==14.0)"] [package.source] type = "legacy" @@ -577,13 +594,13 @@ reference = "ali" [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [package.source] @@ -593,63 +610,78 @@ reference = "ali" [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, ] [package.dependencies] @@ -837,13 +869,13 @@ reference = "ali" [[package]] name = "cookiecutter" -version = "2.5.0" +version = "2.6.0" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." optional = false python-versions = ">=3.7" files = [ - {file = "cookiecutter-2.5.0-py3-none-any.whl", hash = "sha256:8aa2f12ed11bc05628651e9dc4353a10571dd9908aaaaeec959a2b9ea465a5d2"}, - {file = "cookiecutter-2.5.0.tar.gz", hash = "sha256:e61e9034748e3f41b8bd2c11f00d030784b48711c4d5c42363c50989a65331ec"}, + {file = "cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d"}, + {file = "cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c"}, ] [package.dependencies] @@ -983,17 +1015,20 @@ reference = "ali" [[package]] name = "emoji" -version = "2.10.1" +version = "2.12.1" description = "Emoji for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.7" files = [ - {file = "emoji-2.10.1-py2.py3-none-any.whl", hash = "sha256:11fb369ea79d20c14efa4362c732d67126df294a7959a2c98bfd7447c12a218e"}, - {file = "emoji-2.10.1.tar.gz", hash = "sha256:16287283518fb7141bde00198f9ffff4e1c1cb570efb68b2f1ec50975c3a581d"}, + {file = "emoji-2.12.1-py3-none-any.whl", hash = "sha256:a00d62173bdadc2510967a381810101624a2f0986145b8da0cffa42e29430235"}, + {file = "emoji-2.12.1.tar.gz", hash = "sha256:4aa0488817691aa58d83764b6c209f8a27c0b3ab3f89d1b8dceca1a62e4973eb"}, ] +[package.dependencies] +typing-extensions = ">=4.7.0" + [package.extras] -dev = ["coverage", "coveralls", "pytest"] +dev = ["coverage", "pytest (>=7.4.4)"] [package.source] type = "legacy" @@ -1002,13 +1037,13 @@ reference = "ali" [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -1021,22 +1056,23 @@ reference = "ali" [[package]] name = "fastapi" -version = "0.109.2" +version = "0.112.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, - {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, + {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"}, + {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.36.3,<0.37.0" +starlette = ">=0.37.2,<0.38.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [package.source] type = "legacy" @@ -1064,18 +1100,18 @@ reference = "ali" [[package]] name = "filelock" -version = "3.13.1" +version = "3.15.4" description = "A platform independent file lock." optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] [package.source] @@ -1252,61 +1288,61 @@ reference = "ali" [[package]] name = "grpcio" -version = "1.65.1" +version = "1.65.4" description = "HTTP/2-based RPC framework" optional = false python-versions = ">=3.8" files = [ - {file = "grpcio-1.65.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:3dc5f928815b8972fb83b78d8db5039559f39e004ec93ebac316403fe031a062"}, - {file = "grpcio-1.65.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:8333ca46053c35484c9f2f7e8d8ec98c1383a8675a449163cea31a2076d93de8"}, - {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:7af64838b6e615fff0ec711960ed9b6ee83086edfa8c32670eafb736f169d719"}, - {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbb64b4166362d9326f7efbf75b1c72106c1aa87f13a8c8b56a1224fac152f5c"}, - {file = "grpcio-1.65.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8422dc13ad93ec8caa2612b5032a2b9cd6421c13ed87f54db4a3a2c93afaf77"}, - {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4effc0562b6c65d4add6a873ca132e46ba5e5a46f07c93502c37a9ae7f043857"}, - {file = "grpcio-1.65.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a6c71575a2fedf259724981fd73a18906513d2f306169c46262a5bae956e6364"}, - {file = "grpcio-1.65.1-cp310-cp310-win32.whl", hash = "sha256:34966cf526ef0ea616e008d40d989463e3db157abb213b2f20c6ce0ae7928875"}, - {file = "grpcio-1.65.1-cp310-cp310-win_amd64.whl", hash = "sha256:ca931de5dd6d9eb94ff19a2c9434b23923bce6f767179fef04dfa991f282eaad"}, - {file = "grpcio-1.65.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:bbb46330cc643ecf10bd9bd4ca8e7419a14b6b9dedd05f671c90fb2c813c6037"}, - {file = "grpcio-1.65.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d827a6fb9215b961eb73459ad7977edb9e748b23e3407d21c845d1d8ef6597e5"}, - {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:6e71aed8835f8d9fbcb84babc93a9da95955d1685021cceb7089f4f1e717d719"}, - {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1c84560b3b2d34695c9ba53ab0264e2802721c530678a8f0a227951f453462"}, - {file = "grpcio-1.65.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27adee2338d697e71143ed147fe286c05810965d5d30ec14dd09c22479bfe48a"}, - {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f62652ddcadc75d0e7aa629e96bb61658f85a993e748333715b4ab667192e4e8"}, - {file = "grpcio-1.65.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:71a05fd814700dd9cb7d9a507f2f6a1ef85866733ccaf557eedacec32d65e4c2"}, - {file = "grpcio-1.65.1-cp311-cp311-win32.whl", hash = "sha256:b590f1ad056294dfaeac0b7e1b71d3d5ace638d8dd1f1147ce4bd13458783ba8"}, - {file = "grpcio-1.65.1-cp311-cp311-win_amd64.whl", hash = "sha256:12e9bdf3b5fd48e5fbe5b3da382ad8f97c08b47969f3cca81dd9b36b86ed39e2"}, - {file = "grpcio-1.65.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:54cb822e177374b318b233e54b6856c692c24cdbd5a3ba5335f18a47396bac8f"}, - {file = "grpcio-1.65.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:aaf3c54419a28d45bd1681372029f40e5bfb58e5265e3882eaf21e4a5f81a119"}, - {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:557de35bdfbe8bafea0a003dbd0f4da6d89223ac6c4c7549d78e20f92ead95d9"}, - {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8bfd95ef3b097f0cc86ade54eafefa1c8ed623aa01a26fbbdcd1a3650494dd11"}, - {file = "grpcio-1.65.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e6a8f3d6c41e6b642870afe6cafbaf7b61c57317f9ec66d0efdaf19db992b90"}, - {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1faaf7355ceed07ceaef0b9dcefa4c98daf1dd8840ed75c2de128c3f4a4d859d"}, - {file = "grpcio-1.65.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:60f1f38eed830488ad2a1b11579ef0f345ff16fffdad1d24d9fbc97ba31804ff"}, - {file = "grpcio-1.65.1-cp312-cp312-win32.whl", hash = "sha256:e75acfa52daf5ea0712e8aa82f0003bba964de7ae22c26d208cbd7bc08500177"}, - {file = "grpcio-1.65.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff5a84907e51924973aa05ed8759210d8cdae7ffcf9e44fd17646cf4a902df59"}, - {file = "grpcio-1.65.1-cp38-cp38-linux_armv7l.whl", hash = "sha256:1fbd6331f18c3acd7e09d17fd840c096f56eaf0ef830fbd50af45ae9dc8dfd83"}, - {file = "grpcio-1.65.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de5b6be29116e094c5ef9d9e4252e7eb143e3d5f6bd6d50a78075553ab4930b0"}, - {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e4a3cdba62b2d6aeae6027ae65f350de6dc082b72e6215eccf82628e79efe9ba"}, - {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941c4869aa229d88706b78187d60d66aca77fe5c32518b79e3c3e03fc26109a2"}, - {file = "grpcio-1.65.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f40cebe5edb518d78b8131e87cb83b3ee688984de38a232024b9b44e74ee53d3"}, - {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2ca684ba331fb249d8a1ce88db5394e70dbcd96e58d8c4b7e0d7b141a453dce9"}, - {file = "grpcio-1.65.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8558f0083ddaf5de64a59c790bffd7568e353914c0c551eae2955f54ee4b857f"}, - {file = "grpcio-1.65.1-cp38-cp38-win32.whl", hash = "sha256:8d8143a3e3966f85dce6c5cc45387ec36552174ba5712c5dc6fcc0898fb324c0"}, - {file = "grpcio-1.65.1-cp38-cp38-win_amd64.whl", hash = "sha256:76e81a86424d6ca1ce7c16b15bdd6a964a42b40544bf796a48da241fdaf61153"}, - {file = "grpcio-1.65.1-cp39-cp39-linux_armv7l.whl", hash = "sha256:cb5175f45c980ff418998723ea1b3869cce3766d2ab4e4916fbd3cedbc9d0ed3"}, - {file = "grpcio-1.65.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b12c1aa7b95abe73b3e04e052c8b362655b41c7798da69f1eaf8d186c7d204df"}, - {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:3019fb50128b21a5e018d89569ffaaaa361680e1346c2f261bb84a91082eb3d3"}, - {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ae15275ed98ea267f64ee9ddedf8ecd5306a5b5bb87972a48bfe24af24153e8"}, - {file = "grpcio-1.65.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f096ffb881f37e8d4f958b63c74bfc400c7cebd7a944b027357cd2fb8d91a57"}, - {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2f56b5a68fdcf17a0a1d524bf177218c3c69b3947cb239ea222c6f1867c3ab68"}, - {file = "grpcio-1.65.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:941596d419b9736ab548aa0feb5bbba922f98872668847bf0720b42d1d227b9e"}, - {file = "grpcio-1.65.1-cp39-cp39-win32.whl", hash = "sha256:5fd7337a823b890215f07d429f4f193d24b80d62a5485cf88ee06648591a0c57"}, - {file = "grpcio-1.65.1-cp39-cp39-win_amd64.whl", hash = "sha256:1bceeec568372cbebf554eae1b436b06c2ff24cfaf04afade729fb9035408c6c"}, - {file = "grpcio-1.65.1.tar.gz", hash = "sha256:3c492301988cd720cd145d84e17318d45af342e29ef93141228f9cd73222368b"}, + {file = "grpcio-1.65.4-cp310-cp310-linux_armv7l.whl", hash = "sha256:0e85c8766cf7f004ab01aff6a0393935a30d84388fa3c58d77849fcf27f3e98c"}, + {file = "grpcio-1.65.4-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e4a795c02405c7dfa8affd98c14d980f4acea16ea3b539e7404c645329460e5a"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d7b984a8dd975d949c2042b9b5ebcf297d6d5af57dcd47f946849ee15d3c2fb8"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644a783ce604a7d7c91412bd51cf9418b942cf71896344b6dc8d55713c71ce82"}, + {file = "grpcio-1.65.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5764237d751d3031a36fafd57eb7d36fd2c10c658d2b4057c516ccf114849a3e"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ee40d058cf20e1dd4cacec9c39e9bce13fedd38ce32f9ba00f639464fcb757de"}, + {file = "grpcio-1.65.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4482a44ce7cf577a1f8082e807a5b909236bce35b3e3897f839f2fbd9ae6982d"}, + {file = "grpcio-1.65.4-cp310-cp310-win32.whl", hash = "sha256:66bb051881c84aa82e4f22d8ebc9d1704b2e35d7867757f0740c6ef7b902f9b1"}, + {file = "grpcio-1.65.4-cp310-cp310-win_amd64.whl", hash = "sha256:870370524eff3144304da4d1bbe901d39bdd24f858ce849b7197e530c8c8f2ec"}, + {file = "grpcio-1.65.4-cp311-cp311-linux_armv7l.whl", hash = "sha256:85e9c69378af02e483bc626fc19a218451b24a402bdf44c7531e4c9253fb49ef"}, + {file = "grpcio-1.65.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2bd672e005afab8bf0d6aad5ad659e72a06dd713020554182a66d7c0c8f47e18"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:abccc5d73f5988e8f512eb29341ed9ced923b586bb72e785f265131c160231d8"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:886b45b29f3793b0c2576201947258782d7e54a218fe15d4a0468d9a6e00ce17"}, + {file = "grpcio-1.65.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be952436571dacc93ccc7796db06b7daf37b3b56bb97e3420e6503dccfe2f1b4"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8dc9ddc4603ec43f6238a5c95400c9a901b6d079feb824e890623da7194ff11e"}, + {file = "grpcio-1.65.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ade1256c98cba5a333ef54636095f2c09e6882c35f76acb04412f3b1aa3c29a5"}, + {file = "grpcio-1.65.4-cp311-cp311-win32.whl", hash = "sha256:280e93356fba6058cbbfc6f91a18e958062ef1bdaf5b1caf46c615ba1ae71b5b"}, + {file = "grpcio-1.65.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2b819f9ee27ed4e3e737a4f3920e337e00bc53f9e254377dd26fc7027c4d558"}, + {file = "grpcio-1.65.4-cp312-cp312-linux_armv7l.whl", hash = "sha256:926a0750a5e6fb002542e80f7fa6cab8b1a2ce5513a1c24641da33e088ca4c56"}, + {file = "grpcio-1.65.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2a1d4c84d9e657f72bfbab8bedf31bdfc6bfc4a1efb10b8f2d28241efabfaaf2"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:17de4fda50967679677712eec0a5c13e8904b76ec90ac845d83386b65da0ae1e"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dee50c1b69754a4228e933696408ea87f7e896e8d9797a3ed2aeed8dbd04b74"}, + {file = "grpcio-1.65.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c34fc7562bdd169b77966068434a93040bfca990e235f7a67cdf26e1bd5c63"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:24a2246e80a059b9eb981e4c2a6d8111b1b5e03a44421adbf2736cc1d4988a8a"}, + {file = "grpcio-1.65.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18c10f0d054d2dce34dd15855fcca7cc44ec3b811139437543226776730c0f28"}, + {file = "grpcio-1.65.4-cp312-cp312-win32.whl", hash = "sha256:d72962788b6c22ddbcdb70b10c11fbb37d60ae598c51eb47ec019db66ccfdff0"}, + {file = "grpcio-1.65.4-cp312-cp312-win_amd64.whl", hash = "sha256:7656376821fed8c89e68206a522522317787a3d9ed66fb5110b1dff736a5e416"}, + {file = "grpcio-1.65.4-cp38-cp38-linux_armv7l.whl", hash = "sha256:4934077b33aa6fe0b451de8b71dabde96bf2d9b4cb2b3187be86e5adebcba021"}, + {file = "grpcio-1.65.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0cef8c919a3359847c357cb4314e50ed1f0cca070f828ee8f878d362fd744d52"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:a925446e6aa12ca37114840d8550f308e29026cdc423a73da3043fd1603a6385"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf53e6247f1e2af93657e62e240e4f12e11ee0b9cef4ddcb37eab03d501ca864"}, + {file = "grpcio-1.65.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdb34278e4ceb224c89704cd23db0d902e5e3c1c9687ec9d7c5bb4c150f86816"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e6cbdd107e56bde55c565da5fd16f08e1b4e9b0674851d7749e7f32d8645f524"}, + {file = "grpcio-1.65.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:626319a156b1f19513156a3b0dbfe977f5f93db63ca673a0703238ebd40670d7"}, + {file = "grpcio-1.65.4-cp38-cp38-win32.whl", hash = "sha256:3d1bbf7e1dd1096378bd83c83f554d3b93819b91161deaf63e03b7022a85224a"}, + {file = "grpcio-1.65.4-cp38-cp38-win_amd64.whl", hash = "sha256:a99e6dffefd3027b438116f33ed1261c8d360f0dd4f943cb44541a2782eba72f"}, + {file = "grpcio-1.65.4-cp39-cp39-linux_armv7l.whl", hash = "sha256:874acd010e60a2ec1e30d5e505b0651ab12eb968157cd244f852b27c6dbed733"}, + {file = "grpcio-1.65.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b07f36faf01fca5427d4aa23645e2d492157d56c91fab7e06fe5697d7e171ad4"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:b81711bf4ec08a3710b534e8054c7dcf90f2edc22bebe11c1775a23f145595fe"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88fcabc332a4aef8bcefadc34a02e9ab9407ab975d2c7d981a8e12c1aed92aa1"}, + {file = "grpcio-1.65.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9ba3e63108a8749994f02c7c0e156afb39ba5bdf755337de8e75eb685be244b"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8eb485801957a486bf5de15f2c792d9f9c897a86f2f18db8f3f6795a094b4bb2"}, + {file = "grpcio-1.65.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075f3903bc1749ace93f2b0664f72964ee5f2da5c15d4b47e0ab68e4f442c257"}, + {file = "grpcio-1.65.4-cp39-cp39-win32.whl", hash = "sha256:0a0720299bdb2cc7306737295d56e41ce8827d5669d4a3cd870af832e3b17c4d"}, + {file = "grpcio-1.65.4-cp39-cp39-win_amd64.whl", hash = "sha256:a146bc40fa78769f22e1e9ff4f110ef36ad271b79707577bf2a31e3e931141b9"}, + {file = "grpcio-1.65.4.tar.gz", hash = "sha256:2a4f476209acffec056360d3e647ae0e14ae13dcf3dfb130c227ae1c594cbe39"}, ] [package.extras] -protobuf = ["grpcio-tools (>=1.65.1)"] +protobuf = ["grpcio-tools (>=1.65.4)"] [package.source] type = "legacy" @@ -1438,13 +1474,13 @@ reference = "ali" [[package]] name = "idna" -version = "3.6" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [package.source] @@ -1516,13 +1552,13 @@ reference = "ali" [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [package.dependencies] @@ -1561,96 +1597,157 @@ reference = "ali" [[package]] name = "lxml" -version = "5.1.0" +version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.6" files = [ - {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, - {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, - {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, - {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, - {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, - {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, - {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, - {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, - {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, - {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, - {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, - {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, - {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, - {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, - {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, - {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, - {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, - {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, - {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, - {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.8)"] +source = ["Cython (>=3.0.11)"] [package.source] type = "legacy" @@ -1659,13 +1756,13 @@ reference = "ali" [[package]] name = "markdown" -version = "3.5.2" +version = "3.6" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, - {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, ] [package.extras] @@ -1708,71 +1805,71 @@ reference = "ali" [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [package.source] @@ -1798,67 +1895,67 @@ reference = "ali" [[package]] name = "msgpack" -version = "1.0.7" +version = "1.0.8" description = "MessagePack serializer" optional = false python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, - {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, - {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, - {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, - {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, - {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, - {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, - {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, - {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, - {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, - {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, - {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, - {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, - {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, - {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, - {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, - {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, - {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, - {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, - {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, - {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, - {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, - {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, - {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, - {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, - {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, - {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, ] [package.source] @@ -1868,85 +1965,101 @@ reference = "ali" [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [package.source] @@ -1972,24 +2085,24 @@ reference = "ali" [[package]] name = "nb-cli" -version = "1.3.0" +version = "1.4.1" description = "CLI for nonebot2" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "nb_cli-1.3.0-py3-none-any.whl", hash = "sha256:e8346f2bc2cca873037c47738fc3203689a328d583595f8caefadebeecd5fbe4"}, - {file = "nb_cli-1.3.0.tar.gz", hash = "sha256:cc890de5ccb35a498e413ff9deb2049c4ab9c5c73a78340a85372e2f71d6cd87"}, + {file = "nb_cli-1.4.1-py3-none-any.whl", hash = "sha256:57b6111773202bce29c0520f4a281edb8a7643fa33692d4afc70ca5b51b10f70"}, + {file = "nb_cli-1.4.1.tar.gz", hash = "sha256:908dd4cbbf66bf46fe879c23ad1377332f63385cebca1912b627aa686d1816f3"}, ] [package.dependencies] -anyio = ">=3.6,<4.0" -cashews = ">=6.0,<7.0" +anyio = ">=3.6,<5.0" +cashews = ">=6.0,<8.0" click = ">=8.1,<9.0" cookiecutter = ">=2.2,<3.0" httpx = ">=0.18,<1.0" jinja2 = ">=3.0,<4.0" noneprompt = ">=0.1.9,<1.0.0" -pydantic = ">=1.9,<2.0" +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" pyfiglet = ">=1.0.1,<2.0.0" tomlkit = ">=0.10,<1.0" typing-extensions = ">=4.4,<5.0" @@ -2024,17 +2137,17 @@ reference = "ali" [[package]] name = "nonebot-adapter-discord" -version = "0.1.3" +version = "0.1.8" description = "Discord adapter for nonebot2" optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.9,<4.0" files = [ - {file = "nonebot_adapter_discord-0.1.3-py3-none-any.whl", hash = "sha256:4af3ef98e9d70d68880b43cb8a4421be8ca4961832eaf95377e2a339c73f0644"}, - {file = "nonebot_adapter_discord-0.1.3.tar.gz", hash = "sha256:73b492c63747ff2d5c8c1e49236f5bf2ff2ca01adacf273f64899193f9210ffe"}, + {file = "nonebot_adapter_discord-0.1.8-py3-none-any.whl", hash = "sha256:d063bf524f6a75c5c123f2d04227e0ec62c2433f56b28fb92fa5eb2aebef1c16"}, + {file = "nonebot_adapter_discord-0.1.8.tar.gz", hash = "sha256:5d3a7a8e0ab23b7ae84551b479c40c5d09733b15d09538d64765c5af54721781"}, ] [package.dependencies] -nonebot2 = ">=2.0.0,<3.0.0" +nonebot2 = ">=2.2.1,<3.0.0" [package.source] type = "legacy" @@ -2062,17 +2175,17 @@ reference = "ali" [[package]] name = "nonebot-adapter-kaiheila" -version = "0.3.0" +version = "0.3.4" description = "kaiheila adapter for nonebot2" optional = false python-versions = ">=3.8,<4.0" files = [ - {file = "nonebot_adapter_kaiheila-0.3.0-py3-none-any.whl", hash = "sha256:8868f57f9ac591dff46d5cf711c419aa76ece22a5dd2380abc33d337132b5157"}, - {file = "nonebot_adapter_kaiheila-0.3.0.tar.gz", hash = "sha256:b32b4e9d911b98ae0270540ed7fa414e94f74b228363b6b012ae2f2d54ed4e21"}, + {file = "nonebot_adapter_kaiheila-0.3.4-py3-none-any.whl", hash = "sha256:a4cc0e43bd24e015b8312f1753705116274d5b7e9a68be266384dd413ca4f510"}, + {file = "nonebot_adapter_kaiheila-0.3.4.tar.gz", hash = "sha256:1fea823e5bc2bb5dc8e56a4c10a8f6698dac6e4f77d4526768275fa0925340f2"}, ] [package.dependencies] -nonebot2 = ">=2.0.0,<3.0.0" +nonebot2 = ">=2.2.0,<3.0.0" typing-extensions = ">=4.8.0,<5.0.0" [package.source] @@ -2082,18 +2195,19 @@ reference = "ali" [[package]] name = "nonebot-adapter-onebot" -version = "2.3.1" +version = "2.4.4" description = "OneBot(CQHTTP) adapter for nonebot2" optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.9,<4.0" files = [ - {file = "nonebot_adapter_onebot-2.3.1-py3-none-any.whl", hash = "sha256:c4085f1fc1a62e46c737452b9ce3d6eb374812c78a419bb4fa378f48bd8e4088"}, - {file = "nonebot_adapter_onebot-2.3.1.tar.gz", hash = "sha256:10cec3aee454700e6d2144748bd898772db7bd95247d51d3ccd3b31919e24689"}, + {file = "nonebot_adapter_onebot-2.4.4-py3-none-any.whl", hash = "sha256:4dceeec7332bb560652c764405e9dd350268303f69b7c0e92b7cfebe876e8d39"}, + {file = "nonebot_adapter_onebot-2.4.4.tar.gz", hash = "sha256:c8a3645f74a3e43c85f092fb670508c662c36831f019a15e4d74eaac686089f0"}, ] [package.dependencies] msgpack = ">=1.0.3,<2.0.0" -nonebot2 = ">=2.1.0,<3.0.0" +nonebot2 = ">=2.2.0,<3.0.0" +pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0" typing-extensions = ">=4.0.0,<5.0.0" [package.source] @@ -2148,20 +2262,20 @@ reference = "ali" [[package]] name = "nonebot-plugin-htmlrender" -version = "0.3.0" +version = "0.3.3" description = "通过浏览器渲染图片" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" files = [ - {file = "nonebot_plugin_htmlrender-0.3.0-py3-none-any.whl", hash = "sha256:c05588bad4738421a49a47a7db974359adeb624c1ed6af49d6237023fa014bcf"}, - {file = "nonebot_plugin_htmlrender-0.3.0.tar.gz", hash = "sha256:34b4ff5b898ea47480d3488a2a0b01c46e0ca3d938ab4b891d1db91a70d83d2d"}, + {file = "nonebot_plugin_htmlrender-0.3.3-py3-none-any.whl", hash = "sha256:2ac871d345c94103aa630153e007caa6319b5f5468491347513d746ba98b70d7"}, + {file = "nonebot_plugin_htmlrender-0.3.3.tar.gz", hash = "sha256:ab46ecc6dbd102628af8f88437fdc24da11839487950d07d0c5fd8db0db98ae8"}, ] [package.dependencies] aiofiles = ">=0.8.0" jinja2 = ">=3.0.3" markdown = ">=3.3.6" -nonebot2 = {version = ">=2.2.0", extras = ["fastapi"]} +nonebot2 = ">=2.2.0" playwright = ">=1.17.2" Pygments = ">=2.10.0" pymdown-extensions = ">=9.1" @@ -2172,28 +2286,6 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" reference = "ali" -[[package]] -name = "nonebot-plugin-send-anything-anywhere" -version = "0.5.0" -description = "An adaptor for nonebot2 adaptors" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "nonebot_plugin_send_anything_anywhere-0.5.0-py3-none-any.whl", hash = "sha256:bff64f5f337643ba34b9ea0bdd8d86d3ee6285a29b9083d416a67d4815e83ddf"}, - {file = "nonebot_plugin_send_anything_anywhere-0.5.0.tar.gz", hash = "sha256:0230db94ca5654e2b0462b144db7ea74b763ee04fa7bc53deecacf32362e5268"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<4.0.0" -nonebot2 = ">=2.0.0,<3.0.0" -pydantic = ">=1.10.5,<2.0.0" -strenum = ">=0.4.8,<0.5.0" - -[package.source] -type = "legacy" -url = "https://mirrors.aliyun.com/pypi/simple" -reference = "ali" - [[package]] name = "nonebot-plugin-session" version = "0.2.3" @@ -2312,47 +2404,56 @@ reference = "ali" [[package]] name = "numpy" -version = "1.26.4" +version = "2.0.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"}, + {file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"}, + {file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"}, + {file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"}, + {file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"}, + {file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"}, + {file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"}, + {file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"}, + {file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"}, + {file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"}, + {file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}, + {file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}, + {file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}, + {file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}, + {file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}, + {file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"}, + {file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"}, + {file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"}, + {file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"}, + {file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"}, + {file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"}, + {file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"}, + {file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"}, ] [package.source] @@ -2362,18 +2463,18 @@ reference = "ali" [[package]] name = "opencv-python" -version = "4.9.0.80" +version = "4.10.0.84" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, + {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"}, + {file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"}, ] [package.dependencies] @@ -2507,18 +2608,19 @@ reference = "ali" [[package]] name = "platformdirs" -version = "4.1.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [package.source] type = "legacy" @@ -2527,23 +2629,23 @@ reference = "ali" [[package]] name = "playwright" -version = "1.41.1" +version = "1.45.1" description = "A high-level API to automate web browsers" optional = false python-versions = ">=3.8" files = [ - {file = "playwright-1.41.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b456f25db38e4d93afc3c671e1093f3995afb374f14cee284152a30f84cfff02"}, - {file = "playwright-1.41.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53ff152506dbd8527aa815e92757be72f5df60810e8000e9419d29fd4445f53c"}, - {file = "playwright-1.41.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:70c432887b8b5e896fa804fb90ca2c8baf05b13a3590fb8bce8b3c3efba2842d"}, - {file = "playwright-1.41.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:f227a8d616fd3a02d45d68546ee69947dce4a058df134a9e7dc6167c543de3cd"}, - {file = "playwright-1.41.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:475130f879b4ba38b9db7232a043dd5bc3a8bd1a84567fbea7e21a02ee2fcb13"}, - {file = "playwright-1.41.1-py3-none-win32.whl", hash = "sha256:ef769414ea0ceb76085c67812ab6bc0cc6fac0adfc45aaa09d54ee161d7f637b"}, - {file = "playwright-1.41.1-py3-none-win_amd64.whl", hash = "sha256:316e1ba0854a712e9288b3fe49509438e648d43bade77bf724899de8c24848de"}, + {file = "playwright-1.45.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:360607e37c00cdf97c74317f010e106ac4671aeaec6a192431dd71a30941da9d"}, + {file = "playwright-1.45.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:20adc2abf164c5e8969f9066011b152e12c210549edec78cd05bd0e9cf4135b7"}, + {file = "playwright-1.45.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5f047cdc6accf4c7084dfc7587a2a5ef790cddc44cbb111e471293c5a91119db"}, + {file = "playwright-1.45.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:f06f6659abe0abf263e5f6661d379fbf85c112745dd31d82332ceae914f58df7"}, + {file = "playwright-1.45.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87dc3b3d17e12c68830c29b7fdf5e93315221bbb4c6090e83e967e154e2c1828"}, + {file = "playwright-1.45.1-py3-none-win32.whl", hash = "sha256:2b8f517886ef1e2151982f6e7be84be3ef7d8135bdcf8ee705b4e4e99566e866"}, + {file = "playwright-1.45.1-py3-none-win_amd64.whl", hash = "sha256:0d236cf427784e77de352ba1b7d700693c5fe455b8e5f627f6d84ad5b84b5bf5"}, ] [package.dependencies] greenlet = "3.0.3" -pyee = "11.0.1" +pyee = "11.1.0" [package.source] type = "legacy" @@ -2568,13 +2670,13 @@ reference = "ali" [[package]] name = "prompt-toolkit" -version = "3.0.43" +version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, - {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, + {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, + {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] [package.dependencies] @@ -2677,47 +2779,54 @@ reference = "ali" [[package]] name = "pydantic" -version = "1.10.14" +version = "1.10.17" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, + {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, + {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, + {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, + {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, + {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, + {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, + {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, + {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, + {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, ] [package.dependencies] @@ -2734,20 +2843,20 @@ reference = "ali" [[package]] name = "pyee" -version = "11.0.1" +version = "11.1.0" description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false python-versions = ">=3.8" files = [ - {file = "pyee-11.0.1-py3-none-any.whl", hash = "sha256:9bcc9647822234f42c228d88de63d0f9ffa881e87a87f9d36ddf5211f6ac977d"}, - {file = "pyee-11.0.1.tar.gz", hash = "sha256:a642c51e3885a33ead087286e35212783a4e9b8d6514a10a5db4e57ac57b2b29"}, + {file = "pyee-11.1.0-py3-none-any.whl", hash = "sha256:5d346a7d0f861a4b2e6c47960295bd895f816725b27d656181947346be98d7c1"}, + {file = "pyee-11.1.0.tar.gz", hash = "sha256:b53af98f6990c810edd9b56b87791021a8f54fd13db4edd1142438d44ba2263f"}, ] [package.dependencies] typing-extensions = "*" [package.extras] -dev = ["black", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] [package.source] type = "legacy" @@ -2772,17 +2881,16 @@ reference = "ali" [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [package.source] @@ -2808,17 +2916,17 @@ reference = "ali" [[package]] name = "pymdown-extensions" -version = "10.7" +version = "10.9" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.7-py3-none-any.whl", hash = "sha256:6ca215bc57bc12bf32b414887a68b810637d039124ed9b2e5bd3325cbb2c050c"}, - {file = "pymdown_extensions-10.7.tar.gz", hash = "sha256:c0d64d5cf62566f59e6b2b690a4095c931107c250a8c8e1351c1de5f6b036deb"}, + {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, + {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, ] [package.dependencies] -markdown = ">=3.5" +markdown = ">=3.6" pyyaml = "*" [package.extras] @@ -2863,13 +2971,13 @@ reference = "ali" [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2966,13 +3074,13 @@ reference = "ali" [[package]] name = "python-slugify" -version = "8.0.2" +version = "8.0.4" description = "A Python slugify application that also handles Unicode" optional = false python-versions = ">=3.7" files = [ - {file = "python-slugify-8.0.2.tar.gz", hash = "sha256:a1a02b127a95c124fd84f8f88be730e557fd823774bf19b1cd5e8704e2ae0e5e"}, - {file = "python_slugify-8.0.2-py2.py3-none-any.whl", hash = "sha256:428ea9b00c977b8f6c097724398f190b2c18e2a6011094d1001285875ccacdbf"}, + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, ] [package.dependencies] @@ -2988,13 +3096,13 @@ reference = "ali" [[package]] name = "pytz" -version = "2023.3.post1" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, - {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] [package.source] @@ -3054,62 +3162,64 @@ reference = "ali" [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [package.source] @@ -3212,13 +3322,13 @@ reference = "ali" [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -3279,13 +3389,13 @@ reference = "ali" [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -3321,13 +3431,13 @@ reference = "ali" [[package]] name = "ruamel-yaml" -version = "0.18.5" +version = "0.18.6" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.7" files = [ - {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, - {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, + {file = "ruamel.yaml-0.18.6-py3-none-any.whl", hash = "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636"}, + {file = "ruamel.yaml-0.18.6.tar.gz", hash = "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b"}, ] [package.dependencies] @@ -3408,45 +3518,45 @@ reference = "ali" [[package]] name = "scipy" -version = "1.13.1" +version = "1.14.0" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, - {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, - {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, - {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, - {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, - {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, - {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, - {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, - {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, - {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, - {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, - {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, - {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, - {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, - {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, - {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, - {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, - {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7e911933d54ead4d557c02402710c2396529540b81dd554fc1ba270eb7308484"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:687af0a35462402dd851726295c1a5ae5f987bd6e9026f52e9505994e2f84ef6"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:07e179dc0205a50721022344fb85074f772eadbda1e1b3eecdc483f8033709b7"}, + {file = "scipy-1.14.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a9c9a9b226d9a21e0a208bdb024c3982932e43811b62d202aaf1bb59af264b1"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076c27284c768b84a45dcf2e914d4000aac537da74236a0d45d82c6fa4b7b3c0"}, + {file = "scipy-1.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42470ea0195336df319741e230626b6225a740fd9dce9642ca13e98f667047c0"}, + {file = "scipy-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:176c6f0d0470a32f1b2efaf40c3d37a24876cebf447498a4cefb947a79c21e9d"}, + {file = "scipy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:ad36af9626d27a4326c8e884917b7ec321d8a1841cd6dacc67d2a9e90c2f0359"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d056a8709ccda6cf36cdd2eac597d13bc03dba38360f418560a93050c76a16e"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f0a50da861a7ec4573b7c716b2ebdcdf142b66b756a0d392c236ae568b3a93fb"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:94c164a9e2498e68308e6e148646e486d979f7fcdb8b4cf34b5441894bdb9caf"}, + {file = "scipy-1.14.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a7d46c3e0aea5c064e734c3eac5cf9eb1f8c4ceee756262f2c7327c4c2691c86"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eee2989868e274aae26125345584254d97c56194c072ed96cb433f32f692ed8"}, + {file = "scipy-1.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3154691b9f7ed73778d746da2df67a19d046a6c8087c8b385bc4cdb2cfca74"}, + {file = "scipy-1.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c40003d880f39c11c1edbae8144e3813904b10514cd3d3d00c277ae996488cdb"}, + {file = "scipy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b083c8940028bb7e0b4172acafda6df762da1927b9091f9611b0bcd8676f2bc"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bff2438ea1330e06e53c424893ec0072640dac00f29c6a43a575cbae4c99b2b9"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bbc0471b5f22c11c389075d091d3885693fd3f5e9a54ce051b46308bc787e5d4"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:64b2ff514a98cf2bb734a9f90d32dc89dc6ad4a4a36a312cd0d6327170339eb0"}, + {file = "scipy-1.14.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:7d3da42fbbbb860211a811782504f38ae7aaec9de8764a9bef6b262de7a2b50f"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d91db2c41dd6c20646af280355d41dfa1ec7eead235642178bd57635a3f82209"}, + {file = "scipy-1.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a01cc03bcdc777c9da3cfdcc74b5a75caffb48a6c39c8450a9a05f82c4250a14"}, + {file = "scipy-1.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:65df4da3c12a2bb9ad52b86b4dcf46813e869afb006e58be0f516bc370165159"}, + {file = "scipy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:4c4161597c75043f7154238ef419c29a64ac4a7c889d588ea77690ac4d0d9b20"}, + {file = "scipy-1.14.0.tar.gz", hash = "sha256:b5923f48cb840380f9854339176ef21763118a7300a88203ccd0bdd26e58527b"}, ] [package.dependencies] -numpy = ">=1.22.4,<2.3" +numpy = ">=1.23.5,<2.3" [package.extras] -dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] -test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [package.source] type = "legacy" @@ -3455,13 +3565,13 @@ reference = "ali" [[package]] name = "setuptools" -version = "71.1.0" +version = "72.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, - {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, + {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, + {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, ] [package.extras] @@ -3507,13 +3617,13 @@ reference = "ali" [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [package.source] @@ -3539,13 +3649,13 @@ reference = "ali" [[package]] name = "starlette" -version = "0.36.3" +version = "0.37.2" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, - {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, ] [package.dependencies] @@ -3681,13 +3791,13 @@ reference = "ali" [[package]] name = "tomlkit" -version = "0.12.3" +version = "0.13.0" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, - {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, + {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, + {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, ] [package.source] @@ -3728,13 +3838,13 @@ reference = "ali" [[package]] name = "types-python-dateutil" -version = "2.8.19.20240106" +version = "2.9.0.20240316" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"}, - {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"}, + {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, + {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, ] [package.source] @@ -3744,13 +3854,13 @@ reference = "ali" [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [package.source] @@ -3760,13 +3870,13 @@ reference = "ali" [[package]] name = "tzdata" -version = "2023.4" +version = "2024.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, - {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [package.source] @@ -3798,76 +3908,89 @@ reference = "ali" [[package]] name = "ujson" -version = "5.9.0" +version = "5.10.0" description = "Ultra fast JSON encoder and decoder for Python" optional = false python-versions = ">=3.8" files = [ - {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, - {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, - {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, - {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, - {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, - {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, - {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, - {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, - {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, - {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, - {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, - {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, - {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, - {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, - {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, - {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, - {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, - {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, - {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, - {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, - {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, - {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, - {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, - {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, - {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, - {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, - {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, - {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, - {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, - {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, + {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, + {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, + {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, + {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, + {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, + {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, + {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, + {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"}, + {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"}, + {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"}, + {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"}, + {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"}, + {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"}, + {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"}, + {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"}, + {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"}, + {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"}, + {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, + {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, ] [package.source] @@ -3877,17 +4000,18 @@ reference = "ali" [[package]] name = "urllib3" -version = "2.1.0" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"}, - {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"}, + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3898,13 +4022,13 @@ reference = "ali" [[package]] name = "uvicorn" -version = "0.27.1" +version = "0.30.5" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, - {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, + {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"}, + {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"}, ] [package.dependencies] @@ -3978,13 +4102,13 @@ reference = "ali" [[package]] name = "virtualenv" -version = "20.25.0" +version = "20.26.3" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, ] [package.dependencies] @@ -3993,7 +4117,7 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [package.source] @@ -4003,86 +4127,98 @@ reference = "ali" [[package]] name = "watchfiles" -version = "0.21.0" +version = "0.23.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.8" files = [ - {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, - {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, - {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, - {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, - {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, - {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, - {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, - {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, - {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, - {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, - {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, - {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, - {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, - {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, - {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, - {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, - {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, - {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, - {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, - {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, - {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, - {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, - {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, - {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, - {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, - {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, - {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, - {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, - {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, - {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, - {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, - {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, + {file = "watchfiles-0.23.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bee8ce357a05c20db04f46c22be2d1a2c6a8ed365b325d08af94358e0688eeb4"}, + {file = "watchfiles-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ccd3011cc7ee2f789af9ebe04745436371d36afe610028921cab9f24bb2987b"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb02d41c33be667e6135e6686f1bb76104c88a312a18faa0ef0262b5bf7f1a0f"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf12ac34c444362f3261fb3ff548f0037ddd4c5bb85f66c4be30d2936beb3c5"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0b2c25040a3c0ce0e66c7779cc045fdfbbb8d59e5aabfe033000b42fe44b53e"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf2be4b9eece4f3da8ba5f244b9e51932ebc441c0867bd6af46a3d97eb068d6"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40cb8fa00028908211eb9f8d47744dca21a4be6766672e1ff3280bee320436f1"}, + {file = "watchfiles-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f48c917ffd36ff9a5212614c2d0d585fa8b064ca7e66206fb5c095015bc8207"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9d183e3888ada88185ab17064079c0db8c17e32023f5c278d7bf8014713b1b5b"}, + {file = "watchfiles-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9837edf328b2805346f91209b7e660f65fb0e9ca18b7459d075d58db082bf981"}, + {file = "watchfiles-0.23.0-cp310-none-win32.whl", hash = "sha256:296e0b29ab0276ca59d82d2da22cbbdb39a23eed94cca69aed274595fb3dfe42"}, + {file = "watchfiles-0.23.0-cp310-none-win_amd64.whl", hash = "sha256:4ea756e425ab2dfc8ef2a0cb87af8aa7ef7dfc6fc46c6f89bcf382121d4fff75"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e397b64f7aaf26915bf2ad0f1190f75c855d11eb111cc00f12f97430153c2eab"}, + {file = "watchfiles-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4ac73b02ca1824ec0a7351588241fd3953748d3774694aa7ddb5e8e46aef3e3"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a896d53b48a1cecccfa903f37a1d87dbb74295305f865a3e816452f6e49e4"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5e7803a65eb2d563c73230e9d693c6539e3c975ccfe62526cadde69f3fda0cf"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1aa4cc85202956d1a65c88d18c7b687b8319dbe6b1aec8969784ef7a10e7d1a"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f889f6e58849ddb7c5d2cb19e2e074917ed1c6e3ceca50405775166492cca8"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37fd826dac84c6441615aa3f04077adcc5cac7194a021c9f0d69af20fb9fa788"}, + {file = "watchfiles-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee7db6e36e7a2c15923072e41ea24d9a0cf39658cb0637ecc9307b09d28827e1"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2368c5371c17fdcb5a2ea71c5c9d49f9b128821bfee69503cc38eae00feb3220"}, + {file = "watchfiles-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:857af85d445b9ba9178db95658c219dbd77b71b8264e66836a6eba4fbf49c320"}, + {file = "watchfiles-0.23.0-cp311-none-win32.whl", hash = "sha256:1d636c8aeb28cdd04a4aa89030c4b48f8b2954d8483e5f989774fa441c0ed57b"}, + {file = "watchfiles-0.23.0-cp311-none-win_amd64.whl", hash = "sha256:46f1d8069a95885ca529645cdbb05aea5837d799965676e1b2b1f95a4206313e"}, + {file = "watchfiles-0.23.0-cp311-none-win_arm64.whl", hash = "sha256:e495ed2a7943503766c5d1ff05ae9212dc2ce1c0e30a80d4f0d84889298fa304"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1db691bad0243aed27c8354b12d60e8e266b75216ae99d33e927ff5238d270b5"}, + {file = "watchfiles-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62d2b18cb1edaba311fbbfe83fb5e53a858ba37cacb01e69bc20553bb70911b8"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e087e8fdf1270d000913c12e6eca44edd02aad3559b3e6b8ef00f0ce76e0636f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd41d5c72417b87c00b1b635738f3c283e737d75c5fa5c3e1c60cd03eac3af77"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e5f3ca0ff47940ce0a389457b35d6df601c317c1e1a9615981c474452f98de1"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6991e3a78f642368b8b1b669327eb6751439f9f7eaaa625fae67dd6070ecfa0b"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f7252f52a09f8fa5435dc82b6af79483118ce6bd51eb74e6269f05ee22a7b9f"}, + {file = "watchfiles-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e01bcb8d767c58865207a6c2f2792ad763a0fe1119fb0a430f444f5b02a5ea0"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8e56fbcdd27fce061854ddec99e015dd779cae186eb36b14471fc9ae713b118c"}, + {file = "watchfiles-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bd3e2d64500a6cad28bcd710ee6269fbeb2e5320525acd0cfab5f269ade68581"}, + {file = "watchfiles-0.23.0-cp312-none-win32.whl", hash = "sha256:eb99c954291b2fad0eff98b490aa641e128fbc4a03b11c8a0086de8b7077fb75"}, + {file = "watchfiles-0.23.0-cp312-none-win_amd64.whl", hash = "sha256:dccc858372a56080332ea89b78cfb18efb945da858fabeb67f5a44fa0bcb4ebb"}, + {file = "watchfiles-0.23.0-cp312-none-win_arm64.whl", hash = "sha256:6c21a5467f35c61eafb4e394303720893066897fca937bade5b4f5877d350ff8"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba31c32f6b4dceeb2be04f717811565159617e28d61a60bb616b6442027fd4b9"}, + {file = "watchfiles-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85042ab91814fca99cec4678fc063fb46df4cbb57b4835a1cc2cb7a51e10250e"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24655e8c1c9c114005c3868a3d432c8aa595a786b8493500071e6a52f3d09217"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b1a950ab299a4a78fd6369a97b8763732bfb154fdb433356ec55a5bce9515c1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8d3c5cd327dd6ce0edfc94374fb5883d254fe78a5e9d9dfc237a1897dc73cd1"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ff785af8bacdf0be863ec0c428e3288b817e82f3d0c1d652cd9c6d509020dd0"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02b7ba9d4557149410747353e7325010d48edcfe9d609a85cb450f17fd50dc3d"}, + {file = "watchfiles-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a1b05c0afb2cd2f48c1ed2ae5487b116e34b93b13074ed3c22ad5c743109f0"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:109a61763e7318d9f821b878589e71229f97366fa6a5c7720687d367f3ab9eef"}, + {file = "watchfiles-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9f8e6bb5ac007d4a4027b25f09827ed78cbbd5b9700fd6c54429278dacce05d1"}, + {file = "watchfiles-0.23.0-cp313-none-win32.whl", hash = "sha256:f46c6f0aec8d02a52d97a583782d9af38c19a29900747eb048af358a9c1d8e5b"}, + {file = "watchfiles-0.23.0-cp313-none-win_amd64.whl", hash = "sha256:f449afbb971df5c6faeb0a27bca0427d7b600dd8f4a068492faec18023f0dcff"}, + {file = "watchfiles-0.23.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2dddc2487d33e92f8b6222b5fb74ae2cfde5e8e6c44e0248d24ec23befdc5366"}, + {file = "watchfiles-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e75695cc952e825fa3e0684a7f4a302f9128721f13eedd8dbd3af2ba450932b8"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2537ef60596511df79b91613a5bb499b63f46f01a11a81b0a2b0dedf645d0a9c"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20b423b58f5fdde704a226b598a2d78165fe29eb5621358fe57ea63f16f165c4"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b98732ec893975455708d6fc9a6daab527fc8bbe65be354a3861f8c450a632a4"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee1f5fcbf5bc33acc0be9dd31130bcba35d6d2302e4eceafafd7d9018c7755ab"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f195338a5a7b50a058522b39517c50238358d9ad8284fd92943643144c0c03"}, + {file = "watchfiles-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524fcb8d59b0dbee2c9b32207084b67b2420f6431ed02c18bd191e6c575f5c48"}, + {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0eff099a4df36afaa0eea7a913aa64dcf2cbd4e7a4f319a73012210af4d23810"}, + {file = "watchfiles-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8323daae27ea290ba3350c70c836c0d2b0fb47897fa3b0ca6a5375b952b90d3"}, + {file = "watchfiles-0.23.0-cp38-none-win32.whl", hash = "sha256:aafea64a3ae698695975251f4254df2225e2624185a69534e7fe70581066bc1b"}, + {file = "watchfiles-0.23.0-cp38-none-win_amd64.whl", hash = "sha256:c846884b2e690ba62a51048a097acb6b5cd263d8bd91062cd6137e2880578472"}, + {file = "watchfiles-0.23.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a753993635eccf1ecb185dedcc69d220dab41804272f45e4aef0a67e790c3eb3"}, + {file = "watchfiles-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6bb91fa4d0b392f0f7e27c40981e46dda9eb0fbc84162c7fb478fe115944f491"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1f67312efa3902a8e8496bfa9824d3bec096ff83c4669ea555c6bdd213aa516"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ca6b71dcc50d320c88fb2d88ecd63924934a8abc1673683a242a7ca7d39e781"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aec5c29915caf08771d2507da3ac08e8de24a50f746eb1ed295584ba1820330"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1733b9bc2c8098c6bdb0ff7a3d7cb211753fecb7bd99bdd6df995621ee1a574b"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02ff5d7bd066c6a7673b17c8879cd8ee903078d184802a7ee851449c43521bdd"}, + {file = "watchfiles-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e2de19801b0eaa4c5292a223effb7cfb43904cb742c5317a0ac686ed604765"}, + {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8ada449e22198c31fb013ae7e9add887e8d2bd2335401abd3cbc55f8c5083647"}, + {file = "watchfiles-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3af1b05361e1cc497bf1be654a664750ae61f5739e4bb094a2be86ec8c6db9b6"}, + {file = "watchfiles-0.23.0-cp39-none-win32.whl", hash = "sha256:486bda18be5d25ab5d932699ceed918f68eb91f45d018b0343e3502e52866e5e"}, + {file = "watchfiles-0.23.0-cp39-none-win_amd64.whl", hash = "sha256:d2d42254b189a346249424fb9bb39182a19289a2409051ee432fb2926bad966a"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9265cf87a5b70147bfb2fec14770ed5b11a5bb83353f0eee1c25a81af5abfe"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f02a259fcbbb5fcfe7a0805b1097ead5ba7a043e318eef1db59f93067f0b49b"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ebaebb53b34690da0936c256c1cdb0914f24fb0e03da76d185806df9328abed"}, + {file = "watchfiles-0.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd257f98cff9c6cb39eee1a83c7c3183970d8a8d23e8cf4f47d9a21329285cee"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aba037c1310dd108411d27b3d5815998ef0e83573e47d4219f45753c710f969f"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a96ac14e184aa86dc43b8a22bb53854760a58b2966c2b41580de938e9bf26ed0"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11698bb2ea5e991d10f1f4f83a39a02f91e44e4bd05f01b5c1ec04c9342bf63c"}, + {file = "watchfiles-0.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efadd40fca3a04063d40c4448c9303ce24dd6151dc162cfae4a2a060232ebdcb"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:556347b0abb4224c5ec688fc58214162e92a500323f50182f994f3ad33385dcb"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1cf7f486169986c4b9d34087f08ce56a35126600b6fef3028f19ca16d5889071"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18de0f82c62c4197bea5ecf4389288ac755896aac734bd2cc44004c56e4ac47"}, + {file = "watchfiles-0.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:532e1f2c491274d1333a814e4c5c2e8b92345d41b12dc806cf07aaff786beb66"}, + {file = "watchfiles-0.23.0.tar.gz", hash = "sha256:9338ade39ff24f8086bb005d16c29f8e9f19e55b18dcb04dfa26fcbc09da497b"}, ] [package.dependencies] @@ -4345,4 +4481,4 @@ reference = "ali" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "92e7c882369238f6e25599c854fb715013d2d51323a8ef930e8fc03db6b4715b" +content-hash = "0c19bc0955bde3a4f368395f6d7117c3ab638e36b9af0d9ac05d08d1922dabbc" diff --git a/pyproject.toml b/pyproject.toml index 6d56c5c7..dc2d6501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ cattrs = "^23.2.3" ruamel-yaml = "^0.18.5" strenum = "^0.4.15" nonebot-plugin-session = "^0.2.3" -nonebot-plugin-send-anything-anywhere = "^0.5.0" ujson = "^5.9.0" nonebot-adapter-kaiheila = "^0.3.0" nb-cli = "^1.3.0" diff --git a/zhenxun/builtin_plugins/__init__.py b/zhenxun/builtin_plugins/__init__.py index aae5acb0..52042412 100644 --- a/zhenxun/builtin_plugins/__init__.py +++ b/zhenxun/builtin_plugins/__init__.py @@ -15,12 +15,8 @@ from zhenxun.utils.decorator.shop import shop_register require("nonebot_plugin_apscheduler") require("nonebot_plugin_alconna") require("nonebot_plugin_session") -require("nonebot_plugin_saa") require("nonebot_plugin_userinfo") -from nonebot_plugin_saa import enable_auto_select_bot - -enable_auto_select_bot() import nonebot import ujson as json diff --git a/zhenxun/builtin_plugins/admin/admin_help.py b/zhenxun/builtin_plugins/admin/admin_help.py index a0cb4166..e98df047 100644 --- a/zhenxun/builtin_plugins/admin/admin_help.py +++ b/zhenxun/builtin_plugins/admin/admin_help.py @@ -155,6 +155,6 @@ async def _( try: await build_help() except EmptyError: - await MessageUtils.build_message("管理员帮助为空").finish(reply=True) + await MessageUtils.build_message("管理员帮助为空").finish(reply_to=True) await MessageUtils.build_message(ADMIN_HELP_IMAGE).send() logger.info("查看管理员帮助", arparma.header_result, session=session) diff --git a/zhenxun/builtin_plugins/admin/ban/__init__.py b/zhenxun/builtin_plugins/admin/ban/__init__.py index 19e8f455..0fab59b8 100644 --- a/zhenxun/builtin_plugins/admin/ban/__init__.py +++ b/zhenxun/builtin_plugins/admin/ban/__init__.py @@ -237,7 +237,7 @@ async def _( At(flag="user", target=user_id) if isinstance(user.result, At) else user_id, # type: ignore f" 从黑屋中拉了出来并急救了一下!", ] - ).finish(reply=True) + ).finish(reply_to=True) else: await MessageUtils.build_message(f"该用户不在黑名单中捏...").finish( reply_to=True diff --git a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py index ad134625..9a790625 100644 --- a/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py +++ b/zhenxun/builtin_plugins/admin/group_member_update/_data_source.py @@ -2,10 +2,12 @@ import time from datetime import datetime, timedelta, timezone from nonebot.adapters import Bot -from nonebot.adapters.discord import Bot as DiscordBot -from nonebot.adapters.dodo import Bot as DodoBot + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot from nonebot.adapters.dodo.models import MemberInfo -from nonebot.adapters.kaiheila import Bot as KaiheilaBot + +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot @@ -23,63 +25,63 @@ class MemberUpdateManage: await cls.v11(bot, group_id) elif isinstance(bot, v12Bot): await cls.v12(bot, group_id) - elif isinstance(bot, KaiheilaBot): - await cls.kaiheila(bot, group_id) - elif isinstance(bot, DodoBot): - await cls.dodo(bot, group_id) - elif isinstance(bot, DiscordBot): - await cls.discord(bot, group_id) + # elif isinstance(bot, KaiheilaBot): + # await cls.kaiheila(bot, group_id) + # elif isinstance(bot, DodoBot): + # await cls.dodo(bot, group_id) + # elif isinstance(bot, DiscordBot): + # await cls.discord(bot, group_id) - @classmethod - async def discord(cls, bot: DiscordBot, group_id: str): - # TODO: discord更新群组成员信息 - pass + # @classmethod + # async def discord(cls, bot: DiscordBot, group_id: str): + # # TODO: discord更新群组成员信息 + # pass - @classmethod - async def dodo(cls, bot: DodoBot, group_id: str): - page_size = 100 - result_size = 100 - max_id = 0 - exist_member_list = [] - group_member_list: list[MemberInfo] = [] - while result_size == page_size: - group_member_data = await bot.get_member_list( - island_source_id=group_id, page_size=page_size - ) - result_size = len(group_member_data.list) - group_member_list += group_member_data.list - max_id = group_member_data.max_id - if group_member_list: - for user in group_member_list: - exist_member_list.append(user.dodo_source_id) - await GroupInfoUser.update_or_create( - user_id=user.dodo_source_id, - group_id=group_id, - defaults={ - "user_name": user.nick_name or user.personal_nick_name, - "user_join_time": user.join_time, - "platform": "dodo", - }, - ) - if delete_member_list := list( - set(exist_member_list).difference( - set(await GroupInfoUser.get_group_member_id_list(group_id)) - ) - ): - await GroupInfoUser.filter( - user_id__in=delete_member_list, group_id=group_id - ).delete() - logger.info( - f"删除已退群用户", - "更新群组成员信息", - group_id=group_id, - platform="dodo", - ) + # @classmethod + # async def dodo(cls, bot: DodoBot, group_id: str): + # page_size = 100 + # result_size = 100 + # max_id = 0 + # exist_member_list = [] + # group_member_list: list[MemberInfo] = [] + # while result_size == page_size: + # group_member_data = await bot.get_member_list( + # island_source_id=group_id, page_size=page_size + # ) + # result_size = len(group_member_data.list) + # group_member_list += group_member_data.list + # max_id = group_member_data.max_id + # if group_member_list: + # for user in group_member_list: + # exist_member_list.append(user.dodo_source_id) + # await GroupInfoUser.update_or_create( + # user_id=user.dodo_source_id, + # group_id=group_id, + # defaults={ + # "user_name": user.nick_name or user.personal_nick_name, + # "user_join_time": user.join_time, + # "platform": "dodo", + # }, + # ) + # if delete_member_list := list( + # set(exist_member_list).difference( + # set(await GroupInfoUser.get_group_member_id_list(group_id)) + # ) + # ): + # await GroupInfoUser.filter( + # user_id__in=delete_member_list, group_id=group_id + # ).delete() + # logger.info( + # f"删除已退群用户", + # "更新群组成员信息", + # group_id=group_id, + # platform="dodo", + # ) - @classmethod - async def kaiheila(cls, bot: KaiheilaBot, group_id: str): - # TODO: kaiheila 更新群组成员信息 - pass + # @classmethod + # async def kaiheila(cls, bot: KaiheilaBot, group_id: str): + # # TODO: kaiheila 更新群组成员信息 + # pass @classmethod async def v11(cls, bot: v11Bot, group_id: str): diff --git a/zhenxun/builtin_plugins/hooks/_auth_checker.py b/zhenxun/builtin_plugins/hooks/_auth_checker.py index d480a88f..8a277165 100644 --- a/zhenxun/builtin_plugins/hooks/_auth_checker.py +++ b/zhenxun/builtin_plugins/hooks/_auth_checker.py @@ -1,8 +1,7 @@ from nonebot.adapters import Bot from nonebot.exception import IgnoredException from nonebot.matcher import Matcher -from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_alconna import At, UniMsg from nonebot_plugin_session import EventSession from pydantic import BaseModel from tortoise.exceptions import IntegrityError @@ -22,6 +21,7 @@ from zhenxun.utils.enum import ( PluginType, ) from zhenxun.utils.exception import InsufficientGold +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import CountLimiter, FreqLimiter, UserBlockLimiter @@ -146,7 +146,7 @@ class LimitManage: key_type = channel_id or group_id if is_limit and not limiter.check(key_type): if limit.result: - await Text(limit.result).send() + await MessageUtils.build_message(limit.result).send() logger.debug( f"{limit.module}({limit.limit_type}) 正在限制中...", "HOOK", @@ -296,7 +296,9 @@ class AuthChecker: """超级用户群组插件状态""" if self._flmt_s.check(group_id or user_id): self._flmt_s.start_cd(group_id or user_id) - await Text("超级管理员禁用了该群此功能...").send(reply=True) + await MessageUtils.build_message( + "超级管理员禁用了该群此功能..." + ).send(reply_to=True) logger.debug( f"{plugin.name}({plugin.module}) 超级管理员禁用了该群此功能...", "HOOK", @@ -309,7 +311,9 @@ class AuthChecker: """群组插件状态""" if self._flmt_s.check(group_id or user_id): self._flmt_s.start_cd(group_id or user_id) - await Text("该群未开启此功能...").send(reply=True) + await MessageUtils.build_message("该群未开启此功能...").send( + reply_to=True + ) logger.debug( f"{plugin.name}({plugin.module}) 未开启此功能...", "HOOK", @@ -321,7 +325,9 @@ class AuthChecker: try: if self._flmt_c.check(group_id): self._flmt_c.start_cd(group_id) - await Text("该功能在群组中已被禁用...").send(reply=True) + await MessageUtils.build_message( + "该功能在群组中已被禁用..." + ).send(reply_to=True) except Exception as e: logger.error( "auth_plugin 发送消息失败", "HOOK", session=session, e=e @@ -338,7 +344,9 @@ class AuthChecker: try: if self._flmt_c.check(user_id): self._flmt_c.start_cd(user_id) - await Text("该功能在私聊中已被禁用...").send() + await MessageUtils.build_message( + "该功能在私聊中已被禁用..." + ).send() except Exception as e: logger.error( "auth_admin 发送消息失败", "HOOK", session=session, e=e @@ -356,7 +364,7 @@ class AuthChecker: raise IsSuperuserException() if self._flmt_s.check(group_id or user_id): self._flmt_s.start_cd(group_id or user_id) - await Text("全局未开启此功能...").send() + await MessageUtils.build_message("全局未开启此功能...").send() logger.debug( f"{plugin.name}({plugin.module}) 全局未开启此功能...", "HOOK", @@ -381,14 +389,12 @@ class AuthChecker: try: if self._flmt.check(user_id): self._flmt.start_cd(user_id) - await MessageFactory( + await MessageUtils.build_message( [ - Mention(user_id), - Text( - f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" - ), + At(flag="user", target=user_id), + f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}", ] - ).send(reply=True) + ).send(reply_to=True) except Exception as e: logger.error( "auth_admin 发送消息失败", "HOOK", session=session, e=e @@ -402,7 +408,7 @@ class AuthChecker: else: if not await LevelUser.check_level(user_id, None, plugin.admin_level): try: - await Text( + await MessageUtils.build_message( f"你的权限不足喔,该功能需要的权限等级: {plugin.admin_level}" ).send() except Exception as e: @@ -428,9 +434,7 @@ class AuthChecker: """ if group_id := session.id3 or session.id2: text = message.extract_plain_text() - group = await GroupConsole.get_or_none( - group_id=group_id, channel_id__isnull=True - ) + group = await GroupConsole.get_group(group_id) if not group: """群不存在""" raise IgnoredException("群不存在") @@ -468,7 +472,9 @@ class AuthChecker: if user.gold < plugin.cost_gold: """插件消耗金币不足""" try: - await Text(f"金币不足..该功能需要{plugin.cost_gold}金币..").send() + await MessageUtils.build_message( + f"金币不足..该功能需要{plugin.cost_gold}金币.." + ).send() except Exception as e: logger.error("auth_cost 发送消息失败", "HOOK", session=session, e=e) logger.debug( diff --git a/zhenxun/builtin_plugins/hooks/ban_hook.py b/zhenxun/builtin_plugins/hooks/ban_hook.py index 3d82e56f..b4923eff 100644 --- a/zhenxun/builtin_plugins/hooks/ban_hook.py +++ b/zhenxun/builtin_plugins/hooks/ban_hook.py @@ -3,13 +3,13 @@ from nonebot.exception import IgnoredException from nonebot.matcher import Matcher from nonebot.message import run_preprocessor from nonebot.typing import T_State -from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_alconna import At from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.models.ban_console import BanConsole -from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import FreqLimiter Config.add_plugin_config( @@ -61,10 +61,10 @@ async def _( time_str = f"{minute} 分钟" if ban_result and _flmt.check(user_id): _flmt.start_cd(user_id) - await MessageFactory( + await MessageUtils.build_message( [ - Mention(user_id), - Text(f"{ban_result}\n在..在 {time_str} 后才会理你喔"), + At(flag="user", target=user_id), + f"{ban_result}\n在..在 {time_str} 后才会理你喔", ] ).send() raise IgnoredException("用户处于黑名单中...") diff --git a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py index a279d9d0..dd42e3ea 100644 --- a/zhenxun/builtin_plugins/hooks/chkdsk_hook.py +++ b/zhenxun/builtin_plugins/hooks/chkdsk_hook.py @@ -1,20 +1,19 @@ import time from collections import defaultdict -from click import command -from nonebot.adapters.onebot.v11 import ActionFailed, Bot, GroupMessageEvent +from nonebot.adapters.onebot.v11 import Bot from nonebot.exception import IgnoredException from nonebot.matcher import Matcher from nonebot.message import run_preprocessor from nonebot.typing import T_State -from nonebot_plugin_alconna import Arparma -from nonebot_plugin_saa import Mention, MessageFactory, Text +from nonebot_plugin_alconna import At from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.models.ban_console import BanConsole from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils malicious_check_time = Config.get_config("hook", "MALICIOUS_CHECK_TIME") malicious_ban_count = Config.get_config("hook", "MALICIOUS_BAN_COUNT") @@ -88,10 +87,10 @@ async def _(matcher: Matcher, bot: Bot, session: EventSession, state: T_State): "HOOK", session=session, ) - await MessageFactory( + await MessageUtils.build_message( [ - Mention(user_id), - Text(f"检测到恶意触发命令,您将被封禁 30 分钟"), + At(flag="user", target=user_id), + f"检测到恶意触发命令,您将被封禁 30 分钟", ] ).send() logger.debug( diff --git a/zhenxun/builtin_plugins/record_request.py b/zhenxun/builtin_plugins/record_request.py index 0628dca7..e894d61e 100644 --- a/zhenxun/builtin_plugins/record_request.py +++ b/zhenxun/builtin_plugins/record_request.py @@ -9,7 +9,6 @@ from nonebot.adapters.onebot.v11 import FriendRequestEvent, GroupRequestEvent from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import TargetQQPrivate, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME, Config @@ -19,6 +18,8 @@ from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType, RequestHandleType, RequestType +from zhenxun.utils.message import MessageUtils +from zhenxun.utils.platform import PlatformUtils base_config = Config.get("invite_manager") @@ -76,14 +77,15 @@ async def _(bot: v12Bot | v11Bot, event: FriendRequestEvent, session: EventSessi # sex = user["sex"] # age = str(user["age"]) comment = event.comment - superuser = int(superuser) - await Text( - f"*****一份好友申请*****\n" - f"昵称:{nickname}({event.user_id})\n" - f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n" - f"日期:{str(datetime.now()).split('.')[0]}\n" - f"备注:{event.comment}" - ).send_to(target=TargetQQPrivate(user_id=superuser), bot=bot) + if superuser: + superuser = int(superuser) + await MessageUtils.build_message( + f"*****一份好友申请*****\n" + f"昵称:{nickname}({event.user_id})\n" + f"自动同意:{'√' if base_config.get('AUTO_ADD_FRIEND') else '×'}\n" + f"日期:{str(datetime.now()).split('.')[0]}\n" + f"备注:{event.comment}" + ).send(target=PlatformUtils.get_target(bot, superuser)) if base_config.get("AUTO_ADD_FRIEND"): logger.debug( f"已开启好友请求自动同意,成功通过该请求", diff --git a/zhenxun/builtin_plugins/scheduler/morning.py b/zhenxun/builtin_plugins/scheduler/morning.py index edbc959f..d12e0765 100644 --- a/zhenxun/builtin_plugins/scheduler/morning.py +++ b/zhenxun/builtin_plugins/scheduler/morning.py @@ -58,8 +58,8 @@ async def _(): # # 睡觉了 @scheduler.scheduled_job( "cron", - hour=1, - minute=16, + hour=23, + minute=59, ) async def _(): message = MessageUtils.build_message( diff --git a/zhenxun/builtin_plugins/shop/_data_source.py b/zhenxun/builtin_plugins/shop/_data_source.py index 8f509631..c7b5c68b 100644 --- a/zhenxun/builtin_plugins/shop/_data_source.py +++ b/zhenxun/builtin_plugins/shop/_data_source.py @@ -6,7 +6,6 @@ from typing import Any, Callable, Literal from nonebot.adapters import Bot, Event from nonebot_plugin_alconna import UniMessage, UniMsg -from nonebot_plugin_saa import MessageFactory from nonebot_plugin_session import EventSession from pydantic import BaseModel, create_model @@ -298,8 +297,9 @@ class ShopManage: raise ValueError("该商品使用函数已被注册!") kwargs["send_success_msg"] = send_success_msg kwargs["max_num_limit"] = max_num_limit + # TODO: create_model(f"{uuid}_model", __base__=ShopParam, **kwargs) cls.uuid2goods[uuid] = Goods( - model=create_model(f"{uuid}_model", __base__=ShopParam, **kwargs), + model=None,# create_model(f"{uuid}_model", __base__=ShopParam, **kwargs), params=kwargs, before_handle=before_handle, after_handle=after_handle, diff --git a/zhenxun/builtin_plugins/sign_in/_data_source.py b/zhenxun/builtin_plugins/sign_in/_data_source.py index 3fdf38f2..539972c8 100644 --- a/zhenxun/builtin_plugins/sign_in/_data_source.py +++ b/zhenxun/builtin_plugins/sign_in/_data_source.py @@ -17,7 +17,6 @@ from zhenxun.utils.image_utils import BuildImage, ImageTemplate from zhenxun.utils.utils import get_user_avatar from ._random_event import random_event -from .goods_register import driver from .utils import get_card ICON_PATH = IMAGE_PATH / "_icon" diff --git a/zhenxun/builtin_plugins/sign_in/utils.py b/zhenxun/builtin_plugins/sign_in/utils.py index cd9380a8..b8fe2853 100644 --- a/zhenxun/builtin_plugins/sign_in/utils.py +++ b/zhenxun/builtin_plugins/sign_in/utils.py @@ -172,8 +172,8 @@ async def _generate_card( uid_img = await BuildImage.build_text_image( f"UID: {uid}", size=30, font_color=(255, 255, 255) ) - image1 = await bk.build_text_image("Accumulative check-in for", bk.font) - image2 = await bk.build_text_image("days", bk.font) + image1 = await bk.build_text_image("Accumulative check-in for", bk.font, size=30) + image2 = await bk.build_text_image("days", bk.font, size=30) sign_day_img = await BuildImage.build_text_image( f"{user.sign_count}", size=40, font_color=(211, 64, 33) ) @@ -181,8 +181,8 @@ async def _generate_card( tip_height = max([image1.height, image2.height, sign_day_img.height]) tip_image = BuildImage(tip_width, tip_height, (255, 255, 255, 0)) await tip_image.paste(image1, (0, 7)) - await tip_image.paste(sign_day_img, (image1.width + 15, 0)) - await tip_image.paste(image2, (image1.width + sign_day_img.width + 30, 7)) + await tip_image.paste(sign_day_img, (image1.width + 7, 0)) + await tip_image.paste(image2, (image1.width + sign_day_img.width + 15, 7)) lik_text1_img = await BuildImage.build_text_image("当前", size=20) lik_text2_img = await BuildImage.build_text_image( diff --git a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py index 9b823f54..7395ff90 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/__init__.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/__init__.py @@ -7,13 +7,12 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Text as alcText from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession -from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import BroadcastManage @@ -56,10 +55,10 @@ async def _( if isinstance(msg, alcText) and msg.text.strip().startswith(command[0]): msg.text = msg.text.replace(command[0], "", 1).strip() break - await Text("正在发送..请等一下哦!").send() + await MessageUtils.build_message("正在发送..请等一下哦!").send() count, error_count = await BroadcastManage.send(bot, message, session) result = f"成功广播 {count} 个群组" if error_count: result += f"\n广播失败 {error_count} 个群组" - await Text(f"发送广播完成!\n{result}").send(reply=True) + await MessageUtils.build_message(f"发送广播完成!\n{result}").send(reply_to=True) logger.info(f"发送广播信息: {message}", "广播", session=session) diff --git a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py index 1544506b..617f0d44 100644 --- a/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py +++ b/zhenxun/builtin_plugins/superuser/broadcast/_data_source.py @@ -1,17 +1,17 @@ import nonebot_plugin_alconna as alc from nonebot.adapters import Bot -from nonebot.adapters.discord import Bot as DiscordBot -from nonebot.adapters.dodo import Bot as DodoBot -from nonebot.adapters.kaiheila import Bot as KaiheilaBot -from nonebot.adapters.onebot.v11 import Bot as v11Bot -from nonebot.adapters.onebot.v12 import Bot as v12Bot -from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Image, MessageFactory, Text + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot +# from nonebot.adapters.onebot.v11 import Bot as v11Bot +# from nonebot.adapters.onebot.v12 import Bot as v12Bot +from nonebot_plugin_alconna import Image, UniMsg from nonebot_plugin_session import EventSession -from zhenxun.models.group_console import GroupConsole from zhenxun.models.task_info import TaskInfo from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils @@ -34,9 +34,9 @@ class BroadcastManage: message_list = [] for msg in message: if isinstance(msg, alc.Image) and msg.url: - message_list.append(Image(msg.url)) + message_list.append(Image(url=msg.url)) elif isinstance(msg, alc.Text): - message_list.append(Text(msg.text)) + message_list.append(msg.text) group_list, _ = await PlatformUtils.get_group_list(bot) if group_list: error_count = 0 @@ -50,7 +50,9 @@ class BroadcastManage: bot, None, group.channel_id or group.group_id ) if target: - await MessageFactory(message_list).send_to(target, bot) + await MessageUtils.build_message(message_list).send( + target, bot + ) logger.debug( "发送成功", "广播", diff --git a/zhenxun/builtin_plugins/superuser/clear_data.py b/zhenxun/builtin_plugins/superuser/clear_data.py index 5455cfcb..10770bf9 100644 --- a/zhenxun/builtin_plugins/superuser/clear_data.py +++ b/zhenxun/builtin_plugins/superuser/clear_data.py @@ -7,13 +7,13 @@ from nonebot.rule import to_me from nonebot.utils import run_sync from nonebot_plugin_alconna import Alconna, on_alconna from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.path_config import TEMP_PATH from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.utils import ResourceDirManager __plugin_meta__ = PluginMetadata( @@ -44,11 +44,14 @@ ResourceDirManager.add_temp_dir(TEMP_PATH, True) @_matcher.handle() async def _(session: EventSession): - await Text("开始清理临时数据...").send() + await MessageUtils.build_message("开始清理临时数据...").send() size = await _clear_data() - await Text("共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024)).send() + await MessageUtils.build_message( + "共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024) + ).send() logger.info( - "清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), session=session + "清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), + session=session, ) @@ -71,8 +74,14 @@ def _clear_data() -> float: dir_size += file_size logger.debug(f"移除临时文件: {file.absolute()}", "清理临时数据") except Exception as e: - logger.error(f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", "清理临时数据", e=e) - logger.debug("清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据") + logger.error( + f"清理临时数据错误,临时文件夹: {dir_.absolute()}...", + "清理临时数据", + e=e, + ) + logger.debug( + "清理临时文件夹大小: {:.2f}MB".format(size / 1024 / 1024), "清理临时数据" + ) return float(size) @@ -83,4 +92,7 @@ def _clear_data() -> float: ) async def _(): size = await _clear_data() - logger.info("自动清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), "定时任务") + logger.info( + "自动清理临时数据完成,共清理了 {:.2f}MB 的数据...".format(size / 1024 / 1024), + "定时任务", + ) diff --git a/zhenxun/builtin_plugins/superuser/fg_manage.py b/zhenxun/builtin_plugins/superuser/fg_manage.py index e2d30344..e2ff51d7 100644 --- a/zhenxun/builtin_plugins/superuser/fg_manage.py +++ b/zhenxun/builtin_plugins/superuser/fg_manage.py @@ -4,12 +4,12 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import admin_check, ensure_group __plugin_meta__ = PluginMetadata( @@ -70,13 +70,13 @@ async def _( msg = ["{user_id} {nickname}".format_map(g) for g in fl] msg = "\n".join(msg) msg = f"| UID | 昵称 | 共{len(fl)}个好友\n" + msg - await Text(msg).send() + await MessageUtils.build_message(msg).send() logger.info("查看好友列表", "好友列表", session=session) except (ApiNotAvailable, AttributeError) as e: - await Text("Api未实现...").send() + await MessageUtils.build_message("Api未实现...").send() except Exception as e: logger.error("好友列表发生错误", "好友列表", session=session, e=e) - await Text("其他未知错误...").send() + await MessageUtils.build_message("其他未知错误...").send() @_group_matcher.handle() @@ -90,10 +90,10 @@ async def _( msg = ["{group_id} {group_name}".format_map(g) for g in gl] msg = "\n".join(msg) msg = f"| GID | 名称 | 共{len(gl)}个群组\n" + msg - await Text(msg).send() + await MessageUtils.build_message(msg).send() logger.info("查看群组列表", "群组列表", session=session) except (ApiNotAvailable, AttributeError) as e: - await Text("Api未实现...").send() + await MessageUtils.build_message("Api未实现...").send() except Exception as e: logger.error("查看群组列表发生错误", "群组列表", session=session, e=e) - await Text("其他未知错误...").send() + await MessageUtils.build_message("其他未知错误...").send() diff --git a/zhenxun/builtin_plugins/superuser/group_manage.py b/zhenxun/builtin_plugins/superuser/group_manage.py index 1b207c58..41044f94 100644 --- a/zhenxun/builtin_plugins/superuser/group_manage.py +++ b/zhenxun/builtin_plugins/superuser/group_manage.py @@ -14,7 +14,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME @@ -22,6 +21,7 @@ from zhenxun.configs.utils import PluginExtraData from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="管理群操作", @@ -139,7 +139,7 @@ def CheckGroupId(): if group_id.available: gid = group_id.result if not gid: - await Text("群组id不能为空...").finish() + await MessageUtils.build_message("群组id不能为空...").finish() state["group_id"] = gid return Depends(dependency) @@ -152,7 +152,7 @@ async def _(session: EventSession, arparma: Arparma, state: T_State, level: int) old_level = group.level group.level = level await group.save(update_fields=["level"]) - await Text("群权限修改成功!").send(reply=True) + await MessageUtils.build_message("群权限修改成功!").send(reply_to=True) logger.info( f"修改群权限: {old_level} -> {level}", arparma.header_result, @@ -166,11 +166,11 @@ async def _(session: EventSession, arparma: Arparma, state: T_State): gid = state["group_id"] group = await GroupConsole.get_or_none(group_id=gid) if not group: - await Text("群组信息不存在, 请更新群组信息...").finish() + await MessageUtils.build_message("群组信息不存在, 请更新群组信息...").finish() s = "删除" if arparma.find("del") else "添加" group.is_super = not arparma.find("del") await group.save(update_fields=["is_super"]) - await Text(f"{s}群白名单成功!").send(reply=True) + await MessageUtils.build_message(f"{s}群白名单成功!").send(reply_to=True) logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) @@ -181,7 +181,7 @@ async def _(session: EventSession, arparma: Arparma, state: T_State): group_id=gid, defaults={"group_flag": 0 if arparma.find("del") else 1} ) s = "删除" if arparma.find("del") else "添加" - await Text(f"{s}群认证成功!").send(reply=True) + await MessageUtils.build_message(f"{s}群认证成功!").send(reply_to=True) logger.info(f"{s}群白名单", arparma.header_result, session=session, target=gid) @@ -191,17 +191,17 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, group_id: int): group_list = [g["group_id"] for g in await bot.get_group_list()] if group_id not in group_list: logger.debug("群组不存在", "退群", session=session, target=group_id) - await Text(f"{NICKNAME}未在该群组中...").finish() + await MessageUtils.build_message(f"{NICKNAME}未在该群组中...").finish() try: await bot.set_group_leave(group_id=group_id) logger.info( f"{NICKNAME}退出群组成功", "退群", session=session, target=group_id ) - await Text(f"退出群组 {group_id} 成功!").send() + await MessageUtils.build_message(f"退出群组 {group_id} 成功!").send() await GroupConsole.filter(group_id=group_id).delete() except Exception as e: logger.error(f"退出群组失败", "退群", session=session, target=group_id, e=e) - await Text(f"退出群组 {group_id} 失败...").send() + await MessageUtils.build_message(f"退出群组 {group_id} 失败...").send() else: # TODO: 其他平台的退群操作 - await Text(f"暂未支持退群操作...").send() + await MessageUtils.build_message(f"暂未支持退群操作...").send() diff --git a/zhenxun/builtin_plugins/superuser/reload_setting.py b/zhenxun/builtin_plugins/superuser/reload_setting.py index e32c7b0c..d4c6d3a1 100644 --- a/zhenxun/builtin_plugins/superuser/reload_setting.py +++ b/zhenxun/builtin_plugins/superuser/reload_setting.py @@ -3,13 +3,13 @@ from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="重载配置", @@ -55,7 +55,7 @@ _matcher = on_alconna( async def _(session: EventSession, arparma: Arparma): Config.reload() logger.debug("自动重载配置文件", arparma.header_result, session=session) - await Text("重载完成!").send(reply=True) + await MessageUtils.build_message("重载完成!").send(reply_to=True) @scheduler.scheduled_job( diff --git a/zhenxun/builtin_plugins/superuser/set_admin.py b/zhenxun/builtin_plugins/superuser/set_admin.py index 3696fc2a..033d7a3d 100644 --- a/zhenxun/builtin_plugins/superuser/set_admin.py +++ b/zhenxun/builtin_plugins/superuser/set_admin.py @@ -9,13 +9,13 @@ from nonebot_plugin_alconna import ( Subcommand, on_alconna, ) -from nonebot_plugin_saa import Mention, MessageFactory, Text from nonebot_plugin_session import EventSession, SessionLevel from zhenxun.configs.utils import PluginExtraData from zhenxun.models.level_user import LevelUser from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="用户权限管理", @@ -43,7 +43,9 @@ _matcher = on_alconna( Alconna( "权限设置", Subcommand( - "add", Args["level", int]["uid", [str, At]]["gid?", str], help_text="添加权限" + "add", + Args["level", int]["uid", [str, At]]["gid?", str], + help_text="添加权限", ), Subcommand("delete", Args["uid", [str, At]]["gid?", str], help_text="删除权限"), ), @@ -72,13 +74,17 @@ async def _( f"修改权限: {old_level} -> {level}", arparma.header_result, session=session ) if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: - await MessageFactory( - [Text("成功为 "), Mention(uid), Text(f" 设置权限:{old_level} -> {level}")] - ).finish(reply=True) - await Text( + await MessageUtils.build_message( + [ + "成功为 ", + At(flag="user", target=uid), + f" 设置权限:{old_level} -> {level}", + ] + ).finish(reply_to=True) + await MessageUtils.build_message( f"成功为 \n群组:{group_id}\n用户:{uid} \n设置权限!\n权限:{old_level} -> {level}" ).finish() - await Text(f"设置权限时群组不能为空...").finish() + await MessageUtils.build_message(f"设置权限时群组不能为空...").finish() @_matcher.assign("delete") @@ -95,11 +101,21 @@ async def _( if user := await LevelUser.get_or_none(user_id=uid, group_id=group_id): await user.delete() if session.level in [SessionLevel.LEVEL2, SessionLevel.LEVEL3]: - await MessageFactory( - [Text("成功删除 "), Mention(uid), Text(f" 的权限等级!")] - ).finish(reply=True) - await Text( + logger.info( + f"删除权限: {user.user_level} -> 0", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( + ["成功删除 ", At(flag="user", target=uid), f" 的权限等级!"] + ).finish(reply_to=True) + logger.info( + f"删除群组用户权限: {user.user_level} -> 0", + arparma.header_result, + session=session, + ) + await MessageUtils.build_message( f"成功删除 \n群组:{group_id}\n用户:{uid} \n的权限等级!\n权限:{user.user_level} -> 0" ).finish() - await Text(f"对方目前暂无权限喔...").finish() - await Text(f"设置权限时群组不能为空...").finish() + await MessageUtils.build_message(f"对方目前暂无权限喔...").finish() + await MessageUtils.build_message(f"设置权限时群组不能为空...").finish() diff --git a/zhenxun/builtin_plugins/superuser/update_fg_info.py b/zhenxun/builtin_plugins/superuser/update_fg_info.py index 0cc1be36..6afac3d4 100644 --- a/zhenxun/builtin_plugins/superuser/update_fg_info.py +++ b/zhenxun/builtin_plugins/superuser/update_fg_info.py @@ -3,12 +3,12 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils __plugin_meta__ = PluginMetadata( @@ -60,12 +60,12 @@ async def _( arparma.header_result, session=session, ) - await Text(f"成功更新了 {num} 个群组的信息").send() + await MessageUtils.build_message(f"成功更新了 {num} 个群组的信息").send() except Exception as e: logger.error( "更新群组信息发生错误", arparma.header_result, session=session, e=e ) - await Text("其他未知错误...").send() + await MessageUtils.build_message("其他未知错误...").send() @_friend_matcher.handle() @@ -75,15 +75,15 @@ async def _( arparma: Arparma, ): try: - num = await PlatformUtils.update_friend(bot, session.platform) + num = await PlatformUtils.update_friend(bot) logger.info( f"更新好友信息完成,共更新了 {num} 个好友的信息!", arparma.header_result, session=session, ) - await Text(f"成功更新了 {num} 个好友的信息").send() + await MessageUtils.build_message(f"成功更新了 {num} 个好友的信息").send() except Exception as e: logger.error( "更新好友信息发生错误", arparma.header_result, session=session, e=e ) - await Text("其他未知错误...").send() + await MessageUtils.build_message("其他未知错误...").send() diff --git a/zhenxun/plugins/about.py b/zhenxun/plugins/about.py index 35a0d237..7c3b923e 100644 --- a/zhenxun/plugins/about.py +++ b/zhenxun/plugins/about.py @@ -1,14 +1,13 @@ from pathlib import Path -from nonebot import on_regex from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="识番", @@ -38,5 +37,5 @@ async def _(session: EventSession, arparma: Arparma): 项目地址:https://github.com/HibiKier/zhenxun_bot 文档地址:https://hibikier.github.io/zhenxun_bot/ """.strip() - await Text(info).send() + await MessageUtils.build_message(info).send() logger.info("查看关于", arparma.header_result, session=session) diff --git a/zhenxun/plugins/alapi/comments_163.py b/zhenxun/plugins/alapi/comments_163.py index bac2587e..d05a5aa9 100644 --- a/zhenxun/plugins/alapi/comments_163.py +++ b/zhenxun/plugins/alapi/comments_163.py @@ -1,11 +1,11 @@ from nonebot import on_regex from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import get_data @@ -43,11 +43,13 @@ _matcher.shortcut( async def _(session: EventSession, arparma: Arparma): data, code = await get_data(comments_163_url) if code != 200 and isinstance(data, str): - await Text(data).finish(reply=True) + await MessageUtils.build_message(data).finish(reply_to=True) data = data["data"] # type: ignore comment = data["comment_content"] # type: ignore song_name = data["title"] # type: ignore - await Text(f"{comment}\n\t——《{song_name}》").send(reply=True) + await MessageUtils.build_message(f"{comment}\n\t——《{song_name}》").send( + reply_to=True + ) logger.info( f" 发送网易云热评: {comment} \n\t\t————{song_name}", arparma.header_result, diff --git a/zhenxun/plugins/alapi/jitang.py b/zhenxun/plugins/alapi/jitang.py index a0a29e5f..63dc93e2 100644 --- a/zhenxun/plugins/alapi/jitang.py +++ b/zhenxun/plugins/alapi/jitang.py @@ -1,10 +1,10 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import get_data @@ -36,13 +36,13 @@ async def _(session: EventSession, arparma: Arparma): try: data, code = await get_data(url) if code != 200 and isinstance(data, str): - await Text(data).finish(reply=True) - await Text(data["data"]["content"]).send(reply=True) # type: ignore + await MessageUtils.build_message(data).finish(reply_to=True) + await MessageUtils.build_message(data["data"]["content"]).send(reply_to=True) # type: ignore logger.info( f" 发送鸡汤:" + data["data"]["content"], # type:ignore arparma.header_result, session=session, ) except Exception as e: - await Text("鸡汤煮坏掉了...").send() + await MessageUtils.build_message("鸡汤煮坏掉了...").send() logger.error(f"鸡汤煮坏掉了", e=e) diff --git a/zhenxun/plugins/alapi/poetry.py b/zhenxun/plugins/alapi/poetry.py index f315999f..4d359498 100644 --- a/zhenxun/plugins/alapi/poetry.py +++ b/zhenxun/plugins/alapi/poetry.py @@ -1,10 +1,10 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import get_data @@ -42,12 +42,14 @@ poetry_url = "https://v2.alapi.cn/api/shici" async def _(session: EventSession, arparma: Arparma): data, code = await get_data(poetry_url) if code != 200 and isinstance(data, str): - await Text(data).finish(reply=True) + await MessageUtils.build_message(data).finish(reply_to=True) data = data["data"] # type: ignore content = data["content"] # type: ignore title = data["origin"] # type: ignore author = data["author"] # type: ignore - await Text(f"{content}\n\t——{author}《{title}》").send(reply=True) + await MessageUtils.build_message(f"{content}\n\t——{author}《{title}》").send( + reply_to=True + ) logger.info( f" 发送古诗: f'{content}\n\t--{author}《{title}》'", arparma.header_result, diff --git a/zhenxun/plugins/bt/__init__.py b/zhenxun/plugins/bt/__init__.py index 96d82308..3aff4f8b 100644 --- a/zhenxun/plugins/bt/__init__.py +++ b/zhenxun/plugins/bt/__init__.py @@ -3,11 +3,11 @@ from asyncio.exceptions import TimeoutError from httpx import ConnectTimeout from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import ensure_private from .data_source import get_bt_info @@ -58,7 +58,7 @@ async def _( async for title, type_, create_time, file_size, link in get_bt_info( keyword, page.result if page.available else 1 ): - await Text( + await MessageUtils.build_message( f"标题:{title}\n" f"类型:{type_}\n" f"创建时间:{create_time}\n" @@ -67,12 +67,12 @@ async def _( ).send() send_flag = True except (TimeoutError, ConnectTimeout): - await Text(f"搜索 {keyword} 超时...").finish() + await MessageUtils.build_message(f"搜索 {keyword} 超时...").finish() except Exception as e: logger.error(f"bt 错误", arparma.header_result, session=session, e=e) - await Text(f"bt 其他未知错误..").finish() + await MessageUtils.build_message(f"bt 其他未知错误..").finish() if not send_flag: - await Text(f"{keyword} 未搜索到...").send() + await MessageUtils.build_message(f"{keyword} 未搜索到...").send() logger.info( f"BT搜索 {keyword} 第 {page} 页", arparma.header_result, session=session ) diff --git a/zhenxun/plugins/check/__init__.py b/zhenxun/plugins/check/__init__.py index 198f9f60..ee63b50d 100644 --- a/zhenxun/plugins/check/__init__.py +++ b/zhenxun/plugins/check/__init__.py @@ -2,7 +2,6 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Image from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py index 527cffad..d89eb017 100644 --- a/zhenxun/plugins/dialogue/__init__.py +++ b/zhenxun/plugins/dialogue/__init__.py @@ -7,13 +7,13 @@ from nonebot_plugin_alconna import At as alcAt from nonebot_plugin_alconna import Target from nonebot_plugin_alconna import Text as alcText from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.utils import PluginExtraData from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from ._data_source import DialogueManage @@ -69,9 +69,9 @@ async def _( if platform == "discord": superuser_id = config.platform_superusers["discord"][0] except IndexError: - await Text("管理员失联啦...").finish() + await MessageUtils.build_message("管理员失联啦...").finish() if not superuser_id: - await Text("管理员失联啦...").finish() + await MessageUtils.build_message("管理员失联啦...").finish() uname = user_info.user_displayname or user_info.user_name group_name = "" gid = session.id3 or session.id2 @@ -89,9 +89,9 @@ async def _( message.insert(0, alcText("*****一份交流报告*****\n")) DialogueManage.add(uname, session.id1, gid, group_name, message, platform) await message.send(bot=bot, target=Target(superuser_id, private=True)) - await Text("已成功发送给管理员啦!").send(reply=True) + await MessageUtils.build_message("已成功发送给管理员啦!").send(reply_to=True) else: - await Text("用户id为空...").send() + await MessageUtils.build_message("用户id为空...").send() @_reply_matcher.handle() @@ -99,7 +99,6 @@ async def _( bot: Bot, message: UniMsg, session: EventSession, - user_info: UserInfo = EventUserInfo(), ): message[0] = alcText(str(message[0]).replace("/t", "", 1).strip()) if session.id1: @@ -108,7 +107,7 @@ async def _( platform = PlatformUtils.get_platform(bot) data = DialogueManage._data if not data: - await Text("暂无待回复消息...").finish() + await MessageUtils.build_message("暂无待回复消息...").finish() if platform: data = [data[d] for d in data if data[d].platform == platform] for d in data: @@ -127,7 +126,7 @@ async def _( user_id = model.user_id group_id = model.group_id else: - return Text("未获取此id数据").finish() + return MessageUtils.build_message("未获取此id数据").finish() message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) else: user_id = 0 @@ -137,7 +136,9 @@ async def _( " ".join(str(message[0]).split(" ")[2:]) ) else: - await Text("群组id错误...").finish(at_sender=True) + await MessageUtils.build_message("群组id错误...").finish( + at_sender=True + ) DialogueManage.remove(_id) else: user_id = msg[0] @@ -148,17 +149,17 @@ async def _( group_id = 0 message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) else: - await Text("参数错误...").finish(at_sender=True) + await MessageUtils.build_message("参数错误...").finish(at_sender=True) if group_id: if user_id: message.insert(0, alcAt("user", user_id)) message.insert(1, "\n管理员回复\n=======\n") await message.send(Target(group_id), bot) - await Text("消息发送成功!").finish(at_sender=True) + await MessageUtils.build_message("消息发送成功!").finish(at_sender=True) elif user_id: await message.send(Target(user_id, private=True), bot) - await Text("消息发送成功!").finish(at_sender=True) + await MessageUtils.build_message("消息发送成功!").finish(at_sender=True) else: - await Text("群组id与用户id为空...").send() + await MessageUtils.build_message("群组id与用户id为空...").send() else: - await Text("用户id为空...").send() + await MessageUtils.build_message("用户id为空...").send() diff --git a/zhenxun/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py index e8e68aad..2aeeb30e 100644 --- a/zhenxun/plugins/draw_card/__init__.py +++ b/zhenxun/plugins/draw_card/__init__.py @@ -13,11 +13,11 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.typing import T_Handler from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData +from zhenxun.utils.message import MessageUtils from .handles.azur_handle import AzurHandle from .handles.ba_handle import BaHandle @@ -165,13 +165,17 @@ def create_matchers(): try: num = int(cn2an(num, mode="smart")) except ValueError: - await Text("必!须!是!数!字!").finish(reply=True) + await MessageUtils.build_message("必!须!是!数!字!").finish( + reply_to=True + ) if unit == "井": num *= game.max_count if num < 1: - await Text("虚空抽卡???").finish(reply=True) + await MessageUtils.build_message("虚空抽卡???").finish(reply_to=True) elif num > game.max_count: - await Text("一井都满不足不了你嘛!快爬开!").finish(reply=True) + await MessageUtils.build_message( + "一井都满不足不了你嘛!快爬开!" + ).finish(reply_to=True) pool_name = ( pool_name.replace("池", "") .replace("武器", "arms") @@ -192,7 +196,7 @@ def create_matchers(): ) except: logger.warning(traceback.format_exc()) - await Text("出错了...").finish(reply=True) + await MessageUtils.build_message("出错了...").finish(reply_to=True) await res.send() return handler @@ -215,9 +219,9 @@ def create_matchers(): def reset_handler(game: Game) -> T_Handler: async def handler(matcher: Matcher, session: EventSession): if not session.id1: - await Text("获取用户id失败...").finish() + await MessageUtils.build_message("获取用户id失败...").finish() if game.handle.reset_count(session.id1): - await Text("重置成功!").send() + await MessageUtils.build_message("重置成功!").send() return handler diff --git a/zhenxun/plugins/draw_card/handles/azur_handle.py b/zhenxun/plugins/draw_card/handles/azur_handle.py index 06949085..67242a77 100644 --- a/zhenxun/plugins/draw_card/handles/azur_handle.py +++ b/zhenxun/plugins/draw_card/handles/azur_handle.py @@ -4,12 +4,13 @@ from urllib.parse import unquote import dateparser import ujson as json from lxml import etree -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import ImageDraw from pydantic import ValidationError from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import draw_config from ..util import cn2py, load_font, remove_prohibited_str @@ -97,13 +98,13 @@ class AzurHandle(BaseHandle[AzurChar]): ) return acquire_char - async def draw(self, count: int, **kwargs) -> MessageFactory: + async def draw(self, count: int, **kwargs) -> UniMessage: index2card = self.get_cards(count, **kwargs) cards = [card[0] for card in index2card] up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else [] result = self.format_result(index2card, **{**kwargs, "up_list": up_list}) gen_img = await self.generate_img(cards) - return MessageFactory([Image(gen_img.pic2bytes()), Text(result)]) + return MessageUtils.build_message([gen_img.pic2bytes(), result]) async def generate_card_img(self, card: AzurChar) -> BuildImage: sep_w = 5 @@ -298,10 +299,10 @@ class AzurHandle(BaseHandle[AzurChar]): except Exception as e: logger.warning(f"{self.game_name_cn}UP更新出错", e=e) - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: await self.update_up_char() self.load_up_char() if self.UP_EVENT: - return MessageFactory( - [Text(f"重载成功!\n当前活动:{self.UP_EVENT.title}")] + return MessageUtils.build_message( + f"重载成功!\n当前活动:{self.UP_EVENT.title}" ) diff --git a/zhenxun/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py index 61b0d78f..d347504a 100644 --- a/zhenxun/plugins/draw_card/handles/ba_handle.py +++ b/zhenxun/plugins/draw_card/handles/ba_handle.py @@ -61,8 +61,10 @@ class BaHandle(BaseHandle[BaChar]): bar_h = 20 bar_w = 90 bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5") - img_path = str(self.img_path / f"{cn2py(card.name)}.png") - img = BuildImage(img_w, img_h, background=img_path) + img_path = self.img_path / f"{cn2py(card.name)}.png" + img = BuildImage( + img_w, img_h, background=img_path if img_path.exists() else None + ) bar = BuildImage(bar_w, bar_h, color="#6495ED") await bg.paste(img, (sep_w, sep_h)) await bg.paste(bar, (sep_w, img_h - bar_h + sep_h)) diff --git a/zhenxun/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py index 28c39807..3483d246 100644 --- a/zhenxun/plugins/draw_card/handles/base_handle.py +++ b/zhenxun/plugins/draw_card/handles/base_handle.py @@ -7,14 +7,14 @@ from typing import Generic, TypeVar import aiohttp import anyio import ujson as json -from nonebot_plugin_saa import Image as SaaImage -from nonebot_plugin_saa import MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import Image from pydantic import BaseModel, Extra from zhenxun.configs.path_config import DATA_PATH from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import DRAW_PATH, draw_config from ..util import circled_number, cn2py @@ -64,12 +64,12 @@ class BaseHandle(Generic[TC]): self.up_path.mkdir(parents=True, exist_ok=True) self.data_files: list[str] = [f"{self.game_name}.json"] - async def draw(self, count: int, **kwargs) -> MessageFactory: + async def draw(self, count: int, **kwargs) -> UniMessage: index2card = self.get_cards(count, **kwargs) cards = [card[0] for card in index2card] result = self.format_result(index2card) gen_img = await self.generate_img(cards) - return MessageFactory([SaaImage(gen_img.pic2bytes()), Text(result)]) + return MessageUtils.build_message([gen_img, result]) # 抽取卡池 def get_card(self, **kwargs) -> TC: @@ -280,10 +280,10 @@ class BaseHandle(Generic[TC]): ) return False - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: return None - async def reload_pool(self) -> MessageFactory | None: + async def reload_pool(self) -> UniMessage | None: try: async with self.client() as session: self.session = session diff --git a/zhenxun/plugins/draw_card/handles/genshin_handle.py b/zhenxun/plugins/draw_card/handles/genshin_handle.py index 3298e988..61edcf30 100644 --- a/zhenxun/plugins/draw_card/handles/genshin_handle.py +++ b/zhenxun/plugins/draw_card/handles/genshin_handle.py @@ -5,13 +5,13 @@ from urllib.parse import unquote import dateparser import ujson as json from lxml import etree -from nonebot_plugin_saa import Image as SaaImage -from nonebot_plugin_saa import MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import Image, ImageDraw from pydantic import ValidationError from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import draw_config from ..count_manager import GenshinCountManager @@ -218,7 +218,7 @@ class GenshinHandle(BaseHandle[GenshinData]): async def draw( self, count: int, user_id: int, pool_name: str = "", **kwargs - ) -> Text | MessageFactory: + ) -> UniMessage: card_index = 0 if "1" in pool_name: card_index = 1 @@ -228,7 +228,7 @@ class GenshinHandle(BaseHandle[GenshinData]): up_event = None if pool_name == "char": if card_index == 1 and len(self.UP_CHAR_LIST) == 1: - return Text("当前没有第二个角色UP池") + return MessageUtils.build_message("当前没有第二个角色UP池") up_event = self.UP_CHAR_LIST[card_index] elif pool_name == "arms": up_event = self.UP_ARMS @@ -249,7 +249,7 @@ class GenshinHandle(BaseHandle[GenshinData]): (0, img.height + 10), "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】", ) - return MessageFactory([Text(pool_info), SaaImage(bk.pic2bytes()), Text(result)]) + return MessageUtils.build_message([pool_info, bk, result]) def _init_data(self): self.ALL_CHAR = [ @@ -439,27 +439,23 @@ class GenshinHandle(BaseHandle[GenshinData]): self.count_manager.reset(user_id) return True - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: await self.update_up_char() self.load_up_char() if self.UP_CHAR_LIST and self.UP_ARMS: if len(self.UP_CHAR_LIST) > 1: - return MessageFactory( + return MessageUtils.build_message( [ - Text( - f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}" - ), - Image(self.UP_CHAR_LIST[0].pool_img), - Image(self.UP_CHAR_LIST[1].pool_img), - Image(self.UP_ARMS.pool_img), + f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}", + self.UP_CHAR_LIST[0].pool_img, + self.UP_CHAR_LIST[1].pool_img, + self.UP_ARMS.pool_img, ] ) - return MessageFactory( + return UniMessage( [ - Text( - f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}" - ), - Image(self.UP_CHAR_LIST[0].pool_img), - Image(self.UP_ARMS.pool_img), + f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}", + self.UP_CHAR_LIST[0].pool_img, + self.UP_ARMS.pool_img, ] ) diff --git a/zhenxun/plugins/draw_card/handles/guardian_handle.py b/zhenxun/plugins/draw_card/handles/guardian_handle.py index c24caa0b..517f126d 100644 --- a/zhenxun/plugins/draw_card/handles/guardian_handle.py +++ b/zhenxun/plugins/draw_card/handles/guardian_handle.py @@ -6,12 +6,13 @@ from urllib.parse import unquote import dateparser import ujson as json from lxml import etree -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import ImageDraw from pydantic import ValidationError from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import draw_config from ..util import cn2py, load_font, remove_prohibited_str @@ -132,7 +133,7 @@ class GuardianHandle(BaseHandle[GuardianData]): info = f"当前up池:{up_event.title}\n{info}" return info.strip() - async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory: + async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage: index2card = self.get_cards(count, pool_name) cards = [card[0] for card in index2card] up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS @@ -140,7 +141,7 @@ class GuardianHandle(BaseHandle[GuardianData]): result = self.format_result(index2card, up_list=up_list) pool_info = self.format_pool_info(pool_name) img = await self.generate_img(cards) - return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + return MessageUtils.build_message([pool_info, img, result]) async def generate_card_img(self, card: GuardianData) -> BuildImage: sep_w = 1 @@ -390,10 +391,10 @@ class GuardianHandle(BaseHandle[GuardianData]): except Exception as e: logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}") - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: await self.update_up_char() self.load_up_char() if self.UP_CHAR and self.UP_ARMS: - return MessageFactory( - [Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}")] + return MessageUtils.build_message( + f"重载成功!\n当前UP池子:{self.UP_CHAR.title}" ) diff --git a/zhenxun/plugins/draw_card/handles/pretty_handle.py b/zhenxun/plugins/draw_card/handles/pretty_handle.py index ecc2dfe3..535e2b19 100644 --- a/zhenxun/plugins/draw_card/handles/pretty_handle.py +++ b/zhenxun/plugins/draw_card/handles/pretty_handle.py @@ -7,12 +7,13 @@ import dateparser import ujson as json from bs4 import BeautifulSoup from lxml import etree -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import ImageDraw from pydantic import ValidationError from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import draw_config from ..util import cn2py, load_font, remove_prohibited_str @@ -125,7 +126,7 @@ class PrettyHandle(BaseHandle[PrettyData]): info = f"当前up池:{up_event.title}\n{info}" return info.strip() - async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory: + async def draw(self, count: int, pool_name: str, **kwargs) -> UniMessage: pool_name = "char" if not pool_name else pool_name index2card = self.get_cards(count, pool_name) cards = [card[0] for card in index2card] @@ -134,7 +135,7 @@ class PrettyHandle(BaseHandle[PrettyData]): result = self.format_result(index2card, up_list=up_list) pool_info = self.format_pool_info(pool_name) img = await self.generate_img(cards) - return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + return MessageUtils.build_message([pool_info, img, result]) async def generate_card_img(self, card: PrettyData) -> BuildImage: if isinstance(card, PrettyChar): @@ -409,14 +410,14 @@ class PrettyHandle(BaseHandle[PrettyData]): except Exception as e: logger.warning(f"{self.game_name_cn}UP更新出错", e=e) - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: await self.update_up_char() self.load_up_char() if self.UP_CHAR and self.UP_CARD: - return MessageFactory( + return MessageUtils.build_message( [ - Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}"), - Image(self.UP_CHAR.pool_img), - Image(self.UP_CARD.pool_img), + f"重载成功!\n当前UP池子:{self.UP_CHAR.title}", + self.UP_CHAR.pool_img, + self.UP_CARD.pool_img, ] ) diff --git a/zhenxun/plugins/draw_card/handles/prts_handle.py b/zhenxun/plugins/draw_card/handles/prts_handle.py index b06fc87c..18a86fc3 100644 --- a/zhenxun/plugins/draw_card/handles/prts_handle.py +++ b/zhenxun/plugins/draw_card/handles/prts_handle.py @@ -7,12 +7,13 @@ import dateparser import ujson as json from lxml import etree from lxml.etree import _Element -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from PIL import ImageDraw from pydantic import ValidationError from zhenxun.services.log import logger from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from ..config import draw_config from ..util import cn2py, load_font, remove_prohibited_str @@ -102,7 +103,7 @@ class PrtsHandle(BaseHandle[Operator]): info = f"当前up池: {self.UP_EVENT.title}\n{info}" return info.strip() - async def draw(self, count: int, **kwargs) -> MessageFactory: + async def draw(self, count: int, **kwargs) -> UniMessage: index2card = self.get_cards(count) """这里cards修复了抽卡图文不符的bug""" cards = [card[0] for card in index2card] @@ -110,7 +111,7 @@ class PrtsHandle(BaseHandle[Operator]): result = self.format_result(index2card, up_list=up_list) pool_info = self.format_pool_info() img = await self.generate_img(cards) - return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)]) + return MessageUtils.build_message([pool_info, img, result]) async def generate_card_img(self, card: Operator) -> BuildImage: sep_w = 5 @@ -331,13 +332,13 @@ class PrtsHandle(BaseHandle[Operator]): ) break - async def _reload_pool(self) -> MessageFactory | None: + async def _reload_pool(self) -> UniMessage | None: await self.update_up_char() self.load_up_char() if self.UP_EVENT: - return MessageFactory( + return MessageUtils.build_message( [ - Text(f"重载成功!\n当前UP池子:{self.UP_EVENT.title}"), - Image(self.UP_EVENT.pool_img), + f"重载成功!\n当前UP池子:{self.UP_EVENT.title}", + self.UP_EVENT.pool_img, ] ) diff --git a/zhenxun/plugins/epic/__init__.py b/zhenxun/plugins/epic/__init__.py index bc756244..23c084aa 100644 --- a/zhenxun/plugins/epic/__init__.py +++ b/zhenxun/plugins/epic/__init__.py @@ -2,13 +2,12 @@ from nonebot.adapters import Bot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.plugin import PluginMetadata -from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import MessageFactory, Text +from nonebot_plugin_alconna import Alconna, Arparma, UniMessage, on_alconna from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from .data_source import get_epic_free @@ -33,9 +32,9 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma): type_ = "Group" if gid else "Private" msg_list, code = await get_epic_free(bot, type_) if code == 404 and isinstance(msg_list, str): - await Text(msg_list).finish() + await MessageUtils.build_message(msg_list).finish() elif isinstance(bot, (v11Bot, v12Bot)) and isinstance(msg_list, list): await bot.send_group_forward_msg(group_id=gid, messages=msg_list) - elif isinstance(msg_list, MessageFactory): + elif isinstance(msg_list, UniMessage): await msg_list.send() logger.info(f"获取epic免费游戏", arparma.header_result, session=session) diff --git a/zhenxun/plugins/fudu.py b/zhenxun/plugins/fudu.py index 338b0c3a..6b1348a0 100644 --- a/zhenxun/plugins/fudu.py +++ b/zhenxun/plugins/fudu.py @@ -113,7 +113,7 @@ async def _(message: UniMsg, event: Event, session: EventSession): if not plain_text and not image_list: return if plain_text and plain_text.startswith(f"@可爱的{NICKNAME}"): - await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply=True) + await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply_to=True) if image_list: img_hash = await get_download_image_hash(image_list[0], group_id) else: diff --git a/zhenxun/plugins/gold_redbag/__init__.py b/zhenxun/plugins/gold_redbag/__init__.py index e3c851af..f46c6639 100644 --- a/zhenxun/plugins/gold_redbag/__init__.py +++ b/zhenxun/plugins/gold_redbag/__init__.py @@ -10,7 +10,6 @@ from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Args, Arparma, At, Match, Option, on_alconna from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME @@ -219,7 +218,7 @@ async def _( if send_msg else MessageUtils.build_message("没有红包给你开!") ) - await send_msg.send(reply=True) + await send_msg.send(reply_to=True) if settlement_list: for red_bag in settlement_list: result_image = await red_bag.build_amount_rank( @@ -263,7 +262,7 @@ async def _( f"已成功退还了 " f"{data[0]} 金币\n", image_result, ] - ).finish(reply=True) + ).finish(reply_to=True) await MessageUtils.build_message("目前没有红包可以退回...").finish(reply_to=True) @@ -297,16 +296,14 @@ async def _( FestiveRedBagManage.remove(festive_red_bag.uuid) rank_image = await festive_red_bag.build_amount_rank(10, platform) try: - await MessageFactory( + await MessageUtils.build_message( [ - Text( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{len(festive_red_bag.open_user)}" - f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" - ), - Image(rank_image.pic2bytes()), + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n", + rank_image, ] - ).send_to(target=target, bot=bot) + ).send(target=target, bot=bot) except ActionFailed: pass try: @@ -336,14 +333,12 @@ async def _( image_result = await RedBagManager.random_red_bag_background( bot.self_id, greetings, session.platform ) - await MessageFactory( + await MessageUtils.build_message( [ - Text( - f"{NICKNAME}发起了节日金币红包\n金额: {amount}\n数量: {num}\n" - ), - Image(image_result.pic2bytes()), + f"{NICKNAME}发起了节日金币红包\n金额: {amount}\n数量: {num}\n", + image_result, ] - ).send_to(target=target, bot=bot) + ).send(target=target, bot=bot) _suc_cnt += 1 logger.debug("节日红包图片信息发送成功...", "节日红包", group_id=g) except ActionFailed: diff --git a/zhenxun/plugins/gold_redbag/data_source.py b/zhenxun/plugins/gold_redbag/data_source.py index b35dee24..ec903100 100644 --- a/zhenxun/plugins/gold_redbag/data_source.py +++ b/zhenxun/plugins/gold_redbag/data_source.py @@ -6,16 +6,16 @@ from typing import Dict from nonebot.adapters import Bot from nonebot.exception import ActionFailed -from nonebot_plugin_saa import Image, MessageFactory, Text +from nonebot_plugin_alconna import UniMessage from zhenxun.configs.config import NICKNAME, Config from zhenxun.configs.path_config import IMAGE_PATH from zhenxun.models.user_console import UserConsole from zhenxun.utils.image_utils import BuildImage +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils -from zhenxun.utils.utils import get_user_avatar -from .config import FESTIVE_KEY, FestiveRedBagManage, GroupRedBag, RedBag +from .config import FestiveRedBagManage, GroupRedBag, RedBag class RedBagManager: @@ -56,16 +56,14 @@ class RedBagManager: FestiveRedBagManage.remove(red_bag.uuid) await asyncio.sleep(random.randint(1, 5)) try: - await MessageFactory( + await MessageUtils.build_message( [ - Text( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{len(red_bag.open_user)}" - f" 个红包,共 {sum(red_bag.open_user.values())} 金币\n" - ), - Image(rank_image.pic2bytes()), + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(red_bag.open_user)}" + f" 个红包,共 {sum(red_bag.open_user.values())} 金币\n", + rank_image, ] - ).send_to(target=target, bot=bot) + ).send(target=target, bot=bot) except ActionFailed: pass @@ -76,7 +74,7 @@ class RedBagManager: user_id: str | None = None, is_festive: bool = False, platform: str = "", - ) -> MessageFactory | None: + ) -> UniMessage | None: """结算红包 参数: @@ -92,14 +90,12 @@ class RedBagManager: if is_festive: if festive_red_bag := group_red_bag.festive_red_bag_expire(): rank_image = await festive_red_bag.build_amount_rank(rank_num, platform) - return MessageFactory( + return MessageUtils.build_message( [ - Text( - f"{NICKNAME}的节日红包过时了,一共开启了 " - f"{len(festive_red_bag.open_user)}" - f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n" - ), - Image(rank_image.pic2bytes()), + f"{NICKNAME}的节日红包过时了,一共开启了 " + f"{len(festive_red_bag.open_user)}" + f" 个红包,共 {sum(festive_red_bag.open_user.values())} 金币\n", + rank_image, ] ) else: @@ -108,10 +104,10 @@ class RedBagManager: return_gold, red_bag = await group_red_bag.settlement(user_id, platform) if red_bag: rank_image = await red_bag.build_amount_rank(rank_num, platform) - return MessageFactory( + return MessageUtils.build_message( [ - Text(f"已成功退还了 " f"{return_gold} 金币\n"), - Image(rank_image.pic2bytes()), + f"已成功退还了 " f"{return_gold} 金币\n", + rank_image.pic2bytes(), ] ) diff --git a/zhenxun/plugins/image_management/delete_image.py b/zhenxun/plugins/image_management/delete_image.py index bccbe3d2..6cabb8e9 100644 --- a/zhenxun/plugins/image_management/delete_image.py +++ b/zhenxun/plugins/image_management/delete_image.py @@ -1,15 +1,14 @@ -from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot.typing import T_State from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import ImageManagementManage @@ -43,16 +42,13 @@ _matcher = on_alconna( @_matcher.handle() async def _( - bot: Bot, - session: EventSession, - arparma: Arparma, name: Match[str], index: Match[str], state: T_State, ): image_dir_list = base_config.get("IMAGE_DIR_LIST") if not image_dir_list: - await Text("未发现任何图库").finish() + await MessageUtils.build_message("未发现任何图库").finish() _text = "" for i, dir in enumerate(image_dir_list): _text += f"{i}. {dir}\n" @@ -71,7 +67,7 @@ async def _( ) async def _(name: str): if name in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() image_dir_list = base_config.get("IMAGE_DIR_LIST") if name.isdigit(): index = int(name) @@ -89,14 +85,14 @@ async def _( index: str, ): if index in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() if not index.isdigit(): await _matcher.reject_path("index", "图片id需要输入数字...") name = _matcher.get_path_arg("name", None) if not name: - await Text("图库名称为空...").finish() + await MessageUtils.build_message("图库名称为空...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if file_name := await ImageManagementManage.delete_image( name, int(index), session.id1, session.platform ): @@ -105,5 +101,7 @@ async def _( arparma.header_result, session=session, ) - await Text(f"删除图片成功!\n图库: {name}\n名称: {index}.jpg").finish() - await Text("图片删除失败...").finish() + await MessageUtils.build_message( + f"删除图片成功!\n图库: {name}\n名称: {index}.jpg" + ).finish() + await MessageUtils.build_message("图片删除失败...").finish() diff --git a/zhenxun/plugins/image_management/move_image.py b/zhenxun/plugins/image_management/move_image.py index 44157f3e..6c1ec5e9 100644 --- a/zhenxun/plugins/image_management/move_image.py +++ b/zhenxun/plugins/image_management/move_image.py @@ -3,13 +3,13 @@ from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot.typing import T_State from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, UniMessage, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import ImageManagementManage @@ -53,7 +53,7 @@ async def _( ): image_dir_list = base_config.get("IMAGE_DIR_LIST") if not image_dir_list: - await Text("未发现任何图库").finish() + await MessageUtils.build_message("未发现任何图库").finish() _text = "" for i, dir in enumerate(image_dir_list): _text += f"{i}. {dir}\n" @@ -74,7 +74,7 @@ async def _( ) async def _(source: str): if source in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() image_dir_list = base_config.get("IMAGE_DIR_LIST") if source.isdigit(): index = int(source) @@ -93,8 +93,9 @@ async def _(source: str): ) async def _(destination: str): if destination in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() image_dir_list = base_config.get("IMAGE_DIR_LIST") + name = None if destination.isdigit(): index = int(destination) if index <= len(image_dir_list) - 1: @@ -111,17 +112,17 @@ async def _( index: str, ): if index in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() if not index.isdigit(): await _matcher.reject_path("index", "图片id需要输入数字...") source = _matcher.get_path_arg("source", None) destination = _matcher.get_path_arg("destination", None) if not source: - await Text("图库名称为空...").finish() + await MessageUtils.build_message("转出图库名称为空...").finish() if not destination: - await Text("图库名称为空...").finish() + await MessageUtils.build_message("转入图库名称为空...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if file_name := await ImageManagementManage.move_image( source, destination, int(index), session.id1, session.platform ): @@ -130,5 +131,7 @@ async def _( arparma.header_result, session=session, ) - await Text(f"移动图片成功!\n图库: {source} -> {destination}").finish() - await Text("图片删除失败...").finish() + await MessageUtils.build_message( + f"移动图片成功!\n图库: {source} -> {destination}" + ).finish() + await MessageUtils.build_message("图片删除失败...").finish() diff --git a/zhenxun/plugins/image_management/upload_image.py b/zhenxun/plugins/image_management/upload_image.py index 6f5a636a..b69281a4 100644 --- a/zhenxun/plugins/image_management/upload_image.py +++ b/zhenxun/plugins/image_management/upload_image.py @@ -1,14 +1,10 @@ from nonebot.adapters import Bot -from nonebot.params import Arg, ArgStr, CommandArg from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot.typing import T_State -from nonebot.utils import P from nonebot_plugin_alconna import Alconna, Args, Arparma from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import Match, UniMessage, UniMsg, image_fetch, on_alconna -from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config @@ -16,6 +12,7 @@ from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils from ._data_source import ImageManagementManage @@ -60,11 +57,11 @@ _show_matcher = on_alconna(Alconna("查看公开图库"), priority=1, block=True async def _(): image_dir_list = base_config.get("IMAGE_DIR_LIST") if not image_dir_list: - await Text("未发现任何图库").finish() + await MessageUtils.build_message("未发现任何图库").finish() text = "公开图库列表:\n" for i, e in enumerate(image_dir_list): text += f"\t{i+1}.{e}\n" - await Text(text[:-1]).send() + await MessageUtils.build_message(text[:-1]).send() @_upload_matcher.handle() @@ -78,7 +75,7 @@ async def _( ): image_dir_list = base_config.get("IMAGE_DIR_LIST") if not image_dir_list: - await Text("未发现任何图库").finish() + await MessageUtils.build_message("未发现任何图库").finish() _text = "" for i, dir in enumerate(image_dir_list): _text += f"{i}. {dir}\n" @@ -94,7 +91,7 @@ async def _( async def _(bot: Bot, state: T_State, name: Match[str]): image_dir_list = base_config.get("IMAGE_DIR_LIST") if not image_dir_list: - await Text("未发现任何图库").finish() + await MessageUtils.build_message("未发现任何图库").finish() _text = "" for i, dir in enumerate(image_dir_list): _text += f"{i}. {dir}\n" @@ -117,7 +114,7 @@ async def _(bot: Bot, state: T_State, name: Match[str]): ) async def _(name: str, state: T_State): if name in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() image_dir_list = base_config.get("IMAGE_DIR_LIST") if name.isdigit(): index = int(name) @@ -137,9 +134,9 @@ async def _( ): name = _upload_matcher.get_path_arg("name", None) if not name: - await Text("图库名称为空...").finish() + await MessageUtils.build_message("图库名称为空...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if file_name := await ImageManagementManage.upload_image( img, name, session.id1, session.platform ): @@ -148,8 +145,10 @@ async def _( arparma.header_result, session=session, ) - await Text(f"上传图片成功!\n图库: {name}\n名称: {file_name}").finish() - await Text("图片上传失败...").finish() + await MessageUtils.build_message( + f"上传图片成功!\n图库: {name}\n名称: {file_name}" + ).finish() + await MessageUtils.build_message("图片上传失败...").finish() @_continuous_upload_matcher.got( @@ -164,21 +163,21 @@ async def _( ): name = _continuous_upload_matcher.get_path_arg("name", None) if not name: - await Text("图库名称为空...").finish() + await MessageUtils.build_message("图库名称为空...").finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() if not state.get("img_list"): state["img_list"] = [] msg = message.extract_plain_text().strip().replace(arparma.header_result, "", 1) if msg in ["取消", "算了"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() if msg != "stop": for msg in message: if isinstance(msg, alcImage): state["img_list"].append(msg.url) await _continuous_upload_matcher.reject("图再来!!【发送‘stop’为停止】") if state["img_list"]: - await Text("正在下载, 请稍后...").send() + await MessageUtils.build_message("正在下载, 请稍后...").send() file_list = [] for img in state["img_list"]: if file_name := await ImageManagementManage.upload_image( @@ -190,7 +189,7 @@ async def _( "上传图片", session=session, ) - await Text( + await MessageUtils.build_message( f"上传图片成功!共上传了{len(file_list)}张图片\n图库: {name}\n名称: {', '.join(file_list)}" ).finish() - await Text("图片上传失败...").finish() + await MessageUtils.build_message("图片上传失败...").finish() diff --git a/zhenxun/plugins/mute/mute_message.py b/zhenxun/plugins/mute/mute_message.py index a9e6d42a..c2c476f0 100644 --- a/zhenxun/plugins/mute/mute_message.py +++ b/zhenxun/plugins/mute/mute_message.py @@ -3,7 +3,6 @@ from nonebot.adapters import Bot from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Image as alcImage from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME @@ -11,6 +10,7 @@ from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType from zhenxun.utils.image_utils import get_download_image_hash +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from ._data_source import mute_manage @@ -44,9 +44,9 @@ async def _(bot: Bot, session: EventSession, message: UniMsg): if duration := mute_manage.add_message(session.id1, group_id, _message): try: await PlatformUtils.ban_user(bot, session.id1, group_id, duration) - await Text(f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!").send( - at_sender=True - ) + await MessageUtils.build_message( + f"检测到恶意刷屏,{NICKNAME}要把你关进小黑屋!" + ).send(at_sender=True) mute_manage.reset(session.id1, group_id) logger.info(f"检测刷屏 被禁言 {duration} 分钟", "禁言检查", session=session) except Exception as e: diff --git a/zhenxun/plugins/mute/mute_setting.py b/zhenxun/plugins/mute/mute_setting.py index 96237f4a..f723f075 100644 --- a/zhenxun/plugins/mute/mute_setting.py +++ b/zhenxun/plugins/mute/mute_setting.py @@ -1,12 +1,12 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginExtraData, RegisterConfig from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.rules import ensure_group from ._data_source import base_config, mute_manage @@ -96,19 +96,19 @@ async def _( _duration = duration.result if duration.available else None group_data = mute_manage.get_group_data(group_id) if _time is None and _count is None and _duration is None: - await Text( + await MessageUtils.build_message( f"最大次数:{group_data.count} 次\n" f"规定时间:{group_data.time} 秒\n" f"禁言时长:{group_data.duration:.2f} 分钟\n" f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】" - ).finish(reply=True) + ).finish(reply_to=True) if _time is not None: group_data.time = _time if _count is not None: group_data.count = _count if _duration is not None: group_data.duration = _duration - await Text("设置成功!").send(reply=True) + await MessageUtils.build_message("设置成功!").send(reply_to=True) logger.info( f"设置禁言配置 time: {_time}, count: {_count}, duration: {_duration}", arparma.header_result, diff --git a/zhenxun/plugins/nbnhhsh.py b/zhenxun/plugins/nbnhhsh.py index 7c46d51e..7ab78aaa 100644 --- a/zhenxun/plugins/nbnhhsh.py +++ b/zhenxun/plugins/nbnhhsh.py @@ -1,12 +1,12 @@ import ujson as json from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="能不能好好说话", @@ -16,6 +16,8 @@ __plugin_meta__ = PluginMetadata( 指令: nbnhhsh [文本] 能不能好好说话 [文本] + 示例: + nbnhhsh xsx """.strip(), extra=PluginExtraData(author="HibiKier", version="0.1", aliases={"nbnhhsh"}).dict(), ) @@ -55,6 +57,6 @@ async def _(session: EventSession, arparma: Arparma, text: str): arparma.header_result, session=session, ) - await Text(f"{tmp}={result}").send(reply=True) + await MessageUtils.build_message(f"{tmp}={result}").send(reply_to=True) except (IndexError, KeyError): - await Text("没有找到对应的翻译....").send() + await MessageUtils.build_message("没有找到对应的翻译....").send() diff --git a/zhenxun/plugins/pix_gallery/pix_add_keyword.py b/zhenxun/plugins/pix_gallery/pix_add_keyword.py index 859f0de6..452213e3 100644 --- a/zhenxun/plugins/pix_gallery/pix_add_keyword.py +++ b/zhenxun/plugins/pix_gallery/pix_add_keyword.py @@ -8,11 +8,11 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from ._data_source import uid_pid_exists from ._model.pixiv import Pixiv @@ -65,12 +65,12 @@ async def _(bot: Bot, session: EventSession, keyword: str, arparma: Arparma): text = f"已成功添加pixiv搜图关键词:{keyword}" if session.id1 not in bot.config.superusers: text += ",请等待管理员通过该关键词!" - await Text(text).send(reply=True) + await MessageUtils.build_message(text).send(reply_to=True) logger.info( f"添加了pixiv搜图关键词: {keyword}", arparma.header_result, session=session ) else: - await Text(f"该关键词 {keyword} 已存在...").send() + await MessageUtils.build_message(f"该关键词 {keyword} 已存在...").send() @_uid_matcher.handle() @@ -85,9 +85,13 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, add_type: str, id else: word = f"pid:{id}" if await Pixiv.get_or_none(pid=int(id), img_p="p0"): - await Text(f"该PID:{id}已存在...").finish(reply=True) + await MessageUtils.build_message(f"该PID:{id}已存在...").finish( + reply_to=True + ) if not await uid_pid_exists(word) and exists_flag: - await Text("画师或作品不存在或搜索正在CD,请稍等...").finish(reply=True) + await MessageUtils.build_message( + "画师或作品不存在或搜索正在CD,请稍等..." + ).finish(reply_to=True) if not await PixivKeywordUser.exists(keyword=word): await PixivKeywordUser.create( user_id=session.id1, @@ -98,9 +102,9 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, add_type: str, id text = f"已成功添加pixiv搜图UID/PID:{id}" if session.id1 not in bot.config.superusers: text += ",请等待管理员通过该关键词!" - await Text(text).send(reply=True) + await MessageUtils.build_message(text).send(reply_to=True) else: - await Text(f"该UID/PID:{id} 已存在...").send() + await MessageUtils.build_message(f"该UID/PID:{id} 已存在...").send() @_black_matcher.handle() @@ -111,7 +115,7 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): pid = pid.replace("_", "") pid = pid[: pid.find("p")] if not pid.isdigit: - await Text("PID必须全部是数字!").finish(reply=True) + await MessageUtils.build_message("PID必须全部是数字!").finish(reply_to=True) if not await PixivKeywordUser.exists( keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}" ): @@ -121,9 +125,11 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, pid: str): keyword=f"black:{pid}{f'_p{img_p}' if img_p else ''}", is_pass=session.id1 in bot.config.superusers, ) - await Text(f"已添加PID:{pid} 至黑名单中...").send() + await MessageUtils.build_message(f"已添加PID:{pid} 至黑名单中...").send() logger.info( f" 添加了pixiv搜图黑名单 PID:{pid}", arparma.header_result, session=session ) else: - await Text(f"PID:{pid} 已添加黑名单中,添加失败...").send() + await MessageUtils.build_message( + f"PID:{pid} 已添加黑名单中,添加失败..." + ).send() diff --git a/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py index 3cab7ca1..9a8f2ea7 100644 --- a/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py +++ b/zhenxun/plugins/pix_gallery/pix_pass_del_keyword.py @@ -5,17 +5,18 @@ from nonebot_plugin_alconna import ( Alconna, Args, Arparma, + At, Match, Option, on_alconna, store_true, ) -from nonebot_plugin_saa import Mention, MessageFactory, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from ._data_source import remove_image @@ -89,7 +90,9 @@ async def _( else: keyword = f"pid:{keyword}" if not keyword[4:].isdigit(): - await Text(f"{keyword} 非全数字...").finish(reply=True) + await MessageUtils.build_message(f"{keyword} 非全数字...").finish( + reply_to=True + ) data = await PixivKeywordUser.get_or_none(keyword=keyword) user_id = 0 group_id = 0 @@ -98,9 +101,9 @@ async def _( await data.save(update_fields=["is_pass"]) user_id, group_id = data.user_id, data.group_id if not user_id: - await Text(f"未找到关键词/UID:{keyword},请检查关键词/UID是否存在...").finish( - reply=True - ) + await MessageUtils.build_message( + f"未找到关键词/UID:{keyword},请检查关键词/UID是否存在..." + ).finish(reply_to=True) if flag: if group_id == -1: if not tmp["private"].get(user_id): @@ -114,7 +117,9 @@ async def _( tmp["group"][group_id][user_id] = {"keyword": [keyword]} else: tmp["group"][group_id][user_id]["keyword"].append(keyword) - await Text(f"已成功{'通过' if flag else '拒绝'}搜图关键词:{keyword}...").send() + await MessageUtils.build_message( + f"已成功{'通过' if flag else '拒绝'}搜图关键词:{keyword}..." + ).send() for user in tmp["private"]: text = ",".join(tmp["private"][user]["keyword"]) await PlatformUtils.send_message( @@ -134,21 +139,13 @@ async def _( bot, None, group_id=group, - message=MessageFactory( + message=MessageUtils.build_message( [ - Mention(user_id=user), - Text( - "你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." - ), + At(flag="user", target=user), + "你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新...", ] ), ) - # await bot.send_group_msg( - # group_id=group, - # message=Message( - # f"{at(user)}你的关键词/UID/PID {x} 已被管理员通过,将在下一次进行更新..." - # ), - # ) logger.info( f" 通过了pixiv搜图关键词/UID: {keyword}", arparma.header_result, session=session ) @@ -160,12 +157,16 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, type: str, keywor keyword = f"{type}:{keyword}" if data := await PixivKeywordUser.get_or_none(keyword=keyword): await data.delete() - await Text(f"删除搜图关键词/UID:{keyword} 成功...").send() + await MessageUtils.build_message( + f"删除搜图关键词/UID:{keyword} 成功..." + ).send() logger.info( f" 删除了pixiv搜图关键词: {keyword}", arparma.header_result, session=session ) else: - await Text(f"未查询到搜索关键词/UID/PID:{keyword},删除失败!").send() + await MessageUtils.build_message( + f"未查询到搜索关键词/UID/PID:{keyword},删除失败!" + ).send() @_del_pic_matcher.handle() @@ -202,16 +203,16 @@ async def _(bot: Bot, session: EventSession, arparma: Arparma, keyword: str): arparma.header_result, session=session, ) - # else: - # await del_pic.send( - # f"PIX:删除pid:{pid}{f'_p{img_p}' if img_p else ''} 失败.." - # ) else: - await Text( + await MessageUtils.build_message( f"PIX:图片pix:{keyword}{f'_p{img_p}' if img_p else ''} 不存在...无法删除.." ).send() else: - await Text(f"PID必须为数字!pid:{keyword}").send(reply=True) - await Text(f"PIX:成功删除图片:{msg[:-1]}").send() + await MessageUtils.build_message(f"PID必须为数字!pid:{keyword}").send( + reply_to=True + ) + await MessageUtils.build_message(f"PIX:成功删除图片:{msg[:-1]}").send() if flag: - await Text(f"成功图片PID加入黑名单:{black_pid[:-1]}").send() + await MessageUtils.build_message( + f"成功图片PID加入黑名单:{black_pid[:-1]}" + ).send() diff --git a/zhenxun/plugins/pix_gallery/pix_update.py b/zhenxun/plugins/pix_gallery/pix_update.py index de2880ef..b0f209dc 100644 --- a/zhenxun/plugins/pix_gallery/pix_update.py +++ b/zhenxun/plugins/pix_gallery/pix_update.py @@ -16,12 +16,12 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from ._data_source import start_update_image_url from ._model.omega_pixiv_illusts import OmegaPixivIllusts @@ -106,10 +106,12 @@ async def _(arparma: Arparma, session: EventSession, type: str, num: Match[int]) else: update_lst = [f"uid:{_num}"] info = f"开始更新Pixiv搜图UID:\nuid:{_num}" - await Text(info).send() + await MessageUtils.build_message(info).send() start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst[:_num], black_pid, type == 'pid') - await Text( + pid_count, pic_count = await start_update_image_url( + update_lst[:_num], black_pid, type == "pid" + ) + await MessageUtils.build_message( f"Pixiv搜图关键词搜图更新完成...\n" f"累计更新PID {pid_count} 个\n" f"累计更新图片 {pic_count} 张" @@ -131,7 +133,7 @@ async def _(bot: Bot, arparma: Arparma, session: EventSession): x_pid.append(img.pid) if img.uid not in x_uid: x_uid.append(img.uid) - await Text( + await MessageUtils.build_message( "从未更新过的UID:" + ",".join([f"uid:{x}" for x in _uid if x not in x_uid]) + "\n" @@ -139,12 +141,14 @@ async def _(bot: Bot, arparma: Arparma, session: EventSession): + ",".join([f"pid:{x}" for x in _pid if x not in x_pid]) ).send() if arparma.find("update"): - await Text("开始自动自动更新PID....").send() + await MessageUtils.build_message("开始自动自动更新PID....").send() update_lst = [f"pid:{x}" for x in _uid if x not in x_uid] black_pid = await PixivKeywordUser.get_black_pid() start_time = time.time() - pid_count, pic_count = await start_update_image_url(update_lst, black_pid, False) - await Text( + pid_count, pic_count = await start_update_image_url( + update_lst, black_pid, False + ) + await MessageUtils.build_message( f"Pixiv搜图关键词搜图更新完成...\n" f"累计更新PID {pid_count} 个\n" f"累计更新图片 {pic_count} 张" diff --git a/zhenxun/plugins/pixiv_rank_search/data_source.py b/zhenxun/plugins/pixiv_rank_search/data_source.py index d3f54dfe..761a93f2 100644 --- a/zhenxun/plugins/pixiv_rank_search/data_source.py +++ b/zhenxun/plugins/pixiv_rank_search/data_source.py @@ -1,8 +1,6 @@ from asyncio.exceptions import TimeoutError from pathlib import Path -from nonebot_plugin_saa import Image, MessageFactory - from zhenxun.configs.config import Config from zhenxun.configs.path_config import TEMP_PATH from zhenxun.services.log import logger diff --git a/zhenxun/plugins/quotations.py b/zhenxun/plugins/quotations.py index e67ec048..e213ee04 100644 --- a/zhenxun/plugins/quotations.py +++ b/zhenxun/plugins/quotations.py @@ -1,11 +1,11 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Arparma, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.http_utils import AsyncHttpx +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="一言二次元语录", @@ -28,5 +28,5 @@ _matcher = on_alconna(Alconna("语录"), aliases={"二次元"}, priority=5, bloc async def _(session: EventSession, arparma: Arparma): data = (await AsyncHttpx.get(URL, timeout=5)).json() result = f'{data["hitokoto"]}\t——{data["from"]}' - await Text(result).send() + await MessageUtils.build_message(result).send() logger.info(f" 发送语录:" + result, arparma.header_result, session=session) diff --git a/zhenxun/plugins/roll.py b/zhenxun/plugins/roll.py index 1427080a..7c953496 100644 --- a/zhenxun/plugins/roll.py +++ b/zhenxun/plugins/roll.py @@ -4,14 +4,13 @@ import random from nonebot import on_command from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import UniMsg -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession -from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.depends import UserName +from zhenxun.utils.message import MessageUtils __plugin_meta__ = PluginMetadata( name="roll", @@ -39,8 +38,10 @@ async def _( ): text = message.extract_plain_text().strip().replace("roll", "", 1).split() if not text: - await Text(f"roll: {random.randint(0, 100)}").finish(reply=True) - await Text( + await MessageUtils.build_message(f"roll: {random.randint(0, 100)}").finish( + reply_to=True + ) + await MessageUtils.build_message( random.choice( [ "转动命运的齿轮,拨开眼前迷雾...", @@ -52,7 +53,7 @@ async def _( ).send() await asyncio.sleep(1) random_text = random.choice(text) - await Text( + await MessageUtils.build_message( random.choice( [ f"让{NICKNAME}看看是什么结果!答案是:‘{random_text}’", @@ -61,5 +62,5 @@ async def _( f"结束了,{user_name},命运之轮停在了 ‘{random_text}’!", ] ) - ).send(reply=True) + ).send(reply_to=True) logger.info(f"发送roll:{text}", "roll", session=session) diff --git a/zhenxun/plugins/russian/data_source.py b/zhenxun/plugins/russian/data_source.py index 589f7a3a..eb8381cb 100644 --- a/zhenxun/plugins/russian/data_source.py +++ b/zhenxun/plugins/russian/data_source.py @@ -4,8 +4,8 @@ from datetime import datetime, timedelta from apscheduler.jobstores.base import JobLookupError from nonebot.adapters import Bot +from nonebot_plugin_alconna import At, UniMessage from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Image, Mention, MessageFactory, Text from pydantic import BaseModel from zhenxun.configs.config import NICKNAME, Config @@ -14,6 +14,7 @@ from zhenxun.models.user_console import UserConsole from zhenxun.utils.enum import GoldHandle from zhenxun.utils.exception import InsufficientGold from zhenxun.utils.image_utils import BuildImage, BuildMat, MatType, text2image +from zhenxun.utils.message import MessageUtils from zhenxun.utils.platform import PlatformUtils from .model import RussianUser @@ -123,9 +124,7 @@ class RussianManage: if result: await PlatformUtils.send_message(bot, None, group_id, result) - async def add_russian( - self, bot: Bot, group_id: str, rus: Russian - ) -> Text | MessageFactory: + async def add_russian(self, bot: Bot, group_id: str, rus: Russian) -> UniMessage: """添加决斗 参数: @@ -134,59 +133,56 @@ class RussianManage: rus: Russian 返回: - Text | MessageFactory: 返回消息 + UniMessage: 返回消息 """ russian = self._data.get(group_id) if russian: if russian.time + 30 < time.time(): if not russian.player2: - return Text( + return MessageUtils.build_message( f"现在是 {russian.player1[1]} 发起的对决, 请接受对决或等待决斗超时..." ) else: - return Text( + return MessageUtils.build_message( f"{russian.player1[1]} 和 {russian.player2[1]}的对决还未结束!" ) - return Text( + return MessageUtils.build_message( f"现在是 {russian.player1[1]} 发起的对决\n请等待比赛结束后再开始下一轮..." ) max_money = base_config.get("MAX_RUSSIAN_BET_GOLD") if rus.money > max_money: - return Text(f"太多了!单次金额不能超过{max_money}!") + return MessageUtils.build_message(f"太多了!单次金额不能超过{max_money}!") user = await UserConsole.get_user(rus.player1[0]) if user.gold < rus.money: - return Text("你没有足够的钱支撑起这场挑战") + return MessageUtils.build_message("你没有足够的钱支撑起这场挑战") rus.bullet_arr = self.__random_bullet(rus.bullet_num) self._data[group_id] = rus - message_list = [] + message_list: list[str | At] = [] if rus.at_user: user = await GroupInfoUser.get_or_none( user_id=rus.at_user, group_id=group_id ) message_list = [ - Text(f"{rus.player1[1]} 向"), - Mention(rus.at_user), - Text( - f"发起了决斗!请 {user.user_name if user else rus.at_user} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!" - ), + f"{rus.player1[1]} 向", + At(flag="user", target=rus.at_user), + f"发起了决斗!请 {user.user_name if user else rus.at_user} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!", ] else: message_list = [ - Text( - "若30秒内无人接受挑战则此次对决作废【首次游玩请at我发送 ’帮助俄罗斯轮盘‘ 来查看命令】" - ) + "若30秒内无人接受挑战则此次对决作废【首次游玩请at我发送 ’帮助俄罗斯轮盘‘ 来查看命令】" ] - result = Text( + result = ( "咔 " * rus.bullet_num + f"装填完毕\n挑战金额:{rus.money}\n第一枪的概率为:{float(rus.bullet_num) / 7.0 * 100:.2f}%\n" ) + message_list.insert(0, result) self.__build_job(bot, group_id, True) - return MessageFactory(message_list) + return MessageUtils.build_message(message_list) # type: ignore async def accept( self, bot: Bot, group_id: str, user_id: str, uname: str - ) -> Text | MessageFactory: + ) -> UniMessage: """接受对决 参数: @@ -200,27 +196,31 @@ class RussianManage: """ if russian := self._data.get(group_id): if russian.at_user and russian.at_user != user_id: - return Text("又不是找你决斗,你接受什么啊!气!") + return MessageUtils.build_message("又不是找你决斗,你接受什么啊!气!") if russian.player2: - return Text("当前决斗已被其他玩家接受!请等待下局对决!") + return MessageUtils.build_message( + "当前决斗已被其他玩家接受!请等待下局对决!" + ) if russian.player1[0] == user_id: - return Text("你发起的对决,你接受什么啊!气!") + return MessageUtils.build_message("你发起的对决,你接受什么啊!气!") user = await UserConsole.get_user(user_id) if user.gold < russian.money: - return Text("你没有足够的钱来接受这场挑战...") + return MessageUtils.build_message("你没有足够的钱来接受这场挑战...") russian.player2 = (user_id, uname) russian.next_user = russian.player1[0] self.__build_job(bot, group_id, True) - return MessageFactory( + return MessageUtils.build_message( [ - Text("决斗已经开始!请"), - Mention(russian.player1[0]), - Text("先开枪!"), + "决斗已经开始!请", + At(flag="user", target=russian.player1[0]), + "先开枪!", ] ) - return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!") + return MessageUtils.build_message( + "目前没有进行的决斗,请发送 装弹 开启决斗吧!" + ) - def refuse(self, group_id: str, user_id: str, uname: str) -> Text | MessageFactory: + def refuse(self, group_id: str, user_id: str, uname: str) -> UniMessage: """拒绝决斗 参数: @@ -234,18 +234,25 @@ class RussianManage: if russian := self._data.get(group_id): if russian.at_user: if russian.at_user != user_id: - return Text("又不是找你决斗,你拒绝什么啊!气!") + return MessageUtils.build_message( + "又不是找你决斗,你拒绝什么啊!气!" + ) del self._data[group_id] self.__remove_job(group_id) - return MessageFactory( - [Mention(russian.player1[0]), Text(f"{uname}拒绝了你的对决!")] + return MessageUtils.build_message( + [ + At(flag="user", target=russian.player1[0]), + f"{uname}拒绝了你的对决!", + ] ) - return Text("当前决斗并没有指定对手,无法拒绝哦!") - return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!") + return MessageUtils.build_message("当前决斗并没有指定对手,无法拒绝哦!") + return MessageUtils.build_message( + "目前没有进行的决斗,请发送 装弹 开启决斗吧!" + ) async def shoot( self, bot: Bot, group_id: str, user_id: str, uname: str, platform: str - ) -> tuple[Text | MessageFactory, Text | MessageFactory | None]: + ) -> tuple[UniMessage, UniMessage | None]: """开枪 参数: @@ -260,11 +267,14 @@ class RussianManage: """ if russian := self._data.get(group_id): if not russian.player2: - return Text("当前还没有玩家接受对决,无法开枪..."), None + return ( + MessageUtils.build_message("当前还没有玩家接受对决,无法开枪..."), + None, + ) if user_id not in [russian.player1[0], russian.player2[0]]: """非玩家1和玩家2发送开枪""" return ( - Text( + MessageUtils.build_message( random.choice( [ f"不要打扰 {russian.player1[1]} 和 {russian.player2[1]} 的决斗啊!", @@ -278,12 +288,14 @@ class RussianManage: if user_id != russian.next_user: """相同玩家连续开枪""" return ( - Text(f"你的左轮不是连发的!该 {russian.player2[1]} 开枪了!"), + MessageUtils.build_message( + f"你的左轮不是连发的!该 {russian.player2[1]} 开枪了!" + ), None, ) if russian.bullet_arr[russian.bullet_index] == 1: """去世""" - result = Text( + result = MessageUtils.build_message( random.choice( [ '"嘭!",你直接去世了', @@ -320,14 +332,19 @@ class RussianManage: russian.bullet_index += 1 self.__build_job(bot, group_id, True) return ( - MessageFactory([Text(result), Mention(next_user), Text(" 了!")]), + MessageUtils.build_message( + [result, At(flag="user", target=next_user), " 了!"] + ), None, ) - return Text("目前没有进行的决斗,请发送 装弹 开启决斗吧!"), None + return ( + MessageUtils.build_message("目前没有进行的决斗,请发送 装弹 开启决斗吧!"), + None, + ) async def settlement( self, group_id: str, user_id: str | None, platform: str | None = None - ) -> Text | MessageFactory: + ) -> UniMessage: """结算 参数: @@ -342,12 +359,14 @@ class RussianManage: if not russian.player2: if self.__check_is_timeout(group_id): del self._data[group_id] - return Text("规定时间内还未有人接受决斗,当前决斗过期...") - return Text("决斗还未开始,,无法结算哦...") + return MessageUtils.build_message( + "规定时间内还未有人接受决斗,当前决斗过期..." + ) + return MessageUtils.build_message("决斗还未开始,,无法结算哦...") if user_id and user_id not in [russian.player1[0], russian.player2[0]]: - return Text(f"吃瓜群众不要捣乱!黄牌警告!") + return MessageUtils.build_message(f"吃瓜群众不要捣乱!黄牌警告!") if not self.__check_is_timeout(group_id): - return Text( + return MessageUtils.build_message( f"{russian.player1[1]} 和 {russian.player2[1]} 比赛并未超时,请继续比赛..." ) win_user = None @@ -393,7 +412,11 @@ class RussianManage: if u := await UserConsole.get_user(lose_user[0]): u.gold = 0 await u.save(update_fields=["gold"]) - result = [Text("这场决斗是 "), Mention(win_user[0]), Text(" 胜利了!")] + result = [ + "这场决斗是 ", + At(flag="user", target=win_user[0]), + " 胜利了!", + ] image = await text2image( f"结算:\n" f"\t胜者:{win_user[1]}\n" @@ -412,11 +435,11 @@ class RussianManage: color="#f9f6f2", ) self.__remove_job(group_id) - result.append(Image(image.pic2bytes())) + result.append(image) del self._data[group_id] - return MessageFactory(result) - return Text("赢家和输家获取错误...") - return Text("比赛并没有开始...无法结算...") + return MessageUtils.build_message(result) + return MessageUtils.build_message("赢家和输家获取错误...") + return MessageUtils.build_message("比赛并没有开始...无法结算...") async def __get_x_index(self, users: list[RussianUser], group_id: str): uid_list = [u.user_id for u in users] diff --git a/zhenxun/plugins/search_anime/__init__.py b/zhenxun/plugins/search_anime/__init__.py index 87f4f2e1..d12ad03e 100644 --- a/zhenxun/plugins/search_anime/__init__.py +++ b/zhenxun/plugins/search_anime/__init__.py @@ -1,11 +1,11 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from .data_source import from_anime_get_info @@ -47,15 +47,15 @@ async def _(name: Match[str]): @_matcher.got_path("name", prompt="是不是少了番名?") async def _(session: EventSession, arparma: Arparma, name: str): gid = session.id3 or session.id2 - await Text(f"开始搜番 {name}...").send() + await MessageUtils.build_message(f"开始搜番 {name}...").send() anime_report = await from_anime_get_info( name, Config.get_config("search_anime", "SEARCH_ANIME_MAX_INFO"), ) if anime_report: if isinstance(anime_report, str): - await Text(anime_report).finish() - await Text("\n\n".join(anime_report)).send() + await MessageUtils.build_message(anime_report).finish() + await MessageUtils.build_message("\n\n".join(anime_report)).send() logger.info( f"搜索番剧 {name} 成功: {anime_report}", arparma.header_result, @@ -63,4 +63,6 @@ async def _(session: EventSession, arparma: Arparma, name: str): ) else: logger.info(f"未找到番剧 {name}...") - await Text(f"未找到番剧 {name}(也有可能是超时,再尝试一下?)").send() + await MessageUtils.build_message( + f"未找到番剧 {name}(也有可能是超时,再尝试一下?)" + ).send() diff --git a/zhenxun/plugins/search_buff_skin_price/__init__.py b/zhenxun/plugins/search_buff_skin_price/__init__.py index f29e963f..da224aaf 100644 --- a/zhenxun/plugins/search_buff_skin_price/__init__.py +++ b/zhenxun/plugins/search_buff_skin_price/__init__.py @@ -2,12 +2,12 @@ from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, on_alconna -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME from zhenxun.configs.utils import BaseBlock, PluginExtraData, RegisterConfig from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from .data_source import get_price, update_buff_cookie @@ -73,29 +73,32 @@ async def arg_handle( skin: str, ): if name in ["算了", "取消"] or skin in ["算了", "取消"]: - await Text("已取消操作...").finish() + await MessageUtils.build_message("已取消操作...").finish() result = "" if name in ["ak", "ak47"]: name = "ak-47" name = name + " | " + skin + status_code = -1 try: result, status_code = await get_price(name) except FileNotFoundError: - await Text(f'请先对{NICKNAME}说"设置cookie"来设置cookie!').send(at_sender=True) + await MessageUtils.build_message( + f'请先对{NICKNAME}说"设置cookie"来设置cookie!' + ).send(at_sender=True) if status_code in [996, 997, 998]: - await Text(result).finish() + await MessageUtils.build_message(result).finish() if result: logger.info(f"查询皮肤: {name}", arparma.header_result, session=session) - await Text(result).finish() + await MessageUtils.build_message(result).finish() else: logger.info( f" 查询皮肤:{name} 没有查询到", arparma.header_result, session=session ) - await Text("没有查询到哦,请检查格式吧").send() + await MessageUtils.build_message("没有查询到哦,请检查格式吧").send() @_cookie_matcher.handle() async def _(session: EventSession, arparma: Arparma, cookie: str): result = update_buff_cookie(cookie) - await Text(result).send(at_sender=True) + await MessageUtils.build_message(result).send(at_sender=True) logger.info("更新BUFF COOKIE", arparma.header_result, session=session) diff --git a/zhenxun/plugins/send_setu_/send_setu/__init__.py b/zhenxun/plugins/send_setu_/send_setu/__init__.py index 8f9bd2ff..3dab91f6 100644 --- a/zhenxun/plugins/send_setu_/send_setu/__init__.py +++ b/zhenxun/plugins/send_setu_/send_setu/__init__.py @@ -14,7 +14,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import NICKNAME @@ -175,11 +174,11 @@ async def _( ): _tags = tags.result.split("#") if tags.available else None if _tags and NICKNAME in _tags: - await Text( + await MessageUtils.build_message( "咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀" ).finish() if not session.id1: - await Text("用户id为空...").finish() + await MessageUtils.build_message("用户id为空...").finish() gid = session.id3 or session.id2 user_console = await UserConsole.get_user(session.id1, session.platform) user, _ = await SignUser.get_or_create( @@ -208,11 +207,11 @@ async def _( """指定id""" result = await SetuManage.get_setu(local_id=local_id.result) if isinstance(result, str): - await MessageUtils.build_message(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) await result[0].finish() result_list = await SetuManage.get_setu(tags=_tags, num=_num, is_r18=is_r18) if isinstance(result_list, str): - await MessageUtils.build_message(result_list).finish(reply=True) + await MessageUtils.build_message(result_list).finish(reply_to=True) max_once_num2forward = base_config.get("MAX_ONCE_NUM2FORWARD") platform = PlatformUtils.get_platform(bot) if ( diff --git a/zhenxun/plugins/send_setu_/update_setu/__init__.py b/zhenxun/plugins/send_setu_/update_setu/__init__.py index fc3aa3ac..2b5b6ae9 100644 --- a/zhenxun/plugins/send_setu_/update_setu/__init__.py +++ b/zhenxun/plugins/send_setu_/update_setu/__init__.py @@ -3,13 +3,13 @@ from nonebot.plugin import PluginMetadata from nonebot.rule import to_me from nonebot_plugin_alconna import Alconna, Arparma, on_alconna from nonebot_plugin_apscheduler import scheduler -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from zhenxun.configs.config import Config from zhenxun.configs.utils import BaseBlock, PluginExtraData from zhenxun.services.log import logger from zhenxun.utils.enum import PluginType +from zhenxun.utils.message import MessageUtils from .data_source import update_setu_img @@ -37,13 +37,13 @@ _matcher = on_alconna( @_matcher.handle() async def _(session: EventSession, arparma: Arparma): if Config.get_config("send_setu", "DOWNLOAD_SETU"): - await Text("开始更新色图...").send(reply=True) + await MessageUtils.build_message("开始更新色图...").send(reply_to=True) result = await update_setu_img(True) if result: - await Text(result).send() + await MessageUtils.build_message(result).send() logger.info("更新色图", arparma.header_result, session=session) else: - await Text("更新色图配置未开启...").send() + await MessageUtils.build_message("更新色图配置未开启...").send() # 更新色图 diff --git a/zhenxun/plugins/statistics/statistics_handle.py b/zhenxun/plugins/statistics/statistics_handle.py index bea1b2d8..fc070e0c 100644 --- a/zhenxun/plugins/statistics/statistics_handle.py +++ b/zhenxun/plugins/statistics/statistics_handle.py @@ -8,7 +8,6 @@ from nonebot_plugin_alconna import ( on_alconna, store_true, ) -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData @@ -160,4 +159,4 @@ async def _( else: await MessageUtils.build_message(result).send() else: - await Text("获取数据失败...").send() + await MessageUtils.build_message("获取数据失败...").send() diff --git a/zhenxun/plugins/translate/__init__.py b/zhenxun/plugins/translate/__init__.py index 146705f0..372a5774 100644 --- a/zhenxun/plugins/translate/__init__.py +++ b/zhenxun/plugins/translate/__init__.py @@ -83,7 +83,7 @@ async def _( if to not in values and to not in keys: await MessageUtils.build_message("目标语种不支持...").finish() result = await translate_message(text, source, to) - await MessageUtils.build_message(result).send(reply=True) + await MessageUtils.build_message(result).send(reply_to=True) logger.info( f"source: {source}, to: {to}, 翻译: {text}", arparma.header_result, diff --git a/zhenxun/plugins/wbtop/__init__.py b/zhenxun/plugins/wbtop/__init__.py index 9b760c9a..fce1b995 100644 --- a/zhenxun/plugins/wbtop/__init__.py +++ b/zhenxun/plugins/wbtop/__init__.py @@ -33,7 +33,7 @@ _matcher = on_alconna(Alconna("微博热搜", Args["idx?", int]), priority=5, bl async def _(session: EventSession, arparma: Arparma, idx: Match[int]): result, data_list = await get_hot_image() if isinstance(result, str): - await MessageUtils.build_message(result).finish(reply=True) + await MessageUtils.build_message(result).finish(reply_to=True) if idx.available: _idx = idx.result url = data_list[_idx - 1]["url"] diff --git a/zhenxun/plugins/what_anime/__init__.py b/zhenxun/plugins/what_anime/__init__.py index 4881f6dc..d3b91876 100644 --- a/zhenxun/plugins/what_anime/__init__.py +++ b/zhenxun/plugins/what_anime/__init__.py @@ -2,11 +2,11 @@ from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import Alconna, Args, Arparma from nonebot_plugin_alconna import Image as alcImg from nonebot_plugin_alconna import Match, on_alconna -from nonebot_plugin_saa import Image, Text from nonebot_plugin_session import EventSession from zhenxun.configs.utils import PluginExtraData from zhenxun.services.log import logger +from zhenxun.utils.message import MessageUtils from .data_source import get_anime @@ -41,11 +41,11 @@ async def _( image: alcImg, ): if not image.url: - await Text("图片url为空...").finish() - await Text("开始识别...").send() + await MessageUtils.build_message("图片url为空...").finish() + await MessageUtils.build_message("开始识别...").send() anime_data_report = await get_anime(image.url) if anime_data_report: - await Text(anime_data_report).send(reply=True) + await MessageUtils.build_message(anime_data_report).send(reply_to=True) logger.info( f" 识番 {image.url} --> {anime_data_report}", arparma.header_result, @@ -55,4 +55,6 @@ async def _( logger.info( f"识番 {image.url} 未找到...", arparma.header_result, session=session ) - await Text(f"没有寻找到该番剧,果咩..").send(reply=True) + await MessageUtils.build_message(f"没有寻找到该番剧,果咩..").send( + reply_to=True + ) diff --git a/zhenxun/utils/_build_image.py b/zhenxun/utils/_build_image.py index 4138fae6..33666f7e 100644 --- a/zhenxun/utils/_build_image.py +++ b/zhenxun/utils/_build_image.py @@ -108,19 +108,21 @@ class BuildImage: 返回: Self: Self """ + if not text.strip(): + return cls(1, 1) _font = None if isinstance(font, FreeTypeFont): _font = font elif isinstance(font, (str, Path)): _font = cls.load_font(font, size) - width, height = cls.get_text_size(text or "A", _font) + width, height = cls.get_text_size(text, _font) if isinstance(padding, int): width += padding * 2 height += padding * 2 elif isinstance(padding, tuple): width += padding[1] + padding[3] height += padding[0] + padding[2] - markImg = cls(width, height, color) + markImg = cls(width, height, color, font=_font) await markImg.text( (0, 0), text, fill=font_color, font=_font, center_type="center" ) @@ -380,7 +382,6 @@ class BuildImage: text = str(text) if center_type and center_type not in ["center", "height", "width"]: raise ValueError("center_type must be 'center', 'width' or 'height'") - width, height = 0, 0 max_length_text = "" sentence = text.split("\n") for x in sentence: @@ -392,7 +393,7 @@ class BuildImage: font = self.font if center_type: ttf_w, ttf_h = self.getsize(max_length_text) # type: ignore - ttf_h = ttf_h * len(sentence) + # ttf_h = ttf_h * len(sentence) pos = self.__center_xy(pos, ttf_w, ttf_h, center_type) self.draw.text(pos, text, fill=fill, font=font) return self diff --git a/zhenxun/utils/depends/__init__.py b/zhenxun/utils/depends/__init__.py index addcabe6..ca6b1ba4 100644 --- a/zhenxun/utils/depends/__init__.py +++ b/zhenxun/utils/depends/__init__.py @@ -3,11 +3,11 @@ from typing import Any from nonebot.internal.params import Depends from nonebot.matcher import Matcher from nonebot.params import Command -from nonebot_plugin_saa import Text from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo from zhenxun.configs.config import Config +from zhenxun.utils.message import MessageUtils def CheckUg(check_user: bool = True, check_group: bool = True): @@ -22,11 +22,11 @@ def CheckUg(check_user: bool = True, check_group: bool = True): if check_user: user_id = session.id1 if not user_id: - await Text("用户id为空").finish() + await MessageUtils.build_message("用户id为空").finish() if check_group: group_id = session.id3 or session.id2 if not group_id: - await Text("群组id为空").finish() + await MessageUtils.build_message("群组id为空").finish() return Depends(dependency) diff --git a/zhenxun/utils/platform.py b/zhenxun/utils/platform.py index a880c877..07213280 100644 --- a/zhenxun/utils/platform.py +++ b/zhenxun/utils/platform.py @@ -10,24 +10,14 @@ from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot.utils import is_coroutine_callable -from nonebot_plugin_saa import ( - Image, - MessageFactory, - TargetDoDoChannel, - TargetDoDoPrivate, - TargetKaiheilaChannel, - TargetKaiheilaPrivate, - TargetQQGroup, - TargetQQPrivate, - Text, -) -from nonebot_plugin_saa.abstract_factories import Receipt +from nonebot_plugin_alconna.uniseg import Receipt, Target, UniMessage from pydantic import BaseModel from zhenxun.models.friend_user import FriendUser from zhenxun.models.group_console import GroupConsole from zhenxun.services.log import logger from zhenxun.utils.exception import NotFindSuperuser +from zhenxun.utils.message import MessageUtils class UserData(BaseModel): @@ -71,7 +61,7 @@ class PlatformUtils: async def send_superuser( cls, bot: Bot, - message: str | MessageFactory | Text | Image, + message: UniMessage, superuser_id: str | None = None, ) -> Receipt | None: """发送消息给超级用户 @@ -324,7 +314,7 @@ class PlatformUtils: bot: Bot, user_id: str | None, group_id: str | None, - message: str | Text | MessageFactory | Image, + message: str | UniMessage, ) -> Receipt | None: """发送消息 @@ -338,8 +328,12 @@ class PlatformUtils: Receipt | None: 是否发送成功 """ if target := cls.get_target(bot, user_id, group_id): - send_message = Text(message) if isinstance(message, str) else message - return await send_message.send_to(target, bot) + send_message = ( + MessageUtils.build_message(message) + if isinstance(message, str) + else message + ) + return await send_message.send(target=target, bot=bot) return None @classmethod @@ -383,12 +377,12 @@ class PlatformUtils: """ if isinstance(bot, (v11Bot, v12Bot)): return "qq" - if isinstance(bot, DodoBot): - return "dodo" - if isinstance(bot, KaiheilaBot): - return "kaiheila" - if isinstance(bot, DiscordBot): - return "discord" + # if isinstance(bot, DodoBot): + # return "dodo" + # if isinstance(bot, KaiheilaBot): + # return "kaiheila" + # if isinstance(bot, DiscordBot): + # return "discord" return None @classmethod @@ -524,6 +518,7 @@ class PlatformUtils: bot: Bot, user_id: str | None = None, group_id: str | None = None, + channel_id: str | None = None, ): """获取发生Target @@ -531,6 +526,7 @@ class PlatformUtils: bot: Bot user_id: 用户id group_id: 频道id或群组id + channel_id: 频道id 返回: target: 对应平台Target @@ -538,25 +534,19 @@ class PlatformUtils: target = None if isinstance(bot, (v11Bot, v12Bot)): if group_id: - target = TargetQQGroup(group_id=int(group_id)) + target = Target(group_id) elif user_id: - target = TargetQQPrivate(user_id=int(user_id)) - elif isinstance(bot, DodoBot): - if group_id: - target = TargetDoDoChannel(channel_id=group_id) + target = Target(user_id, private=True) + elif isinstance(bot, (DodoBot, KaiheilaBot)): + if group_id and channel_id: + target = Target(channel_id, parent_id=group_id, channel=True) elif user_id: - # target = TargetDoDoPrivate(user_id=user_id) - pass - elif isinstance(bot, KaiheilaBot): - if group_id: - target = TargetKaiheilaChannel(channel_id=group_id) - elif user_id: - target = TargetKaiheilaPrivate(user_id=user_id) + target = Target(user_id, private=True) return target async def broadcast_group( - message: str | MessageFactory, + message: str | UniMessage, bot: Bot | list[Bot] | None = None, bot_id: str | Set[str] | None = None, ignore_group: Set[int] | None = None, @@ -624,17 +614,14 @@ async def broadcast_group( if not is_run: continue target = PlatformUtils.get_target( - _bot, - None, - group.channel_id or group.group_id, - # , group.channel_id + _bot, None, group.group_id, group.channel_id ) if target: _used_group.append(key) message_list = message - if isinstance(message, str): - message_list = MessageFactory([Text(message)]) - await MessageFactory(message_list).send_to(target, _bot) + await MessageUtils.build_message(message_list).send( + target, _bot + ) logger.debug("发送成功", log_cmd, target=key) else: logger.warning("target为空", log_cmd, target=key) diff --git a/zhenxun/utils/utils.py b/zhenxun/utils/utils.py index 9e3caca2..7ea698a9 100644 --- a/zhenxun/utils/utils.py +++ b/zhenxun/utils/utils.py @@ -3,16 +3,13 @@ import time from collections import defaultdict from datetime import datetime from pathlib import Path -from re import L from typing import Any import httpx import pypinyin import pytz -from nonebot.adapters.onebot.v11 import Message, MessageSegment -from nonebot_plugin_saa import Image, MessageFactory, Text -from zhenxun.configs.config import NICKNAME, Config +from zhenxun.configs.config import Config from zhenxun.services.log import logger diff --git a/zhenxun/utils/withdraw_manage.py b/zhenxun/utils/withdraw_manage.py index b5b8d176..d88b394f 100644 --- a/zhenxun/utils/withdraw_manage.py +++ b/zhenxun/utils/withdraw_manage.py @@ -1,9 +1,10 @@ import asyncio from nonebot.adapters import Bot -from nonebot.adapters.discord import Bot as DiscordBot -from nonebot.adapters.dodo import Bot as DodoBot -from nonebot.adapters.kaiheila import Bot as KaiheilaBot + +# from nonebot.adapters.discord import Bot as DiscordBot +# from nonebot.adapters.dodo import Bot as DodoBot +# from nonebot.adapters.kaiheila import Bot as KaiheilaBot from nonebot.adapters.onebot.v11 import Bot as v11Bot from nonebot.adapters.onebot.v12 import Bot as v12Bot from nonebot_plugin_session import EventSession @@ -103,9 +104,9 @@ class WithdrawManager: elif isinstance(bot, v12Bot): logger.debug(f"v12Bot 撤回消息ID: {message_id}", "WithdrawManager") await bot.delete_message(message_id=str(message_id)) - elif isinstance(bot, KaiheilaBot): - pass - elif isinstance(bot, DodoBot): - pass - elif isinstance(bot, DiscordBot): - pass + # elif isinstance(bot, KaiheilaBot): + # pass + # elif isinstance(bot, DodoBot): + # pass + # elif isinstance(bot, DiscordBot): + # pass From 61926b648c2f8a09ef2e59ec5fb91f29706bc7b2 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 11 Aug 2024 16:04:48 +0800 Subject: [PATCH 130/132] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E5=9B=9E=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zhenxun/plugins/dialogue/__init__.py | 30 ++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/zhenxun/plugins/dialogue/__init__.py b/zhenxun/plugins/dialogue/__init__.py index d89eb017..5524cf9a 100644 --- a/zhenxun/plugins/dialogue/__init__.py +++ b/zhenxun/plugins/dialogue/__init__.py @@ -4,9 +4,7 @@ from nonebot.adapters import Bot from nonebot.permission import SUPERUSER from nonebot.plugin import PluginMetadata from nonebot_plugin_alconna import At as alcAt -from nonebot_plugin_alconna import Target -from nonebot_plugin_alconna import Text as alcText -from nonebot_plugin_alconna import UniMsg +from nonebot_plugin_alconna import Target, Text, UniMsg from nonebot_plugin_session import EventSession from nonebot_plugin_userinfo import EventUserInfo, UserInfo @@ -58,7 +56,7 @@ async def _( user_info: UserInfo = EventUserInfo(), ): if session.id1: - message[0] = alcText(str(message[0]).replace("滴滴滴-", "", 1)) + message[0] = Text(str(message[0]).replace("滴滴滴-", "", 1)) platform = PlatformUtils.get_platform(bot) try: superuser_id = config.platform_superusers["qq"][0] @@ -81,12 +79,12 @@ async def _( logger.info( f"发送消息至{platform}管理员: {message}", "滴滴滴-", session=session ) - message.insert(0, alcText("消息:\n")) + message.insert(0, Text("消息:\n")) if gid: - message.insert(0, alcText(f"群组: {group_name}({gid})\n")) - message.insert(0, alcText(f"昵称: {uname}({session.id1})\n")) - message.insert(0, alcText(f"Id: {DialogueManage._index}\n")) - message.insert(0, alcText("*****一份交流报告*****\n")) + message.insert(0, Text(f"群组: {group_name}({gid})\n")) + message.insert(0, Text(f"昵称: {uname}({session.id1})\n")) + message.insert(0, Text(f"Id: {DialogueManage._index}\n")) + message.insert(0, Text("*****一份交流报告*****\n")) DialogueManage.add(uname, session.id1, gid, group_name, message, platform) await message.send(bot=bot, target=Target(superuser_id, private=True)) await MessageUtils.build_message("已成功发送给管理员啦!").send(reply_to=True) @@ -100,7 +98,7 @@ async def _( message: UniMsg, session: EventSession, ): - message[0] = alcText(str(message[0]).replace("/t", "", 1).strip()) + message[0] = Text(str(message[0]).replace("/t", "", 1).strip()) if session.id1: msg = message.extract_plain_text() if not msg: @@ -127,14 +125,12 @@ async def _( group_id = model.group_id else: return MessageUtils.build_message("未获取此id数据").finish() - message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) + message[0] = Text(" ".join(str(message[0]).split(" ")[1:])) else: user_id = 0 if msg[1].isdigit(): group_id = msg[1] - message[0] = alcText( - " ".join(str(message[0]).split(" ")[2:]) - ) + message[0] = Text(" ".join(str(message[0]).split(" ")[2:])) else: await MessageUtils.build_message("群组id错误...").finish( at_sender=True @@ -144,16 +140,16 @@ async def _( user_id = msg[0] if msg[1].isdigit() and len(msg[1]) > 5: group_id = msg[1] - message[0] = alcText(" ".join(str(message[0]).split(" ")[2:])) + message[0] = Text(" ".join(str(message[0]).split(" ")[2:])) else: group_id = 0 - message[0] = alcText(" ".join(str(message[0]).split(" ")[1:])) + message[0] = Text(" ".join(str(message[0]).split(" ")[1:])) else: await MessageUtils.build_message("参数错误...").finish(at_sender=True) if group_id: if user_id: message.insert(0, alcAt("user", user_id)) - message.insert(1, "\n管理员回复\n=======\n") + message.insert(1, Text("\n管理员回复\n=======\n")) await message.send(Target(group_id), bot) await MessageUtils.build_message("消息发送成功!").finish(at_sender=True) elif user_id: From 2e66e3f48a03bdf65a06a3f707f86861a2edc938 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 11 Aug 2024 17:53:59 +0800 Subject: [PATCH 131/132] =?UTF-8?q?=E2=9C=A8=20=E4=BF=AE=E6=94=B9=E6=9C=80?= =?UTF-8?q?=E6=96=B0md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 112 ++++++++++++++++++++++++++++++++---------- docs_image/tt.jpg | Bin 0 -> 192033 bytes docs_image/webui1.png | Bin 0 -> 235680 bytes docs_image/webui2.png | Bin 0 -> 218874 bytes docs_image/webui3.png | Bin 0 -> 82994 bytes docs_image/webui4.png | Bin 0 -> 192960 bytes docs_image/webui5.png | Bin 0 -> 193771 bytes docs_image/webui6.png | Bin 0 -> 175489 bytes docs_image/webui7.png | Bin 0 -> 61637 bytes 9 files changed, 87 insertions(+), 25 deletions(-) create mode 100644 docs_image/tt.jpg create mode 100644 docs_image/webui1.png create mode 100644 docs_image/webui2.png create mode 100644 docs_image/webui3.png create mode 100644 docs_image/webui4.png create mode 100644 docs_image/webui5.png create mode 100644 docs_image/webui6.png create mode 100644 docs_image/webui7.png diff --git a/README.md b/README.md index 562b5db7..65075e5b 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,55 @@ -

+
-![maven](https://img.shields.io/badge/python-3.8%2B-blue) -![maven](https://img.shields.io/badge/nonebot-2.0.0-yellow) -![maven](https://img.shields.io/badge/go--cqhttp-1.0.0-red) +
+ +![maven](https://img.shields.io/badge/python-3.9%2B-blue) +![maven](https://img.shields.io/badge/nonebot-2.1.3-yellow) + +
+ +
# 绪山真寻Bot +
+ **** +
+ “真寻是[椛椛](https://github.com/FloatTech/ZeroBot-Plugin)的好朋友!” +
+ **** -此项目基于 Nonebot2 和 go-cqhttp 开发,以 postgresql 作为数据库的QQ群娱乐机器人 ## 关于 用爱发电,某些功能学习借鉴了大佬们的代码,因为绪山真寻实在太可爱了因此开发了 绪山真寻bot,实现了一些对群友的娱乐功能和实用功能(大概)。 -如果该项目的图片等等侵犯猫豆腐老师权益请联系我删除! +如果该项目的图片等等侵犯猫豆腐老师权益请联系我删除! -是新手!希望有个地方讨论绪山真寻Bot,或者有问题或建议,可以发送issues或加入[ [是真寻酱哒(萌新版)](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ] +讨论插件开发,nonebot2开发,或者有 安装使用问题开发建议,可以发送issues或加入[ [真寻酱的技术群](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ] (在这里请不要吹水!) + +希望有个地方讨论绪山真寻Bot,渴望吹水聊天,可以加入[ [是真寻酱哒(萌新版)](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ] -[//]: # (是老手!讨论插件开发,nonebot2开发,可以加入[ [真寻酱的技术群](https://jq.qq.com/?_wv=1027&k=u8PgBkMZ) ]) ## 声明 此项目仅用于学习交流,请勿用于非法用途 + +
+ # Nonebot2 - + 非常 [ **[NICE](https://github.com/nonebot/nonebot2)** ] 的OneBot框架 +
+ ## 未完成的文档 # [传送门](https://hibikier.github.io/zhenxun_bot/) @@ -50,15 +66,40 @@ ![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/html_help.png) -## Web UI -[zhenxun_bot_webui](https://github.com/HibiKier/zhenxun_bot_webui) +## 这是一份扩展 -## 一键安装脚本 +### 0. 体验一下? + +提供dev版本的测试zhenxun +``` +Url: 43.143.112.57:11451/onebot/v11/ws +AccessToken: PUBLIC_ZHENXUN_TEST +``` + +### 1. Web UI + +项目地址: [Web UI](https://github.com/HibiKier/zhenxun_bot_webui) + +
+后台示例图 + +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui1.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui2.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui3.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui4.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui5.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui6.png) +![x](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs_image/webui7.png) + +
+ + +### 一键安装脚本(新版未测试) [zhenxun_bot-deploy](https://github.com/AkashiCoin/zhenxun_bot-deploy) -## 提供符合真寻标准的插件仓库 +### 提供符合真寻标准的插件仓库(旧版) [AkashiCoin/nonebot_plugins_zhenxun_bot](https://github.com/AkashiCoin/nonebot_plugins_zhenxun_bot) @@ -67,7 +108,7 @@ * 实现了许多功能,且提供了大量功能管理命令 * 通过Config配置项将所有插件配置统计保存至config.yaml,利于统一用户修改 * 方便增删插件,原生nonebot2 matcher,不需要额外修改,仅仅通过简单的配置属性就可以生成`帮助图片`和`帮助信息` -* 提供了cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`__plugin_cd_limit__` +* 提供了cd,阻塞,每日次数等限制,仅仅通过简单的属性就可以生成一个限制,例如:`PluginCdBlock` 等 * **..... 更多详细请通过`传送门`查看文档!** ## 功能列表 @@ -226,10 +267,7 @@ ``` -# 配置gocq - -在 https://github.com/Mrs4s/go-cqhttp 下载Releases最新版本,运行后选择反向代理, - 后将gocq的配置文件config.yml中的universal改为universal: ws://127.0.0.1:8080/onebot/v11/ws +# 使用napcat或拉格朗日 # 获取代码 git clone https://github.com/HibiKier/zhenxun.git @@ -247,6 +285,9 @@ poetry install # 安装依赖 # 开始运行 poetry shell # 进入虚拟环境 python bot.py + +# 运行后会在data目录下生成database.json文件,请根据自身数据库配置修改 +# 其他插件配置在data/config.yaml文件中(需要运行一次) ``` ## 简单配置 @@ -256,15 +297,32 @@ python bot.py SUPERUSERS = [""] # 填写你的QQ -2.在configs/config.py文件中 - * 数据库配置 + PLATFORM_SUPERUSERS = ' + { + "qq": [""], # 在此处填写你的qq + "dodo": [], + "kaiheila": [], + "discord": [] + } +' + +2.在data/database.json文件中修改数据库配置 +{ + "bind": "", + "sql_name": "postgres", + "user": "", # 用户们 + "password": "", # 密码 + "address": "", # 数据库地址ip + "port": "", # 数据库端口 + "database": "" # 数据库名称 +} 3.在configs/config.yaml文件中 # 该文件需要启动一次后生成 * 修改插件配置项 ``` -## 使用Docker +## 使用Docker (新版未测试过) **Docker 单机版(仅真寻Bot)** **点击下方的 GitHub 徽标查看教程** @@ -335,11 +393,15 @@ PS: **ARM平台** 请使用全量版 同时 **如果你的机器 RAM < 1G 可能 ## 更新 -### 2024/1/25 +### 2024/8/11 -* 重构webui +* 更新dev -### 2023/12/28 + + +
diff --git a/docs_image/tt.jpg b/docs_image/tt.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3f105a90655eb35b5bcd7ae64189ee22c071a579 GIT binary patch literal 192033 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%0Dk3r%RVF=tf>GgV9pSwTm|J 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 M6j5F|%`MOW*{I19CjbBd literal 0 HcmV?d00001 diff --git a/docs_image/webui1.png b/docs_image/webui1.png new file mode 100644 index 0000000000000000000000000000000000000000..2cfa5822269199b153ebcaad1a1e5fa422b66474 GIT binary patch literal 235680 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

2Ie3v=Qzft5bB5GaS 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 KNl~( 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

;PX#ic}!h( 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 R?gIbZkd%{1zpDS}{{WcXa1#Ik literal 0 HcmV?d00001 diff --git a/docs_image/webui2.png b/docs_image/webui2.png new file mode 100644 index 0000000000000000000000000000000000000000..ff884972ec37e4e94f66c79d3861bc21313b6d18 GIT binary patch literal 218874 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