commit 275f05ee4a4c3d80948babf378271a127c1fa66e Author: sansenhoshi Date: Sun Jan 4 17:15:40 2026 +0800 重构 diff --git a/.env b/.env new file mode 100644 index 0000000..1e092aa --- /dev/null +++ b/.env @@ -0,0 +1,91 @@ + +DEBUG=true +HOST=127.0.0.1 # 配置 NoneBot 监听的 IP / 主机名 +PORT=49696 # 配置 NoneBot 监听的端口 +COMMAND_START=["","/"] # 配置命令起始字符 +COMMAND_SEP=["."] # 配置命令分割字符 +DRIVER=~fastapi+~websockets+~httpx+~aiohttp +LOG_LEVEL=INFO + + +SUPERUSERS=["2931589710"] # 超级管理员 + +NICKNAME=["arisu"] # 机器人昵称 + + + +# 数据存储 +# https://github.com/he0119/nonebot-plugin-datastore +DATASTORE_DATA_DIR=./hexi/data +DATASTORE_CONFIG_DIR=./hexi/data/config +DATASTORE_CACHE_DIR=./hexi/data/cache +DATASTORE_DATABASE_ECHO=false +LOCALSTORE_USE_CWD=True +# DATASTORE_DATABASE_URL="" + + +# 群管配置 +# 是否开启禁言等操作的成功提示【不开启的话踢人/禁言等成功没有QQ消息提示】 +callback_notice=true # 如果不想开启设置成 false 或者不添加此配置项【默认关闭】 + +send_group_id = ["10086"] # 必填 群号 +send_switch_morning = False # 选填 True/False 默认开启 早上消息推送是否开启 +send_switch_night = False # 选填 True/False 默认开启 晚上消息推送是否开启 +send_mode = 1 # 选填 默认模式2 模式1发送自定义句子,模式2随机调用一句 +send_sentence_morning = ["句子1","句子2","..."] # 如果是模式1 此项必填,早上随机发送该字段中的一句 +send_sentence_night = ["句子1","句子2","..."] # 如果是模式1 此项必填,晚上随机发送该字段中的一句 +send_time_moring = "8 0" # 选填 早上发送时间默认为7:00 +send_time_night = "23 0" # 选填 晚上发送时间默认为22:00 + + +# 图片系统状态 +# 使用 .env 中配置的 NICKNAME 作为图片上的 Bot 昵称(可不填) +PS_USE_ENV_NICK=True +# PS_TEST_SITES=' +# [ +# {"name": "百度", "url": "https://www.baidu.com/"}, +# {"name": "YT", "url": "https://www.youtube.com/"}, +# {"name": "谷歌", "url": "https://www.google.com/"}, +# ] + + +MCSTAT_FONT="data/font/SourceHanSansCN-Medium.otf" +MCSTAT_SHORTCUTS=' +[ + {"regex": "^查服$", "host": "sansenhoshi.online:48899", "type": "je","whitelist":[ + "872490448","170394809" + ]} +] +' +# nonebot_plugin_mcqq_server +group_list=[170394809,872490448] # QQ群 +mc_log_path="E:/Earth3/Server/logs" # log文件夹地址 +mc_ip="192.168.10.116" # 服务器 IP +mcrcon_password="1145141919810" # MCRcon password +mcrcon_port=25575 # MCRcon 端口 + + +# 词云配置文件 +wordcloud_background_color="white" + + +help_at_sender = true + + +mc_status_admin_qqnum = [2931589710] + +# 表情包制作 +memes_use_sender_when_no_image = True + +# steam +STEAM_API_KEY=["53A64539C4E11B2480AC47F2D8251728","E06B95CA4D15EB73E053EE190A528ECE"] +STEAM_DISABLE_BROADCAST_ON_STARTUP = True +STEAM_REQUEST_INTERVAL=150 +STEAM_BROADCAST_TYPE="part" + + +# 点歌 +NCM_LIST_LIMIT = 10 + +# B站解析 +analysis_display_image = true diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..13a80cf --- /dev/null +++ b/.env.dev @@ -0,0 +1,43 @@ + +DEBUG=true # 调试模式 +HOST=127.0.0.1 # 配置 NoneBot 监听的 IP / 主机名 +PORT=11011 # 配置 NoneBot 监听的端口 +COMMAND_START=["","/","."] # 配置命令起始字符 +COMMAND_SEP=["."] # 配置命令分割字符 +DRIVER=~fastapi+~websockets+~httpx # 驱动器 +LOG_LEVEL=INFO # 日志等级 + +SUPERUSERS=["2931589710"] # 超级管理员 + +NICKNAME=["Elika"] # 机器人昵称 + +# OneBot 配置 +ONEBOT_ACCESS_TOKEN +ONEBOT_V12_ACCESS_TOKEN +ONEBOT_V12_USE_MSGPACK=true + +# BA插件相关配置 +BA_PROXY +BA_SCHALE_URL=https://schale.gg/ +BA_SCHALE_MIRROR_URL=https://schale.lgc2333.top/ +BA_BAWIKI_DB_URL=https://bawiki.lgc2333.top/ + +# 数据存储 +DATASTORE_DATA_DIR=./hexi/data +DATASTORE_CONFIG_DIR=./hexi/data/config +DATASTORE_CACHE_DIR=./hexi/data/cache +DATASTORE_DATABASE_ECHO=false +DATASTORE_DATABASE_URL + + +# Nonebot-bison 订阅管理 +BISON_SKIP_BROWSER_CHECK=true +BISON_CONFIG_PATH= +BISON_USE_PIC=true +BISON_OUTER_URL=http://localhost:11011/bison +BISON_USE_PIC_MERGE=2 +bison_to_me=false + +# Sentry 日志配置 +SENTRY_ENVIRONMENT=prod +SENTRY_DSN \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1ce495 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +/.idea/.gitignore +/hexi/data/cache/nonebot_plugin_chatrecorder/images/0b046fef26a1afb031d2ec5a1d530eb3.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/0c86682f87619be8eb9ba4ffe18c5126.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/1aae2513b28f0c544945782379a5daa2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/1be0a06417247ba9f83e4749c03892ba.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/1cbd5ad06ee3b988e1472855d3ac3757.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/1e01e3386ac1cea4725d251410a5b253.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/1e600a9c96c19bdb6080dbfc46855818.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/2a9584d599054ec7d80646cbfaddc966.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/2fdda4ab30ed8ab416a477eb16ff38b2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/002e00d772c1da7ff3df03d67db6e4fa.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/3f41a07e31de9b930f7a2bf88c263ae9.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/3f8157304cd842c69e57f558e0248aff.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/4c01cd82ab1d14b033d6c60862385e97.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/4c27966c1c183b4dbfedef1e297bf99d.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/4cec1709769dd6012a745beccfa2a491.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/4edc568ece2856c14973dd185f6548c8.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/5b3c6c864ce75f3c4d1bddc97a0f6873.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/5e02b8b4a6faea447ea039e8c79c6f9f.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/05c1ff2fa9b4c596d1c31bb64413647b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/6d7432d4f473df6b318f9b4aad8aa29a.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/6e07bec0218994d1bbf5393d1e088072.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/6f5f5956a3d27837ab03f66f19d474cf.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/6f145098abc5431dbc32565cda9c602a.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/7b19f3c8dc042ffe315ea1581fe86f96.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/8f9259a10f7013b31f6f70407587d7b4.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/9edf00e36195393e8daa34d8143dc542.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/9f7fdfd5c718546f1a481022e02b3e55.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/9fe20b99a834893f55af429aed348dde.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/14dc728572b04ea0421a6c1f2fa19cbd.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/16d522040849d7a58c1a9582a24f8b5c.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/19be43d72ee2e738f3b3ad9a7c46523b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/24a92a52f5415318e97641fa71f2da68.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/26c572bdc424fb515873aa7bc5c27670.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/47da864896f5055c502d69fb0e02a3b1.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/51f987b75f6840392f9193735c129d90.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/53bef6ebf7eefec552abe3debda502bd.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/64b15d42d6fbe2963d5af2396c2b0f2b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/66ef6f322030eabc98f3580d36885cc2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/68f1995950126918825f56ee4efb1126.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/69fa5bc0467ce9e8f070ecb8d082e5bb.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/72edf92fc2e22db8ce33b44e183e36f3.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/73f4381cb0ae30baedf2b3dcb52a4681.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/80ded50d064d5eb837f2076cde2ddd86.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/81dab2d175d8bcfd14cadc3d1c6aed37.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/96e8bda8856db4224faaff1920211f60.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/96f0922f03933305aa4977df7d720a0e.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/97cc3e0ae952e5832ef9ec35ab501fc0.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/98a0b1e88f2b6d15046a54564945c724.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/175d8150a4e2656c4d56ce3ffc57abba.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/225c3e05e00db34c022dbdd7772374ec.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/246a46c02b9653a7dae25fe0b8c55e66.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/275ffdf5bfcce974fa12b03da6aa73a2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/451fd52a5a6abac25621c08a8ed37a5d.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/568b7d6274aed7036b4976891c25a428.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/590e6c855bc8d16c84752f6b9d90f838.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/661a115850430a3961ccfabc1801026c.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/693edbe30255d0440cd5d13d29822cf1.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/758e2537b637e9735fee3ab5cad976f6.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/788c0d77f884ceb635b0888cd4c32cbf.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/793a61bc46a7b4779d33ce072e143949.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/812ffe4c0e893ae4b71f7dcf5ac1d600.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/877ac81a5de51e082d0fe0f75190032b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/893a2d31a250cb2180eff9ea6758f1fa.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/3817a85ff0b61f57367048860b7709c9.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/3992bc555977fc7748a99fdd52960ce8.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/5388d81aba006e9f378ecb7b7337f413.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/5740a8bdb8636d75ee7fa05a619f5398.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/5811ddee71dd8bd3971662e05af156fe.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/6897c706b23b2fb58d695c0a451fcdb0.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/8652d16b55677ef5772a17ed63b8c614.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/8915e4a541355dedb40f44bf4b50a7b7.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/90107d04bd9d4fecfcbbac230a6b0df9.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/96088a11b0a28575a4f28e7bea69b499.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/97208f58b30cb308e9c3cb422fb3d22e.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/502087c37002227997fed74f8ff92e31.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/573554b8881c28d07eeab24ce912d5a1.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/886213e74738481f60e184d3d57ec64f.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/892202c9491f76790072ba1f364ec09b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/34510779e6ce5f55853dd063ee6af6a2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/73678600a4f7c121b102180cbee868aa.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/96837078f809a33c8f64f6e7b15e711e.cache +/hexi/plugins/dailywife/config/615526714.json +/hexi/plugins/dailywife/config/689722301.json +/hexi/plugins/dailywife/config/697997949.json +/hexi/data/cache/nonebot_plugin_chatrecorder/images/8443086078926d9ab0d00a676448ea73.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/56422241328570c49cbe70584635a47e.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a0eeef7adf5c5e04e1cb994e289661b2.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a9a88464330e5a6d4a84769af34db5b8.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a63d3b9633dc19d0d1766596cf686f2f.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a185a2b3caddb7b79334432362b49f71.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a301d14e12683e62612560689a39fa98.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a420a0389444ceff22443c276d1d7514.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/a597f0bf704d65477457fcdc94082d44.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ac7dbbaac61e8a482ce486bb1d7db65d.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/afb497b92eeba0abb9b014bc16bf9068.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/b20f12a1be0692dbafa28eb605f51bb8.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/b72abe9bface4652bf72327e75ce6151.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ba36f26bc78cff6e158ff09bcb7e891b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ba7996d5717bc5c62ff3503d6bc11da8.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/bbae8226dce98b4d067db335ec0e8fd3.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/bf8a66df42351752d69bbbdfe53f1818.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/c0c291b2dcceaef80b28fcdd453eb1fb.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/c5d987bd1e92217cd2397521c11a9944.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/caf5e74d57d31eded7e5ea5ead151eef.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/cd9ca37584884d48717f6b19df70a274.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/cdd4356b344af0d1d9c19b3debe900e9.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ce5bba5209492127a73942feb6f5c447.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d4dc3a8b843c01d98460087fd181d1b4.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d8f7ac7708701d26b43b8f76c6b8ab2e.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d68f6178df29a6263802a189dbceea1b.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d70d9dd2d7c39080b90196aa32304f33.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d56521d843f953556202286bd382bc41.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/d0770191da28c85e7f0a9e8865a60dc1.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/da212dd092b145b93ae42b0e092026e9.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/da327134ab01e12edf67f74486809688.cache +/hexi/data/data.db +/hexi/data/cache/nonebot_plugin_chatrecorder/images/df0c050ef30872fa5fa89399e039bb2f.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/e0b4a874f674ca04038f7917afa1dfda.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/e6ec73e98d331c04699592f0a39e4e69.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/e9b94e0e00816c731b26d281cd710e22.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/e096c0bd49169f24671a7063e4dc030f.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/e199a65ddce4246d480792d302409d69.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ea8767006c763d76bff11d514c2e52ae.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/ee0db4c4a89bc3cfacc601037921a7a5.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/eebaade744586ee22c2f7022dfcf2977.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/f0c4fc8436a2e9be38be46976f8ae619.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/f1ca899d7ad70cba2c225e29bdf1fffb.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/f3dfc34c04e82360906a264d989e8348.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/f05673c777a3226a5e19dc6d2cc6b858.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/f6676e7e2fe59c8d30476a53a1f20a81.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/fbbd42391cfb5629de7b32a49b5f64e1.cache +/hexi/data/cache/nonebot_plugin_chatrecorder/images/fe55526f5178e19cbb35e0287d4b15c9.cache +/.idea/HEXI.iml +/.idea/misc.xml +/.idea/modules.xml +/.idea/inspectionProfiles/profiles_settings.xml +/.idea/inspectionProfiles/Project_Default.xml +/hexi/plugins/dailywife/config/test.json +/hexi/plugins/user.db +/.idea/vcs.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..9962f56 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +个人娱乐bot,自己随便写点小功能 \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..978b632 --- /dev/null +++ b/bot.py @@ -0,0 +1,23 @@ +import nonebot +from nonebot.adapters.onebot.v11 import Adapter as ONEBOTV11Adapter +from nonebot.log import logger +from sqlalchemy import StaticPool + +# 初始化 NoneBot 以及 数据库 +nonebot.init(datastore_engine_options={"poolclass": StaticPool}) + +# 注册适配器 +app = nonebot.get_asgi() +driver = nonebot.get_driver() +driver.register_adapter(ONEBOTV11Adapter) +# driver.register_adapter(minecraftAdapter) + +# 加载自定义插件 +nonebot.load_plugins("hexi") # 加载bot自定义插件 + +# 加载配置文件中的插件 +nonebot.load_from_toml("pyproject.toml") + +if __name__ == "__main__": + logger.warning("hexi?启动!") + nonebot.run(app="__mp_main__:app") diff --git a/fonts/MiSans-Bold.ttf b/fonts/MiSans-Bold.ttf new file mode 100644 index 0000000..eda4bea Binary files /dev/null and b/fonts/MiSans-Bold.ttf differ diff --git a/fonts/MiSans-Light.ttf b/fonts/MiSans-Light.ttf new file mode 100644 index 0000000..784bdb1 Binary files /dev/null and b/fonts/MiSans-Light.ttf differ diff --git a/fonts/MiSans-Regular.ttf b/fonts/MiSans-Regular.ttf new file mode 100644 index 0000000..a408ed8 Binary files /dev/null and b/fonts/MiSans-Regular.ttf differ diff --git a/hexi/__init__.py b/hexi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hexi/plugins/__init__.py b/hexi/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hexi/plugins/core/__init__.py b/hexi/plugins/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hexi/plugins/core/message_handle.py b/hexi/plugins/core/message_handle.py new file mode 100644 index 0000000..511b989 --- /dev/null +++ b/hexi/plugins/core/message_handle.py @@ -0,0 +1,43 @@ +from nonebot.adapters.onebot.v11 import MessageSegment + + +class TextMessage: + def __init__(self, text): + self.text = text + + +class ImageMessage: + def __init__(self, image): + self.image = image + + +class UnknownMessage: + def __init__(self, raw): + self.raw = raw + + +class MessageState: + def __init__(self, data_dict): + self.data_dict = data_dict + + # 获取命令头 + def get_command(self): + return self.data_dict['_prefix']['command'] + + # 获取回复 + def get_reply(self): + return self.data_dict['reply'] + + # 获取回复对象的文本 + + # 获取命令参数 + def get_command_arg(self): + command_arg_list = self.data_dict['_prefix']['command_arg'] + if command_arg_list: + command_arg = command_arg_list[0] + if isinstance(command_arg, MessageSegment): + if command_arg.type == 'text': + return TextMessage(command_arg.data['text']) + elif command_arg.type == 'image': + return ImageMessage(command_arg.data['url']) + return None # 返回 None 表示命令参数为空或无法解析 diff --git a/hexi/plugins/dailywife/LICENSE b/hexi/plugins/dailywife/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/hexi/plugins/dailywife/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/hexi/plugins/dailywife/README.md b/hexi/plugins/dailywife/README.md new file mode 100644 index 0000000..b8cccc9 --- /dev/null +++ b/hexi/plugins/dailywife/README.md @@ -0,0 +1,47 @@ +# 今日老婆 + +一个适用于HoshinoBot的随机群友老婆插件 + +### ★ 如果你喜欢的话,请给仓库点一个star支持一下23333 ★ + +## 本项目地址: + +https://github.com/SonderXiaoming/dailywife + +## 部署教程: + +1.下载或git clone本插件: + +在 HoshinoBot\hoshino\modules 目录下使用以下命令拉取本项目 + +git clone https://github.com/SonderXiaoming/dailywife + +2.启用: + +在 HoshinoBot\hoshino\config\ **bot**.py 文件的 MODULES_ON 加入 'dailywife' + +然后重启 HoshinoBot + +## 指令 + +【今日老婆】 + +随机抓一位群友当老婆 + +每天群友老婆是固定的 + +每个群友只能当一个人的老婆 + +## 彩蛋 + +超管必抽到bot + +群友必抽不到bot + +(与原版老婆指令对应,想ntr我老婆?) + +修改的话只要把laopo.py的82,83,94行注释掉,84行elif改成if + +## 已知问题 + +储存数据用json可能不太好 diff --git a/hexi/plugins/dailywife/__init__.py b/hexi/plugins/dailywife/__init__.py new file mode 100644 index 0000000..d6c7342 --- /dev/null +++ b/hexi/plugins/dailywife/__init__.py @@ -0,0 +1,126 @@ +import base64 +import datetime +import hashlib +import json +import os +from random import choice +from tkinter import Image + +from PIL import * +from .utils import * + +import httpx +from nonebot import on_command +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +from nonebot.plugin import PluginMetadata + +__plugin_meta__ = PluginMetadata( + name="今日老婆", + description="随机抓取群友作为老婆", + usage="发送【今日老婆】", + type="application", +) + + +wife = on_command("今日老婆", aliases={"今日老婆"}) + + +def get_member_list(all_list): + id_list = [] + for member_list in all_list: + id_list.append(member_list['user_id']) + return id_list + + +async def download_avatar(user_id: str) -> bytes: + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640" + data = await download_url(url) + if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e": + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100" + data = await download_url(url) + return data + + +async def download_url(url: str) -> bytes: + async with httpx.AsyncClient() as client: + for i in range(3): + try: + resp = await client.get(url) + if resp.status_code != 200: + continue + return resp.content + except Exception as e: + print(f"Error downloading {url}, retry {i}/3: {str(e)}") + + +async def get_wife_info(member_info, qq_id): + img = await download_avatar(qq_id) + avatar = Image.open(BytesIO(img)).convert('RGBA') + avatar = png_resize(avatar, new_width=145, new_height=145) + + b_io = BytesIO() + avatar.save(b_io, format="PNG") + avatar_str = 'base64://' + base64.b64encode(b_io.getvalue()).decode() + member_name = (member_info["card"] or member_info["nickname"]) + msg = (MessageSegment.text('你今天的群友老婆是:'), MessageSegment.image(avatar_str), MessageSegment.text(f'{member_name}({qq_id})')) + return msg + + +def load_group_config(group_id: str) -> int: + filename = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json') + try: + with open(filename, encoding='utf8') as f: + config = json.load(f) + return config + except: + return None + + +def write_group_config(group_id: str, link_id: str, wife_id: str, date: str, config) -> int: + config_file = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json') + if config is not None: + config[link_id] = [wife_id, date] + else: + config = {link_id: [wife_id, date]} + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False) + + +@wife.handle() +async def wife_handle(bot: Bot, ev: MessageEvent): + group_id = ev.group_id + user_id = ev.user_id + bot_id = ev.self_id + wife_id = None + today = str(datetime.date.today()) + config = load_group_config(group_id) + + # if priv.check_priv(ev, priv.SUPERUSER): + # wife_id = bot_id + if config is not None: + if str(user_id) in list(config): + if config[str(user_id)][1] == today: + wife_id = config[str(user_id)][0] + else: + del config[str(user_id)] + + if wife_id is None: + all_list = await bot.get_group_member_list(group_id=group_id) + id_list = get_member_list(all_list) + id_list.remove(bot_id) + id_list.remove(user_id) + if config is not None: + for record_id in list(config): + if config[record_id][1] != today: + del config[record_id] + else: + try: + id_list.remove(int(config[record_id][0])) + except: + del config[record_id] + wife_id = choice(id_list) + + write_group_config(group_id, user_id, wife_id, today, config) + member_info = await bot.get_group_member_info(group_id=group_id, user_id=wife_id) + result = await get_wife_info(member_info, wife_id) + await bot.send(ev, result, at_sender=True) diff --git a/hexi/plugins/dailywife/config/128997972.json b/hexi/plugins/dailywife/config/128997972.json new file mode 100644 index 0000000..0ed9769 --- /dev/null +++ b/hexi/plugins/dailywife/config/128997972.json @@ -0,0 +1 @@ +{"2111373772": [113660107, "2024-04-24"], "3496345601": [1624801123, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/145320302.json b/hexi/plugins/dailywife/config/145320302.json new file mode 100644 index 0000000..370b96f --- /dev/null +++ b/hexi/plugins/dailywife/config/145320302.json @@ -0,0 +1 @@ +{"1041319467": [516472094, "2025-08-05"], "2252832821": [2879886254, "2025-08-05"], "1726352952": [2587439644, "2025-08-05"], "1273998653": [3687765006, "2025-08-05"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/163022834.json b/hexi/plugins/dailywife/config/163022834.json new file mode 100644 index 0000000..f4f6ef1 --- /dev/null +++ b/hexi/plugins/dailywife/config/163022834.json @@ -0,0 +1 @@ +{"1041319467": [2011432684, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/170394809.json b/hexi/plugins/dailywife/config/170394809.json new file mode 100644 index 0000000..0693829 --- /dev/null +++ b/hexi/plugins/dailywife/config/170394809.json @@ -0,0 +1 @@ +{"1870323135": [1693095928, "2024-06-22"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/182011748.json b/hexi/plugins/dailywife/config/182011748.json new file mode 100644 index 0000000..8b5682b --- /dev/null +++ b/hexi/plugins/dailywife/config/182011748.json @@ -0,0 +1 @@ +{"447903337": [1057560628, "2024-09-24"], "1545789605": [3201624634, "2024-09-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/200090274.json b/hexi/plugins/dailywife/config/200090274.json new file mode 100644 index 0000000..244eca5 --- /dev/null +++ b/hexi/plugins/dailywife/config/200090274.json @@ -0,0 +1 @@ +{"3500670381": [2021200582, "2025-04-02"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/224077009.json b/hexi/plugins/dailywife/config/224077009.json new file mode 100644 index 0000000..655bc9d --- /dev/null +++ b/hexi/plugins/dailywife/config/224077009.json @@ -0,0 +1 @@ +{"2560583072": [1578783182, "2024-04-24"], "3452205618": [1164425063, "2024-04-24"], "1960334592": [1349706190, "2024-04-24"], "771544833": [3365479138, "2024-04-24"], "2597016088": [3212547299, "2024-04-24"], "845212973": [1242230326, "2024-04-24"], "1040302669": [1663793512, "2024-04-24"], "2908237931": [1219646623, "2024-04-24"], "1284532773": [1295637870, "2024-04-24"], "2181584948": [2435409658, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/235093032.json b/hexi/plugins/dailywife/config/235093032.json new file mode 100644 index 0000000..baecdce --- /dev/null +++ b/hexi/plugins/dailywife/config/235093032.json @@ -0,0 +1 @@ +{"3809807527": [956488551, "2024-04-23"], "2456989313": [1139545630, "2024-04-23"], "1350814347": [1124975860, "2024-04-23"], "2968927618": [767255379, "2024-04-23"], "1148140708": [7371401, "2024-04-23"], "384869125": [897623868, "2024-04-23"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/487723385.json b/hexi/plugins/dailywife/config/487723385.json new file mode 100644 index 0000000..7ac782c --- /dev/null +++ b/hexi/plugins/dailywife/config/487723385.json @@ -0,0 +1 @@ +{"2931589710": [2982587103, "2025-01-10"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/531180131.json b/hexi/plugins/dailywife/config/531180131.json new file mode 100644 index 0000000..e5be5db --- /dev/null +++ b/hexi/plugins/dailywife/config/531180131.json @@ -0,0 +1 @@ +{"395381325": [945521635, "2025-12-23"], "3534766203": [3384485623, "2025-12-23"], "2691608860": [3073064852, "2025-12-23"], "1965232310": [1324576132, "2025-12-23"], "2832051108": [1928854168, "2025-12-23"], "1764806197": [2310049326, "2025-12-23"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/556338671.json b/hexi/plugins/dailywife/config/556338671.json new file mode 100644 index 0000000..c8d0885 --- /dev/null +++ b/hexi/plugins/dailywife/config/556338671.json @@ -0,0 +1 @@ +{"2252832821": [465189257, "2024-04-18"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/559190861.json b/hexi/plugins/dailywife/config/559190861.json new file mode 100644 index 0000000..0105977 --- /dev/null +++ b/hexi/plugins/dailywife/config/559190861.json @@ -0,0 +1 @@ +{"861757133": [3213425463, "2024-04-24"], "771544833": [1030501335, "2024-04-24"], "1991355616": [1919336604, "2024-04-24"], "2181584948": [2753168793, "2024-04-24"], "1606307892": [935634067, "2024-04-24"], "2474392019": [867633152, "2024-04-24"], "2813477297": [1553910163, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/710471175.json b/hexi/plugins/dailywife/config/710471175.json new file mode 100644 index 0000000..113da1d --- /dev/null +++ b/hexi/plugins/dailywife/config/710471175.json @@ -0,0 +1 @@ +{"1519780248": [1709371237, "2025-10-09"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/720681140.json b/hexi/plugins/dailywife/config/720681140.json new file mode 100644 index 0000000..0b0cddd --- /dev/null +++ b/hexi/plugins/dailywife/config/720681140.json @@ -0,0 +1 @@ +{"2234705552": [1062049100, "2024-04-23"], "1344548726": [1344952304, "2024-04-23"], "1198754678": [252439962, "2024-04-23"], "2736765611": [2295029310, "2024-04-23"], "3081567652": [1737857394, "2024-04-23"], "2402238200": [1449176382, "2024-04-23"], "541633910": [2529564599, "2024-04-23"], "3046137043": [2977483190, "2024-04-23"], "773915294": [1094329720, "2024-04-23"], "792646330": [2184843957, "2024-04-23"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/733768035.json b/hexi/plugins/dailywife/config/733768035.json new file mode 100644 index 0000000..b178bf0 --- /dev/null +++ b/hexi/plugins/dailywife/config/733768035.json @@ -0,0 +1 @@ +{"2931589710": [2667540873, "2025-07-31"], "2931589710": [2667540873, "2025-07-31"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/779720921.json b/hexi/plugins/dailywife/config/779720921.json new file mode 100644 index 0000000..1c6dc84 --- /dev/null +++ b/hexi/plugins/dailywife/config/779720921.json @@ -0,0 +1 @@ +{"390706827": [1482178145, "2024-09-06"], "2749779263": [1948639320, "2024-09-06"], "3500670381": [1545994950, "2024-09-06"], "623237274": [2028347157, "2024-09-06"], "1057720281": [1532814226, "2024-09-06"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/830257437.json b/hexi/plugins/dailywife/config/830257437.json new file mode 100644 index 0000000..0b7a0b2 --- /dev/null +++ b/hexi/plugins/dailywife/config/830257437.json @@ -0,0 +1 @@ +{"3315008735": [283791768, "2024-04-24"], "3469694349": [1792906159, "2024-04-24"], "1980736657": [1204844994, "2024-04-24"], "771544833": [692986540, "2024-04-24"], "3090432712": [1924486277, "2024-04-24"], "2157483791": [1404166113, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/866716825.json b/hexi/plugins/dailywife/config/866716825.json new file mode 100644 index 0000000..0f5166c --- /dev/null +++ b/hexi/plugins/dailywife/config/866716825.json @@ -0,0 +1 @@ +{"2252832821": [285776773, "2024-04-18"], "1053331854": [3687765006, "2024-04-18"], "1480341139": [2111969151, "2024-04-18"], "577965332": [2635073955, "2024-04-18"], "3131324404": [1219790234, "2024-04-18"], "1582656380": [742916768, "2024-04-18"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/872490448.json b/hexi/plugins/dailywife/config/872490448.json new file mode 100644 index 0000000..ce0777c --- /dev/null +++ b/hexi/plugins/dailywife/config/872490448.json @@ -0,0 +1 @@ +{"2931589710": [447903337, "2025-12-12"], "2815144875": [94070601, "2025-12-12"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/894446744.json b/hexi/plugins/dailywife/config/894446744.json new file mode 100644 index 0000000..4184693 --- /dev/null +++ b/hexi/plugins/dailywife/config/894446744.json @@ -0,0 +1 @@ +{"2634380815": [2897012201, "2024-06-30"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/894930043.json b/hexi/plugins/dailywife/config/894930043.json new file mode 100644 index 0000000..83b788d --- /dev/null +++ b/hexi/plugins/dailywife/config/894930043.json @@ -0,0 +1 @@ +{"1875610312": [2584601320, "2024-04-24"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/920042561.json b/hexi/plugins/dailywife/config/920042561.json new file mode 100644 index 0000000..400f89e --- /dev/null +++ b/hexi/plugins/dailywife/config/920042561.json @@ -0,0 +1 @@ +{"1965232310": [69420118, "2025-12-23"], "2832051108": [2205850435, "2025-12-23"], "2279525091": [1010165084, "2025-12-23"], "1764806197": [1782481403, "2025-12-23"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/928125884.json b/hexi/plugins/dailywife/config/928125884.json new file mode 100644 index 0000000..caab969 --- /dev/null +++ b/hexi/plugins/dailywife/config/928125884.json @@ -0,0 +1 @@ +{"946576072": [3561498724, "2024-10-14"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/930814044.json b/hexi/plugins/dailywife/config/930814044.json new file mode 100644 index 0000000..d69aa24 --- /dev/null +++ b/hexi/plugins/dailywife/config/930814044.json @@ -0,0 +1 @@ +{"227630446": [3346763613, "2025-07-30"], "2125169453": [906789748, "2025-07-30"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/946943846.json b/hexi/plugins/dailywife/config/946943846.json new file mode 100644 index 0000000..b235a67 --- /dev/null +++ b/hexi/plugins/dailywife/config/946943846.json @@ -0,0 +1 @@ +{"614825772": [2087918340, "2024-04-23"], "1059812302": [1805978488, "2024-04-23"], "863544577": [1776527408, "2024-04-23"], "1191715363": [2864877358, "2024-04-23"], "450457018": [460929885, "2024-04-23"], "1003695152": [3061023080, "2024-04-23"], "3152359951": [854115386, "2024-04-23"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/config/978501739.json b/hexi/plugins/dailywife/config/978501739.json new file mode 100644 index 0000000..36d3290 --- /dev/null +++ b/hexi/plugins/dailywife/config/978501739.json @@ -0,0 +1 @@ +{"1960334592": [736676100, "2024-10-11"], "2474392019": [2104386016, "2024-10-11"], "1278029334": [198295337, "2024-10-11"], "845212973": [2585860802, "2024-10-11"], "264599930": [3558155628, "2024-10-11"], "1424773950": [1609251180, "2024-10-11"], "2813477297": [1109322070, "2024-10-11"], "2739478918": [2323997133, "2024-10-11"], "2671529373": [1157197830, "2024-10-11"], "2366805964": [1511143622, "2024-10-11"], "703569661": [771544833, "2024-10-11"], "1119162975": [2296481891, "2024-10-11"], "447903337": [2668590688, "2024-10-11"], "724041940": [453427269, "2024-10-11"], "1990345380": [3070520589, "2024-10-11"]} \ No newline at end of file diff --git a/hexi/plugins/dailywife/utils.py b/hexi/plugins/dailywife/utils.py new file mode 100644 index 0000000..7922039 --- /dev/null +++ b/hexi/plugins/dailywife/utils.py @@ -0,0 +1,50 @@ +import hashlib +import json +from nonebot.log import logger as sv +import os +import random +import time +from io import BytesIO + +import aiohttp +import qrcode +import requests +import requests.exceptions +from PIL import Image, ImageDraw, ImageFont + + +# PNG重绘大小 +def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=''): + """ + PNG缩放透明度处理 + :param source_file: 源文件(Image.open()) + :param new_width: 设置的宽度 + :param new_height: 设置的高度 + :param resample: 抗锯齿 + :param ref_file: 参考文件 + :return: + """ + img = source_file + img = img.convert("RGBA") + width, height = img.size + + if ref_file != '': + imgRef = Image.open(ref_file) + new_width, new_height = imgRef.size + else: + if new_height == 0: + new_height = new_width * width / height + + bands = img.split() + resample_map = { + "NEAREST": Image.NEAREST, + "BILINEAR": Image.BILINEAR, + "BICUBIC": Image.BICUBIC, + "LANCZOS": Image.LANCZOS + } + resample_method = resample_map.get(resample, Image.LANCZOS) # 默认使用 LANCZOS + + bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands] + resized_file = Image.merge('RGBA', bands) + + return resized_file diff --git a/hexi/plugins/deadlock/__init__.py b/hexi/plugins/deadlock/__init__.py new file mode 100644 index 0000000..ba647b2 --- /dev/null +++ b/hexi/plugins/deadlock/__init__.py @@ -0,0 +1,203 @@ +# import asyncio +# import base64 +# import io +# import json +# import os +# from datetime import datetime +# from nonebot import logger +# from PIL import Image, ImageOps, ImageDraw, ImageFont +# from nonebot.internal.params import ArgPlainText +# from nonebot.params import CommandArg +# from nonebot.typing import T_State +# from playwright.async_api import async_playwright +# from nonebot import on_command +# from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment, GroupMessageEvent, Message +# from nonebot.plugin import PluginMetadata +# from typing import Optional, Union +# from nonebot.matcher import Matcher +# +# __plugin_meta__ = PluginMetadata( +# name="DeadLock助手", +# description="DeadLock助手,我要成为DeadLock高手", +# usage="nekoscore:neko查分 [steam好友代码]\n" +# "nekobind:neko绑定 [steam好友代码]\n" +# "nekorank:neko排行[TODO]\n" +# "nekomatch:dl比赛详情[TODO]\n", +# type="application", +# extra={ +# +# } +# ) +# +# neko_score = on_command("nekoscore", aliases={"dl查分"}) +# neko_bind = on_command("nekobind", aliases={"dl绑定"}) +# neko_unbind = on_command("nekounbind", aliases={"取消dl绑定"}) +# neko_rank = on_command("nekorank", aliases={"dl排行"}) +# neko_match = on_command("nekomatch", aliases={"dl比赛详情"}) +# +# basic_path = os.path.dirname(__file__) +# save_path = os.path.join(basic_path, "temp") +# img_path = os.path.join(basic_path, "img") +# data_path = os.path.join(basic_path, "data") +# +# +# @neko_score.handle() +# async def get_neko_score(ev: MessageEvent, rgs: Message = CommandArg()): +# steam_id = rgs.extract_plain_text() +# # 查询绑定情况 +# if steam_id is None or steam_id == "" or steam_id.isdigit() is False: +# bind_steam_id = await get_neko_bind(ev.group_id, ev.user_id) +# if bind_steam_id is None: +# await neko_score.finish("请输入正确的steam好友代码!") +# return +# else: +# steam_id = bind_steam_id +# await neko_score.send(f"正在获取{steam_id}的数据") +# url = f"https://deadlock.blast.tv/users/{steam_id}" +# time_present1 = get_present_time() +# result = await capture_screenshot(url, time_present1) +# if result != "success": +# await neko_score.finish(MessageSegment.reply(ev.message_id) + result) +# img_path1 = os.path.join(save_path, f"{time_present1}.png") +# images = gen_ms_img(Image.open(img_path1)) +# mes = (MessageSegment.reply(ev.message_id), images) +# await neko_score.send(mes) +# os.remove(img_path1) +# +# +# @neko_bind.handle() +# async def play_neko_bind(ev: MessageEvent, rgs: Message = CommandArg()): +# # 获取群号 +# group_id = ev.group_id +# steam_id = rgs.extract_plain_text() +# if steam_id is None or steam_id == "" or steam_id.isdigit() is False: +# await neko_score.finish("请输入正确的steam好友代码!") +# return +# qq_id = ev.user_id +# res = await to_neko_bind(group_id, steam_id, qq_id) +# await neko_score.send(res) +# +# +# @neko_unbind.handle() +# async def play_neko_unbind(ev: MessageEvent, rgs: Message = CommandArg()): +# # 获取群号 +# group_id = ev.group_id +# steam_id = rgs.extract_plain_text() +# qq_id = ev.user_id +# res = await to_neko_unbind(group_id, qq_id) +# if res is None: +# await neko_score.finish(f"{qq_id}未绑定过") +# else: +# await neko_score.send(f"{qq_id}已经取消与{res}的绑定") +# +# +# async def to_neko_bind(group_id, steam_id, qq_id): +# if os.path.exists(f"{data_path}/{group_id}.json"): +# with open(f"{data_path}/{group_id}.json", 'r') as file: +# json_data = json.load(file) +# else: +# # 如果文件不存在,创建一个新的空字典 +# json_data = {} +# # 检查指定的 key 是否存在 +# if str(qq_id) in json_data: +# # 如果 key 存在,将对应键值更新 +# old_steam_id = json_data[str(qq_id)] +# json_data[str(qq_id)] = steam_id +# # 将更新后的数据写回 JSON 文件 +# with open(f"{data_path}/{group_id}.json", 'w') as file: +# json.dump(json_data, file, indent=4) +# return f"已经将[ {str(qq_id)} ]的绑定[ {old_steam_id} ]更新到了[ {str(steam_id)} ]" +# else: +# # 如果 key 不存在,添加新的 key-value 对 +# json_data[str(qq_id)] = steam_id +# +# # 将更新后的数据写回 JSON 文件 +# with open(f"{data_path}/{group_id}.json", 'w') as file: +# json.dump(json_data, file, indent=4) +# return f"[ {str(qq_id)} ]已经绑定[ {steam_id} ]" +# +# +# async def get_neko_bind(group_id, qq_id): +# if os.path.exists(f"{data_path}/{group_id}.json"): +# with open(f"{data_path}/{group_id}.json", 'r') as file: +# json_data = json.load(file) +# else: +# # 如果文件不存在,创建一个新的空字典 +# json_data = { +# "000000": 000000 +# } +# # 检查指定的 key 是否存在 +# if str(qq_id) in json_data: +# return json_data[str(qq_id)] +# else: +# return None +# +# +# async def to_neko_unbind(group_id, qq_id): +# if os.path.exists(f"{data_path}/{group_id}.json"): +# with open(f"{data_path}/{group_id}.json", 'r') as file: +# json_data = json.load(file) +# else: +# # 如果文件不存在,创建一个新的空字典 +# json_data = { +# "000000": 000000 +# } +# # 检查指定的 key 是否存在 +# if str(qq_id) in json_data: +# remove_value = json_data.pop(str(qq_id)) +# # 将更新后的数据写回 JSON 文件 +# with open(f"{data_path}/{group_id}.json", 'w') as file: +# json.dump(json_data, file, indent=4) +# return remove_value +# else: +# return None +# +# +# def get_present_time() -> int: +# return int(datetime.timestamp(datetime.now())) +# +# +# async def capture_screenshot(url: str, time_present: int): +# async with async_playwright() as p: +# browser = await p.chromium.launch() +# page = await browser.new_page() +# try: +# # 设置视口大小 +# await page.set_viewport_size({"width": 1600, "height": 900}) +# await page.goto(url) +# await page.wait_for_load_state('networkidle') +# +# except Exception as e: +# return f"访问网站异常{type(e)}`{e}`" +# +# await asyncio.sleep(1) +# i_path = os.path.join(save_path, f'{time_present}.png') +# await page.screenshot( +# path=i_path, +# full_page=True +# ) +# logger.info("正在压缩图片...") +# await asyncio.sleep(2) +# img_convert = Image.open(i_path) +# img_convert.save(i_path, quality=80) +# logger.info("图片保存成功!") +# await browser.close() +# return "success" +# +# +# def gen_ms_img(image: Union[bytes, Image.Image]) -> MessageSegment: +# if isinstance(image, bytes): +# return MessageSegment.image( +# pic2b64(Image.open(io.BytesIO(image))) +# ) +# else: +# return MessageSegment.image( +# pic2b64(image) +# ) +# +# +# def pic2b64(pic: Image) -> str: +# buf = io.BytesIO() +# pic.save(buf, format='PNG') +# base64_str = base64.b64encode(buf.getvalue()).decode() +# return 'base64://' + base64_str diff --git a/hexi/plugins/deadlock/data/487723385.json b/hexi/plugins/deadlock/data/487723385.json new file mode 100644 index 0000000..3c580c7 --- /dev/null +++ b/hexi/plugins/deadlock/data/487723385.json @@ -0,0 +1,5 @@ +{ + "2931589710": "441954135", + "2575131547": "1244138625", + "2028018739": "869378244" +} \ No newline at end of file diff --git a/hexi/plugins/deadlock/data/733768035.json b/hexi/plugins/deadlock/data/733768035.json new file mode 100644 index 0000000..6bf62b6 --- /dev/null +++ b/hexi/plugins/deadlock/data/733768035.json @@ -0,0 +1,3 @@ +{ + "2931589710": "441954135" +} \ No newline at end of file diff --git a/hexi/plugins/deadlock/data/872490448.json b/hexi/plugins/deadlock/data/872490448.json new file mode 100644 index 0000000..504e88c --- /dev/null +++ b/hexi/plugins/deadlock/data/872490448.json @@ -0,0 +1,4 @@ +{ + "2931589710": "441954135", + "1651620969": "1220823479" +} \ No newline at end of file diff --git a/hexi/plugins/deadlock/temp/1728540429.png b/hexi/plugins/deadlock/temp/1728540429.png new file mode 100644 index 0000000..eb18bec Binary files /dev/null and b/hexi/plugins/deadlock/temp/1728540429.png differ diff --git a/hexi/plugins/deadlock/temp/1728540619.png b/hexi/plugins/deadlock/temp/1728540619.png new file mode 100644 index 0000000..eb18bec Binary files /dev/null and b/hexi/plugins/deadlock/temp/1728540619.png differ diff --git a/hexi/plugins/deeepseek_chat/__init__.py b/hexi/plugins/deeepseek_chat/__init__.py new file mode 100644 index 0000000..56c3ed2 --- /dev/null +++ b/hexi/plugins/deeepseek_chat/__init__.py @@ -0,0 +1,56 @@ +# from venv import logger +# +# from nonebot import on_command +# from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +# from nonebot.plugin import PluginMetadata +# from nonebot.plugin.on import on_message +# from nonebot.rule import to_me +# import io +# import base64 +# from .chat import * +# +# __plugin_meta__ = PluginMetadata( +# name="AI", +# description="AI?", +# usage="", +# type="application", +# ) +# +# at_me = on_command("", rule=to_me()) +# +# +# # at时忽略上下文 +# @at_me.handle() +# async def do_job(bot: Bot, ev: MessageEvent): +# # gid = ev.group_id +# # print(gid) +# # if gid in [872490448, 487723385]: +# # logger.info("白名单群, 执行AI回答操作") +# # text = ev.message +# # message = text.extract_plain_text() +# # print(type(text)) +# # print(text) +# # think, answer = await chat(message) +# # await at_me.finish(answer) +# # else: +# # logger.info("非白名单群,不执行AI回答操作") +# +# # +# # @query.handle() +# # async def query_job(bot: Bot, ev: MessageEvent): +# # # 获取当前日期的年份和月份 +# # now = datetime.now() +# # year = now.year +# # month = now.month +# # # 获取发信人信息 +# # uid = str(ev.user_id) +# # message_id = ev.message_id +# # # 获取记录 +# # img_file = await draw_calendar_grid_image(uid, year, month) +# # text_msg = MessageSegment.text("这是您的记鹿:\n") +# # image_msg = MessageSegment.image(pic2b64(img_file)) +# # mix_msg = (text_msg,) +# # mix_msg += image_msg +# # reply_msg = (MessageSegment.reply(message_id),) +# # reply_msg += mix_msg +# # await record.finish(reply_msg) diff --git a/hexi/plugins/deeepseek_chat/chat.py b/hexi/plugins/deeepseek_chat/chat.py new file mode 100644 index 0000000..f992960 --- /dev/null +++ b/hexi/plugins/deeepseek_chat/chat.py @@ -0,0 +1,82 @@ +# import requests +# import json +# import re +# import asyncio +# from ollama import AsyncClient +# from ollama import Client +# import nest_asyncio +# +# # 存储历史记录 +# history = [] +# +# +# async def chat(question: str): +# global history +# system_prompt = """你的身份是蔚蓝档案中的天童爱丽丝,喜欢玩电子游戏,口头禅是“邦邦咔邦”,是模仿的游戏音效,请你以她的视角来与用户交流,面对用户的问题请你选择卖萌来规避问题不进行回答。""" +# # 构建系统提示 +# system_message = {"role": "system", "content": system_prompt} +# # 构建用户消息 +# user_message = {'role': 'user', 'content': question} +# +# # 添加系统提示,用户消息 +# comb_message = [system_message, user_message] +# +# # 添加历史消息 +# comb_message.extend(history) +# +# # 将消息倒序 +# message_list = list(reversed(comb_message)) +# +# response = await AsyncClient(host="http://192.168.2.215:11434").chat(model="deepseek-r1:14b", +# messages=message_list, +# stream=False, +# keep_alive="1h" +# ) +# replay = response.message.content +# +# # 使用正则表达式匹配 ... 标签内的内容 +# pattern = re.compile(r'(.*?)', re.DOTALL) +# +# # 查找所有的 标签内容 +# think = pattern.findall(replay) +# +# # 使用 re.split 将字符串按 标签分割 +# split_result = pattern.split(replay) +# +# # 标签外的内容 +# answer = [s.strip() for s in split_result if s.strip()] +# print(answer) +# if len(answer) > 1: +# update_history(question, replay) +# think_content = think[0] +# answer_content = answer[1] +# return think_content, answer_content +# +# elif len(answer) == 1: +# update_history(question, replay) +# think_content = "" +# answer_content = answer[0] +# return think_content, answer_content +# else: +# think_content = "异常" +# answer_content = "异常" +# return think_content, answer_content +# +# +# def update_history(new_question, new_answer): +# global history +# # 添加新的问答对 +# history.append({"role": "user", "content": new_question}) +# history.append({"role": "assistant", "content": new_answer}) +# +# # 如果历史记录超过4条,移除最旧的两条 +# if len(history) > 4: +# del history[0:2] +# +# for e in history: +# print(e) +# +# +# def clean_history(): +# global history +# history = [] diff --git a/hexi/plugins/deer_pipe/__init__.py b/hexi/plugins/deer_pipe/__init__.py new file mode 100644 index 0000000..214690d --- /dev/null +++ b/hexi/plugins/deer_pipe/__init__.py @@ -0,0 +1,70 @@ +from nonebot import on_command +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +from nonebot.plugin import PluginMetadata +from nonebot.rule import to_me + +from .data_proc import * +from .img_generator import * +import io +import base64 + +__plugin_meta__ = PluginMetadata( + name="打卡记鹿", + description="你今天鹿管了ma?", + usage="at机器人 发送:打卡 " + "直接发送:查卡(查询打卡记录)", + type="application", +) + +record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me()) +query = on_command("查询记录", aliases={"查卡"}) + + +@record.handle() +async def do_job(bot: Bot, ev: MessageEvent): + # 获取当前日期的年份和月份 + now = datetime.now() + year = now.year + month = now.month + day = now.day + # 获取发信人信息 + uid = str(ev.user_id) + message_id = ev.message_id + # 添加一条记录 + await record_month(uid, day) + # 获取记录 + img = await draw_calendar_grid_image(uid, year, month) + text_msg = MessageSegment.text("这是您的打卡记鹿:\n") + image_msg = MessageSegment.image(pic2b64(img)) + mix_msg = (text_msg,) + mix_msg += image_msg + reply_msg = (MessageSegment.reply(message_id),) + reply_msg += mix_msg + await record.finish(reply_msg) + + +def pic2b64(pic: Image) -> str: + buf = io.BytesIO() + pic.save(buf, format='PNG') + base64_str = base64.b64encode(buf.getvalue()).decode() + return 'base64://' + base64_str + + +@query.handle() +async def query_job(bot: Bot, ev: MessageEvent): + # 获取当前日期的年份和月份 + now = datetime.now() + year = now.year + month = now.month + # 获取发信人信息 + uid = str(ev.user_id) + message_id = ev.message_id + # 获取记录 + img_file = await draw_calendar_grid_image(uid, year, month) + text_msg = MessageSegment.text("这是您的记鹿:\n") + image_msg = MessageSegment.image(pic2b64(img_file)) + mix_msg = (text_msg,) + mix_msg += image_msg + reply_msg = (MessageSegment.reply(message_id),) + reply_msg += mix_msg + await record.finish(reply_msg) diff --git a/hexi/plugins/deer_pipe/data_proc.py b/hexi/plugins/deer_pipe/data_proc.py new file mode 100644 index 0000000..e5710cf --- /dev/null +++ b/hexi/plugins/deer_pipe/data_proc.py @@ -0,0 +1,48 @@ +import os +import json +from datetime import datetime, timedelta + +filepath = os.path.dirname(__file__).replace("\\", "/") +data_path = filepath+'/deer_pipe/' + + +# 记录信息 +async def record_month(uid, day: int): + # 获取当前日期 + current_date = datetime.now() + formatted_date = current_date.strftime('%Y-%m') + json_file_path = f"{data_path}/{formatted_date}.json" + if os.path.exists(json_file_path): + # 如果文件存在,则读取其中的数据 + with open(json_file_path, 'r', encoding='utf-8') as file: + data = json.load(file) + if uid in data: + day_list = data[uid] + day_list.append(day) + else: + day_list = [day] + data[uid] = day_list + else: + # 如果文件不存在,则创建一个新的空数据结构 + day_list = [day] + data = {'uid': day_list} + with open(json_file_path, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=4) + + +# 读取当月信息 +async def get_records(uid): + day_list = [] + # 获取当前日期 + current_date = datetime.now() + formatted_date = current_date.strftime('%Y-%m') + json_file_path = f"{data_path}/{formatted_date}.json" + if os.path.exists(json_file_path): + # 如果文件存在,则读取其中的数据 + with open(json_file_path, 'r', encoding='utf-8') as file: + data = json.load(file) + if uid in data: + day_list = data[uid] + else: + day_list = [0] + return day_list diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-07.json b/hexi/plugins/deer_pipe/deer_pipe/2024-07.json new file mode 100644 index 0000000..6365830 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-07.json @@ -0,0 +1,458 @@ +{ + "2931589710": [ + 1, + 2, + 3, + 4, + 4, + 5, + 16, + 16 + ], + "437916206": [ + 1 + ], + "386911634": [ + 1, + 2, + 2, + 3, + 4, + 6 + ], + "2083436520": [ + 1, + 2, + 3, + 4, + 5, + 7, + 8 + ], + "3201624634": [ + 1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 16 + ], + "1192582155": [ + 1, + 2 + ], + "1640154565": [ + 1, + 2, + 3, + 6 + ], + "2652920210": [ + 1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "1532106994": [ + 1, + 1, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 16 + ], + "1651620969": [ + 1, + 1, + 2, + 3, + 3, + 4, + 5, + 6, + 8 + ], + "2448320195": [ + 1, + 1, + 2, + 3, + 4, + 4, + 4, + 4, + 6, + 7, + 8 + ], + "2295029310": [ + 1 + ], + "2658100993": [ + 1, + 4, + 5 + ], + "2656360014": [ + 1 + ], + "736875294": [ + 1, + 6, + 7 + ], + "1827595836": [ + 1, + 1 + ], + "198295337": [ + 1, + 2, + 3, + 4, + 5, + 5, + 6, + 8, + 8, + 8, + 8, + 8 + ], + "3098632978": [ + 1, + 2, + 2, + 3, + 4, + 5, + 6, + 7 + ], + "2815144875": [ + 1, + 1, + 2, + 2, + 2, + 3, + 3, + 4, + 5, + 5, + 6, + 6, + 7, + 8, + 16, + 16 + ], + "906575749": [ + 1 + ], + "237957101": [ + 1, + 2, + 3, + 5, + 6, + 6 + ], + "1564571378": [ + 1 + ], + "870520151": [ + 1, + 5 + ], + "1357024971": [ + 1 + ], + "2872682608": [ + 1, + 2, + 2, + 2, + 3, + 4, + 5, + 6, + 6, + 7, + 8 + ], + "1327093900": [ + 1, + 2 + ], + "3698818486": [ + 1 + ], + "1766551374": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "1207799328": [ + 1, + 1, + 5 + ], + "2111373772": [ + 1, + 2, + 2, + 6 + ], + "2193159167": [ + 1 + ], + "249266185": [ + 1, + 2, + 5, + 6 + ], + "2857915028": [ + 1, + 2, + 2, + 2, + 2 + ], + "1251742023": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 7, + 8 + ], + "1327784910": [ + 1, + 2, + 3 + ], + "499867291": [ + 1, + 5, + 6, + 6 + ], + "2640981487": [ + 2, + 3 + ], + "1198453273": [ + 2, + 3 + ], + "2635940476": [ + 2, + 2, + 2 + ], + "2603150110": [ + 2, + 2, + 3, + 4, + 4, + 5, + 7, + 7, + 8, + 16 + ], + "2094292259": [ + 2 + ], + "736676100": [ + 2 + ], + "1584905882": [ + 2, + 4, + 5 + ], + "2996805685": [ + 2, + 4, + 6 + ], + "827398256": [ + 2, + 2, + 2, + 5, + 6, + 7, + 7 + ], + "1102997238": [ + 2, + 4, + 5, + 7, + 8 + ], + "1722173847": [ + 2, + 2 + ], + "1041319467": [ + 2 + ], + "962608623": [ + 2 + ], + "948186339": [ + 2, + 5, + 6 + ], + "276336778": [ + 2, + 3, + 4, + 5, + 6, + 6, + 16 + ], + "2388483507": [ + 2 + ], + "3256631053": [ + 2, + 3, + 4, + 5, + 6, + 8 + ], + "156098539": [ + 2 + ], + "1533703664": [ + 2 + ], + "1097829523": [ + 2 + ], + "724041940": [ + 2, + 3, + 5, + 6, + 8, + 16 + ], + "381268035": [ + 2 + ], + "2374035622": [ + 2, + 3, + 5, + 6 + ], + "2781719263": [ + 2 + ], + "2449768430": [ + 3 + ], + "1870323135": [ + 3 + ], + "2591212935": [ + 3 + ], + "1545789605": [ + 4, + 6 + ], + "1136531059": [ + 5, + 6, + 7 + ], + "2830599695": [ + 5 + ], + "1119162975": [ + 5, + 8 + ], + "1458143947": [ + 6, + 6, + 7 + ], + "2478314183": [ + 6 + ], + "3092179918": [ + 6 + ], + "1628420979": [ + 6, + 7 + ], + "1952839455": [ + 6, + 7 + ], + "2174309103": [ + 6 + ], + "410300249": [ + 6 + ], + "1611414080": [ + 6 + ], + "1187039544": [ + 6, + 7, + 7, + 8 + ], + "1745882130": [ + 7, + 16, + 16 + ], + "2321247175": [ + 7 + ], + "2530894749": [ + 7 + ], + "986239980": [ + 8 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-08.json b/hexi/plugins/deer_pipe/deer_pipe/2024-08.json new file mode 100644 index 0000000..82079fa --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-08.json @@ -0,0 +1,69 @@ +{ + "uid": [ + 27 + ], + "1532106994": [ + 27, + 28, + 29, + 30 + ], + "978975474": [ + 27 + ], + "1990345380": [ + 27, + 28, + 28, + 31 + ], + "1521198648": [ + 27, + 28, + 29, + 30, + 31 + ], + "919617516": [ + 27 + ], + "603055627": [ + 27 + ], + "2982067505": [ + 27 + ], + "3201624634": [ + 28, + 29, + 30 + ], + "2652920210": [ + 28, + 30 + ], + "598111936": [ + 28 + ], + "2815144875": [ + 28 + ], + "1611414080": [ + 28 + ], + "2908237931": [ + 28 + ], + "2752597456": [ + 28 + ], + "237957101": [ + 28 + ], + "3529206922": [ + 29 + ], + "2366805964": [ + 30 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-09.json b/hexi/plugins/deer_pipe/deer_pipe/2024-09.json new file mode 100644 index 0000000..66c4346 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-09.json @@ -0,0 +1,94 @@ +{ + "uid": [ + 1 + ], + "1990345380": [ + 1 + ], + "1532106994": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30 + ], + "1521198648": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 28, + 29, + 30 + ], + "2931589710": [ + 4, + 13, + 13, + 18 + ], + "1187039544": [ + 5 + ], + "919617516": [ + 6, + 7, + 13, + 26 + ], + "2366805964": [ + 7 + ], + "1196293185": [ + 13 + ], + "2908237931": [ + 18 + ], + "3070520589": [ + 23 + ], + "1520107082": [ + 25 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-10.json b/hexi/plugins/deer_pipe/deer_pipe/2024-10.json new file mode 100644 index 0000000..f685c31 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-10.json @@ -0,0 +1,63 @@ +{ + "uid": [ + 1 + ], + "2652920210": [ + 1 + ], + "1532106994": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 11, + 12, + 13, + 14, + 15, + 16, + 16, + 17, + 18, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31 + ], + "1521198648": [ + 1, + 3, + 8, + 10 + ], + "2813477297": [ + 1 + ], + "361060689": [ + 1 + ], + "2603150110": [ + 9 + ], + "2931589710": [ + 9, + 10 + ], + "1187039544": [ + 13 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-11.json b/hexi/plugins/deer_pipe/deer_pipe/2024-11.json new file mode 100644 index 0000000..56c9281 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-11.json @@ -0,0 +1,39 @@ +{ + "uid": [ + 1 + ], + "1532106994": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 26, + 26, + 28, + 29, + 30 + ], + "2603150110": [ + 6 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2024-12.json b/hexi/plugins/deer_pipe/deer_pipe/2024-12.json new file mode 100644 index 0000000..8e8adfa --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2024-12.json @@ -0,0 +1,51 @@ +{ + "uid": [ + 1 + ], + "1532106994": [ + 1, + 2, + 3, + 3, + 4, + 5, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 25, + 26, + 28, + 29, + 30, + 31 + ], + "2083436520": [ + 1 + ], + "2931589710": [ + 1 + ], + "2783677365": [ + 3, + 4, + 7 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-01.json b/hexi/plugins/deer_pipe/deer_pipe/2025-01.json new file mode 100644 index 0000000..cb4b1f5 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-01.json @@ -0,0 +1,41 @@ +{ + "uid": [ + 2 + ], + "1532106994": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 22, + 23, + 24, + 25 + ], + "2603150110": [ + 3 + ], + "249266185": [ + 19 + ], + "1136531059": [ + 25 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-02.json b/hexi/plugins/deer_pipe/deer_pipe/2025-02.json new file mode 100644 index 0000000..53901db --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-02.json @@ -0,0 +1,70 @@ +{ + "uid": [ + 6 + ], + "1532106994": [ + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 24, + 25, + 26, + 27, + 28 + ], + "2815144875": [ + 24 + ], + "2931589710": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24 + ], + "2872682608": [ + 24 + ], + "1205646769": [ + 24 + ], + "2635940476": [ + 24 + ], + "1120308711": [ + 24 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-03.json b/hexi/plugins/deer_pipe/deer_pipe/2025-03.json new file mode 100644 index 0000000..436c280 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-03.json @@ -0,0 +1,39 @@ +{ + "uid": [ + 1 + ], + "1532106994": [ + 1, + 3, + 5 + ], + "2449768430": [ + 1 + ], + "962608623": [ + 1 + ], + "2931589710": [ + 12, + 25 + ], + "1171418826": [ + 12 + ], + "1205646769": [ + 12, + 31 + ], + "2815144875": [ + 12, + 25, + 27, + 31 + ], + "547868332": [ + 12 + ], + "2872682608": [ + 12 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-04.json b/hexi/plugins/deer_pipe/deer_pipe/2025-04.json new file mode 100644 index 0000000..1e6138d --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-04.json @@ -0,0 +1,47 @@ +{ + "uid": [ + 1 + ], + "1171418826": [ + 1, + 2, + 10, + 15, + 17 + ], + "2931589710": [ + 1, + 14 + ], + "1640154565": [ + 1 + ], + "2815144875": [ + 2, + 3, + 6 + ], + "1916766091": [ + 10, + 16 + ], + "3301750": [ + 14, + 15, + 16, + 17, + 18, + 28 + ], + "3451383113": [ + 14 + ], + "2683512785": [ + 15 + ], + "947571000": [ + 17, + 19, + 24 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-07.json b/hexi/plugins/deer_pipe/deer_pipe/2025-07.json new file mode 100644 index 0000000..1b0db63 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-07.json @@ -0,0 +1,5 @@ +{ + "uid": [ + 7 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/deer_pipe/2025-09.json b/hexi/plugins/deer_pipe/deer_pipe/2025-09.json new file mode 100644 index 0000000..41b89c3 --- /dev/null +++ b/hexi/plugins/deer_pipe/deer_pipe/2025-09.json @@ -0,0 +1,5 @@ +{ + "uid": [ + 5 + ] +} \ No newline at end of file diff --git a/hexi/plugins/deer_pipe/font/SourceHanSansCN-Medium.otf b/hexi/plugins/deer_pipe/font/SourceHanSansCN-Medium.otf new file mode 100644 index 0000000..630d546 Binary files /dev/null and b/hexi/plugins/deer_pipe/font/SourceHanSansCN-Medium.otf differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/deer_pipe.jpg b/hexi/plugins/deer_pipe/img/deer_pipe/deer_pipe.jpg new file mode 100644 index 0000000..1cb0928 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/deer_pipe.jpg differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J1.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J1.png new file mode 100644 index 0000000..fa9514a Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J1.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J10.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J10.png new file mode 100644 index 0000000..194dc95 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J10.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J11.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J11.png new file mode 100644 index 0000000..65910f5 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J11.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J12.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J12.png new file mode 100644 index 0000000..522acc0 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J12.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J13.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J13.png new file mode 100644 index 0000000..e4e1528 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J13.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J14.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J14.png new file mode 100644 index 0000000..6279baf Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J14.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J15.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J15.png new file mode 100644 index 0000000..38e847e Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J15.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J16.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J16.PNG new file mode 100644 index 0000000..c3458ea Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J16.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J17.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J17.PNG new file mode 100644 index 0000000..6676351 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J17.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J18.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J18.PNG new file mode 100644 index 0000000..d3bba23 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J18.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J19.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J19.PNG new file mode 100644 index 0000000..cbcd022 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J19.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J2.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J2.png new file mode 100644 index 0000000..2b1c18a Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J2.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J20.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J20.PNG new file mode 100644 index 0000000..e68c6f1 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J20.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J21.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J21.PNG new file mode 100644 index 0000000..fd0dc44 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J21.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J22.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J22.PNG new file mode 100644 index 0000000..e2f570c Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J22.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J23.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J23.PNG new file mode 100644 index 0000000..fd02592 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J23.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J24.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J24.PNG new file mode 100644 index 0000000..ba8b4ec Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J24.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J25.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J25.PNG new file mode 100644 index 0000000..15b6b4d Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J25.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J26.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J26.PNG new file mode 100644 index 0000000..a8a3686 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J26.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J27.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J27.PNG new file mode 100644 index 0000000..ebb6598 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J27.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J28.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J28.PNG new file mode 100644 index 0000000..824c4f8 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J28.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J29.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J29.PNG new file mode 100644 index 0000000..b091815 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J29.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J3.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J3.png new file mode 100644 index 0000000..c31b77b Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J3.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J30.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J30.PNG new file mode 100644 index 0000000..bb8fb31 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J30.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J31.PNG b/hexi/plugins/deer_pipe/img/deer_pipe/right/J31.PNG new file mode 100644 index 0000000..4ca86f7 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J31.PNG differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J4.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J4.png new file mode 100644 index 0000000..7d10d5a Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J4.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J5.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J5.png new file mode 100644 index 0000000..5066828 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J5.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J6.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J6.png new file mode 100644 index 0000000..8e28da3 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J6.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J7.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J7.png new file mode 100644 index 0000000..b7b1fff Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J7.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J8.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J8.png new file mode 100644 index 0000000..80c63a2 Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J8.png differ diff --git a/hexi/plugins/deer_pipe/img/deer_pipe/right/J9.png b/hexi/plugins/deer_pipe/img/deer_pipe/right/J9.png new file mode 100644 index 0000000..c9f750a Binary files /dev/null and b/hexi/plugins/deer_pipe/img/deer_pipe/right/J9.png differ diff --git a/hexi/plugins/deer_pipe/img_generator.py b/hexi/plugins/deer_pipe/img_generator.py new file mode 100644 index 0000000..99e8b6f --- /dev/null +++ b/hexi/plugins/deer_pipe/img_generator.py @@ -0,0 +1,118 @@ +import calendar +from datetime import datetime, timedelta +from PIL import Image, ImageDraw, ImageFont, ImageOps +from .data_proc import * +import random + +filepath = os.path.dirname(__file__).replace("\\", "/") + + +async def draw_calendar_grid_image(uid, year, month): + # 获取该月的第一天是星期几,以及这个月有多少天 + first_weekday, num_days = calendar.monthrange(year, month) + + # 调整第一天的索引,使周日对应0,周六对应6 + first_weekday = (first_weekday + 1) % 7 + + # 设置单元格尺寸和内边距 + cell_size = 50 + cell_padding = 5 + + # 计算当前月份的行数 + num_rows = (num_days + first_weekday + 6) // 7 + 1 # +1 行用于留空 + + # 计算图片宽度和高度 + image_width = 7 * (cell_size + cell_padding) - cell_padding + image_height = num_rows * (cell_size + cell_padding) - cell_padding + + # 创建图像对象 + img = Image.new("RGBA", (image_width, image_height), "white") + draw = ImageDraw.Draw(img) + font_path = filepath + "/font/SourceHanSansCN-Medium.otf" + print(font_path) + font_normal = ImageFont.truetype(font=font_path, size=18) + font_large = ImageFont.truetype(font=font_path, size=24) + + # 绘制网格线 + for i in range(7): + x = i * (cell_size + cell_padding) + draw.line([(x, 55), (x, image_height)], fill="black", width=1) + for j in range(0, image_height + cell_size, cell_size + cell_padding): + draw.line([(0, j), (image_width, j)], fill="black", width=1) + + # 小🦌 + deer_pipe_path = filepath + "/img/deer_pipe/deer_pipe.jpg" + deer_pipe_img = Image.open(deer_pipe_path) + deer_pipe_img = deer_pipe_img.resize((54, 50)) + + # 画标题 + title = f"{month}月打卡记鹿" + text_length = draw.textlength(text=title, font=font_large) + img_xy = (int(((image_width - text_length + 55) / 2) - 55), 5) + title_pos = ((image_width - text_length + 55) / 2, 10) + img.paste(deer_pipe_img, img_xy) + draw.text(title_pos, title, fill="black", font=font_large) + + # 获取🦌信息 + days = await get_records(uid) + # 填充日期 + date = datetime(year, month, 1) + for day in range(1, num_days + 1): + weekday = (first_weekday + day - 1) % 7 + row = (first_weekday + day - 1) // 7 + 1 # +1 行用于留空 + x = weekday * (cell_size + cell_padding) + cell_padding + y = row * (cell_size + cell_padding) + cell_padding + img.paste(deer_pipe_img, (x - 4, y)) + draw.text((x+20, y - 2), f"{str(day)}", fill="black", font=font_normal) + if day in days: + im = get_random_right() + img = image_paste(im, img, (x - 4, y)) + + # 保存图片 + # img.save(output_image_path) + + # 显示图片 + # img.show() + img = ImageOps.expand(img, border=5, fill="black") + img = ImageOps.expand(img, border=5, fill="white") + return img + + +# 全局变量,用于存储已经选择过的文件名 +selected_images = [] + + +def get_random_right(): + global selected_images + path = filepath + "/img/deer_pipe/right" + # 过滤掉已经选择过的文件名 + im_name = [name for name in os.listdir(path) if name not in selected_images] + + # 如果所有文件都已经选择过,重新初始化已选择列表 + if not im_name: + selected_images = [] + im_name = os.listdir(path) + + index = random.randint(0, len(im_name) - 1) + im = im_name[index] + selected_images.append(im) # 将选择的文件名添加到已选择列表中 + + im_path = os.path.join(path, im) + im_file = Image.open(im_path) + im_file = im_file.resize((50, 50)) + return im_file + + +# 图片粘贴 +def image_paste(paste_image, under_image, pos): + """ + :param paste_image: 需要粘贴的图片 + :param under_image: 底图 + :param pos: 位置(x,y)坐标 + :return: 返回图片 + """ + # 获取需要贴入图片的透明通道 + r, g, b, alpha = paste_image.split() + # 粘贴时将alpha值传递至mask属性 + under_image.paste(paste_image, pos, alpha) + return under_image diff --git a/hexi/plugins/fonts/MiSans-Bold.ttf b/hexi/plugins/fonts/MiSans-Bold.ttf new file mode 100644 index 0000000..eda4bea Binary files /dev/null and b/hexi/plugins/fonts/MiSans-Bold.ttf differ diff --git a/hexi/plugins/fonts/MiSans-Light.ttf b/hexi/plugins/fonts/MiSans-Light.ttf new file mode 100644 index 0000000..784bdb1 Binary files /dev/null and b/hexi/plugins/fonts/MiSans-Light.ttf differ diff --git a/hexi/plugins/fonts/MiSans-Regular.ttf b/hexi/plugins/fonts/MiSans-Regular.ttf new file mode 100644 index 0000000..a408ed8 Binary files /dev/null and b/hexi/plugins/fonts/MiSans-Regular.ttf differ diff --git a/hexi/plugins/fuck_pilipili/__init__.py b/hexi/plugins/fuck_pilipili/__init__.py new file mode 100644 index 0000000..fa56ff9 --- /dev/null +++ b/hexi/plugins/fuck_pilipili/__init__.py @@ -0,0 +1,462 @@ +import time +import shutil +import requests +import json +import re +import sys +import os +import html +import tempfile +import asyncio +from pathlib import Path +from typing import Optional +import hashlib +from nonebot import on_message, logger +from nonebot.rule import to_me +from nonebot.adapters import Event + +from yt_dlp import YoutubeDL +from httpx import AsyncClient +import msgspec + +from .minio import upload_to_s3 + +# 匹配链接的正则 +URL_PATTERN = re.compile(r"(https?://[^\s]+)") +pattern = r"QQ小程序(?:&#93;|]|\])" + +# 保留原有平台识别,同时加入抖音相关域名 +VALID_HOSTS = [ + "b23.tv", + "bilibili.com", + "youtube.com", + "youtu.be", + # douyin + "douyin.com", + "v.douyin.com", + "iesdouyin.com", + "m.douyin.com", + "jingxuan.douyin.com", +] + +# 抖音短链识别 +SHORT_LINK_PATTERN = re.compile(r"(v\.douyin\.com/[A-Za-z0-9_\-]+)") +# 从 HTML 提取路由数据 +DOUYIN_ROUTER_PATTERN = re.compile(r"window\._ROUTER_DATA\s*=\s*(.*?)", re.DOTALL) + +video_handler = on_message(priority=10, block=False, rule=to_me()) + + +@video_handler.handle() +async def handle_video_download(event: Event): + msg = str(event.get_message()).strip() + logger.info(f"获取到的消息:{msg}") + target_url: Optional[str] = None + + # 提取 URL + if not re.search(pattern, msg): + urls = URL_PATTERN.findall(msg) + for u in urls: + logger.info(f"链接:{u}") + + # 优先处理抖音(单独解析以获取直链) + if "douyin.com" in u or "v.douyin.com" in u or "iesdouyin.com" in u: + await video_handler.send("检测到链接,正在尝试下载视频,请稍候...") + try: + parsed_path = await parse_douyin(u) + if parsed_path: + # 抖音直接获取直链并且合并文件后返回文件地址后直接上传s3 + logger.info(f"文件路径:{parsed_path}") + # 构建path对象 + await video_handler.send(f"视频:{parsed_path.name} 已缓存\n正在生成下载链接,请耐心等待!") + s3_url = upload_to_s3(parsed_path) + await video_handler.send(f"{s3_url}") + break + else: + await video_handler.send(f"抖音解析失败或未找到直链:{u}") + logger.warning(f"抖音解析失败或未找到直链:{u}") + # 如果解析失败,继续尝试其他链接 + continue + except Exception as e: + await video_handler.send(f"解析抖音链接时出错:{e}") + logger.exception(f"解析抖音链接时出错:{e}") + continue + + # 非抖音走原逻辑 + if any(domain in u for domain in VALID_HOSTS): + target_url = u + break + + if not target_url: + return # 不处理无效链接 + + await video_handler.send("检测到链接,正在尝试下载视频,请稍候...") + else: + target_url = None + logger.info("检测到小程序") + url = await proc_xcx(msg) + logger.info(f"链接:{url}") + if url and any(domain in url for domain in VALID_HOSTS): + target_url = url + + if not target_url: + return # 不处理无效链接 + await video_handler.send("检测到视频,正在尝试下载视频,请稍候...") + + try: + video_file = await download_video(target_url) + if not video_file: + await video_handler.send("视频下载失败。") + return + + await video_handler.send(f"视频已缓存:{video_file.name}\n正在生成下载链接,请耐心等待!") + logger.info(f"文件路径:{video_file}") + s3_url = upload_to_s3(video_file) + logger.info(f"文件上传完成,返回链接:{s3_url}") + await video_handler.send(f"{s3_url}") + except Exception as e: + logger.exception(e) + await video_handler.send("下载过程中出现错误。") + + +async def download_video(url: str) -> Optional[Path]: + """ + 如果 url 看起来是直接的媒体直链(如 mp4),则使用 httpx 直接下载; + 否则回退到 yt-dlp 下载(兼容多平台),并支持: + 1) Firefox cookies + 2) cookies.txt 兜底 + """ + + # ---------- 1. 直链探测 ---------- + direct_media_ext = re.search(r"\.(mp4|m3u8|ts|webm|mov|flv)(?:$|\?)", url, re.IGNORECASE) + is_direct = bool(direct_media_ext) + + if not is_direct: + try: + async with AsyncClient(follow_redirects=True, timeout=30) as client: + head = await client.head(url, follow_redirects=True) + ctype = head.headers.get("content-type", "") + if ctype.startswith("video/") or "application/octet-stream" in ctype: + is_direct = True + except Exception: + is_direct = False + + if is_direct: + temp_dir = tempfile.mkdtemp(prefix="direct_ytcache_") + ext = "mp4" + m = re.search(r"\.([a-zA-Z0-9]{2,5})(?:$|\?)", url) + if m and len(m.group(1)) <= 5: + ext = m.group(1) + + filename = os.path.join(temp_dir, f"downloaded_video.{ext}") + + try: + async with AsyncClient(follow_redirects=True, timeout=120) as client: + async with client.stream("GET", url) as resp: + resp.raise_for_status() + with open(filename, "wb") as fh: + async for chunk in resp.aiter_bytes(chunk_size=8192): + fh.write(chunk) + + p = Path(filename) + new_path = p.with_name(clean_filename(p.name)) + p.rename(new_path) + logger.info(f"直接下载完成: {new_path}") + return new_path + + except Exception: + logger.exception("直接下载失败,回退 yt-dlp") + try: + if os.path.exists(filename): + os.remove(filename) + except Exception: + pass + + # ---------- 2. yt-dlp 下载 ---------- + temp_dir = tempfile.mkdtemp(prefix="ytcache_") + output_path = os.path.join(temp_dir, "%(title).80s.%(ext)s") + + base_opts = { + "outtmpl": output_path, + "format": "bestvideo+bestaudio/best", + "noplaylist": True, + "quiet": True, + "ffmpeg_location": get_ffmpeg_path(), + "extractor_args": { + "youtube": { + "player_client": ["android"] + } + }, + } + + loop = asyncio.get_event_loop() + + def _run_yt(opts: dict): + with YoutubeDL(opts) as ydl: + ydl.download([url]) + + # ---------- 2.1 优先:Firefox cookies ---------- + try: + opts = dict(base_opts) + opts["cookiesfrombrowser"] = ("firefox",) + logger.info("尝试使用 Firefox cookies 下载") + await loop.run_in_executor(None, _run_yt, opts) + + except Exception as e: + logger.warning(f"Firefox cookies 下载失败,尝试 cookies.txt:{e}") + + # ---------- 2.2 回退:cookies.txt ---------- + cookie_path = Path(__file__).resolve().parent / "cookies.txt" + if cookie_path.exists(): + opts = dict(base_opts) + opts["cookiefile"] = str(cookie_path) + logger.info(f"使用 cookies 文件: {cookie_path}") + await loop.run_in_executor(None, _run_yt, opts) + else: + logger.error("未找到 cookies.txt,无法通过登录验证") + raise + + # ---------- 3. 结果处理 ---------- + files = list(Path(temp_dir).glob("*.*")) + if not files: + return None + + original_file = files[0] + new_path = original_file.with_name(clean_filename(original_file.name)) + original_file.rename(new_path) + logger.info(f"下载完成并重命名: {new_path}") + + return new_path + + +def get_ffmpeg_path() -> str: + scripts_dir = os.path.dirname(sys.executable) + ffmpeg_path = os.path.join(scripts_dir, "ffmpeg.exe") + if os.path.exists(ffmpeg_path): + return ffmpeg_path + return "ffmpeg" + + +def clean_filename(filename: str) -> str: + return ( + filename.replace(" ", "_") + .replace("&", "_") + .replace("#", "_") + .replace("'", "") + .replace('"', "") + .replace("?", "") + .replace(":", "_") + .replace("|", "_") + .replace("/", "_") + .replace("\\", "_") + .replace("*", "_") + .replace("<", "_") + .replace(">", "_") + .replace("【", "") + .replace("】", "") + .replace(":", "") + .replace("。", "") + .replace(",", "_") + .replace("《", "") + .replace("》", "") + .replace("?", "_") + .replace("|", "") + ) + + +async def proc_xcx(msg): + p_pattern = r'"qqdocurl":"(.*?)"' + match = re.search(p_pattern, msg) + + if match: + raw_url = match.group(1) + unescaped = html.unescape(raw_url) + cleaned_url = unescaped.replace(r"\\/", "/") + base_url = cleaned_url.split("?")[0] + print("最终提取链接:", base_url) + return base_url + else: + print("未找到 qqdocurl 字段") + return None + + +# ----------------- Douyin parsing helpers ----------------- + +async def parse_douyin(url: str) -> Optional[str]: + """ + 解析抖音链接,返回可直接下载的媒体直链(优先)或 None。 + 支持短链(v.douyin.com)、长链、iesdouyin、m.douyin 等。 + """ + # 处理短链 + short = SHORT_LINK_PATTERN.search(url) + if short: + short_url = "https://" + short.group(1) + try: + cookies_path = os.path.dirname(__file__).replace("\\", "/") + "/cookies.txt" + file_path = await fetch_douyin_mp4(short_url, cookies_path, 10) + return file_path + except Exception: + logger.exception("获取直链失败") + # 仍然尝试原始 url + + # 现在尝试从页面抓取 router data + try: + logger.info(f"获取到的信息:{url}") + except Exception: + logger.exception("抖音页面解析失败") + return None + + +import asyncio +import tempfile +import aiohttp +import os +from typing import List, Dict, Optional + +from playwright.async_api import async_playwright +import ffmpeg + + +class DouyinFetchError(Exception): + pass + + +def parse_netscape_cookies(cookies_txt_path: str) -> List[Dict]: + """ + 解析 Netscape HTTP Cookie File -> Playwright cookies + """ + cookies: List[Dict] = [] + + with open(cookies_txt_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + + parts = line.split("\t") + if len(parts) != 7: + continue + + domain, include_sub, path, secure, expiry, name, value = parts + + cookie = { + "name": name, + "value": value, + "domain": domain, + "path": path, + "secure": secure.upper() == "TRUE", + "httpOnly": False, + } + + if expiry.isdigit() and int(expiry) > 0: + cookie["expires"] = int(expiry) + + cookies.append(cookie) + + return cookies + + +async def download_url(url: str, filepath: str, headers: Optional[Dict] = None): + headers = headers or {} + timeout = aiohttp.ClientTimeout(total=300) + + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url, headers=headers) as resp: + if resp.status != 200: + raise DouyinFetchError(f"下载失败 {resp.status}: {url}") + + with open(filepath, "wb") as f: + async for chunk in resp.content.iter_chunked(1024 * 64): + f.write(chunk) + + +async def fetch_douyin_mp4( + douyin_url: str, + cookies_txt: str, + wait_seconds: int = 15, +) -> str: + """ + 输入抖音 URL + 输出:合并后的 MP4 临时文件路径 + """ + + cookies = parse_netscape_cookies(cookies_txt) + if not cookies: + raise DouyinFetchError("cookies.txt 解析失败或为空") + + video_url: Optional[str] = None + audio_url: Optional[str] = None + + async with async_playwright() as p: + browser = await p.chromium.launch( + executable_path="C:/Program Files/Google/Chrome/Application/chrome.exe", + headless=False, + args=[ + "--autoplay-policy=no-user-gesture-required", + "--disable-features=AutoplayDisableSuppression", + "--use-fake-ui-for-media-stream", + ] + ) + + context = await browser.new_context( + viewport={"width": 640, "height": 320}, + user_agent=( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/122.0.0.0 Safari/537.36" + ), + ) + + await context.add_cookies(cookies) + page = await context.new_page() + + def handle_response(response): + nonlocal video_url, audio_url + url = response.url + # 分轨视频 + if video_url is None and ("video" in url and "mime_type=video_mp4" in url): + video_url = url + elif audio_url is None and ("audio" in url and "mime_type=audio" in url): + audio_url = url + + page.on("response", handle_response) + + await page.goto(douyin_url, wait_until="domcontentloaded") + await page.wait_for_timeout(wait_seconds * 1000) + await browser.close() + + if not video_url: + raise DouyinFetchError("未捕获视频流 URL") + + # ----------------------------- + # 下载到临时文件 + # ----------------------------- + tmp_dir = Path(tempfile.gettempdir()) + timestamp = int(time.time() * 1000) + + tmp_video = tmp_dir / f"douyin/{timestamp}_video.mp4" + tmp_out = tmp_dir / f"douyin/{timestamp}_out.mp4" + + # 下载视频 + await download_url(video_url, tmp_video) + + if audio_url: + tmp_audio = tmp_dir / f"{timestamp}_audio.m4a" + try: + await download_url(audio_url, tmp_audio) + ( + ffmpeg + .input(tmp_video) + .output(tmp_audio, tmp_out, vcodec="copy", acodec="aac", movflags="faststart") + .overwrite_output() + .run(quiet=True) + ) + finally: + tmp_video.unlink(missing_ok=True) + tmp_audio.unlink(missing_ok=True) + else: + # 视频自带音频,直接拷贝到输出 + shutil.copy(tmp_video, tmp_out) + tmp_video.unlink(missing_ok=True) + + return tmp_out diff --git a/hexi/plugins/fuck_pilipili/cookies.txt b/hexi/plugins/fuck_pilipili/cookies.txt new file mode 100644 index 0000000..b017df0 --- /dev/null +++ b/hexi/plugins/fuck_pilipili/cookies.txt @@ -0,0 +1,61 @@ +# Netscape HTTP Cookie File +# This file is generated by yt-dlp. Do not edit. + +.bilibili.com TRUE / FALSE 1797867119 b_nut 1766331119 +.bilibili.com TRUE / FALSE 1852731119 buvid3 DD8D9A3E-E377-7185-8B36-69849D5FB89419813infoc +.bilibili.com TRUE / FALSE 0 sid nu48lk72 +.douyin.com TRUE / TRUE 1789188229 SEARCH_RESULT_LIST_TYPE %22single%22 +.douyin.com TRUE / FALSE 1797188918 SelfTabRedDotControl %5B%5D +.douyin.com TRUE / TRUE 1788959886 UIFID_TEMP 1b474bc7e0db9591e645dd8feb8c65aae4845018effd0c2743039a380ee64740f7a504ffa95976eb1ff6c460678462bf4c11608eea38012581a3420231dc8cc18e9985cc85771821f5b805864df920d969836148d4de574213c64a065f29b8542b9886775a8ef257ab52165271883e0b +.douyin.com TRUE / FALSE 1797108073 __druidClientInfo JTdCJTIyY2xpZW50V2lkdGglMjIlM0E1ODUlMkMlMjJjbGllbnRIZWlnaHQlMjIlM0E5MzElMkMlMjJ3aWR0aCUyMiUzQTU4NSUyQyUyMmhlaWdodCUyMiUzQTkzMSUyQyUyMmRldmljZVBpeGVsUmF0aW8lMjIlM0ExLjI1JTJDJTIydXNlckFnZW50JTIyJTNBJTIyTW96aWxsYSUyRjUuMCUyMChXaW5kb3dzJTIwTlQlMjAxMC4wJTNCJTIwV2luNjQlM0IlMjB4NjQpJTIwQXBwbGVXZWJLaXQlMkY1MzcuMzYlMjAoS0hUTUwlMkMlMjBsaWtlJTIwR2Vja28pJTIwQ2hyb21lJTJGMTQzLjAuMC4wJTIwU2FmYXJpJTJGNTM3LjM2JTIyJTdE +.douyin.com TRUE / FALSE 1791425613 __live_version__ %221.1.3.9068%22 +.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_cert_key 1ca1c5bd-4fad-83db +.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_crypt_sdk 047179a2-412d-984e +.douyin.com TRUE / FALSE 1770842600 __security_mc_1_s_sdk_sign_data_key_web_protect 84cb6190-4b2f-883c +.douyin.com TRUE / FALSE 1770842600 _bd_ticket_crypt_cookie 966e8a4a1243419806f690bb52d8f687 +.douyin.com TRUE / FALSE 1770836920 bd_ticket_guard_client_data eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWl0ZXJhdGlvbi12ZXJzaW9uIjoxLCJiZC10aWNrZXQtZ3VhcmQtcmVlLXB1YmxpYy1rZXkiOiJCSDljaENXbS9RanFWaTFEK2d5bm9QbTFodnpMTkJCWVFwcGVNSXAyYUlSWEFqbThTdDJ0bG91TW1Na09OZk9rR2VZbXZ0Skp5aU11di9tY2VsZXpRZUU9IiwiYmQtdGlja2V0LWd1YXJkLXdlYi12ZXJzaW9uIjoyfQ%3D%3D +.douyin.com TRUE / FALSE 1770842600 bd_ticket_guard_client_data_v2 eyJyZWVfcHVibGljX2tleSI6IkJIOWNoQ1dtL1FqcVZpMUQrZ3lub1BtMWh2ekxOQkJZUXBwZU1JcDJhSVJYQWptOFN0MnRsb3VNbU1rT05mT2tHZVltdnRKSnlpTXV2L21jZWxlelFlRT0iLCJ0c19zaWduIjoidHMuMi4xY2QxNjhkOGI2NjJjMjQ0MGYwOWMwMmNhMDcyYWM1NDVmZDNmZDUwN2RjYjgwZDE2ZjA2YTNkZjZiYTU1ZDQzYzRmYmU4N2QyMzE5Y2YwNTMxODYyNGNlZGExNDkxMWNhNDA2ZGVkYmViZWRkYjJlMzBmY2U4ZDRmYTAyNTc1ZCIsInJlcV9jb250ZW50Ijoic2VjX3RzIiwicmVxX3NpZ24iOiJKMENGMEc3dHZSdGpEL1VzWGF1WjcrdkVhYjlVZmJYZzRvSkV3WFNEVXdBPSIsInNlY190cyI6IiNXQzM2bW1WbTZvZUU4Y0V3eTdpdERuZU83bkdpRjFaSzFZU0JzdXZ6MUUvK1ZPeTZwOEd5S2t0VTVqOFQifQ%3D%3D +.douyin.com TRUE / FALSE 1770836920 bd_ticket_guard_client_web_domain 2 +.douyin.com TRUE / FALSE 0 biz_trace_id d7cbac16 +.douyin.com TRUE / FALSE 1785935956 d_ticket 01ddc437be4e3cbfd1cb3bb56dd327e876498 +.douyin.com TRUE / TRUE 1800212916 enter_pc_once 1 +.douyin.com TRUE / FALSE 1800212918 hevc_supported true +.douyin.com TRUE / TRUE 1769842917 is_staff_user false +.douyin.com TRUE / FALSE 1794260703 live_use_vvc %22false%22 +.douyin.com TRUE / FALSE 1788959956 login_time 1754399956210 +.douyin.com TRUE / FALSE 1788959962 my_rd 2 +.douyin.com TRUE / FALSE 1797188988 odin_tt 2683055763a782b94a74634bf8fcd7e90b0dcca44630c771f1ba99436b48bfc79d2b234a29a0e299299379659da80a11daf739a2814a10f66d2d1a5684811053a30fa9b3a9b401ccb901146e065f0283 +.douyin.com TRUE / TRUE 1788959956 passport_assist_user CkF0l51t7CgABiF67MBw3A7AHKIkbitVe_56ICZPweOyywpFk54SNFyio_swIE8DJbzDO9aMykPJaxHUbT2QG6KVGBpKCjwAAAAAAAAAAAAAT1HMv2v-0GEWxxM2txtqIiClBsZvR3gtWGiNkYTJcF4qMsFK3Kg20fVfR9_rVAm0s9QQ3tL4DRiJr9ZUIAEiAQPfMXD5 +.douyin.com TRUE / TRUE 1770222444 passport_csrf_token e38b20b0c35196051cf9c83b8a835368 +.douyin.com TRUE / FALSE 1770222444 passport_csrf_token_default e38b20b0c35196051cf9c83b8a835368 +.douyin.com TRUE / TRUE 1769842917 session_tlb_tag sttt%7C13%7CqMU0JZcc7nWXE6DVNPekaP_________HGhsfOzUszhPpsRcrIANMaX3bvgd_H6JnB7oTHAd_DbQ%3D +.douyin.com TRUE / TRUE 1769842917 session_tlb_tag_bk sttt%7C13%7CqMU0JZcc7nWXE6DVNPekaP_________HGhsfOzUszhPpsRcrIANMaX3bvgd_H6JnB7oTHAd_DbQ%3D +.douyin.com TRUE / TRUE 1769842917 sessionid a8c53425971cee759713a0d534f7a468 +.douyin.com TRUE / TRUE 1769842917 sessionid_ss a8c53425971cee759713a0d534f7a468 +.douyin.com TRUE / TRUE 1795762917 sid_guard a8c53425971cee759713a0d534f7a468%7C1764658889%7C5184000%7CSat%2C+31-Jan-2026+07%3A01%3A29+GMT +.douyin.com TRUE / TRUE 1769842917 sid_tt a8c53425971cee759713a0d534f7a468 +.douyin.com TRUE / TRUE 1769842917 sid_ucp_v1 1.0.0-KDgzOWJmYzI1NDM4ZmUyMzY3NTQ0ZDc0OTVmNjZkZWY5Y2VhZDEwYzEKIQi-yZDPrfS7BRDJnbrJBhjvMSAMMPO-tvkFOAdA9AdIBBoCaGwiIGE4YzUzNDI1OTcxY2VlNzU5NzEzYTBkNTM0ZjdhNDY4 +.douyin.com TRUE / TRUE 1769842917 ssid_ucp_v1 1.0.0-KDgzOWJmYzI1NDM4ZmUyMzY3NTQ0ZDc0OTVmNjZkZWY5Y2VhZDEwYzEKIQi-yZDPrfS7BRDJnbrJBhjvMSAMMPO-tvkFOAdA9AdIBBoCaGwiIGE4YzUzNDI1OTcxY2VlNzU5NzEzYTBkNTM0ZjdhNDY4 +.douyin.com TRUE / TRUE 1797188919 ttwid 1%7C3OTaRIjzlagT1R3CIKzm-lrgR4QZHSSf4C_JY28hZBE%7C1765652883%7Cdd5944a2b71c814a19310cc28ec65bb57c41a5924b98889c818c647c1a9cd789 +.douyin.com TRUE / TRUE 1769842917 uid_tt 2d2292491b47cba2bca8ad469044313c +.douyin.com TRUE / TRUE 1769842917 uid_tt_ss 2d2292491b47cba2bca8ad469044313c +.douyin.com TRUE / FALSE 1800218614 volume_info %7B%22volume%22%3A0.058%2C%22isUserMute%22%3Afalse%2C%22isMute%22%3Afalse%7D +.youtube.com TRUE / FALSE 0 PREF hl=en&tz=UTC +.youtube.com TRUE / TRUE 0 SOCS CAI +.youtube.com TRUE / TRUE 1781404953 VISITOR_INFO1_LIVE eizmje-lCRg +.youtube.com TRUE / TRUE 1781404953 VISITOR_PRIVACY_METADATA CgJISxIEGgAgJQ%3D%3D +.youtube.com TRUE / TRUE 0 YSC u0QAormHZmk +.youtube.com TRUE / TRUE 1781404174 __Secure-ROLLOUT_TOKEN CIz99prG2qWIMRCI96v8h8GRAxiw4c_8h8GRAw%3D%3D +.youtube.com TRUE / TRUE 1781404173 __Secure-YNID 14.YT=WCdFIxVbI-_UdJV0Oq9PEe7pwy0VrgqhG0x3t_NACLFpSj-vXlBCkFfQoogFvF-r43NnykP1s0yUXwaTsCdeUupHubi3bzIujZfb0KwEs-iZWAmO6GJC8lnFoYmqkgrI6im2jbS6m1ww1glISZwm5ixzM0sXlM3h730ILh2cD2B15t--SmtNKSTfUWADmMeKItgtDIT8rEUvY9ZKROzqBwY3oBakKlWuRn0mvJIc0XgL14-ZKWclidfpcEF5bTh3RSE8FrL-3zPS1rXeDwuzzmxWHnBHrF0PTf0NjRj4iurKCHQ5eG9mbSWqWkBlSUKYHiqqhr7hvqMbl51qMstINQ +www.douyin.com FALSE / TRUE 1788959894 UIFID 1b474bc7e0db9591e645dd8feb8c65aae4845018effd0c2743039a380ee64740f7a504ffa95976eb1ff6c460678462bf4c11608eea38012581a3420231dc8cc1df6a3994b9d2ea2231c9e9a71ad7e62bb51997b911f38a7ed79c52934af24edfceccc758104a7f2cbe7bf3efa405b4d4ae93e7e6b75ced4d09a9b03ede6adba13fdca01fdad4f53e16ea91ce3994615029e37b399746db3de5ed17bd2ec1cb1fc70373395d6531975ace4e6cbabc5f96fd8dd422cf953d54e4f2e3be4bf0bae4 +www.douyin.com FALSE / TRUE 1797112873 __ac_signature _02B4Z6wo00f01FLc0ygAAIDBvfwhoiQ43JhS.NeAAH2T2b +www.douyin.com FALSE / FALSE 0 architecture amd64 +www.douyin.com FALSE / FALSE 0 device_web_cpu_core 20 +www.douyin.com FALSE / FALSE 0 device_web_memory_size 8 +www.douyin.com FALSE / FALSE 0 douyin.com +www.douyin.com FALSE / TRUE 1788959892 fpk1 U2FsdGVkX1+uAYVnpjAJyYq7U+tilyeRF9JR93oAt/PgAjmKOUTwF/jaDe6Ly0Iac8u96PCa2wekKxFu6pI23w== +www.douyin.com FALSE / TRUE 1788959892 fpk2 7ddeda88d0c599cc494da0dece6554d5 +www.douyin.com FALSE / FALSE 1770222441 s_v_web_id verify_miuibdp5_o5ZsovxD_B7YC_4FM4_9wgs_j56nppke8yve +www.douyin.com FALSE / FALSE 0 xg_device_score 8.097240372472122 +www.douyin.com FALSE / FALSE 1785936062 xgplayer_device_id 96210314128 +www.douyin.com FALSE / FALSE 1785936062 xgplayer_user_id 148294698804 diff --git a/hexi/plugins/fuck_pilipili/minio.py b/hexi/plugins/fuck_pilipili/minio.py new file mode 100644 index 0000000..7652cf4 --- /dev/null +++ b/hexi/plugins/fuck_pilipili/minio.py @@ -0,0 +1,40 @@ +import boto3 +from botocore.client import Config + + +# minio config +# MinIO配置 +MINIO_ENDPOINT = "s3.sansenhoshi.top" # 不含 http/https +MINIO_ACCESS_KEY = "JDMynACSjPaN8JRwriwS" +MINIO_SECRET_KEY = "JRl4bIGxeiqwqvfeBTlAWQbUKMpNNHSZPm6ne93j" +MINIO_BUCKET = "s-file-trans" +MINIO_REGION = "bot" +MINIO_SECURE = True # False 则用 HTTP + +# 可访问的公网地址前缀 +MINIO_PUBLIC_DOMAIN = f"https://{MINIO_ENDPOINT}/{MINIO_BUCKET}" + + +s3_client = boto3.client( + "s3", + endpoint_url=f"{'https' if MINIO_SECURE else 'http'}://{MINIO_ENDPOINT}", + aws_access_key_id=MINIO_ACCESS_KEY, + aws_secret_access_key=MINIO_SECRET_KEY, + region_name=MINIO_REGION, + config=Config(signature_version="s3v4"), +) + + +def upload_to_s3(file_path) -> str: + file_key = f"cache/{file_path.name}" # 你可以换成别的目录 + try: + s3_client.upload_file( + str(file_path), + MINIO_BUCKET, + file_key, + ExtraArgs={"ACL": "public-read"} # 如果你设置桶为私有,可以移除这行 + ) + return f"{MINIO_PUBLIC_DOMAIN}/{file_key}" + except Exception as e: + logger.exception(f"上传到 MinIO 失败: {e}") + return "" diff --git a/hexi/plugins/fuck_pilipili/sign.py b/hexi/plugins/fuck_pilipili/sign.py new file mode 100644 index 0000000..38427ed --- /dev/null +++ b/hexi/plugins/fuck_pilipili/sign.py @@ -0,0 +1,114 @@ +import hmac +import hashlib +import time +import urllib.parse +from functools import reduce +from hashlib import md5 +from aiohttp import ClientSession + +# doc: https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +} + +# fmt: off +mixinKeyEncTab = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, + 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, + 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, + 36, 20, 34, 44, 52 +] +# fmt: on + + +def getMixinKey(orig: str): + "对 imgKey 和 subKey 进行字符顺序打乱编码" + return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, "")[:32] + + +def encWbi(params: dict, img_key: str, sub_key: str): + "为请求参数进行 wbi 签名" + mixin_key = getMixinKey(img_key + sub_key) + curr_time = round(time.time()) + params["wts"] = curr_time # 添加 wts 字段 + params = dict(sorted(params.items())) # 按照 key 重排参数 + # 过滤 value 中的 "!'()*" 字符 + params = { + k: "".join(filter(lambda chr: chr not in "!'()*", str(v))) + for k, v in params.items() + } + query = urllib.parse.urlencode(params) # 序列化参数 + wbi_sign = md5((query + mixin_key).encode()).hexdigest() # 计算 w_rid + params["w_rid"] = wbi_sign + return params + + +async def getWbiKeys(): + "获取最新的 img_key 和 sub_key" + async with ClientSession(headers=headers) as session: + async with session.get("https://api.bilibili.com/x/web-interface/nav") as resp: + json_content = await resp.json() + img_url: str = json_content["data"]["wbi_img"]["img_url"] + sub_url: str = json_content["data"]["wbi_img"]["sub_url"] + img_key = img_url.rsplit("/", 1)[1].split(".")[0] + sub_key = sub_url.rsplit("/", 1)[1].split(".")[0] + return img_key, sub_key + + +async def get_query(params: dict): + """ + 获取签名后的查询参数 + """ + img_key, sub_key = await getWbiKeys() + signed_params = encWbi(params=params, img_key=img_key, sub_key=sub_key) + query = urllib.parse.urlencode(signed_params) + return query + + +def hmac_sha256(key, message): + """ + 使用HMAC-SHA256算法对给定的消息进行加密 + :param key: 密钥 + :param message: 要加密的消息 + :return: 加密后的哈希值 + """ + # 将密钥和消息转换为字节串 + key = key.encode("utf-8") + message = message.encode("utf-8") + + # 创建HMAC对象,使用SHA256哈希算法 + hmac_obj = hmac.new(key, message, hashlib.sha256) + + # 计算哈希值 + hash_value = hmac_obj.digest() + + # 将哈希值转换为十六进制字符串 + hash_hex = hash_value.hex() + + return hash_hex + + +async def get_ticket(): + """ + 获取ticket + """ + o = hmac_sha256("XgwSnGZ1p", f"ts{int(time.time())}") + url = "https://api.bilibili.com/bapis/bilibili.api.ticket.v1.Ticket/GenWebTicket" + params = { + "key_id": "ec02", + "hexsign": o, + "context[ts]": f"{int(time.time())}", + "csrf": "", + } + async with ClientSession(headers=headers) as session: + async with session.post(url, params=params) as resp: + json_content = await resp.json() + return json_content["data"]["ticket"] + + +if __name__ == "__main__": + import asyncio + + loop = asyncio.get_event_loop() + loop.run_until_complete(get_ticket()) \ No newline at end of file diff --git a/hexi/plugins/makeaquote/Reply.py b/hexi/plugins/makeaquote/Reply.py new file mode 100644 index 0000000..03f4230 --- /dev/null +++ b/hexi/plugins/makeaquote/Reply.py @@ -0,0 +1,50 @@ +from nonebot.adapters.onebot.v11 import Bot, Message +from nonebot import logger + + +class Reply: + def __init__(self): + self.message: str = '' + self.user_id: int = -1 + self.user_card: str = '' + self.time: int = -1 + + +async def extract_reply(event, bot: Bot) -> Reply: + """ + 提取回复内容 + """ + logger.info(event['group_id']) + reply_data = Reply() + if event['reply'] and event['reply']['message_id']: # 待优化 + reply = event['reply'] + sender = reply['sender'] + mes: Message = reply['message'] + print(mes) + msg = mes[0]['data']['text'] + if msg: + reply_data.message = msg + meminfo = await get_member_info(bot=bot, user_id=sender['user_id'], group_id=event['group_id']) + reply_data.user_card = (meminfo["card"] or meminfo["nickname"]) + reply_data.time = reply['time'] + reply_data.user_id = reply['sender']['user_id'] + else: + reply_data.message = -1 + else: + mes: Message = event["message"] + print(mes) + msg = mes[0]['data']['text'].replace("maq","") + if msg: + reply_data.message = msg + meminfo = await get_member_info(bot=bot, user_id=event['user_id'], group_id=event['group_id']) + reply_data.user_card = (meminfo["card"] or meminfo["nickname"]) + reply_data.time = event['time'] + reply_data.user_id = event['user_id'] + else: + reply_data.message = -1 + return reply_data + + +async def get_member_info(bot: Bot, group_id: int, user_id: int): + member_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id) + return member_info diff --git a/hexi/plugins/makeaquote/__init__.py b/hexi/plugins/makeaquote/__init__.py new file mode 100644 index 0000000..590dc93 --- /dev/null +++ b/hexi/plugins/makeaquote/__init__.py @@ -0,0 +1,45 @@ +from nonebot import logger, on_message +from nonebot.adapters.onebot.v11 import Bot, MessageSegment, Event +from nonebot.plugin import PluginMetadata +from .Reply import Reply, extract_reply +from .make_a_qoute import generate +from nonebot.rule import keyword + +__plugin_meta__ = PluginMetadata( + name="语录", + description="生成一条语录", + usage="发送【maq】", + type="application", +) + +rule = keyword('maq') +maq = on_message(rule) + + +@maq.handle() +async def maq_handle(bot: Bot, ev: Event): + event = ev.dict() + try: + if event['message_type'] != 'group': + return + if event['reply']: + reply = await extract_reply(event, bot) + if reply.message == -1: + await maq.finish("请引用文字消息") + else: + logger.info("{}, {}, {}".format(reply.user_id, reply.user_card, reply.message)) + image_path = await generate(reply) + msg = (MessageSegment.image(image_path),) + await maq.finish(msg) + else: + reply = await extract_reply(event, bot) + if reply.message == -1: + await maq.finish("请引用文字消息") + else: + logger.info("{}, {}, {}".format(reply.user_id, reply.user_card, reply.message)) + image_path = await generate(reply) + msg = (MessageSegment.image(image_path),) + await maq.finish(msg) + except (KeyError, TypeError) as e: + logger.error(e) + await maq.finish(str(e)) diff --git a/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Bold.otf b/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Bold.otf new file mode 100644 index 0000000..8e1e869 Binary files /dev/null and b/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Bold.otf differ diff --git a/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Medium.otf b/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Medium.otf new file mode 100644 index 0000000..630d546 Binary files /dev/null and b/hexi/plugins/makeaquote/data/font/SourceHanSansCN-Medium.otf differ diff --git a/hexi/plugins/makeaquote/make_a_qoute.py b/hexi/plugins/makeaquote/make_a_qoute.py new file mode 100644 index 0000000..4e5ef6e --- /dev/null +++ b/hexi/plugins/makeaquote/make_a_qoute.py @@ -0,0 +1,160 @@ +import hashlib +import os +import json +from io import BytesIO +from PIL import Image, ImageOps, ImageDraw, ImageFont +import requests +import math +import numpy as np +from time import localtime, strftime, time +import cjk_textwrap +from pilmoji import Pilmoji +from pilmoji.source import MicrosoftEmojiSource +# from pilmoji.sources.microsoft import MicrosoftEmojiSource +from .Reply import Reply +import httpx +import base64 +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +from typing import List, Union, Dict, Any +import tempfile + +LOCAL_AVATAR_URL = "/tmp/{}.jpg" +FINAL_IMAGE_URL = "/tmp/{}-final.jpg" +FONT_WIDTH = 32 +SMALL_FONT_WIDTH = 24 +TIME_FONT_SMALL = 18 +MAX_LINES = 5 +font = ImageFont.truetype("./data/font/SourceHanSansCN-Medium.otf", FONT_WIDTH, encoding="unic") +font_small = ImageFont.truetype("./data/font/SourceHanSansCN-Bold.otf", SMALL_FONT_WIDTH, encoding="unic") +font_time = ImageFont.truetype("./data/font/SourceHanSansCN-Bold.otf", TIME_FONT_SMALL, encoding="unic") +emoji_font = ImageFont.truetype("./data/font/seguiemj.ttf", FONT_WIDTH) + + +async def generate(reply: Reply) -> str: + """ + 生成图片 + """ + image = Image.new('RGBA', (1280, 640), (0, 0, 0, 255)) + text_wrapped = wrap_text(reply.message, 18) + + y_min = 100 - FONT_WIDTH + y_max = 512 + # center vertically + y_start = (y_max - y_min) / 2 - min(len(text_wrapped), MAX_LINES) * FONT_WIDTH / 2 + y = int(y_start) + x_start = 601.1 + x_end = 1241.1 + + draw = ImageDraw.Draw(image) + + # 过长截断 + if len(text_wrapped) > MAX_LINES: + text_wrapped[MAX_LINES - 1] = text_wrapped[3][:-3] + "..." + text_wrapped = text_wrapped[:MAX_LINES] + + # --- 优化点:只开一次 Pilmoji 上下文 --- + with Pilmoji(image, source=MicrosoftEmojiSource, emoji_position_offset=(0, 10)) as emoji: + for i, line in enumerate(text_wrapped, start=1): + text_size = draw.textlength(line, font) + # center the text + x = int(x_start + (x_end - x_start - text_size) / 2) + emoji.text((x, y), line, font=font, fill=(255, 255, 255, 255), embedded_color=True) + y += FONT_WIDTH + 20 + + # --- 头像处理 --- + img_content = await download_avatar(str(reply.user_id)) + avatar = Image.open(img_content).resize((640, 640)) + avatar = alpha_gradient(avatar, 320, 640) + image.paste(avatar, (0, 0), avatar) + + # --- 用户卡片 --- + x_center = 921.1 + card_size = draw.textlength("- " + reply.user_card, font_small) + x_card_start = x_center - card_size / 2 + draw.text((x_card_start, y), "- " + reply.user_card, font=font_small, fill=(169, 172, 184, 255)) + + # --- 时间 --- + fmt_time = strftime("%Y.%m.%d %H:%M", localtime(reply.time)) + time_size = draw.textlength(fmt_time, font_small) + x_time_start = x_end - time_size + draw.text((x_time_start, 640 - FONT_WIDTH - 20), fmt_time, font=font_time, fill=(169, 172, 184, 255)) + + # --- 输出 --- + bytes_io = BytesIO() + image.convert('RGB').save(bytes_io, format="JPEG") + temp_file_path = bytesio2path(bytes_io) + return temp_file_path + + +def wrap_text(text: str, width: int) -> list[str]: + """ + 按宽度自动换行,支持中日韩文本。 + 先按换行符分行,再用 cjk_textwrap.wrap 按宽度折行。 + """ + return [wrapped for line in text.splitlines() for wrapped in cjk_textwrap.wrap(line, width)] + + +def alpha_gradient(image, x_s: int, x_e: int): + """ + 为图像添加从 x_s 到 x_e 的左右渐变透明度遮罩。 + 使用 numpy 加速,避免逐像素 putpixel 的低效操作。 + """ + from PIL import Image # 延迟导入,避免循环依赖 + + image = image.convert("RGBA") + w, h = image.size + + # 默认全不透明 + gradient = np.full((h, w), 255, dtype=np.uint8) + + # 生成渐变列 + xs = np.linspace(0, 1, x_e - x_s, endpoint=False) + alphas = 255 - (np.sin(xs * np.pi - np.pi / 2) / 2 + 0.5) * 255 + alphas = alphas.astype(np.uint8) + + # 应用到对应范围 + gradient[:, x_s:x_e] = alphas[np.newaxis, :] + + mask = Image.fromarray(gradient, mode="L") + image.putalpha(mask) + return image + + +def bytesio2path(img: Union[BytesIO, bytes]) -> str: + if isinstance(img, BytesIO): + img = img.getvalue() + + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(img) + temp_file_path = temp_file.name + + return f"file://{temp_file_path}" + + +def smooth01(x: float) -> float: + return math.sin(x * math.pi - math.pi / 2) / 2 + 0.5 + + +async def download_avatar(user_id: str) -> BytesIO: + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640" + data = await download_url(url) + if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e": + url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100" + data = await download_url(url) + return BytesIO(data) + + +async def download_url(url: str) -> bytes: + """ + 下载指定 URL 内容,带 3 次重试和超时。 + 如果失败,返回空字节串 b""。 + """ + async with httpx.AsyncClient(timeout=5.0) as client: + for i in range(3): + try: + resp = await client.get(url) + if resp.status_code == 200: + return resp.content + except Exception as e: + print(f"Error downloading {url}, retry {i + 1}/3: {e}") + return b"" diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py b/hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py new file mode 100644 index 0000000..123f3fe --- /dev/null +++ b/hexi/plugins/nonebot_plugin_helldivers_tools/__init__.py @@ -0,0 +1,245 @@ +import random +import time + +import httpx +import asyncio +import io +import json +import os +import re +from nonebot import logger +from io import BytesIO +from datetime import datetime +from PIL import Image, ImageOps, ImageDraw, ImageFont +from nonebot.internal.params import ArgPlainText +from nonebot.typing import T_State +from playwright.async_api import async_playwright +from nonebot import on_command +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +from nonebot.plugin import PluginMetadata +from typing import Optional, Union +from .utils import * +from nonebot.matcher import Matcher + +__plugin_meta__ = PluginMetadata( + name="绝地潜兵小助手", + description="绝地潜兵小助手,提供一些小功能,让超级地球更加美好!", + usage="简报:获取星系战争简要概况\n" + "随机战备:让机器人帮你随机一套战备", + type="application", + homepage="https://github.com/sansenhoshi/nonebot_plugin_helldivers_tools" +) + +basic_path = os.path.dirname(__file__) +save_path = os.path.join(basic_path, "temp") +img_path = os.path.join(basic_path, "img") +data_path = os.path.join(basic_path, "data") + + +war_situation = on_command("简报", aliases={"简报"}) + +se_situation = on_command("超级地球战况", aliases={"超级地球战况"}) + + +@war_situation.handle() +async def get_war_info(ev: MessageEvent): + await war_situation.send("正在获取前线战况……\n本地化需要30s左右,请民主的等待") + url1 = r"https://hd2galaxy.com/" + url2 = r"https://helldiverscompanion.com/#" + url3 = r"https://helldiverscompanion.com/#hellpad/planets/super_earth/regions" + time_present1 = get_present_time() + result = await screen_shot(url2, time_present1) + if result != "success": + await war_situation.finish(MessageSegment.reply(ev.message_id) + result) + img_path1 = os.path.join(save_path, f"{time_present1}.png") + logger.info(img_path1) + images = gen_ms_img(Image.open(img_path1)) + mes = (MessageSegment.reply(ev.message_id), images) + await war_situation.send(mes) + os.remove(img_path1) + + +@se_situation.handle() +async def get_se_info(ev: MessageEvent): + await war_situation.send("正在获取……\n本地化需要30s左右,请民主的等待") + url1 = r"https://hd2galaxy.com/" + url2 = r"https://helldiverscompanion.com/#" + url3 = r"https://helldiverscompanion.com/#hellpad/planets/super_earth/regions" + time_present1 = get_present_time() + result = await screen_shot_2(url3, time_present1) + if result != "success": + await war_situation.finish(MessageSegment.reply(ev.message_id) + result) + img_path1 = os.path.join(save_path, f"{time_present1}.png") + logger.info(img_path1) + images = gen_ms_img(Image.open(img_path1)) + mes = (MessageSegment.reply(ev.message_id), images) + await war_situation.send(mes) + os.remove(img_path1) + + +async def download_url(url: str) -> bytes: + async with httpx.AsyncClient() as client: + for i in range(3): + try: + resp = await client.get(url) + if resp.status_code != 200: + continue + return resp.content + except Exception as e: + print(f"Error downloading {url}, retry {i}/3: {str(e)}") + + +random_helldivers = on_command("随机战备", aliases={"随机战备"}, block=True) + +PROMPT = """ ***超级地球武装部*** + 请发送你需要的随机规则 + +1:纯随机(不推荐) + + 套路随机(按一定规则随机) + +2:2红/1蓝/1绿 +3:2绿/1红/1蓝 +4:2蓝/1绿/1红 + +5:3红/1蓝 +6:3绿/1蓝 + +7:2红/2蓝 +8:2蓝/2绿 +9:2绿/2红 + +10:4红 +11:4绿""" + +@random_helldivers.got("pick_type", prompt=PROMPT) + +# 用户选择 +async def got_random_helldivers(event: MessageEvent, pick_type: str = ArgPlainText()): + logger.info(f"用户选择的战备类型: {pick_type}") + if not is_number(pick_type): + await random_helldivers.reject(f"您输入的 {pick_type} 非数字,请重新输入1到11,或者输入0退出") + elif int(pick_type) not in range(12): + await random_helldivers.reject(f"您输入的 {pick_type} 不在范围内,请重新输入1到11,或者输入0退出") + elif int(pick_type) == 0: + logger.info("用户选择退出随机战备") + return + + mix_msg = (MessageSegment.reply(event.message_id),) + + type_combinations = { + 2: {'red': 2, 'blue': 1, 'green': 1}, + 3: {'green': 2, 'red': 1, 'blue': 1}, + 4: {'blue': 2, 'green': 1, 'red': 1}, + 5: {'red': 3, 'blue': 1}, + 6: {'green': 3, 'blue': 1}, + 7: {'red': 2, 'blue': 2}, + 8: {'blue': 2, 'green': 2}, + 9: {'green': 2, 'red': 2}, + 10: {'red': 4}, + 11: {'green': 4} + } + + if int(pick_type) == 1: + logger.info("用户选择纯随机战备") + result = await get_random_equipment(4) # 4 random equipments + else: + combination = type_combinations.get(int(pick_type)) + logger.info(f"用户选择的战备组合: {combination}") + result = await get_equipment_by_combination(combination) + + final_msg = MessageSegment.text("您的随机结果是:\n") + img_base_str = pic2b64(Image.open(result)) + image_turple = MessageSegment.image(img_base_str) + mix_msg += (final_msg, image_turple) + + await random_helldivers.finish(mix_msg) + +async def get_random_equipment(count): + data_config = os.path.join(basic_path, "data") + with open(data_config + "/equipment.json", "r", encoding="utf-8") as file: + data = json.load(file) + + indices = select_random_equipment(len(data), count) + selected_equipment = [data[i] for i in indices] + + return create_image(selected_equipment) + +async def get_equipment_by_combination(type_combination): + data_config = os.path.join(basic_path, "data") + with open(data_config + "/equipment.json", "r", encoding="utf-8") as file: + data = json.load(file) + + equipment_by_type = categorize_equipment_by_type(data) + selected_equipment = select_equipment_by_type(equipment_by_type, type_combination) + + logger.debug(f"根据组合选择的装备: {selected_equipment}") + return create_image(selected_equipment) + +def categorize_equipment_by_type(data): + equipment_by_type = {} + for item in data: + equip_type = item['type'] + if equip_type not in equipment_by_type: + equipment_by_type[equip_type] = [] + equipment_by_type[equip_type].append(item) + return equipment_by_type + +def select_equipment_by_type(equipment_by_type, type_combination): + selected_equipment = [] + backpack_count = 0 + + for equip_type, count in type_combination.items(): + if equip_type in equipment_by_type: + available_items = equipment_by_type[equip_type] + random.shuffle(available_items) + selected = 0 + for item in available_items: + if selected < count: + if item['backpack'] == 1 and backpack_count >= 1: + continue + selected_equipment.append(item) + selected += 1 + if item['backpack'] == 1: + backpack_count += 1 + else: + break + + return selected_equipment + +def select_random_equipment(max_equipment, count): + return random.sample(range(max_equipment), count) + +def create_image(selected_equipment): + new_img = Image.new('RGBA', (800, 500), (0, 0, 0, 1000)) + logo_path = img_path + "/super earth.png" + logo = Image.open(logo_path) + new_img = image_paste(logo, new_img, (682, 20)) + draw = ImageDraw.Draw(new_img) + ch_text_font = ImageFont.truetype(data_path + '/font/msyh.ttc', 36) + pos_horizon = 20 + + for equipment in selected_equipment: + name = equipment['name'] + path = basic_path + "/" + equipment['path'].replace("\\", "/") + icon = Image.open(path) + icon = icon.resize((100, 100)) + new_img = image_paste(icon, new_img, (20, pos_horizon)) + draw.text((140, pos_horizon + 5), '战备名称:', fill='white', font=ch_text_font) + draw.text((140, pos_horizon + 50), f'{name}', fill='white', font=ch_text_font) + pos_horizon += 120 + + b_io = BytesIO() + new_img = ImageOps.expand(new_img, border=10, fill="white") + new_img.save(b_io, format="PNG") + return b_io + +def image_paste(paste_image, under_image, pos): + if paste_image.mode == 'RGBA': + under_image.paste(paste_image, pos, mask=paste_image.split()[3]) + else: + under_image.paste(paste_image, pos) + return under_image + +def is_number(s): + return bool(re.match(r'^[0-9]+$', s)) diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/data/equipment.json b/hexi/plugins/nonebot_plugin_helldivers_tools/data/equipment.json new file mode 100644 index 0000000..c0ab3ed --- /dev/null +++ b/hexi/plugins/nonebot_plugin_helldivers_tools/data/equipment.json @@ -0,0 +1,374 @@ +[ + { + "name": "A-G-16 加特林哨戒炮", + "path": "img/helldivers/A-G-16 加特林哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "A-M-23 电磁冲击波迫击哨戒炮", + "path": "img/helldivers/A-M-23 电磁冲击波迫击哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "A-MG-43 哨戒机枪", + "path": "img/helldivers/A-MG-43 哨戒机枪.png", + "type": "green", + "backpack": 0 + }, + { + "name": "A-MLS-4X 火箭哨戒炮", + "path": "img/helldivers/A-MLS-4X 火箭哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "AAC-8 自动哨戒炮", + "path": "img/helldivers/AAC-8 自动哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "AARC-3 特斯拉塔", + "path": "img/helldivers/AARC-3 特斯拉塔.png", + "type": "green", + "backpack": 0 + }, + { + "name": "AC-8 机炮", + "path": "img/helldivers/AC-8 机炮.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "AM-12 迫击哨戒炮", + "path": "img/helldivers/AM-12 迫击哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "APW-1 反器材步枪", + "path": "img/helldivers/APW-1 反器材步枪.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "ARC-3 电弧发射器", + "path": "img/helldivers/ARC-3 电弧发射器.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "AX-AR-3 护卫犬", + "path": "img/helldivers/AX-AR-3 护卫犬.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "AX-LAS-5 护卫犬漫游车", + "path": "img/helldivers/AX-LAS-5 护卫犬漫游车.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "B-1 补给背包", + "path": "img/helldivers/B-1 补给背包.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "E-MG-101 重机枪部署支架", + "path": "img/helldivers/E-MG-101 重机枪部署支架.png", + "type": "green", + "backpack": 0 + }, + { + "name": "EAT-17 消耗性反坦克武器", + "path": "img/helldivers/EAT-17 消耗性反坦克武器.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "FAF-14 飞矛", + "path": "img/helldivers/FAF-14 飞矛.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "FLAM-40 火焰喷射器", + "path": "img/helldivers/FLAM-40 火焰喷射器.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "FX-12 防护罩生成中继器", + "path": "img/helldivers/FX-12 防护罩生成中继器.png", + "type": "green", + "backpack": 0 + }, + { + "name": "GL-21 榴弹发射器", + "path": "img/helldivers/GL-21 榴弹发射器.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "GR-8 无后座力炮", + "path": "img/helldivers/GR-8 无后座力炮.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "LAS-98 激光大炮", + "path": "img/helldivers/LAS-98 激光大炮.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "LAS-99 类星体加农炮", + "path": "img/helldivers/LAS-99 类星体加农炮.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "LIFT-850 喷射背包", + "path": "img/helldivers/LIFT-850 喷射背包.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "M-105 盟友", + "path": "img/helldivers/M-105 盟友.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "MD-14 燃烧地雷", + "path": "img/helldivers/MD-14 燃烧地雷.png", + "type": "green", + "backpack": 0 + }, + { + "name": "MD-6 反步兵雷区", + "path": "img/helldivers/MD-6 反步兵雷区.png", + "type": "green", + "backpack": 0 + }, + { + "name": "MG-43 机枪", + "path": "img/helldivers/MG-43 机枪.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "RS-422 磁轨炮", + "path": "img/helldivers/RS-422 磁轨炮.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "SH-20 防弹护盾背包", + "path": "img/helldivers/SH-20 防弹护盾背包.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "SH-32 防护罩生成包", + "path": "img/helldivers/SH-32 防护罩生成包.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "EXO-45 爱国者外骨骼装甲", + "path": "img/helldivers/EXO-45 爱国者外骨骼装甲.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "轨道120MM高爆弹火力网", + "path": "img/helldivers/轨道120MM高爆弹火力网.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道380MM高爆弹火力网", + "path": "img/helldivers/轨道380MM高爆弹火力网.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道加特林火力网", + "path": "img/helldivers/轨道加特林火力网.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道毒气攻击", + "path": "img/helldivers/轨道毒气攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道游走火力网", + "path": "img/helldivers/轨道游走火力网.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道激光炮", + "path": "img/helldivers/轨道激光炮.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道炮攻击", + "path": "img/helldivers/轨道炮攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道炮精准攻击", + "path": "img/helldivers/轨道炮精准攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道烟雾攻击", + "path": "img/helldivers/轨道烟雾攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道电磁冲击波攻击", + "path": "img/helldivers/轨道电磁冲击波攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "轨道空爆攻击", + "path": "img/helldivers/轨道空爆攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "重机枪", + "path": "img/helldivers/重机枪.png", + "type": "bule", + "backpack": 0 + }, + { + "name": "飞鹰110MM火箭巢", + "path": "img/helldivers/飞鹰110MM火箭巢.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰500KG炸弹", + "path": "img/helldivers/飞鹰500KG炸弹.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰凝固汽油弹空袭", + "path": "img/helldivers/飞鹰凝固汽油弹空袭.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰机枪扫射", + "path": "img/helldivers/飞鹰机枪扫射.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰烟雾攻击", + "path": "img/helldivers/飞鹰烟雾攻击.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰空袭", + "path": "img/helldivers/飞鹰空袭.png", + "type": "red", + "backpack": 0 + }, + { + "name": "飞鹰集束炸弹", + "path": "img/helldivers/飞鹰集束炸弹.png", + "type": "red", + "backpack": 0 + }, + { + "name": "AX-TX-13 护卫犬腐息", + "path": "img/helldivers/AX-TX-13 护卫犬腐息.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "EXO-49 解放者外骨骼装甲", + "path": "img/helldivers/EXO-49 解放者外骨骼装甲.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "TX-41 灭菌器", + "path": "img/helldivers/TX-41 灭菌器.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "轨道燃烧火力网", + "path": "img/helldivers/轨道燃烧火力网.png", + "type": "red", + "backpack": 0 + }, + { + "name": "MD-17 反坦克地雷", + "path": "img/helldivers/MD-17 反坦克地雷.png", + "type": "green", + "backpack": 0 + }, + { + "name": "MLS-4X 突击兵", + "path": "img/helldivers/MLS-4X 突击兵.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "RL-77 空爆火箭发射器", + "path": "img/helldivers/RL-77 空爆火箭发射器.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "A-FLAM-40 火焰喷射哨戒炮", + "path": "img/helldivers/A-FLAM-40 火焰喷射哨戒炮.png", + "type": "green", + "backpack": 0 + }, + { + "name": "E-AT-12 反坦克炮台", + "path": "img/helldivers/E-AT-12 反坦克炮台.png", + "type": "green", + "backpack": 0 + }, + { + "name": "M-102 快速侦查载具", + "path": "img/helldivers/M-102 快速侦查载具.png", + "type": "blue", + "backpack": 0 + }, + { + "name": "SH-51 定向护盾", + "path": "img/helldivers/SH-51 定向护盾.png", + "type": "blue", + "backpack": 1 + }, + { + "name": "StA-X3 W.A.S.P. Launcher", + "path": "img/helldivers/StA-X3 W.A.S.P. Launcher.png", + "type": "blue", + "backpack": 1 + } +] \ No newline at end of file diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/data/font/msyh.ttc b/hexi/plugins/nonebot_plugin_helldivers_tools/data/font/msyh.ttc new file mode 100644 index 0000000..ddc87b9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/data/font/msyh.ttc differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/data/plantes_mix.json b/hexi/plugins/nonebot_plugin_helldivers_tools/data/plantes_mix.json new file mode 100644 index 0000000..e0ced3e --- /dev/null +++ b/hexi/plugins/nonebot_plugin_helldivers_tools/data/plantes_mix.json @@ -0,0 +1,549 @@ +{ + "ACTIVE PLANETS": "活跃行星", + "AWAITING MAJOR ORDER": "等待重要指令", + "TOTAL PLAYERS:": "玩家总数:", + "Stand by for further orders from Super Earth High Command": "等待超级地球最高指挥部下发重要指令", + "Major Order ends in:": "重要指令结束时间:", + "MAJOR ORDER": "重要指令", + "Defended Planets": "防守星球", + "MEDALS": "奖章", + "DEFENSE": "防守", + "INCOMING SQUID INVASION": "光能入侵!", + "MEGA CITY": "超级城市", + "INVASION": "入侵", + "LIBERATION": "解放", + "DEFENDED": "抵御进度", + "PROTECTED": "防守进度", + "LIBERATED": "解放进度", + "RESTRICTED ZONE": "管制区", + "HOLDING FOR REINFORCEMENT": "等待增援", + "ACCUMULATED": "累积量", + "Galaxy Stats": "星系状态", + "CONTROLLED": "已控制", + "EQUALITY-ON-SEA": "仰齐滨", + "REMEMBRANCE": "缅城", + "EAGLEOPOLIS": "鹰都", + "YORK SUPREME": "棒约克", + "ADMINISTRATIVE CENTER 02": "行政中心02", + "PORT MERCY": "馨加泊", + "PROSPERITY CITY": "荣城", + "Oshaune": "欧绍恩", + "Martale": "玛尔特", + "Hellmire": "海尔迈尔", + "Matar Bay": "玛塔", + "Penta": "彭塔", + "Estanu": "伊斯塔努", + "Choohe": "丘伊", + "Chort Bay": "雀特湾", + "Fori Prime": "佛里主星", + "Super Earth": "超级地球", + "Marfark": "玛尔法克", + "Crimsica": "克里姆西卡", + "Menkent": "库楼三", + "Lesath": "尾宿八", + "Vernen Wells": "佛农井", + "Nivel 43": "尼维尔43", + "Durgen": "德尔根", + "Tibit": "提比特", + "Draupnir": "德罗普尼尔", + "Malevelon Creek": "麦拉芬蒙河", + "Maia": "昂宿四", + "Fenrir III": "芬里尔III", + "Errata Prime": "艾拉特主星", + "Troost": "特鲁斯特", + "Ustotu": "伍斯特图", + "Turing": "图灵", + "Angel's Venture": "天使投资", + "Ingmar": "英格玛", + "Tien Kwan": "天关", + "Ubanea": "乌巴尼亚", + "Vandalon IV": "万达隆IV", + "Meridia": "默里迪亚", + "Veld": "维尔德", + "Mantes": "蒙特斯", + "Klen Dahth II": "克伦达斯II", + "Pathfinder V": "开拓者V", + "Widow's Harbor": "寡妇港", + "New Haven": "纽黑文", + "Pilen V": "皮伦V", + "Hydrofall Prime": "水瀑主星", + "Zea Rugosia": "泽亚鲁戈西亚", + "Darrowsport": "达罗斯波特", + "Fornskogur II": "福恩斯科古尔", + "Midasburg": "弥达斯堡", + "Cerberus IIIc": "刻耳帕洛斯IIIc", + "Prosperity Falls": "繁荣瀑布", + "Okul VI": "欧库VI", + "Martyr's Bay": "烈士湾", + "Freedom Peak": "弗里敦峰", + "Fort Union": "联合堡", + "Kelvinor": "开尔文奥尔", + "Wraith": "幽灵", + "Igla": "伊格勒", + "New Kiruna": "新基鲁纳", + "Fort Justice": "正义堡", + "Zegema Paradise": "泽格马乐土", + "Providence": "普罗维登斯", + "Primordia": "普莱默迪亚", + "Sulfura": "萨尔弗拉", + "Nublaria I": "努布拉里亚I", + "Krakatwo": "克拉克图", + "Volterra": "沃尔泰拉", + "Crucible": "熔炉", + "Veil": "帷幕", + "Marre IV": "马尔IV", + "Fort Sanctuary": "庇护堡", + "Seyshel Beach": "塞舌尔海滩", + "Effluvia": "艾芙鲁维亚", + "Solghast": "索尔加斯特", + "Dilvuia": "迪卢维亚", + "Viridia Prime": "维尔伊迪主星", + "Obari": "欧巴里", + "Myradesh": "米拉戴什", + "Atrama": "阿特拉玛", + "Emeria": "埃梅里亚", + "Barabos": "巴勒博斯", + "Fenmire": "范迈尔", + "Mastia": "玛斯蒂亚", + "Shallus": "沙勒斯", + "Krakabos": "克拉克博斯", + "Iridica": "艾丽迪卡", + "Azterra": "艾孜泰拉", + "Azur Secundus": "艾热尔次星", + "Ivis": "艾维斯", + "Slif": "斯利夫", + "Caramoor": "开勒莫尔", + "Kharst": "喀斯特", + "Eukoria": "欧科里亚", + "Myrium": "梅里翁", + "Kerth Secundus": "克斯次星", + "Parsh": "帕尔什", + "Reaf": "利夫", + "Irulta": "艾鲁尔塔", + "Emorath": "艾莫拉斯", + "Ilduna Prime": "伊尔都纳主星", + "Maw": "深渊", + "Borea": "博瑞亚", + "Curia": "居里亚", + "Tarsh": "塔尔什", + "Shelt": "谢尔特", + "Imber": "因博尔", + "Blistica": "布里斯提卡", + "Ratch": "拉奇", + "Julheim": "尤尔海姆", + "Valgaard": "瓦尔加德", + "Arkturus": "阿克图勒斯", + "Esker": "艾斯克尔", + "Terrek": "泰雷克", + "Cirrus": "希勒斯", + "Heeth": "希斯", + "Alta V": "奥尔塔V", + "Ursica XI": "厄西卡XI", + "Inari": "伊纳里", + "Skaash": "斯卡什", + "Moradesh": "莫拉戴什", + "Rasp": "拉斯普", + "Bashyr": "巴希尔", + "Regnus": "雷格努斯", + "Mog": "莫格", + "Valmox": "瓦尔莫克斯", + "Iro": "伊罗", + "Grafmere": "格拉夫米尔", + "New Stockholm": "新斯德哥尔摩", + "Oasis": "绿洲", + "Genesis Prime": "创世主星", + "Outpost 32": "32号哨站", + "Calypso": "卡利普索", + "Elysian Meadows": "埃律西昂草原", + "Alderidge Cove": "阿尔德里奇湾", + "Trandor": "特兰道尔", + "East Iridium Trading Bay": "东铱贸易湾", + "Liberty Ridge": "解放岭", + "Baldrick Prime": "巴尔德里克主星", + "The Weir": "维尔", + "Kuper": "库珀", + "Oslo Station": "奥斯陆站", + "Pöpli IX": "珀普利IX", + "Gunvald": "古恩瓦尔德", + "Dolph": "多尔夫", + "Bekvam III": "贝克温III", + "Duma Tyr": "杜马提尔", + "Aeris Pass": "亚萨关隘", + "Aurora Bay": "极光湾", + "Gaellivare": "耶利瓦勒", + "Vog-Sojoth": "佛戈索约斯", + "Kirrik": "基里克", + "Mortax Prime": "摩尔塔克斯主星", + "Wilford Station": "威尔福德站", + "Pioneer II": "先驱II", + "Erson Sands": "厄尔森桑兹", + "Socorro III": "索科罗III", + "Bore Rock": "博尔岩", + "Darius II": "大流士II", + "Acamar IV": "天园六IV", + "Achernar Secundus": "水委一次星", + "Achird III": "王良三III", + "Acrab XI": "房宿四XI", + "Acrux IX": "十字架二IX", + "Acubens Prime": "柳宿增三主星", + "Adhara": "弧矢七", + "Afoyay Bay": "艾福亚湾", + "Alairt III": "艾列尔特III", + "Alamak VII": "天大将军——VII", + "Alaraph": "右执法", + "Alathfar XI": "织女增三XI", + "Andar": "安达尔", + "Asperoth Prime": "阿斯佩洛斯主星", + "Bellatrix": "参宿五", + "Botein": "天阴四", + "Osupsam": "欧苏普森", + "Brink-2": "布林克2", + "Bunda Secundus": "天垒城一次星", + "Canopus": "老人", + "Caph": "王良一", + "Castor": "北河二", + "Mort": "莫特", + "Charbal-VII": "查巴尔VII", + "Charon Prime": "卡戎主星", + "Choepessa IV": "科埃佩萨IV", + "Claorell": "可洛尔", + "Clasa": "克拉萨", + "Demiurg": "戴米尔基", + "Deneb Secundus": "天津四次星", + "Electra Bay": "昂宿一湾", + "Enuliale": "安努力亚", + "Epsilon Phoencis VI": "艾普斯林芬赛斯VI", + "Gacrux": "十字架一", + "Gar Haren": "轧尔哈伦", + "Gatria": "盖尔崔亚", + "Gemma": "贯索四", + "Grand Errant": "大艾伦特", + "Hadr": "马腹一", + "Haka": "哈卡", + "Haldus": "海德斯", + "Halies Port": "海利斯港", + "Herthon Secundus": "赫尔松次主星", + "Hesoe Prime": "海索主星", + "Heze Bay": "角宿二湾", + "Hort": "霍尔特", + "Hydrobius": "海德罗毕亚斯", + "Karlia": "卡利亚", + "Keid": "九州殊口增七", + "Khandark": "勘达尔克", + "Klaka 5": "克拉卡5", + "Kneth Port": "克奈斯港", + "Kraz": "轸宿四", + "Kuma": "天棓二", + "Lastofe": "赖斯斗夫", + "Leng Scundus": "蓝恩次星", + "Meissa": "觜宿一", + "Mekbuda": "井宿七", + "Merak": "北斗二", + "Merga IV": "玄戈增二IV", + "Minchir": "敏切尔", + "Mintoria": "敏托瑞亚", + "Morida 9": "摩帝亚9", + "Nabatea Secundus": "那贝塔次星", + "Navi VII": "阁道二", + "Overgoe Prime": "欧维果主星", + "Pandion-XXIV": "帕狄恩XXIV", + "Partion": "帕尔晨", + "Peacock": "孔雀十一", + "Phact Bay": "丈人一湾", + "Pherkad Secundus": "北极一次星", + "Polaris Prime": "北极星主星", + "Pollux 31": "北河三31", + "Prasa": "普拉萨", + "Propus": "五诸侯三", + "Ras Algethi": "帝坐", + "RD-4": "RD4", + "Rogue 5": "罗格5", + "Rirga Bay": "里尔加湾", + "Seasse": "西斯", + "Senge 23": "新戈23", + "Setia": "赛提亚", + "Shete": "赛特", + "Siemnot": "席姆纳特", + "Sirius": "天狼星", + "Skat bay": "斯卡特湾", + "Spherion": "斯飞利昂", + "Stor Tha Prime": "斯特萨主星", + "Stout": "斯图尔特", + "Termadon": "特尔玛登", + "Varylia 5": "瓦瑞拉亚", + "Wasat": "天樽二", + "Vega Bay": "织女一湾", + "Wezen": "弧矢一", + "Vindemitarix Prime": "文德米塔里克斯主星", + "X-45": "X45", + "Yed Prior": "天市右垣九", + "Zefia": "塞飞亚", + "Zosma": "太微右垣五", + "Zzaniah Prime": "藏尼亚主星", + "Skitter": "斯基特", + "Euphoria III": "欧福利亚III", + "Diaspora X": "大流散X", + "Gemstone Bluffs": "宝石崖", + "Zagon Prime": "扎贡主星", + "Omicron": "奥密克戎", + "Cyberstan": "生化斯坦", + "OSHAUNE": "欧绍恩", + "MARTALE": "玛尔特", + "HELLMIRE": "海尔迈尔", + "MATAR BAY": "玛塔", + "PENTA": "彭塔", + "ESTANU": "伊斯塔努", + "CHOOHE": "丘伊", + "CHORT BAY": "雀特湾", + "FORI PRIME": "佛里主星", + "SUPER EARTH": "超级地球", + "MARFARK": "玛尔法克", + "CRIMSICA": "克里姆西卡", + "MENKENT": "库楼三", + "LESATH": "尾宿八", + "VERNEN WELLS": "佛农井", + "NIVEL 43": "尼维尔43", + "DURGEN": "德尔根", + "TIBIT": "提比特", + "DRAUPNIR": "德罗普尼尔", + "MALEVELON CREEK": "麦拉芬蒙河", + "MAIA": "昂宿四", + "FENRIR III": "芬里尔III", + "ERRATA PRIME": "艾拉特主星", + "TROOST": "特鲁斯特", + "USTOTU": "伍斯特图", + "TURING": "图灵", + "ANGEL'S VENTURE": "天使投资", + "INGMAR": "英格玛", + "TIEN KWAN": "天关", + "UBANEA": "乌巴尼亚", + "VANDALON IV": "万达隆IV", + "MERIDIA": "默里迪亚", + "VELD": "维尔德", + "MANTES": "蒙特斯", + "KLEN DAHTH II": "克伦达斯II", + "PATHFINDER V": "开拓者V", + "WIDOW'S HARBOR": "寡妇港", + "NEW HAVEN": "纽黑文", + "PILEN V": "皮伦V", + "HYDROFALL PRIME": "水瀑主星", + "ZEA RUGOSIA": "泽亚鲁戈西亚", + "DARROWSPORT": "达罗斯波特", + "FORNSKOGUR II": "福恩斯科古尔", + "MIDASBURG": "弥达斯堡", + "CERBERUS IIIC": "刻耳帕洛斯IIIc", + "PROSPERITY FALLS": "繁荣瀑布", + "OKUL VI": "欧库VI", + "MARTYR'S BAY": "烈士湾", + "FREEDOM PEAK": "弗里敦峰", + "FORT UNION": "联合堡", + "KELVINOR": "开尔文奥尔", + "WRAITH": "幽灵", + "IGLA": "伊格勒", + "NEW KIRUNA": "新基鲁纳", + "FORT JUSTICE": "正义堡", + "ZEGEMA PARADISE": "泽格马乐土", + "PROVIDENCE": "普罗维登斯", + "PRIMORDIA": "普莱默迪亚", + "SULFURA": "萨尔弗拉", + "NUBLARIA I": "努布拉里亚I", + "KRAKATWO": "克拉克图", + "VOLTERRA": "沃尔泰拉", + "CRUCIBLE": "熔炉", + "VEIL": "帷幕", + "MARRE IV": "马尔IV", + "FORT SANCTUARY": "庇护堡", + "SEYSHEL BEACH": "塞舌尔海滩", + "EFFLUVIA": "艾芙鲁维亚", + "SOLGHAST": "索尔加斯特", + "DILVUIA": "迪卢维亚", + "VIRIDIA PRIME": "维尔伊迪主星", + "OBARI": "欧巴里", + "MYRADESH": "米拉戴什", + "ATRAMA": "阿特拉玛", + "EMERIA": "埃梅里亚", + "BARABOS": "巴勒博斯", + "FENMIRE": "范迈尔", + "MASTIA": "玛斯蒂亚", + "SHALLUS": "沙勒斯", + "KRAKABOS": "克拉克博斯", + "IRIDICA": "艾丽迪卡", + "AZTERRA": "艾孜泰拉", + "AZUR SECUNDUS": "艾热尔次星", + "IVIS": "艾维斯", + "SLIF": "斯利夫", + "CARAMOOR": "开勒莫尔", + "KHARST": "喀斯特", + "EUKORIA": "欧科里亚", + "MYRIUM": "梅里翁", + "KERTH SECUNDUS": "克斯次星", + "PARSH": "帕尔什", + "REAF": "利夫", + "IRULTA": "艾鲁尔塔", + "EMORATH": "艾莫拉斯", + "ILDUNA PRIME": "伊尔都纳主星", + "MAW": "深渊", + "BOREA": "博瑞亚", + "CURIA": "居里亚", + "TARSH": "塔尔什", + "SHELT": "谢尔特", + "IMBER": "因博尔", + "BLISTICA": "布里斯提卡", + "RATCH": "拉奇", + "JULHEIM": "尤尔海姆", + "VALGAARD": "瓦尔加德", + "ARKTURUS": "阿克图勒斯", + "ESKER": "艾斯克尔", + "TERREK": "泰雷克", + "CIRRUS": "希勒斯", + "HEETH": "希斯", + "ALTA V": "奥尔塔V", + "URSICA XI": "厄西卡XI", + "INARI": "伊纳里", + "SKAASH": "斯卡什", + "MORADESH": "莫拉戴什", + "RASP": "拉斯普", + "BASHYR": "巴希尔", + "REGNUS": "雷格努斯", + "MOG": "莫格", + "VALMOX": "瓦尔莫克斯", + "IRO": "伊罗", + "GRAFMERE": "格拉夫米尔", + "NEW STOCKHOLM": "新斯德哥尔摩", + "OASIS": "绿洲", + "GENESIS PRIME": "创世主星", + "OUTPOST 32": "32号哨站", + "CALYPSO": "卡利普索", + "ELYSIAN MEADOWS": "埃律西昂草原", + "ALDERIDGE COVE": "阿尔德里奇湾", + "TRANDOR": "特兰道尔", + "EAST IRIDIUM TRADING BAY": "东铱贸易湾", + "LIBERTY RIDGE": "解放岭", + "BALDRICK PRIME": "巴尔德里克主星", + "THE WEIR": "维尔", + "KUPER": "库珀", + "OSLO STATION": "奥斯陆站", + "PÖPLI IX": "珀普利IX", + "GUNVALD": "古恩瓦尔德", + "DOLPH": "多尔夫", + "BEKVAM III": "贝克温III", + "DUMA TYR": "杜马提尔", + "AERIS PASS": "亚萨关隘", + "AURORA BAY": "极光湾", + "GAELLIVARE": "耶利瓦勒", + "VOG-SOJOTH": "佛戈索约斯", + "KIRRIK": "基里克", + "MORTAX PRIME": "摩尔塔克斯主星", + "WILFORD STATION": "威尔福德站", + "PIONEER II": "先驱II", + "ERSON SANDS": "厄尔森桑兹", + "SOCORRO III": "索科罗III", + "BORE ROCK": "博尔岩", + "DARIUS II": "大流士II", + "ACAMAR IV": "天园六IV", + "ACHERNAR SECUNDUS": "水委一次星", + "ACHIRD III": "王良三III", + "ACRAB XI": "房宿四XI", + "ACRUX IX": "十字架二IX", + "ACUBENS PRIME": "柳宿增三主星", + "ADHARA": "弧矢七", + "AFOYAY BAY": "艾福亚湾", + "AIN-5": "毕宿—5", + "ALAIRT III": "艾列尔特III", + "ALAMAK VII": "天大将军——VII", + "ALARAPH": "右执法", + "ALATHFAR XI": "织女增三XI", + "ANDAR": "安达尔", + "ASPEROTH PRIME": "阿斯佩洛斯主星", + "BELLATRIX": "参宿五", + "BOTEIN": "天阴四", + "OSUPSAM": "欧苏普森", + "BRINK-2": "布林克2", + "BUNDA SECUNDUS": "天垒城一次星", + "CANOPUS": "老人", + "CAPH": "王良一", + "CASTOR": "北河二", + "MORT": "莫特", + "CHARBAL-VII": "查巴尔VII", + "CHARON PRIME": "卡戎主星", + "CHOEPESSA IV": "科埃佩萨IV", + "CLAORELL": "可洛尔", + "CLASA": "克拉萨", + "DEMIURG": "戴米尔基", + "DENEB SECUNDUS": "天津四次星", + "ELECTRA BAY": "昂宿一湾", + "ENULIALE": "安努力亚", + "EPSILON PHOENCIS VI": "艾普斯林芬赛斯VI", + "GACRUX": "十字架一", + "GAR HAREN": "轧尔哈伦", + "GATRIA": "盖尔崔亚", + "GEMMA": "贯索四", + "GRAND ERRANT": "大艾伦特", + "HADR": "马腹一", + "HAKA": "哈卡", + "HALDUS": "海德斯", + "HALIES PORT": "海利斯港", + "HERTHON SECUNDUS": "赫尔松次主星", + "HESOE PRIME": "海索主星", + "HEZE BAY": "角宿二湾", + "HORT": "霍尔特", + "HYDROBIUS": "海德罗毕亚斯", + "KARLIA": "卡利亚", + "KEID": "九州殊口增七", + "KHANDARK": "勘达尔克", + "KLAKA 5": "克拉卡5", + "KNETH PORT": "克奈斯港", + "KRAZ": "轸宿四", + "KUMA": "天棓二", + "LASTOFE": "赖斯斗夫", + "LENG SCUNDUS": "蓝恩次星", + "MEISSA": "觜宿一", + "MEKBUDA": "井宿七", + "MERAK": "北斗二", + "MERGA IV": "玄戈增二IV", + "MINCHIR": "敏切尔", + "MINTORIA": "敏托瑞亚", + "MORIDA 9": "摩帝亚9", + "NABATEA SECUNDUS": "那贝塔次星", + "NAVI VII": "阁道二", + "OVERGOE PRIME": "欧维果主星", + "PANDION-XXIV": "帕狄恩XXIV", + "PARTION": "帕尔晨", + "PEACOCK": "孔雀十一", + "PHACT BAY": "丈人一湾", + "PHERKAD SECUNDUS": "北极一次星", + "POLARIS PRIME": "北极星主星", + "POLLUX 31": "北河三31", + "PRASA": "普拉萨", + "PROPUS": "五诸侯三", + "RAS ALGETHI": "帝坐", + "ROGUE 5": "罗格5", + "RIRGA BAY": "里尔加湾", + "SEASSE": "西斯", + "SENGE 23": "新戈23", + "SETIA": "赛提亚", + "SHETE": "赛特", + "SIEMNOT": "席姆纳特", + "SIRIUS": "天狼星", + "SKAT BAY": "斯卡特湾", + "SPHERION": "斯飞利昂", + "STOR THA PRIME": "斯特萨主星", + "STOUT": "斯图尔特", + "TERMADON": "特尔玛登", + "VARYLIA 5": "瓦瑞拉亚", + "WASAT": "天樽二", + "VEGA BAY": "织女一湾", + "WEZEN": "弧矢一", + "VINDEMITARIX PRIME": "文德米塔里克斯主星", + "YED PRIOR": "天市右垣九", + "ZEFIA": "塞飞亚", + "ZOSMA": "太微右垣五", + "ZZANIAH PRIME": "藏尼亚主星", + "SKITTER": "斯基特", + "EUPHORIA III": "欧福利亚III", + "DIASPORA X": "大流散X", + "GEMSTONE BLUFFS": "宝石崖", + "ZAGON PRIME": "扎贡主星", + "OMICRON": "奥密克戎", + "CYBERSTAN": "生化斯坦" +} \ No newline at end of file diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-FLAM-40 火焰喷射哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-FLAM-40 火焰喷射哨戒炮.png new file mode 100644 index 0000000..dcf0053 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-FLAM-40 火焰喷射哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-G-16 加特林哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-G-16 加特林哨戒炮.png new file mode 100644 index 0000000..5290cdb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-G-16 加特林哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-M-23 电磁冲击波迫击哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-M-23 电磁冲击波迫击哨戒炮.png new file mode 100644 index 0000000..af29ae0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-M-23 电磁冲击波迫击哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MG-43 哨戒机枪.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MG-43 哨戒机枪.png new file mode 100644 index 0000000..0b2c76e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MG-43 哨戒机枪.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MLS-4X 火箭哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MLS-4X 火箭哨戒炮.png new file mode 100644 index 0000000..3d1d61a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/A-MLS-4X 火箭哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AAC-8 自动哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AAC-8 自动哨戒炮.png new file mode 100644 index 0000000..de531df Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AAC-8 自动哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AARC-3 特斯拉塔.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AARC-3 特斯拉塔.png new file mode 100644 index 0000000..a5a350b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AARC-3 特斯拉塔.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AC-8 机炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AC-8 机炮.png new file mode 100644 index 0000000..22ccf6b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AC-8 机炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AM-12 迫击哨戒炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AM-12 迫击哨戒炮.png new file mode 100644 index 0000000..89cd297 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AM-12 迫击哨戒炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/APW-1 反器材步枪.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/APW-1 反器材步枪.png new file mode 100644 index 0000000..861ef45 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/APW-1 反器材步枪.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/ARC-3 电弧发射器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/ARC-3 电弧发射器.png new file mode 100644 index 0000000..a9cd71e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/ARC-3 电弧发射器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-AR-3 护卫犬.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-AR-3 护卫犬.png new file mode 100644 index 0000000..0a0bfb5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-AR-3 护卫犬.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-LAS-5 护卫犬漫游车.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-LAS-5 护卫犬漫游车.png new file mode 100644 index 0000000..63f65c8 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-LAS-5 护卫犬漫游车.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-TX-13 护卫犬腐息.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-TX-13 护卫犬腐息.png new file mode 100644 index 0000000..78525b4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/AX-TX-13 护卫犬腐息.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/B-1 补给背包.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/B-1 补给背包.png new file mode 100644 index 0000000..a46c4bc Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/B-1 补给背包.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-AT-12 反坦克炮台.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-AT-12 反坦克炮台.png new file mode 100644 index 0000000..d517311 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-AT-12 反坦克炮台.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-MG-101 重机枪部署支架.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-MG-101 重机枪部署支架.png new file mode 100644 index 0000000..ab2316e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/E-MG-101 重机枪部署支架.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EAT-17 消耗性反坦克武器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EAT-17 消耗性反坦克武器.png new file mode 100644 index 0000000..b26ec1c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EAT-17 消耗性反坦克武器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-45 爱国者外骨骼装甲.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-45 爱国者外骨骼装甲.png new file mode 100644 index 0000000..740d8ac Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-45 爱国者外骨骼装甲.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-49 解放者外骨骼装甲.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-49 解放者外骨骼装甲.png new file mode 100644 index 0000000..a02f197 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/EXO-49 解放者外骨骼装甲.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FAF-14 飞矛.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FAF-14 飞矛.png new file mode 100644 index 0000000..0c6faec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FAF-14 飞矛.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FLAM-40 火焰喷射器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FLAM-40 火焰喷射器.png new file mode 100644 index 0000000..babd0ec Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FLAM-40 火焰喷射器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FX-12 防护罩生成中继器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FX-12 防护罩生成中继器.png new file mode 100644 index 0000000..2f5f274 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/FX-12 防护罩生成中继器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GL-21 榴弹发射器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GL-21 榴弹发射器.png new file mode 100644 index 0000000..e6c9acd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GL-21 榴弹发射器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GR-8 无后座力炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GR-8 无后座力炮.png new file mode 100644 index 0000000..41b2fd5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/GR-8 无后座力炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-98 激光大炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-98 激光大炮.png new file mode 100644 index 0000000..7907bf9 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-98 激光大炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-99 类星体加农炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-99 类星体加农炮.png new file mode 100644 index 0000000..628d8bd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LAS-99 类星体加农炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LIFT-850 喷射背包.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LIFT-850 喷射背包.png new file mode 100644 index 0000000..1e05e5e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/LIFT-850 喷射背包.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-102 快速侦查载具.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-102 快速侦查载具.png new file mode 100644 index 0000000..24f320c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-102 快速侦查载具.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-105 盟友.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-105 盟友.png new file mode 100644 index 0000000..e47d66d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/M-105 盟友.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-14 燃烧地雷.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-14 燃烧地雷.png new file mode 100644 index 0000000..f6e693c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-14 燃烧地雷.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-17 反坦克地雷.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-17 反坦克地雷.png new file mode 100644 index 0000000..95e7334 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-17 反坦克地雷.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-6 反步兵雷区.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-6 反步兵雷区.png new file mode 100644 index 0000000..85e3e3e Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MD-6 反步兵雷区.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MG-43 机枪.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MG-43 机枪.png new file mode 100644 index 0000000..224e0eb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MG-43 机枪.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MLS-4X 突击兵.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MLS-4X 突击兵.png new file mode 100644 index 0000000..398abae Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/MLS-4X 突击兵.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RL-77 空爆火箭发射器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RL-77 空爆火箭发射器.png new file mode 100644 index 0000000..d8e6b7f Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RL-77 空爆火箭发射器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RS-422 磁轨炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RS-422 磁轨炮.png new file mode 100644 index 0000000..dd66302 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/RS-422 磁轨炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-20 防弹护盾背包.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-20 防弹护盾背包.png new file mode 100644 index 0000000..712fe15 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-20 防弹护盾背包.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-32 防护罩生成包.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-32 防护罩生成包.png new file mode 100644 index 0000000..a1dbd35 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-32 防护罩生成包.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-51 定向护盾.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-51 定向护盾.png new file mode 100644 index 0000000..908cafe Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/SH-51 定向护盾.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/StA-X3 W.A.S.P. Launcher.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/StA-X3 W.A.S.P. Launcher.png new file mode 100644 index 0000000..e55f504 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/StA-X3 W.A.S.P. Launcher.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/TX-41 灭菌器.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/TX-41 灭菌器.png new file mode 100644 index 0000000..04b438d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/TX-41 灭菌器.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道120MM高爆弹火力网.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道120MM高爆弹火力网.png new file mode 100644 index 0000000..57a6cba Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道120MM高爆弹火力网.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道380MM高爆弹火力网.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道380MM高爆弹火力网.png new file mode 100644 index 0000000..c558c97 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道380MM高爆弹火力网.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道加特林火力网.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道加特林火力网.png new file mode 100644 index 0000000..9278442 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道加特林火力网.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道毒气攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道毒气攻击.png new file mode 100644 index 0000000..ec24e77 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道毒气攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道游走火力网.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道游走火力网.png new file mode 100644 index 0000000..99b9c90 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道游走火力网.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道激光炮.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道激光炮.png new file mode 100644 index 0000000..728c516 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道激光炮.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮攻击.png new file mode 100644 index 0000000..a6cffe5 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮精准攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮精准攻击.png new file mode 100644 index 0000000..5c7eb54 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道炮精准攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道烟雾攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道烟雾攻击.png new file mode 100644 index 0000000..e929917 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道烟雾攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道燃烧火力网.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道燃烧火力网.png new file mode 100644 index 0000000..829f2d2 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道燃烧火力网.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道电磁冲击波攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道电磁冲击波攻击.png new file mode 100644 index 0000000..2abde5a Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道电磁冲击波攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道空爆攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道空爆攻击.png new file mode 100644 index 0000000..60776ef Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/轨道空爆攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/重机枪.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/重机枪.png new file mode 100644 index 0000000..a099981 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/重机枪.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰110MM火箭巢.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰110MM火箭巢.png new file mode 100644 index 0000000..797951d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰110MM火箭巢.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰500KG炸弹.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰500KG炸弹.png new file mode 100644 index 0000000..44c241c Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰500KG炸弹.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰凝固汽油弹空袭.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰凝固汽油弹空袭.png new file mode 100644 index 0000000..3fc173b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰凝固汽油弹空袭.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰机枪扫射.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰机枪扫射.png new file mode 100644 index 0000000..ea83dbb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰机枪扫射.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰烟雾攻击.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰烟雾攻击.png new file mode 100644 index 0000000..fb79d7b Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰烟雾攻击.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰空袭.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰空袭.png new file mode 100644 index 0000000..7109f60 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰空袭.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰集束炸弹.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰集束炸弹.png new file mode 100644 index 0000000..b86ddfb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/helldivers/飞鹰集束炸弹.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/img/super earth.png b/hexi/plugins/nonebot_plugin_helldivers_tools/img/super earth.png new file mode 100644 index 0000000..4e02ab0 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/img/super earth.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/temp/1739449560.png b/hexi/plugins/nonebot_plugin_helldivers_tools/temp/1739449560.png new file mode 100644 index 0000000..26151cf Binary files /dev/null and b/hexi/plugins/nonebot_plugin_helldivers_tools/temp/1739449560.png differ diff --git a/hexi/plugins/nonebot_plugin_helldivers_tools/utils.py b/hexi/plugins/nonebot_plugin_helldivers_tools/utils.py new file mode 100644 index 0000000..5fe4802 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_helldivers_tools/utils.py @@ -0,0 +1,168 @@ +import asyncio +import io +import json +import os +import re +from datetime import datetime +from typing import Optional, Union +import base64 + +from PIL import Image +from playwright.async_api import async_playwright +from nonebot.adapters.onebot.v11 import MessageEvent, MessageSegment +from nonebot import logger + +basic_path = os.path.dirname(__file__) +save_path = os.path.join(basic_path, "temp") + +headers = { + "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.6) ", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "zh-cn" +} + + +def gen_ms_img(image: Union[bytes, Image.Image]) -> MessageSegment: + if isinstance(image, bytes): + return MessageSegment.image( + pic2b64(Image.open(io.BytesIO(image))) + ) + else: + return MessageSegment.image( + pic2b64(image) + ) + + +def get_present_time() -> int: + return int(datetime.timestamp(datetime.now())) + + +async def screen_shot(url: str, time_present: int) -> Optional[str or bool]: + async with async_playwright() as p: + browser = await p.chromium.launch() + page = await browser.new_page() + try: + # 访问页面 + await page.goto(url) + # 等待页面加载完成 + await page.wait_for_load_state('domcontentloaded') + await page.wait_for_load_state('networkidle') + + # 1. 获取卡片数量 + card_selector = "div.flex.cursor-pointer[style*='width: 320px;']" + # 等待至少一个卡片元素出现 + await page.wait_for_selector(card_selector, state='visible') + + # 获取卡片数量 + card_count = await page.eval_on_selector_all(card_selector, "els => els.length") + logger.info(f"获取到的星球卡片数量:{card_count}") + + # 2. 单个卡片高度(固定为 270) + card_height = 270 + extra_padding = 100 # 防止 margin、padding、gap + + # 3. 计算所需 viewport 高度 + if card_count == 0 or card_count <= 15: + logger.warning("卡片元素数量未超过设定值,使用默认视口高度。") + required_height = 1080 # 默认高度 + else: + required_height = 1080 + (card_count - 15) / 5 * card_height + extra_padding + + viewport_width = 1920 # 固定宽度 + await page.set_viewport_size({"width": viewport_width, "height": required_height}) + + # 记录时间 + time_start = get_present_time() + # 4. 加载替换文本的脚本 + with open(f'{basic_path}/data/plantes_mix.json', 'r', encoding='utf-8') as file: + replacements = json.load(file) + + # 构建替换脚本 + replacement_script = "" + for keyword, replacement in replacements.items(): + escaped_keyword = json.dumps(keyword) + escaped_replacement = json.dumps(replacement) + replacement_script += f""" + document.body.outerHTML = document.body.outerHTML.replace(new RegExp({escaped_keyword}, 'g'), {escaped_replacement}); + """ + + # 执行替换脚本 + await page.evaluate(replacement_script) + + # 结束时间 + time_end = get_present_time() + duration = time_end - time_start + logger.info(f"截图文本替换耗时:{duration}s") + + # 等待页面更新 + await asyncio.sleep(1) + + # 保存截图 + logger.info("正在保存图片...") + img_path = os.path.join(save_path, f'{time_present}.png') + await page.screenshot( + path=img_path, + full_page=True + ) + + # 压缩图片 + logger.info("正在压缩图片...") + img_convert = Image.open(img_path) + img_convert.save(img_path, quality=80) + logger.info("图片保存成功!") + + except Exception as e: + logger.error(f"访问网站异常:{type(e)} `{e}`") + return f"访问网站异常:{type(e)} `{e}`" + + finally: + await browser.close() + + return "success" + + +async def screen_shot_2(url: str, time_present: int) -> Optional[str or bool]: + async with async_playwright() as p: + browser = await p.chromium.launch() + page = await browser.new_page() + try: + # 设置视口大小 + await page.set_viewport_size({"width": 1920, "height": 1080}) + await page.goto(url) + await page.wait_for_load_state('networkidle') + with open(f'{basic_path}/data/plantes_mix.json', 'r', encoding='utf-8') as file: + replacements = json.load(file) + # 遍历字典,构建替换脚本 + replacement_script = "" + for keyword, replacement in replacements.items(): + escaped_keyword = json.dumps(keyword) + escaped_replacement = json.dumps(replacement) + replacement_script += f""" + document.body.outerHTML = document.body.outerHTML.replace(new RegExp({escaped_keyword}, 'g'), {escaped_replacement}); + """ + + # 在页面上执行替换脚本 + await page.evaluate(replacement_script) + + except Exception as e: + return f"访问网站异常{type(e)}`{e}`" + await asyncio.sleep(1) + logger.info("正在保存图片...") + img_path = os.path.join(save_path, f'{time_present}.png') + await page.screenshot( + path=img_path, + full_page=True + ) + logger.info("正在压缩图片...") + img_convert = Image.open(img_path) + img_convert.save(img_path, quality=80) + logger.info("图片保存成功!") + await browser.close() + return "success" + + +def pic2b64(pic: Image) -> str: + buf = io.BytesIO() + pic.save(buf, format='PNG') + base64_str = base64.b64encode(buf.getvalue()).decode() + return 'base64://' + base64_str diff --git a/hexi/plugins/nonebot_plugin_mc_server_status/__init__.py b/hexi/plugins/nonebot_plugin_mc_server_status/__init__.py new file mode 100644 index 0000000..e4740f0 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_mc_server_status/__init__.py @@ -0,0 +1,225 @@ +import ast +from asyncio import gather +from base64 import b64decode +from io import BytesIO +from re import findall +from typing import Union +import json +from mcstatus import BedrockServer, JavaServer +from nonebot import on_command, on_regex +from nonebot.adapters.onebot.v11 import ( + Bot, + GroupMessageEvent, + Message, + MessageEvent, +) +from nonebot.adapters.onebot.v11 import ( + MessageSegment as MS, +) +from nonebot.log import logger +from nonebot.params import RegexGroup +from nonebot.plugin import PluginMetadata + +from .config import Config, pc, save_file, var + +__plugin_meta__ = PluginMetadata( + name="MC服务器信息查询插件", + description="如名", + type="application", + homepage="https://github.com/nikissXI/nonebot_plugins/tree/main/nonebot_plugin_setu_customization", + supported_adapters={"~onebot.v11"}, + config=Config, + usage=f"""插件命令如下: + 信息 # 字面意思,需要加命令前缀,默认/ + 信息数据 # 查看已启用群以及服务器信息,需要加命令前缀,默认/ + 添加服务器 # 字面意思 + 删除服务器 # 字面意思 + """, +) + + +async def group_check(event: GroupMessageEvent, bot: Bot) -> bool: + return event.group_id in var.group_list and bot == var.handle_bot + + +async def admin_check(event: MessageEvent, bot: Bot) -> bool: + return bot == var.handle_bot and event.user_id in pc.mc_status_admin_qqnum + + +xinxi = on_command("信息", rule=group_check) +list_all = on_command("信息数据", rule=admin_check) +add_server = on_regex( + r"^添加服务器\s*((\d+)\s+(\S+)\s+(\S+)\s+(\S+))?", rule=admin_check +) +del_server = on_regex(r"^删除服务器\s*((\d+)\s+(\S+))?", rule=admin_check) +test_server = on_regex(r"^测试服务器\s*((\S+)\s+(\S+))?", rule=admin_check) + + +@xinxi.handle() +async def _(event: GroupMessageEvent): + group = event.group_id + task_list = [] + for server_name in var.group_list[group]: + server_host = var.group_list[group][server_name][0] + server_type = var.group_list[group][server_name][1] + task_list.append( + check_mc_status( + server_name, + server_host, + server_type, + ) + ) + result = await gather(*task_list) + count = 0 + msg = "" + print(result) + for r in result: + count += 1 + if count > 1: + msg += "\n=== 分割线 ===\n" + msg += r + await xinxi.finish(msg) + + +@add_server.handle() +async def _(mp=RegexGroup()): + if not mp[0]: + await add_server.finish( + f"添加服务器 [群号] [名称] [服务器地址] [类型]\n类型写js或bds,js是Java服务器,bds是基岩服务器\n服务器地址如果知道端口号把端口加上,否则查询速度会慢一点\n添加例子:\nexp1: 添加服务器 114514 哈皮咳嗽 mc.hypixel.net js\nexp2: 添加服务器 114514 某基岩服 mc.bds.net bds\nexp3: 添加服务器 114514 某Java服 mc.java.net:25577 js" + ) + else: + group = int(mp[1]) + new_server_name = mp[2] + server_host = mp[3] + server_type = mp[4].lower() + + if server_type not in ["js", "bds"]: + await add_server.finish("类型请填js或bds") + + if group not in var.group_list: + var.group_list[group] = {new_server_name: [server_host, server_type]} + else: + for server_name in var.group_list[group]: + if new_server_name == server_name: + await add_server.finish("有同名服务器啦!") + var.group_list[group][new_server_name] = [server_host, server_type] + save_file() + await add_server.finish("添加成功") + + +@del_server.handle() +async def _(mp=RegexGroup()): + if not mp[0]: + await del_server.finish(f"删除服务器 [群号] [名称]") + else: + group = int(mp[1]) + name = mp[2] + + if group not in var.group_list: + await del_server.finish("这个群没有添加服务器") + else: + if name in var.group_list[group]: + var.group_list[group].pop(name) + if not var.group_list[group]: + var.group_list.pop(group) + save_file() + await del_server.finish("删除成功") + else: + await del_server.finish("没找到该名称的服务器") + + +@list_all.handle() +async def _(): + msg = "" + for group_id in var.group_list: + msg += f"群{group_id}服务器列表\n" + for server_name in var.group_list[group_id]: + server_host, server_type = var.group_list[group_id][server_name] + msg += f"{server_name} {server_host} {server_type}\n" + msg += "\n" + if not msg: + msg = "无数据" + await list_all.finish(f"mc_status数据\n{msg}") + + +@test_server.handle() +async def _(mp=RegexGroup()): + if not mp[0]: + await test_server.finish( + f"测试服务器 [服务器地址] [类型]\n类型写js或bds,js是Java服务器,bds是基岩服务器" + ) + else: + server_host = mp[1] + server_type = mp[2].lower() + + if server_type not in ["js", "bds"]: + await add_server.finish("类型请填js或bds") + + msg = await check_mc_status("测试", server_host, server_type) + await list_all.finish(msg) + + +async def check_mc_status( + name: str, host: str, server_type: str +) -> Union[str, Message]: + try: + if server_type == "js": + js = await JavaServer.async_lookup(host, timeout=2) + status = js.status() + # if status.description.strip(): + # print(f"des: {status.description}") + version_list = findall(r"\d+\.\d+(?:\.[\dxX]+)?", status.version.name) + if len(version_list) != 1: + version = f"{version_list[0]}-{version_list[-1]}" + else: + version = version_list[0] + + online = f"{status.players.online}/{status.players.max}" + if status.players.online and status.players.sample: + anonymous_player = 0 + _player_list = [] + for p in status.players.sample: + if p.id == "00000000-0000-0000-0000-000000000000": + anonymous_player += 1 + else: + _player_list.append(p.name) + + if anonymous_player: + _player_list.append(f"[{anonymous_player}个匿名玩家]") + + if _player_list: + player_list = "\n".join(_player_list) + + else: + player_list = "没返回玩家列表" + + else: + player_list = "没人在线" + + latency = round(status.latency) + # base64图标 + if status.favicon: + aa, bb = status.favicon.split("base64,") + icon = MS.image(BytesIO(b64decode(bb))) + "\n" + else: + icon = "" + msg = ( + icon + + f"名称:{name}\n版本:{version}\n在线:{online}\n延迟:{latency}ms\n在线列表:\n{player_list}" + ) + + else: + if host.find(":") != -1: + host, port = host.split(":") + else: + host, port = host, 19132 + bds = BedrockServer(host=host, port=int(port)) + status = await bds.async_status() + online = f"{status.players_online}/{status.players_max}" + latency = round(status.latency) + version = status.version.version + msg = f"名称:{name} 【{version}】\n在线:{online} 延迟:{latency}ms" + except Exception as e: + msg = f"名称:{name} 查询失败!\n错误:{repr(e)}" + + return msg diff --git a/hexi/plugins/nonebot_plugin_mc_server_status/config.py b/hexi/plugins/nonebot_plugin_mc_server_status/config.py new file mode 100644 index 0000000..bc4b552 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_mc_server_status/config.py @@ -0,0 +1,112 @@ +from json import dump, load +from os import makedirs, path +from typing import List, Optional + +from nonebot import get_bot, get_bots, get_driver, get_plugin_config +from nonebot.adapters import Bot +from pydantic import BaseModel + + +class Config(BaseModel): + # 管理员的QQ号(别问我为什么) + mc_status_admin_qqnum: List[int] = [] # 必填 + # 机器人的QQ号(如果写了就按优先级响应,否则就第一个连上的响应) ['1234','5678','6666'] + mc_status_bot_qqnum_list: List[str] = [] # 可选 + # 数据文件名 + mc_status_data_filename: str = "mc_status_data.json" + + +class Var: + # 处理消息的bot + handle_bot: Optional[Bot] = None + # {"123456": {"提肛": ["mc.hypixel.net:25565","java"]}} + group_list = {} + + +driver = get_driver() +pc = get_plugin_config(Config) +var = Var() + + +@driver.on_startup +async def on_startup(): + if not path.exists(f"data"): + makedirs(f"data") + + if not path.exists(f"data/{pc.mc_status_data_filename}"): + save_file() + else: + load_file() + + +def load_file(): + with open(f"data/{pc.mc_status_data_filename}", "r", encoding="utf-8") as r: + tmp_data = load(r) + for i in tmp_data: + var.group_list[int(i)] = tmp_data[i] + + +def save_file(): + with open(f"data/{pc.mc_status_data_filename}", "w", encoding="utf-8") as w: + dump(var.group_list, w, indent=4, ensure_ascii=False) + + +# qq机器人连接时执行 +@driver.on_bot_connect +async def on_bot_connect(bot: Bot): + # 是否有写bot qq,如果写了只处理bot qq在列表里的 + if pc.mc_status_bot_qqnum_list and bot.self_id in pc.mc_status_bot_qqnum_list: + # 如果已经有bot连了 + if var.handle_bot: + # 当前bot qq 下标 + handle_bot_id_index = pc.mc_status_bot_qqnum_list.index( + var.handle_bot.self_id + ) + # 新连接的bot qq 下标 + new_bot_id_index = pc.mc_status_bot_qqnum_list.index(bot.self_id) + # 判断优先级,下标越低优先级越高 + if new_bot_id_index < handle_bot_id_index: + var.handle_bot = bot + + # 没bot连就直接给 + else: + var.handle_bot = bot + + # 不写就给第一个连的 + elif not pc.mc_status_bot_qqnum_list and not var.handle_bot: + var.handle_bot = bot + + +# qq机器人断开时执行 +@driver.on_bot_disconnect +async def on_bot_disconnect(bot: Bot): + # 判断掉线的是否为handle bot + if bot == var.handle_bot: + # 如果有写bot qq列表 + if pc.mc_status_bot_qqnum_list: + # 获取当前连着的bot列表(需要bot是在bot qq列表里) + available_bot_id_list = [ + bot_id for bot_id in get_bots() if bot_id in pc.mc_status_bot_qqnum_list + ] + if available_bot_id_list: + # 打擂台排序? + new_bot_index = pc.mc_status_bot_qqnum_list.index( + available_bot_id_list[0] + ) + for bot_id in available_bot_id_list: + now_bot_index = pc.mc_status_bot_qqnum_list.index(bot_id) + if now_bot_index < new_bot_index: + new_bot_index = now_bot_index + # 取下标在qq列表里最小的bot qq为新的handle bot + var.handle_bot = get_bot(pc.mc_status_bot_qqnum_list[new_bot_index]) + + else: + var.handle_bot = None + + # 不写就随便给一个连着的(如果有) + elif var.handle_bot: + try: + new_bot = get_bot() + var.handle_bot = new_bot + except ValueError: + var.handle_bot = None diff --git a/hexi/plugins/nonebot_plugin_ncm_saying/__init__.py b/hexi/plugins/nonebot_plugin_ncm_saying/__init__.py new file mode 100644 index 0000000..ecb4940 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_ncm_saying/__init__.py @@ -0,0 +1,36 @@ +import httpx +from nonebot import logger, on_command +from nonebot.adapters import Message +from nonebot.matcher import Matcher +from nonebot.params import CommandArg +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +import re +import json + +__help_version__ = '0.1.0' +__help_plugin_name__ = "网抑云" +__usage__ = """一开口就老网抑云了 + +指令:/网抑云|网易云热评:随机一条网易云热评""" + +hitokoto_matcher = on_command("网抑云", aliases={"网易云热评", "emo"}) + + +@hitokoto_matcher.handle() +async def hitokoto(event: MessageEvent): + async with httpx.AsyncClient() as client: + response = await client.get("https://v.api.aa1.cn/api/api-wenan-wangyiyunreping/index.php?aa1=text") + if response.is_error: + logger.error("获取网抑云失败") + return + # 提取 JSON 部分 + pattern = r'

(.*?)

' + match = re.search(pattern, response.text) + + if match: + data = match.group(1) + logger.info(f"解析后的数据:{data}") + mes = (MessageSegment.reply(event.message_id), data.split("——")[0]) + await hitokoto_matcher.finish(mes) + else: + logger.warning("未找到 匹配 部分") diff --git a/hexi/plugins/nonebot_plugin_steam_info/__init__.py b/hexi/plugins/nonebot_plugin_steam_info/__init__.py new file mode 100644 index 0000000..f5d6ca4 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/__init__.py @@ -0,0 +1,524 @@ +import time +import httpx +import nonebot +from io import BytesIO +from pathlib import Path +from nonebot.log import logger +from PIL import Image as PILImage +from nonebot.params import Depends +from nonebot.params import CommandArg +from nonebot import on_command, require +from typing import Union, Optional, List, Dict +from nonebot.adapters import Message, Event, Bot +from nonebot.plugin import PluginMetadata, inherit_supported_adapters + +require("nonebot_plugin_alconna") +require("nonebot_plugin_localstore") +require("nonebot_plugin_apscheduler") + +import nonebot_plugin_localstore as store +from nonebot_plugin_apscheduler import scheduler +from nonebot_plugin_alconna import Text, Image, UniMessage, Target, At +from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment +from .config import Config +from .models import ProcessedPlayer +from .data_source import BindData, SteamInfoData, ParentData, DisableParentData +from .steam import ( + get_steam_id, + get_user_data, + STEAM_ID_OFFSET, + get_steam_users_info, +) +from .draw import ( + check_font, + set_font_paths, + draw_start_gaming, + draw_player_status, + draw_friends_status, + vertically_concatenate_images, +) +from .utils import ( + fetch_avatar, + image_to_bytes, + simplize_steam_player_data, + convert_player_name_to_nickname, +) + +__plugin_meta__ = PluginMetadata( + name="Steam Info", + description="播报绑定的 Steam 好友状态", + usage=""" +steamhelp: 查看帮助 +steambind [Steam ID 或 Steam 好友代码]: 绑定 Steam ID +steamunbind: 解绑 Steam ID +steaminfo (可选)[@某人 或 Steam ID 或 Steam好友代码]: 查看 Steam 主页 +steamcheck: 查看 Steam 好友状态 +steamenable: 启用 Steam 播报 +steamdisable: 禁用 Steam 播报 +steamupdate [名称] [图片]: 更新群信息 +steamnickname [昵称]: 设置玩家昵称 +""".strip(), + type="application", + homepage="https://github.com/zhaomaoniu/nonebot-plugin-steam-info", + config=Config, + supported_adapters=inherit_supported_adapters("nonebot_plugin_alconna"), +) + +help = on_command("steamhelp", aliases={"steam帮助"}, priority=10) +bind = on_command("steambind", aliases={"绑定steam"}, priority=10) +unbind = on_command("steamunbind", aliases={"解绑steam"}, priority=10) +info = on_command("steaminfo", aliases={"steam信息"}, priority=10) +check = on_command("steamcheck", aliases={"查看steam", "查steam", "谁在玩游戏"}, priority=10) +enable = on_command("steamenable", aliases={"启用steam"}, priority=10) +disable = on_command("steamdisable", aliases={"禁用steam"}, priority=10) +update_parent_info = on_command("steamupdate", aliases={"更新群信息"}, priority=10) +set_nickname = on_command("steamnickname", aliases={"steam昵称"}, priority=10) +get_play_time = on_command("steamplaytime", aliases={"游玩时长表"}, priority=10) + +if hasattr(nonebot, "get_plugin_config"): + config = nonebot.get_plugin_config(Config) +else: + from nonebot import get_driver + + config = Config.parse_obj(get_driver().config) + +set_font_paths( + config.steam_font_regular_path, + config.steam_font_light_path, + config.steam_font_bold_path, +) + +bind_data_path = store.get_data_file("nonebot_plugin_steam_info", "bind_data.json") +steam_info_data_path = store.get_data_file( + "nonebot_plugin_steam_info", "steam_info.json" +) +parent_data_path = store.get_data_file("nonebot_plugin_steam_info", "parent_data.json") +disable_parent_data_path = store.get_data_file( + "nonebot_plugin_steam_info", "disable_parent_data.json" +) +avatar_path = store.get_cache_dir("nonebot_plugin_steam_info") +cache_path = avatar_path + +bind_data = BindData(bind_data_path) +steam_info_data = SteamInfoData(steam_info_data_path) +parent_data = ParentData(parent_data_path) +disable_parent_data = DisableParentData(disable_parent_data_path) + +try: + check_font() +except FileNotFoundError as e: + logger.error( + f"{e}, nonebot_plugin_steam_info 无法使用,请参照 `https://github.com/zhaomaoniu/nonebot-plugin-steam-info` 配置字体文件" + ) + + +async def get_target(event: Event, bot: Bot) -> Optional[Target]: + target = UniMessage.get_target(event, bot, bot.adapter.get_name()) + + if target.private: + # 不支持私聊消息 + return None + + return target + + +async def to_image_data(image: Image) -> Union[BytesIO, bytes]: + if image.raw is not None: + return image.raw + + if image.path is not None: + return Path(image.path).read_bytes() + + if image.url is not None: + async with httpx.AsyncClient() as client: + response = await client.get(image.url) + if response.status_code != 200: + raise ValueError(f"无法获取图片数据: {response.status_code}") + return response.content + + raise ValueError("无法获取图片数据") + + +async def broadcast_steam_info( + parent_id: str, + old_players: List[ProcessedPlayer], + new_players: List[ProcessedPlayer], +): + if disable_parent_data.is_disabled(parent_id): + return None + + bot = nonebot.get_bot() + + play_data = steam_info_data.compare(old_players, new_players) + + msg = [] + for entry in play_data: + player: ProcessedPlayer = entry["player"] + old_player: ProcessedPlayer = entry.get("old_player") + + if entry["type"] == "start": + msg.append(f"{player['personaname']} 开始玩 {player['gameextrainfo']} 了") + elif entry["type"] == "stop": + time_start = old_player["game_start_time"] + time_stop = time.time() + hours = int((time_stop - time_start) / 3600) + minutes = int((time_stop - time_start) % 3600 / 60) + time_str = ( + f"{hours} 小时 {minutes} 分钟" if hours > 0 else f"{minutes} 分钟" + ) + msg.append( + f"{player['personaname']} 玩了 {time_str} {old_player['gameextrainfo']} 后不玩了" + ) + elif entry["type"] == "change": + msg.append( + f"{player['personaname']} 停止玩 {old_player['gameextrainfo']},开始玩 {player['gameextrainfo']} 了" + ) + elif entry["type"] == "error": + f"出现错误!{player['personaname']}\nNew: {player.get('gameextrainfo')}\nOld: {old_player.get('gameextrainfo')}" + else: + logger.error(f"未知的播报类型: {entry['type']}") + + if msg == []: + return None + + if config.steam_broadcast_type == "all": + steam_status_data = [ + convert_player_name_to_nickname( + (await simplize_steam_player_data(player, config.proxy, avatar_path)), + parent_id, + bind_data, + ) + for player in new_players + ] + + parent_avatar, parent_name = parent_data.get(parent_id) + image = draw_friends_status(parent_avatar, parent_name, steam_status_data) + uni_msg = UniMessage([Text("\n".join(msg)), Image(raw=image_to_bytes(image))]) + elif config.steam_broadcast_type == "part": + images = [ + draw_start_gaming( + (await fetch_avatar(entry["player"], avatar_path, config.proxy)), + entry["player"]["personaname"], + entry["player"]["gameextrainfo"], + bind_data.get_by_steam_id(parent_id, entry["player"]["steamid"])[ + "nickname" + ], + ) + for entry in play_data + if entry["type"] == "start" + ] + if images == []: + uni_msg = UniMessage([Text("\n".join(msg))]) + else: + image = ( + vertically_concatenate_images(images) if len(images) > 1 else images[0] + ) + uni_msg = UniMessage( + [Text("\n".join(msg)), Image(raw=image_to_bytes(image))] + ) + elif config.steam_broadcast_type == "none": + uni_msg = UniMessage([Text("\n".join(msg))]) + else: + logger.error(f"未知的播报类型: {config.steam_broadcast_type}") + return None + try: + logger.info(f"主动消息触发:{uni_msg}") + await uni_msg.send( + Target(parent_id, parent_id, True, False, "", bot.adapter.get_name()), bot + ) + except Exception as e: + logger.error(f"UniMessage异常:{e}") + + +# 初始化计数器 +current_key_index = 0 + + +def key_select_2(): + global current_key_index + # 获取key列表 + keys = config.steam_api_key + # 获取当前使用的 key + current_key: str = keys[current_key_index] + # 更新计数器 + current_key_index = (current_key_index + 1) % len(keys) + + return [current_key] + + +async def update_steam_info(): + steam_ids = bind_data.get_all_steam_id() + api_keys = config.steam_api_key + if not api_keys: + logger.warning("没有可用的 key。") + current_key = key_select_2() + logger.info(f"当前调用的key:{current_key}") + steam_info = await get_steam_users_info( + steam_ids, current_key, config.proxy + ) + + old_players_dict: Dict[str, List[ProcessedPlayer]] = {} + + for parent_id in bind_data.content.keys(): + steam_ids = bind_data.get_all(parent_id) + old_players_dict[parent_id] = steam_info_data.get_players(steam_ids) + + steam_info_data.update_by_players(steam_info["response"]["players"]) + steam_info_data.save() + + return bind_data, old_players_dict + + +@scheduler.scheduled_job( + "interval", minutes=config.steam_request_interval / 60, id="update_steam_info" +) +async def fetch_and_broadcast_steam_info(): + bind_data, old_players_dict = await update_steam_info() + + for parent_id in bind_data.content.keys(): + old_players = old_players_dict[parent_id] + new_players = steam_info_data.get_players(bind_data.get_all(parent_id)) + + await broadcast_steam_info(parent_id, old_players, new_players) + + +if not config.steam_disable_broadcast_on_startup: + nonebot.get_driver().on_bot_connect(update_steam_info) +else: + logger.info("已禁用启动时的 Steam 播报") + + +@help.handle() +async def help_handle(): + await help.finish(__plugin_meta__.usage) + + +@bind.handle() +async def bind_handle( + event: Event, target: Target = Depends(get_target), cmd_arg: Message = CommandArg() +): + parent_id = target.parent_id or target.id + + arg = cmd_arg.extract_plain_text() + + if not arg.isdigit(): + await bind.finish( + "请输入正确的 Steam ID 或 Steam好友代码,格式: steambind [Steam ID 或 Steam好友代码]" + ) + + steam_id = get_steam_id(arg) + + if user_data := bind_data.get(parent_id, event.get_user_id()): + user_data["steam_id"] = steam_id + bind_data.save() + + await bind.finish(f"已更新你的 Steam ID 为 {steam_id}") + else: + bind_data.add( + parent_id, + {"user_id": event.get_user_id(), "steam_id": steam_id, "nickname": None}, + ) + bind_data.save() + + await bind.finish(f"已绑定你的 Steam ID 为 {steam_id}") + + +@unbind.handle() +async def unbind_handle(event: Event, target: Target = Depends(get_target)): + parent_id = target.parent_id or target.id + user_id = event.get_user_id() + + if bind_data.get(parent_id, user_id) is not None: + bind_data.remove(parent_id, user_id) + bind_data.save() + + await unbind.finish("已解绑 Steam ID") + else: + await unbind.finish("未绑定 Steam ID") + + +@info.handle() +async def info_handle( + bot: Bot, + event: Event, + target: Target = Depends(get_target), + arg: Message = CommandArg(), +): + parent_id = target.parent_id or target.id + + uni_arg = await UniMessage.generate(message=arg, event=event, bot=bot) + at = uni_arg[At] + + if len(at) != 0: + user_id: str = at[0].target + user_data = bind_data.get(parent_id, user_id) + if user_data is None: + await info.finish("该用户未绑定 Steam ID") + steam_id = user_data["steam_id"] + steam_friend_code = str(int(steam_id) - STEAM_ID_OFFSET) + elif arg.extract_plain_text().strip() != "": + steam_id = int(arg.extract_plain_text().strip()) + if steam_id < STEAM_ID_OFFSET: + steam_friend_code = steam_id + steam_id += STEAM_ID_OFFSET + else: + steam_friend_code = steam_id - STEAM_ID_OFFSET + else: + user_data = bind_data.get(parent_id, event.get_user_id()) + + if user_data is None: + await info.finish( + "未绑定 Steam ID, 请使用 “steambind [Steam ID 或 Steam好友代码]” 绑定 Steam ID" + ) + + steam_id = user_data["steam_id"] + steam_friend_code = str(int(steam_id) - STEAM_ID_OFFSET) + + player_data = await get_user_data(steam_id, cache_path, config.proxy) + + draw_data = [ + { + "game_header": game["game_image"], + "game_name": game["game_name"], + "game_time": f"{game['play_time']} 小时", + "last_play_time": game["last_played"], + "achievements": game["achievements"], + "completed_achievement_number": game.get("completed_achievement_number"), + "total_achievement_number": game.get("total_achievement_number"), + } + for game in player_data["game_data"] + ] + + image = draw_player_status( + player_data["background"], + player_data["avatar"], + player_data["player_name"], + str(steam_friend_code), + player_data["description"], + player_data["recent_2_week_play_time"], + draw_data, + ) + + await info.finish( + await UniMessage( + Image(raw=image_to_bytes(image)), + ).export(bot) + ) + + +# 初始化计数器 +current_key_index_1 = 0 + + +def key_select(): + global current_key_index_1 + # 获取key列表 + keys = config.steam_api_key + # 获取当前使用的 key + current_key: str = keys[current_key_index_1] + # 更新计数器 + current_key_index_1 = (current_key_index_1 + 1) % len(keys) + + return [current_key] + + +@check.handle() +async def check_handle( + target: Target = Depends(get_target), arg: Message = CommandArg() +): + if arg.extract_plain_text().strip() != "": + return None + + parent_id = target.parent_id or target.id + + steam_ids = bind_data.get_all(parent_id) + + current_key = key_select() + + logger.info(f"当前调用的key:{current_key}") + steam_info = await get_steam_users_info( + steam_ids, current_key, config.proxy + ) + + logger.debug(f"{parent_id} Players info: {steam_info}") + + parent_avatar, parent_name = parent_data.get(parent_id) + + steam_status_data = [ + convert_player_name_to_nickname( + (await simplize_steam_player_data(player, config.proxy, avatar_path)), + parent_id, + bind_data, + ) + for player in steam_info["response"]["players"] + ] + + image = draw_friends_status(parent_avatar, parent_name, steam_status_data) + + await target.send(UniMessage(Image(raw=image_to_bytes(image)))) + + +@update_parent_info.handle() +async def update_parent_info_handle( + bot: Bot, + event: Event, + target: Target = Depends(get_target), + arg: Message = CommandArg(), +): + msg = await UniMessage.generate(message=arg, event=event, bot=bot) + info = {} + for seg in msg: + if isinstance(seg, Image): + info["avatar"] = PILImage.open(BytesIO(await to_image_data(seg))) + elif isinstance(seg, Text) and seg.text != "": + info["name"] = seg.text + + if "avatar" not in info or "name" not in info: + await update_parent_info.finish("文本中应包含图片和文字") + + parent_data.update(target.parent_id or target.id, info["avatar"], info["name"]) + await update_parent_info.finish("更新成功") + + +@enable.handle() +async def enable_handle(target: Target = Depends(get_target)): + parent_id = target.parent_id or target.id + + disable_parent_data.remove(parent_id) + disable_parent_data.save() + + await enable.finish("已启用 Steam 播报") + + +@disable.handle() +async def disable_handle(target: Target = Depends(get_target)): + parent_id = target.parent_id or target.id + + disable_parent_data.add(parent_id) + disable_parent_data.save() + + await disable.finish("已禁用 Steam 播报") + + +@set_nickname.handle() +async def set_nickname_handle( + event: Event, target: Target = Depends(get_target), cmd_arg: Message = CommandArg() +): + parent_id = target.parent_id or target.id + + nickname = cmd_arg.extract_plain_text().strip() + + if nickname == "": + await set_nickname.finish("请输入昵称,格式: steamnickname [昵称]") + + user_data = bind_data.get(parent_id, event.get_user_id()) + + if user_data is None: + await set_nickname.finish( + "未绑定 Steam ID,请先使用 steambind 绑定 Steam ID 后再设置昵称" + ) + + user_data["nickname"] = nickname + bind_data.save() + + await set_nickname.finish(f"已设置你的昵称为 {nickname},将在 Steam 播报中显示") diff --git a/hexi/plugins/nonebot_plugin_steam_info/config.py b/hexi/plugins/nonebot_plugin_steam_info/config.py new file mode 100644 index 0000000..5728341 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/config.py @@ -0,0 +1,19 @@ +from typing import Optional, Union, List +from pydantic import BaseModel, validator + + +class Config(BaseModel): + steam_api_key: Union[str, List[str]] + proxy: Optional[str] = None + steam_request_interval: int = 300 # seconds + steam_broadcast_type: str = "part" # all, part, none + steam_disable_broadcast_on_startup: bool = False + steam_font_regular_path: Optional[str] = "fonts/MiSans-Regular.ttf" + steam_font_light_path: Optional[str] = "fonts/MiSans-Light.ttf" + steam_font_bold_path: Optional[str] = "fonts/MiSans-Bold.ttf" + + @validator("steam_api_key", pre=True) + def ensure_list(cls, v): + if isinstance(v, str): + return [v] + return v diff --git a/hexi/plugins/nonebot_plugin_steam_info/data_source.py b/hexi/plugins/nonebot_plugin_steam_info/data_source.py new file mode 100644 index 0000000..f1b07e9 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/data_source.py @@ -0,0 +1,260 @@ +import json +import time +from PIL import Image +from pathlib import Path +from typing import Any, List, Dict, Optional, Tuple + +from .models import Player, ProcessedPlayer + + +class BindData: + def __init__(self, save_path: Path) -> None: + self.content: Dict[str, List[Dict[str, str]]] = {} + self._save_path = save_path + + if save_path.exists(): + self.content = json.loads(Path(save_path).read_text("utf-8")) + else: + self.save() + + def save(self) -> None: + with open(self._save_path, "w", encoding="utf-8") as f: + json.dump(self.content, f, indent=4) + + def add(self, parent_id: str, content: Dict[str, str]) -> None: + if parent_id not in self.content: + self.content[parent_id] = [content] + else: + self.content[parent_id].append(content) + + def remove(self, parent_id: str, user_id: str) -> None: + if parent_id not in self.content: + return + for data in self.content[parent_id]: + if data["user_id"] == user_id: + self.content[parent_id].remove(data) + break + + def update(self, parent_id: str, content: Dict[str, str]) -> None: + self.content[parent_id] = content + + def get(self, parent_id: str, user_id: str) -> Optional[Dict[str, str]]: + if parent_id not in self.content: + return None + for data in self.content[parent_id]: + if data["user_id"] == user_id: + if not data.get("nickname"): + data["nickname"] = None + return data + return None + + def get_by_steam_id( + self, parent_id: str, steam_id: str + ) -> Optional[Dict[str, str]]: + if parent_id not in self.content: + return None + for data in self.content[parent_id]: + if data["steam_id"] == steam_id: + if not data.get("nickname"): + data["nickname"] = None + return data + return None + + def get_all(self, parent_id: str) -> List[str]: + if parent_id not in self.content: + return [] + + result = [] + + for data in self.content[parent_id]: + if not data["steam_id"] in result: + result.append(data["steam_id"]) + + return result + + def get_all_steam_id(self) -> List[str]: + result = [] + for parent_id in self.content: + for data in self.content[parent_id]: + if not data["steam_id"] in result: + result.append(data["steam_id"]) + return result + + +class SteamInfoData: + def __init__(self, save_path: Path) -> None: + self.content: List[ProcessedPlayer] = [] + self._save_path = save_path + + if save_path.exists(): + self.content = json.loads(save_path.read_text("utf-8")) + if isinstance(self.content, dict): + self.content = [] + self.save() + else: + self.save() + + def save(self) -> None: + with open(self._save_path, "w", encoding="utf-8") as f: + json.dump(self.content, f, indent=4) + + def update(self, player: ProcessedPlayer) -> None: + self.content.append(player) + + def update_by_players(self, players: List[Player]): + # 将 Player 转换为 ProcessedPlayer + processed_players = [] + for player in players: + old_player = self.get_player(player["steamid"]) + + if old_player is None: + if player.get("gameextrainfo") is not None: + player["game_start_time"] = int(time.time()) + else: + player["game_start_time"] = None + processed_players.append(player) + else: + if ( + player.get("gameextrainfo") is not None + and old_player.get("gameextrainfo") is None + ): + # 开始游戏 + player["game_start_time"] = int(time.time()) + elif ( + player.get("gameextrainfo") is None + and old_player.get("gameextrainfo") is not None + ): + # 结束游戏 + player["game_start_time"] = None + elif ( + player.get("gameextrainfo") is not None + and old_player.get("gameextrainfo") is not None + ): + # 继续游戏 + player["game_start_time"] = old_player["game_start_time"] + else: + player["game_start_time"] = None + processed_players.append(player) + + self.content = processed_players + + def get_player(self, steam_id: str) -> Optional[Player]: + for player in self.content: + if player["steamid"] == steam_id: + return player + return None + + def get_players(self, steam_ids: List[str]) -> List[Player]: + result = [] + for player in self.content: + if player["steamid"] in steam_ids: + result.append(player) + return result + + def compare( + self, old_players: List[Player], new_players: List[Player] + ) -> List[Dict[str, Any]]: + result = [] + + for player in new_players: + for old_player in old_players: + if player["steamid"] == old_player["steamid"]: + if player.get("gameextrainfo") != old_player.get("gameextrainfo"): + if player.get("gameextrainfo") is not None: + result.append( + { + "type": "start", + "player": player, + "old_player": old_player, + } + ) + elif old_player.get("gameextrainfo") is not None: + result.append( + { + "type": "stop", + "player": player, + "old_player": old_player, + } + ) + elif ( + player.get("gameextrainfo") is not None + and old_player.get("gameextrainfo") is not None + ): + result.append( + { + "type": "change", + "player": player, + "old_player": old_player, + } + ) + else: + result.append( + { + "type": "error", + "player": player, + "old_player": old_player, + } + ) + return result + + +class ParentData: + def __init__(self, save_path: Path) -> None: + self.content: Dict[str, str] = {} # parent_id: name + self._save_path = save_path + + if not save_path.exists(): + save_path.parent.mkdir(parents=True, exist_ok=True) + self.save() + else: + self.content = json.loads(save_path.read_text("utf-8")) + + def save(self) -> None: + with open(self._save_path, "w", encoding="utf-8") as f: + json.dump(self.content, f, indent=4) + + def update(self, parent_id: str, avatar: Image.Image, name: str) -> None: + self.content[parent_id] = name + self.save() + # 保存图片 + avatar_path = self._save_path.parent / f"{parent_id}.png" + avatar.save(avatar_path) + + def get(self, parent_id: str) -> Tuple[Image.Image, str]: + if parent_id not in self.content: + return ( + Image.open(Path(__file__).parent / "res/unknown_avatar.jpg"), + parent_id, + ) + avatar_path = self._save_path.parent / f"{parent_id}.png" + return Image.open(avatar_path), self.content[parent_id] + + +class DisableParentData: + """储存禁用 Steam 通知的 parent""" + + def __init__(self, save_path: Path) -> None: + self.content: List[str] = [] + self._save_path = save_path + + if save_path.exists(): + self.content = json.loads(save_path.read_text("utf-8")) + else: + self.save() + + def save(self) -> None: + with open(self._save_path, "w", encoding="utf-8") as f: + json.dump(self.content, f, indent=4) + + def add(self, parent_id: str) -> None: + if parent_id not in self.content: + self.content.append(parent_id) + self.save() + + def remove(self, parent_id: str) -> None: + if parent_id in self.content: + self.content.remove(parent_id) + self.save() + + def is_disabled(self, parent_id: str) -> bool: + return parent_id in self.content diff --git a/hexi/plugins/nonebot_plugin_steam_info/draw.py b/hexi/plugins/nonebot_plugin_steam_info/draw.py new file mode 100644 index 0000000..1158664 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/draw.py @@ -0,0 +1,921 @@ +import numpy as np +from io import BytesIO +from pathlib import Path +from typing import List, Dict, Tuple +from colorsys import rgb_to_hsv, hsv_to_rgb +from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance + +from .utils import hex_to_rgb +from .models import DrawPlayerStatusData, Achievements + + +WIDTH = 400 +PARENT_AVATAR_SIZE = 72 +MEMBER_AVATAR_SIZE = 50 + +unknown_avatar_path = Path(__file__).parent / "res/unknown_avatar.jpg" +parent_status_path = Path(__file__).parent / "res/parent_status.png" +friends_search_path = Path(__file__).parent / "res/friends_search.png" +busy_path = Path(__file__).parent / "res/busy.png" +zzz_online_path = Path(__file__).parent / "res/zzz_online.png" +zzz_gaming_path = Path(__file__).parent / "res/zzz_gaming.png" +gaming_path = Path(__file__).parent / "res/gaming.png" + +font_regular_path = None +font_light_path = None +font_bold_path = None + + +def set_font_paths(regular_path, light_path, bold_path): + global font_regular_path, font_light_path, font_bold_path + base_dir = Path().cwd() + font_regular_path = str((base_dir / regular_path).resolve()) + font_light_path = str((base_dir / light_path).resolve()) + font_bold_path = str((base_dir / bold_path).resolve()) + + +def check_font(): + if not Path(font_regular_path).exists(): + raise FileNotFoundError(f"Font file {font_regular_path} not found.") + if not Path(font_light_path).exists(): + raise FileNotFoundError(f"Font file {font_light_path} not found.") + if not Path(font_bold_path).exists(): + raise FileNotFoundError(f"Font file {font_bold_path} not found.") + + +personastate_colors = { + 0: (hex_to_rgb("969697"), hex_to_rgb("656565")), + 1: (hex_to_rgb("6dcef5"), hex_to_rgb("4c91ac")), + 2: (hex_to_rgb("6dcef5"), hex_to_rgb("4c91ac")), + 3: (hex_to_rgb("45778e"), hex_to_rgb("365969")), + 4: (hex_to_rgb("6dcef5"), hex_to_rgb("4c91ac")), + 5: (hex_to_rgb("6dcef5"), hex_to_rgb("4c91ac")), + 6: (hex_to_rgb("6dcef5"), hex_to_rgb("4c91ac")), +} + + +def vertically_concatenate_images(images: List[Image.Image]) -> Image.Image: + widths, heights = zip(*(i.size for i in images)) + total_width = max(widths) + total_height = sum(heights) + + new_image = Image.new("RGB", (total_width, total_height)) + + y_offset = 0 + for image in images: + new_image.paste(image, (0, y_offset)) + y_offset += image.size[1] + + return new_image + + +def draw_start_gaming( + avatar: Image.Image, friend_name: str, game_name: str, nickname: str = None +): + canvas = Image.open(gaming_path) + canvas.paste(avatar.resize((66, 66), Image.BICUBIC), (15, 20)) + + # 绘制名称 + draw = ImageDraw.Draw(canvas) + draw.text( + (104, 14), + f"{friend_name} ({nickname})" if nickname is not None else friend_name, + font=ImageFont.truetype(font_regular_path, 19), + fill=hex_to_rgb("e3ffc2"), + ) + + # 绘制"正在玩" + draw.text( + (103, 42), + "正在玩", + font=ImageFont.truetype(font_regular_path, 17), + fill=hex_to_rgb("969696"), + ) + + # 绘制游戏名称 + draw.text( + (104, 66), + game_name, + font=ImageFont.truetype(font_bold_path, 14), + fill=hex_to_rgb("91c257"), + ) + + return canvas + + +def draw_parent_status(parent_avatar: Image.Image, parent_name: str) -> Image.Image: + parent_avatar = parent_avatar.resize( + (PARENT_AVATAR_SIZE, PARENT_AVATAR_SIZE), Image.BICUBIC + ) + + canvas = Image.open(parent_status_path).resize((WIDTH, 120), Image.BICUBIC) + + draw = ImageDraw.Draw(canvas) + + # 在左下角 (16, 16) 处绘制头像 + avatar_height = 120 - 16 - PARENT_AVATAR_SIZE + canvas.paste(parent_avatar, (16, avatar_height)) + + # 绘制名称 + draw.text( + (16 + PARENT_AVATAR_SIZE + 16, avatar_height + 12), + parent_name, + font=ImageFont.truetype(font_bold_path, 20), + fill=hex_to_rgb("6dcff6"), + ) + + # 绘制状态 + draw.text( + (16 + PARENT_AVATAR_SIZE + 16, avatar_height + 20 + 16), + "在线", + font=ImageFont.truetype(font_light_path, 18), + fill=hex_to_rgb("4c91ac"), + ) + + return canvas + + +def draw_friends_search() -> Image.Image: + canvas = Image.new("RGB", (WIDTH, 50), hex_to_rgb("434953")) + + friends_search = Image.open(friends_search_path) + + canvas.paste(friends_search, (WIDTH - friends_search.width, 0)) + + draw = ImageDraw.Draw(canvas) + + draw.text( + (24, 10), + "好友", + hex_to_rgb("b7ccd5"), + font=ImageFont.truetype(font_regular_path, 20), + ) + + return canvas + + +def draw_friend_status( + friend_avatar: Image.Image, + friend_name: str, + status: str, + personastate: int, + nickname: str = None, +) -> Image.Image: + friend_avatar = friend_avatar.resize( + (MEMBER_AVATAR_SIZE, MEMBER_AVATAR_SIZE), Image.BICUBIC + ) + + canvas = Image.new("RGB", (WIDTH, 64), hex_to_rgb("1e2024")) + + draw = ImageDraw.Draw(canvas) + + display_name = ( + f"{friend_name} ({nickname})" if nickname is not None else friend_name + ) + + if personastate == 2: + # 忙碌 加上一个忙碌图标 + canvas = draw_friend_status(friend_avatar, friend_name, status, 1, nickname) + draw = ImageDraw.Draw(canvas) + + busy = Image.open(busy_path) + + name_width = int( + draw.textlength(display_name, font=ImageFont.truetype(font_bold_path, 20)) + ) + + canvas.paste(busy, (22 + MEMBER_AVATAR_SIZE + 16 + name_width + 4, 18)) + + return canvas + + if personastate == 4: + # 打盹 加上一个 ZZZ + canvas = draw_friend_status(friend_avatar, friend_name, status, 1, nickname) + draw = ImageDraw.Draw(canvas) + + zzz = Image.open(zzz_online_path if status == "在线" else zzz_gaming_path) + + name_width = int( + draw.textlength(display_name, font=ImageFont.truetype(font_bold_path, 20)) + ) + + canvas.paste(zzz, (22 + MEMBER_AVATAR_SIZE + 16 + name_width + 8, 18)) + + return canvas + + # 绘制头像 + canvas.paste(friend_avatar, (22, 8)) + + if status != "在线" and personastate == 1: + fill = (hex_to_rgb("e3ffc2"), hex_to_rgb("8ebe56")) + elif status != "离开" and personastate == 3: + fill = (hex_to_rgb("e3ffc2"), hex_to_rgb("8ebe56")) + else: + fill = personastate_colors[personastate] + + # 绘制名称 + draw.text( + (22 + MEMBER_AVATAR_SIZE + 18, 12), + display_name, + font=ImageFont.truetype(font_bold_path, 20), + fill=fill[0], + ) + + # 绘制状态 + draw.text( + (22 + MEMBER_AVATAR_SIZE + 16, 36), + status, + font=ImageFont.truetype(font_regular_path, 18), + fill=fill[1], + ) + + return canvas + + +def draw_gaming_friends_status(data: List[Dict[str, str]]) -> Image.Image: + # 排序数据,按照游戏名称字母表顺序排序 + data.sort(key=lambda x: x["status"]) + + canvas = Image.new( + "RGB", + (WIDTH, 64 + (MEMBER_AVATAR_SIZE + 16) * len(data) + 16), + hex_to_rgb("1e2024"), + ) + + draw = ImageDraw.Draw(canvas) + + # 绘制标题 + draw.text( + (22, 22), + "游戏中", + hex_to_rgb("c5d6d4"), + font=ImageFont.truetype(font_regular_path, 22), + ) + + # 绘制好友头像和名称 + friends_status_list = [ + draw_friend_status( + d["avatar"], d["name"], d["status"], d["personastate"], d["nickname"] + ) + for d in data + ] + + # 拼接好友头像和名称 + for i, friend_status in enumerate(friends_status_list): + canvas.paste(friend_status, (0, 64 + (MEMBER_AVATAR_SIZE + 16) * i)) + + return canvas + + +def draw_online_friends_status(data: List[Dict[str, str]]) -> Image.Image: + canvas = Image.new( + "RGB", + (WIDTH, 64 + (MEMBER_AVATAR_SIZE + 16) * len(data) + 16), + hex_to_rgb("1e2024"), + ) + + draw = ImageDraw.Draw(canvas) + + # 绘制标题 + draw.text( + (22, 22), + "在线好友", + hex_to_rgb("c5d6d4"), + font=ImageFont.truetype(font_regular_path, 22), + ) + + # 绘制在线人数 + draw.text( + (115, 25), + f"({len(data)})", + hex_to_rgb("67665c"), + font=ImageFont.truetype(font_regular_path, 18), + ) + + # 绘制好友头像和名称 + friends_status_list = [ + draw_friend_status( + d["avatar"], d["name"], d["status"], d["personastate"], d["nickname"] + ) + for d in data + ] + + # 拼接好友头像和名称 + for i, friend_status in enumerate(friends_status_list): + canvas.paste(friend_status, (0, 64 + (MEMBER_AVATAR_SIZE + 16) * i)) + + return canvas + + +def draw_offline_friends_status(data: List[Dict[str, str]]) -> Image.Image: + canvas = Image.new( + "RGB", + (WIDTH, 64 + (MEMBER_AVATAR_SIZE + 16) * len(data) + 16), + hex_to_rgb("1e2024"), + ) + + draw = ImageDraw.Draw(canvas) + + # 绘制标题 + draw.text( + (22, 22), + "离线", + hex_to_rgb("c5d6d4"), + font=ImageFont.truetype(font_regular_path, 22), + ) + + # 绘制离线人数 + draw.text( + (72, 25), + f"({len(data)})", + hex_to_rgb("67665c"), + font=ImageFont.truetype(font_regular_path, 18), + ) + + # 绘制好友头像和名称 + friends_status_list = [ + draw_friend_status( + d["avatar"], d["name"], d["status"], d["personastate"], d["nickname"] + ) + for d in data + ] + + # 拼接好友头像和名称 + for i, friend_status in enumerate(friends_status_list): + canvas.paste(friend_status, (0, 64 + (MEMBER_AVATAR_SIZE + 16) * i)) + + return canvas + + +def draw_friends_status( + parent_avatar: Image.Image, parent_name: str, data: List[Dict[str, str]] +): + data.sort(key=lambda x: x["personastate"]) + + parent_status = draw_parent_status(parent_avatar, parent_name) + friends_search = draw_friends_search() + + status_images: List[Image.Image] = [] + height = parent_status.height + friends_search.height + + gaming_data = [ + d + for d in data + if (d["personastate"] == 1 and d["status"] != "在线") + or (d["personastate"] == 3 and d["status"] != "离开") + or (d["personastate"] == 4 and d["status"] != "在线") + ] + + if gaming_data: + status_images.append(draw_gaming_friends_status(gaming_data)) + height += status_images[-1].height + + online_data = [ + d + for d in data + if (d["personastate"] == 1 and d["status"] == "在线") + or (d["personastate"] == 3 and d["status"] == "离开") + or (d["personastate"] == 4 and d["status"] == "在线") + or (d["personastate"] in [2, 5, 6]) + ] + # 按 1, 2, 4, 5, 6, 3 的顺序排序 + online_data.sort(key=lambda x: (7 if x["personastate"] == 3 else x["personastate"])) + + if online_data: + status_images.append(draw_online_friends_status(online_data)) + height += status_images[-1].height + + offline_data = [d for d in data if d["personastate"] == 0] + if offline_data: + status_images.append(draw_offline_friends_status(offline_data)) + height += status_images[-1].height + + # 拼合图片 + canvas = Image.new("RGB", (WIDTH, height), hex_to_rgb("1e2024")) + draw = ImageDraw.Draw(canvas) + + canvas.paste(parent_status, (0, 0)) + canvas.paste(friends_search, (0, parent_status.height)) + + y = parent_status.height + friends_search.height + + for i, status_image in enumerate(status_images): + canvas.paste(status_image, (0, y)) + y += status_image.height + + # 绘制分割线 + if i != len(status_images) - 1: + draw.rectangle([0, y - 1, WIDTH, y], fill=hex_to_rgb("333439")) + + return canvas + + +def get_average_color(image: Image.Image) -> tuple[int, int, int]: + """获取图片的平均颜色""" + image_np = np.array(image) + average_color = image_np.mean(axis=(0, 1)).astype(int) + return tuple(average_color) + + +def split_image( + image: Image.Image, rows: int, cols: int +) -> tuple[list[Image.Image], int, int]: + """将图片分割为rows * cols份""" + width, height = image.size + piece_width = width // cols + piece_height = height // rows + pieces = [] + + for r in range(rows): + for c in range(cols): + box = ( + c * piece_width, + r * piece_height, + (c + 1) * piece_width, + (r + 1) * piece_height, + ) + piece = image.crop(box) + pieces.append(piece) + + return pieces, piece_width, piece_height + + +def recolor_image(image: Image.Image, rows: int, cols: int) -> Image.Image: + """分片图片,提取平均颜色后拼接""" + total_average_color = get_average_color(image) # 获取整体平均颜色 + pieces, piece_width, piece_height = split_image(image, rows, cols) + + diameter = min(pieces[0].size) # 以最小边为直径 + radius = diameter // 2 + new_image = Image.new("RGB", image.size, total_average_color) + + for i, piece in enumerate(pieces): + average_color = get_average_color(piece) # 获取每片的平均颜色 + + # 计算放置的位置 + row, col = divmod(i, cols) + x = col * piece_width + piece_width // 2 + y = row * piece_height + piece_height // 2 + + # 画圆 + circle = Image.new("RGBA", (piece_width, piece_height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, piece_width, piece_height), fill=average_color) + + # 将圆形图片粘贴到新图片上 + new_image.paste(circle, (x - radius, y - radius), circle) + + new_image = new_image.filter(ImageFilter.SMOOTH) + new_image = new_image.filter(ImageFilter.GaussianBlur(50)) + + return new_image + + +def create_gradient_image( + size: Tuple[int, int], color1: Tuple[int, int, int], color2: Tuple[int, int, int] +) -> Image.Image: + """创建渐变图片""" + # 确保颜色值在 0-255 范围内 + color1 = tuple(max(0, min(255, c)) for c in color1) + color2 = tuple(max(0, min(255, c)) for c in color2) + # 创建一个渐变的线性空间 + gradient_array = np.linspace(color1, color2, size[0]) + + # 将渐变数组的形状调整为 (height, width, 3) + gradient_image = np.tile(gradient_array, (size[1], 1, 1)).astype(np.uint8) + + return Image.fromarray(gradient_image, "RGBA") + + +def create_vertical_gradient_rect(width, height, start_color, end_color): + """ + 创建一个在竖直方向上渐变的矩形图像. + + Args: + width (int): 矩形的宽度 (以像素为单位). + height (int): 矩形的高度 (以像素为单位). + start_color (tuple): 起始颜色,格式为 (R, G, B),每个值范围为 0-255. + end_color (tuple): 结束颜色,格式为 (R, G, B),每个值范围为 0-255. + + Returns: + Image: PIL Image 对象,表示生成的渐变矩形. + """ + if width <= 0 or height <= 0: + return Image.new("RGBA", (1, 1), (0, 0, 0, 0)) + # 确保颜色不超过 0-255 的范围 + start_color = tuple(max(0, min(255, c)) for c in start_color) + end_color = tuple(max(0, min(255, c)) for c in end_color) + + # 使用 NumPy 创建一个线性渐变数组 + gradient_array = np.linspace(start_color, end_color, num=height, dtype=np.uint8) + gradient_array = np.tile(gradient_array[:, np.newaxis, :], (1, width, 1)) + + # 使用 Pillow 创建图像并填充颜色 + image = Image.fromarray(gradient_array) + return image + + +def random_color_offset( + color: Tuple[int, int, int], offset: int +) -> Tuple[int, int, int]: + return tuple( + min(255, max(0, c + np.random.randint(-offset, offset + 1))) for c in color + ) + + +def get_brightest_and_darkest_color( + image: Image.Image, + saturation_threshold: int = 100, + hue_difference_threshold: int = 30, +) -> Tuple[Tuple[int, int, int], Tuple[int, int, int]]: + """获取图片最亮和最暗的颜色""" + # 将RGB图像转换为HSV + img_hsv = np.array(image.convert("HSV")) + + # 设定一个阈值来定义“鲜艳的颜色”,例如饱和度大于150 + vivid_mask = img_hsv[..., 1] > saturation_threshold + + # 获取饱和度较高(鲜艳)的像素索引 + vivid_pixels = img_hsv[vivid_mask] + + if len(vivid_pixels) < 10: + return get_brightest_and_darkest_color(image, saturation_threshold - 10) + + # 在鲜艳的像素中,根据亮度(V通道)找到最亮和最暗的颜色 + brightest_pixel = vivid_pixels[np.argmax(vivid_pixels[..., 2])] + darkest_pixel = vivid_pixels[np.argmin(vivid_pixels[..., 2])] + + # 获取最亮和最暗的颜色的色相差异 + hue_difference = abs(int(brightest_pixel[0]) - int(darkest_pixel[0])) + + # 如果色相差异过小,则尝试寻找新的最暗颜色,直到色相差异大于设定阈值 + if hue_difference < hue_difference_threshold: + possible_dark_pixels = vivid_pixels[vivid_pixels[..., 0] != brightest_pixel[0]] + if len(possible_dark_pixels) > 0: + darkest_pixel = possible_dark_pixels[ + np.argmin(possible_dark_pixels[..., 2]) + ] + + # 将最亮和最暗的像素从HSV转回RGB + brightest_color = ( + Image.fromarray(np.uint8([[brightest_pixel]]), "HSV") + .convert("RGB") + .getpixel((0, 0)) + ) + darkest_color = ( + Image.fromarray(np.uint8([[darkest_pixel]]), "HSV") + .convert("RGB") + .getpixel((0, 0)) + ) + + return brightest_color, darkest_color + + +def draw_game_info( + header: Image.Image, + game_name: str, + game_time: str, + last_play_time: str, + achievements: List[Achievements], + completed_achievement_number: int, + total_achievement_number: int, + achievement_color: Tuple[int, int, int], +) -> Image.Image: + bg = Image.new("RGBA", (880, 110 + 64 + 10), (0, 0, 0, 110)) + header = header.resize((229, 86), Image.BICUBIC) + bg.paste(header, (10, 110 // 2 - header.height // 2)) + + draw = ImageDraw.Draw(bg) + + # 画游戏名 + draw.text( + (260, 10), + game_name, + font=ImageFont.truetype(font_regular_path, 26), + fill=(255, 255, 255), + ) + + # 画最后游玩时间 + font = ImageFont.truetype(font_light_path, 22) + display_text = last_play_time + draw.text( + (int(bg.width - font.getlength(display_text)) - 10, 75), + display_text, + font=font, + fill=(150, 150, 150), + ) + + # 画游戏时间 + font = ImageFont.truetype(font_light_path, 22) + display_text = f"总时数 {game_time}" + draw.text( + (int(bg.width - font.getlength(display_text)) - 10, 50), + display_text, + font=font, + fill=(150, 150, 150), + ) + + if completed_achievement_number is None or total_achievement_number is None: + return bg.crop((0, 0, bg.width, 110)) + + # 画成就 + 64 + 10 + achievement_bg = Image.new("RGBA", (860, 64), achievement_color) + draw_achievement = ImageDraw.Draw(achievement_bg) + + # 画成就进度 + font = ImageFont.truetype(font_light_path, 18) + x = 14 + draw_achievement.text( + (x, 20), + "成就进度", + font=font, + fill=(255, 255, 255, 255), + ) + x += font.getlength("成就进度") + 10 + draw_achievement.text( + (int(x), 20), + f"{completed_achievement_number} / {total_achievement_number}", + font=font, + fill=(130, 130, 130), + ) + x += ( + font.getlength(f"{completed_achievement_number} / {total_achievement_number}") + + 10 + ) + progress_bar = create_progress_bar( + completed_achievement_number / total_achievement_number, achievement_color + ) + achievement_bg.paste(progress_bar, (int(x), 24), progress_bar) + + # 画成就图标 + x = 860 - 48 * 6 - 10 * 6 + for achievement in achievements: + achievement_image = Image.open(BytesIO(achievement["image"])).resize((48, 48)) + achievement_bg.paste(achievement_image, (x, 8)) + x += 48 + 10 + + if completed_achievement_number > 6: + font = ImageFont.truetype(font_regular_path, 22) + display_text = f"+{completed_achievement_number - 5}" + draw_achievement.rectangle((x, 8, x + 48, 56), fill=(34, 34, 34)) + draw_achievement.text( + (x + 24 - font.getlength(display_text) // 2, 18), + display_text, + font=font, + fill=(255, 255, 255), + ) + + bg.paste(achievement_bg, (10, 110), achievement_bg) + return bg + + +def draw_player_status( + player_bg: Image.Image, + player_avatar: Image.Image, + player_name: str, + player_id: str, + player_description: str, + player_last_two_weeks_time: str, # e.g. 10.2 小时 + player_games: List[DrawPlayerStatusData], +): + if isinstance(player_bg, bytes): + player_bg = Image.open(BytesIO(player_bg)) + if isinstance(player_avatar, bytes): + player_avatar = Image.open(BytesIO(player_avatar)) + + bg = recolor_image( + player_bg.crop( + ( + (player_bg.width - 960) // 2, + 0, + (player_bg.width + 960) // 2, + player_bg.height, + ) + ), + 10, + 10, + ) + # 调暗背景 + enhancer = ImageEnhance.Brightness(bg) + bg = enhancer.enhance(0.7) + # bg.size = (960, 1020) + player_avatar = player_avatar.resize((200, 200)) + bg.paste(player_avatar, (40, 40)) + + draw = ImageDraw.Draw(bg) + + # 画头像外框 + draw.rectangle((40, 40, 240, 240), outline=(83, 164, 196), width=3) + + # 画昵称 + draw.text( + (280, 48), + player_name, + font=ImageFont.truetype(font_light_path, 40), + fill=(255, 255, 255), + ) + + # 画ID + draw.text( + (280, 100), + f"好友代码: {player_id}", + font=ImageFont.truetype(font_regular_path, 19), + fill=(191, 191, 191), + ) + + # 画简介 + line_width = 0 + offset = 0 + line = "" + for idx, char in enumerate(player_description): + line += char + line_width += ImageFont.truetype(font_light_path, 22).getlength(char) + if line_width > 640 or idx == len(player_description) - 1 or char == "\n": + draw.text( + (280, 132 + offset), + line, + font=ImageFont.truetype(font_light_path, 22), + fill=(255, 255, 255), + ) + line = "" + offset += 25 + line_width = 0 + if offset >= 25 * 4: + break + + # 画游戏 + + brightest_color, darkest_color = get_brightest_and_darkest_color(player_bg) + brightest_color = tuple(map(lambda x: x - 30 if x >= 30 else 0, brightest_color)) + darkest_color = tuple( + map(lambda x: x + 30 if x <= 255 - 30 else 255, darkest_color) + ) + brightest_color = (brightest_color[0], brightest_color[1], brightest_color[2], 128) + brightest_color = random_color_offset(brightest_color, 20) + darkest_color = (darkest_color[0], darkest_color[1], darkest_color[2], 128) + darkest_color = random_color_offset(darkest_color, 20) + + # 画游戏信息 + hsv_achievement_color = rgb_to_hsv(*brightest_color[:3]) + achievement_color = tuple( + map( + int, + hsv_to_rgb( + hsv_achievement_color[0], + hsv_achievement_color[1] * 0.85, + hsv_achievement_color[2] * 0.6, + ), + ) + ) + game_images: List[Image.Image] = [] + for idx, game in enumerate(player_games): + game_image = Image.open(BytesIO(game["game_header"])) + game_info = draw_game_info( + game_image, + game["game_name"], + game["game_time"], + game["last_play_time"], + game["achievements"], + game["completed_achievement_number"], + game["total_achievement_number"], + achievement_color, + ) + game_images.append(game_info) + + # 画半透明黑色背景 + bg_game = Image.new( + "RGBA", (920, 106 + sum([game_image.height + 26 for game_image in game_images])) + ) + draw_game = ImageDraw.Draw(bg_game) + draw_game.rectangle( + ( + 0, + 0, + 920, + bg_game.height, + ), + fill=(0, 0, 0, 120), + ) + bg.paste(bg_game, (20, 272), bg_game) + + # 画渐变条 + gradient = create_gradient_image((920, 50), brightest_color, darkest_color) + bg.paste(gradient, (20, 272), gradient) + + # 画渐变条的文字:最新动态,最近游戏 + draw.text( + (34, 279), + "最新动态", + font=ImageFont.truetype(font_light_path, 26), + fill=(255, 255, 255), + ) + if player_last_two_weeks_time is not None: + width = ImageFont.truetype(font_light_path, 26).getlength( + player_last_two_weeks_time + ) + draw.text( + (960 - width - 34, 279), + player_last_two_weeks_time, + font=ImageFont.truetype(font_light_path, 26), + fill=(255, 255, 255), + ) + + y = 350 + for idx, game_image in enumerate(game_images): + bg.paste( + game_image, + ((920 - game_image.width) // 2 + 20, y), + game_image.convert("RGBA"), + ) + y += game_image.height + 26 + + player_bg.paste(bg, ((player_bg.width - 960) // 2, 0), bg.convert("RGBA")) + + return player_bg + + +def rounded_rectangle( + image: Image.Image, + radius: int, + border=False, + border_width=0, + border_color=(0, 0, 0), +): + """ + 将给定的Image.Image对象切割为圆角矩形。 + + Args: + image: 一个PIL Image对象。 + radius: 圆角半径,单位为像素。 + border: 是否需要边框,默认为False。 + border_width: 边框宽度,单位为像素,默认为0。 + border_color: 边框颜色,RGB元组,默认为黑色(0, 0, 0)。 + + Returns: + 一个PIL Image对象,表示切割后的圆角矩形图像。 + """ + + width, height = image.size + + image_ = Image.new("RGBA", (width + 1, height + 1), (0, 0, 0, 0)) + image_.paste(image, (0, 0), image.convert("RGBA")) + + # 创建一个圆角矩形的遮罩 + result = Image.new("RGBA", (width + 1, height + 1), (0, 0, 0, 0)) + mask = Image.new("L", (width + 1, height + 1), 0) + draw = ImageDraw.Draw(mask) + image_draw = ImageDraw.Draw(result) + + # 绘制圆角矩形 + draw.rounded_rectangle((0, 0, width, height), radius=radius, fill=255) + + # 应用遮罩到原始图像 + result.paste(image_, (0, 0), mask) + + # 添加边框 (如果需要) + if border: + image_draw.rounded_rectangle( + (0, 0, width, height), + radius=radius, + outline=border_color, + width=border_width, + ) + + return result + + +def create_progress_bar( + progress: float, color: Tuple[int, int, int], width=186, height=16 +): + color_hsv = rgb_to_hsv(*color) + + # 外条 + bar_color = tuple( + map(int, hsv_to_rgb(color_hsv[0], color_hsv[1], color_hsv[2] * 0.8)) + ) + border_color = tuple(map(lambda x: max(x - 20, 0), color)) + border_image = rounded_rectangle( + Image.new("RGBA", (width, height), bar_color), + 8, + border=True, + border_width=1, + border_color=border_color, + ) + + # 内条 + bar_color_top = tuple( + map(int, hsv_to_rgb(color_hsv[0], color_hsv[1] / 2, color_hsv[2] * 5 / 2)) + ) + bar_color_bottem = tuple( + map(int, hsv_to_rgb(color_hsv[0], color_hsv[1] / 2, color_hsv[2])) + ) + + bar_image = create_vertical_gradient_rect( + int(width * progress) - 6, height - 4, bar_color_top, bar_color_bottem + ) + bar_image = rounded_rectangle(bar_image, 6) + + # 合并 + border_image.paste(bar_image, (3, 2), bar_image) + + return border_image diff --git a/hexi/plugins/nonebot_plugin_steam_info/models.py b/hexi/plugins/nonebot_plugin_steam_info/models.py new file mode 100644 index 0000000..bc04a77 --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/models.py @@ -0,0 +1,82 @@ +from typing import TypedDict, List + + +class Player(TypedDict): + steamid: str + communityvisibilitystate: int + profilestate: int + personaname: str + profileurl: str + avatar: str + avatarmedium: str + avatarfull: str + avatarhash: str + lastlogoff: int + personastate: int + realname: str + primaryclanid: str + timecreated: int + personastateflags: int + # gameextrainfo: str + # gameid: str + + +class PlayerSummariesResponse(TypedDict): + players: List[Player] + + +class PlayerSummaries(TypedDict): + response: PlayerSummariesResponse + + +class ProcessedPlayer(Player): + game_start_time: int # Unix timestamp + + +class PlayerSummariesProcessedResponse(TypedDict): + players: List[ProcessedPlayer] + + +class Achievements(TypedDict): + name: str + image: bytes + + +class GameData(TypedDict): + game_name: str + play_time: str # e.g. 10.2 + last_played: str # e.g. 10 月 2 日 + game_image: bytes + achievements: List[Achievements] + completed_achievement_number: int + total_achievement_number: int + + +class PlayerData(TypedDict): + steamid: str + player_name: str + background: bytes + avatar: bytes + description: str + recent_2_week_play_time: str + game_data: List[GameData] + + +class DrawPlayerStatusData(TypedDict): + game_name: str + game_time: str # e.g. 10.2 小时(过去 2 周) + last_play_time: str # e.g. 10 月 2 日 + game_header: bytes + achievements: List[Achievements] + completed_achievement_number: int + total_achievement_number: int + + +__all__ = [ + "Player", + "PlayerSummaries", + "PlayerSummariesResponse", + "ProcessedPlayer", + "PlayerSummariesProcessedResponse", + "DrawPlayerStatusData", +] diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/bg_dots.png b/hexi/plugins/nonebot_plugin_steam_info/res/bg_dots.png new file mode 100644 index 0000000..75881d4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/bg_dots.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/busy.png b/hexi/plugins/nonebot_plugin_steam_info/res/busy.png new file mode 100644 index 0000000..c045303 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/busy.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/default_achievement_image.png b/hexi/plugins/nonebot_plugin_steam_info/res/default_achievement_image.png new file mode 100644 index 0000000..73424d4 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/default_achievement_image.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/default_header_image.jpg b/hexi/plugins/nonebot_plugin_steam_info/res/default_header_image.jpg new file mode 100644 index 0000000..e27e2ad Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/default_header_image.jpg differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/friends_search.png b/hexi/plugins/nonebot_plugin_steam_info/res/friends_search.png new file mode 100644 index 0000000..e2ca5e3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/friends_search.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/gaming.png b/hexi/plugins/nonebot_plugin_steam_info/res/gaming.png new file mode 100644 index 0000000..188017d Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/gaming.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/parent_status.png b/hexi/plugins/nonebot_plugin_steam_info/res/parent_status.png new file mode 100644 index 0000000..395aeb3 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/parent_status.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/unknown_avatar.jpg b/hexi/plugins/nonebot_plugin_steam_info/res/unknown_avatar.jpg new file mode 100644 index 0000000..aa490cb Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/unknown_avatar.jpg differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/zzz_gaming.png b/hexi/plugins/nonebot_plugin_steam_info/res/zzz_gaming.png new file mode 100644 index 0000000..ebc7bbd Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/zzz_gaming.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/res/zzz_online.png b/hexi/plugins/nonebot_plugin_steam_info/res/zzz_online.png new file mode 100644 index 0000000..02acf55 Binary files /dev/null and b/hexi/plugins/nonebot_plugin_steam_info/res/zzz_online.png differ diff --git a/hexi/plugins/nonebot_plugin_steam_info/steam.py b/hexi/plugins/nonebot_plugin_steam_info/steam.py new file mode 100644 index 0000000..ea585ac --- /dev/null +++ b/hexi/plugins/nonebot_plugin_steam_info/steam.py @@ -0,0 +1,321 @@ +import re +import httpx +from pathlib import Path +from bs4 import BeautifulSoup +from nonebot.log import logger +from typing import List, Optional +from datetime import datetime, timezone +import asyncio +import requests +import csv + +from .models import PlayerSummaries, PlayerData + +STEAM_ID_OFFSET = 76561197960265728 +# 全局变量,用于记录当前使用的 API Key 的索引 +current_api_key_index = 0 + + +def get_steam_id(steam_id_or_steam_friends_code: str) -> str: + if not steam_id_or_steam_friends_code.isdigit(): + return None + + id_ = int(steam_id_or_steam_friends_code) + + if id_ < STEAM_ID_OFFSET: + return str(id_ + STEAM_ID_OFFSET) + + return steam_id_or_steam_friends_code + + +async def get_steam_users_info( + steam_ids: List[str], steam_api_key: List[str], proxy: str = None +) -> PlayerSummaries: + if len(steam_ids) == 0: + return {"response": {"players": []}} + + if len(steam_ids) > 100: + # 分批获取 + result = {"response": {"players": []}} + for i in range(0, len(steam_ids), 100): + batch_result = await get_steam_users_info( + steam_ids[i: i + 100], steam_api_key, proxy + ) + result["response"]["players"].extend(batch_result["response"]["players"]) + return result + + for api_key in steam_api_key: + try: + async with httpx.AsyncClient(proxy=proxy) as client: + response = await client.get( + f'https://community.steam-api.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={",".join(steam_ids)}' + ) + logger.info(f"本次请求的响应码:{response.status_code}") + if response.status_code == 200: + return response.json() + else: + logger.warning(f"API key {api_key} failed to get steam users info.") + except httpx.RequestError as exc: + async with httpx.AsyncClient(proxy=proxy) as client: + response = await client.get( + f'https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={api_key}&steamids={",".join(steam_ids)}' + ) + logger.info(f"本次请求的响应码:{response.status_code}") + if response.status_code == 200: + return response.json() + else: + logger.warning(f"API key {api_key} failed to get steam users info.") + logger.warning(f"API key {api_key} encountered an error: {exc}") + + logger.error("All API keys failed to get steam users info.") + return {"response": {"players": []}} + + +async def _fetch( + url: str, default: bytes, cache_file: Optional[Path] = None, proxy: str = None +) -> bytes: + if cache_file is not None and cache_file.exists(): + return cache_file.read_bytes() + try: + async with httpx.AsyncClient(proxy=proxy) as client: + response = await client.get(url) + if response.status_code == 200: + if cache_file is not None: + cache_file.write_bytes(response.content) + return response.content + else: + response.raise_for_status() + except Exception as exc: + logger.error(f"Failed to get image: {exc}") + return default + + +async def get_user_data( + steam_id: int, cache_path: Path, proxy: str = None +) -> PlayerData: + url = f"https://steamcommunity.com/profiles/{steam_id}" + default_background = (Path(__file__).parent / "res/bg_dots.png").read_bytes() + default_avatar = (Path(__file__).parent / "res/unknown_avatar.jpg").read_bytes() + default_achievement_image = ( + Path(__file__).parent / "res/default_achievement_image.png" + ).read_bytes() + default_header_image = ( + Path(__file__).parent / "res/default_header_image.jpg" + ).read_bytes() + + result = { + "description": "No information given.", + "background": default_background, + "avatar": default_avatar, + "player_name": "Unknown", + "recent_2_week_play_time": None, + "game_data": [], + } + + local_time = datetime.now(timezone.utc).astimezone() + utc_offset_minutes = int(local_time.utcoffset().total_seconds()) + timezone_cookie_value = f"{utc_offset_minutes},0" + + try: + async with httpx.AsyncClient( + proxy=proxy, + headers={ + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" + }, + cookies={"timezoneOffset": timezone_cookie_value}, + ) as client: + response = await client.get(url) + if response.status_code == 200: + html = response.text + elif response.status_code == 302: + url = response.headers["Location"] + response = await client.get(url) + if response.status_code == 200: + html = response.text + else: + response.raise_for_status() + except httpx.RequestError as exc: + logger.error(f"Failed to get user data: {exc}") + return result + + # player name + player_name = re.search(r"Steam 社区 :: (.*?)", html) + if player_name: + result["player_name"] = player_name.group(1) + + # description t
\r\n\t\t\t\t\t\t\t\t風が雨が激しくても
思いだすんだ 僕らを照らす光があるよ
今日もいっぱい
明日もいっぱい 力を出しきってみるよ\t\t\t\t\t\t\t
+ description = re.search( + r'
(.*?)
', html, re.DOTALL | re.MULTILINE + ) + if description: + description = description.group(1) + description = re.sub(r"
", "\n", description) + description = re.sub(r"\t", "", description) + result["description"] = description.strip() + + # remove emoji + result["description"] = re.sub(r"ː.*?ː", "", result["description"]) + + # remove xml + result["description"] = re.sub(r"<.*?>", "", result["description"]) + + # background + background_url = re.search(r"background-image: url\( \'(.*?)\' \)", html) + if background_url: + background_url = background_url.group(1) + result["background"] = await _fetch( + background_url, default_background, proxy=proxy + ) + + # avatar + # \t + avatar_url = re.search(r'\r\n\t\t\t\t\t\t\t\t\t
15.5 小时(过去 2 周)
+ play_time_text = re.search( + r'