From 7afb099c1205234e3c91bb9bc3d5f15ab5905185 Mon Sep 17 00:00:00 2001 From: HibiKier <775757368@qq.com> Date: Sun, 5 Sep 2021 02:21:38 +0800 Subject: [PATCH] update code --- README.md | 264 ++-------- __version__ | 1 + configs/config.py | 218 ++++++--- models/ban_user.py | 14 + models/bilibili_sub.py | 249 ++++++++++ models/group_member_info.py | 33 +- models/omega_pixiv_illusts.py | 138 ++++++ models/pixiv.py | 102 ++-- models/russian_user.py | 20 + models/setu.py | 14 +- ...sigin_group_user.py => sign_group_user.py} | 12 +- plugins/admin_bot_manage/data_source.py | 14 +- plugins/admin_bot_manage/switch_rule.py | 1 + plugins/admin_help/__init__.py | 28 +- plugins/ai/__init__.py | 2 +- plugins/ai/data_source.py | 5 +- plugins/alapi/wbtop.py | 11 +- plugins/ban/__init__.py | 18 +- plugins/bilibili_sub/__init__.py | 198 ++++++++ plugins/bilibili_sub/data_source.py | 379 ++++++++++++++ plugins/bilibili_sub/utils.py | 72 +++ plugins/bt/__init__.py | 10 +- plugins/check_zhenxun_update/__init__.py | 2 +- plugins/check_zhenxun_update/data_source.py | 17 +- plugins/coser/__init__.py | 36 +- plugins/database_scripts.py | 29 ++ plugins/draw_card/announcement.py | 7 +- plugins/fudu.py | 43 +- .../genshin/query_resource_points/__init__.py | 8 +- plugins/group_handle/__init__.py | 87 ++-- plugins/group_manager/__init__.py | 12 + plugins/help/config.py | 3 +- plugins/help/data_source.py | 37 +- plugins/hook.py | 262 +++++++++- plugins/luxun/__init__.py | 6 +- plugins/mute.py | 1 + plugins/nonebot_plugin_withdraw/__init__.py | 85 ---- plugins/nonebot_plugin_withdraw/config.py | 8 - plugins/one_friend/__init__.py | 11 +- plugins/open_cases/__init__.py | 12 +- plugins/open_cases/open_cases_c.py | 8 +- plugins/parse_bilibili_json.py | 72 +-- plugins/pix_gallery/__init__.py | 80 ++- plugins/pix_gallery/data_source.py | 61 ++- plugins/pix_gallery/pix.py | 114 ++++- plugins/pix_gallery/pix_show_info.py | 10 +- plugins/pixiv/__init__.py | 210 ++++---- plugins/pixiv/data_source.py | 170 ++++--- plugins/reimu/__init__.py | 6 +- plugins/russian/__init__.py | 462 +++++++++++------- plugins/russian/data_source.py | 8 +- plugins/search_anime/__init__.py | 4 +- plugins/search_buff_skin_price/__init__.py | 8 +- plugins/send_img/__init__.py | 68 --- plugins/send_setu/__init__.py | 85 +--- plugins/send_setu/data_source.py | 2 + plugins/shop/gold_redbag/__init__.py | 1 + plugins/shop/use/data_source.py | 9 +- plugins/sign_in/__init__.py | 22 +- plugins/sign_in/config.py | 76 +++ plugins/sign_in/group_user_checkin.py | 98 ++-- plugins/sign_in/random_event.py | 24 + plugins/sign_in/utils.py | 330 +++++++++++++ plugins/statistics_hook.py | 5 +- plugins/super_cmd/__init__.py | 17 +- plugins/super_help/__init__.py | 17 +- plugins/update_gocqhttp/__init__.py | 4 +- plugins/what_anime/__init__.py | 4 +- plugins/withdraw.py | 15 + .../sign_res/background/background_01.jpg | Bin 0 -> 36148 bytes resources/img/sign/sign_res/bar.png | Bin 0 -> 755 bytes resources/img/sign/sign_res/bar_white.png | Bin 0 -> 584 bytes .../sign/sign_res/border/ava_border_01.png | Bin 0 -> 54726 bytes .../sign/sign_res/border/gift_border_02.png | Bin 0 -> 24931 bytes resources/img/sign/sign_res/white.png | Bin 0 -> 3773 bytes services/log.py | 44 +- update_info.json | 49 +- utils/browser.py | 19 +- utils/image_utils.py | 138 +++++- utils/message_builder.py | 7 +- utils/static_data/__init__.py | 5 +- utils/static_data/data_class.py | 13 +- utils/static_data/group_manager.py | 14 +- utils/utils.py | 33 +- 84 files changed, 3377 insertions(+), 1404 deletions(-) create mode 100644 __version__ create mode 100644 models/bilibili_sub.py create mode 100644 models/omega_pixiv_illusts.py rename models/{sigin_group_user.py => sign_group_user.py} (84%) create mode 100644 plugins/bilibili_sub/__init__.py create mode 100644 plugins/bilibili_sub/data_source.py create mode 100644 plugins/bilibili_sub/utils.py create mode 100644 plugins/database_scripts.py delete mode 100644 plugins/nonebot_plugin_withdraw/__init__.py delete mode 100644 plugins/nonebot_plugin_withdraw/config.py create mode 100644 plugins/sign_in/config.py create mode 100644 plugins/sign_in/random_event.py create mode 100644 plugins/sign_in/utils.py create mode 100644 plugins/withdraw.py create mode 100644 resources/img/sign/sign_res/background/background_01.jpg create mode 100644 resources/img/sign/sign_res/bar.png create mode 100644 resources/img/sign/sign_res/bar_white.png create mode 100644 resources/img/sign/sign_res/border/ava_border_01.png create mode 100644 resources/img/sign/sign_res/border/gift_border_02.png create mode 100644 resources/img/sign/sign_res/white.png diff --git a/README.md b/README.md index 717a7acf..65dc33d3 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ - [x] 图灵AI(会把'你'等关键字替换为你的昵称),且带有 [AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus),够味 - [x] 签到/我的签到/好感度排行/好感度总排行(影响色图概率和开箱次数,支持配置) - [x] 发送某文件夹下的随机图片(支持自定义,默认:美图,萝莉,壁纸) -- [x] 尝试搜索不色的图片 ↑↑ - [x] 色图(这不是基础功能嘛喂) - [x] coser - [x] 黑白草图生成器 @@ -47,6 +46,7 @@ - [x] 原神资源查询 (借鉴[Genshin_Impact_bot](https://github.com/H-K-Y/Genshin_Impact_bot)插件) - [x] 金币红包 - [x] 微博热搜 +- [x] B站主播/UP/番剧订阅 - [x] pil对图片的一些操作 - [x] BUFF饰品底价查询(需要session) @@ -69,7 +69,7 @@ - [x] 我的信息(只是为了看看什么时候入群) - [x] 更新信息(如果继续更新的话) - [x] go-cqhttp最新版下载和上传(不需要请删除) -- [x] 撤回 (使用[nonebot-plugin-withdraw](https://github.com/MeetWq/nonebot-plugin-withdraw)插件) +- [x] 撤回 - [x] 滴滴滴-(用户对超级用户发送消息) - [x] 金币红包/金币排行 - [x] 俄罗斯轮盘/胜场排行/败场排行/欧洲人排行/慈善家排行 @@ -89,9 +89,10 @@ - [x] 上传图片/连续上传图片 (上传图片至指定图库) - [x] 移动图片 (同上) - [x] 删除图片 (同上) +- [x] 群内B站订阅 ### 已实现的超级用户功能 -- [x] 添加/删除管理(是真寻的管理员权限,不是群管理员) +- [x] 添加/删除权限(是真寻的管理员权限,不是群管理员) - [x] 开启/关闭指定群的广播通知 - [x] 广播 - [x] 自检(检查系统状态) @@ -103,7 +104,6 @@ - [x] 节日红包发送 - [x] 修改群权限 - [x] ban -- [x] 添加/删除群白名单 - [x] 更新色图 - [x] 更新价格/更加图片(csgo开箱) - [x] 重载原神/方舟/赛马娘/坎公骑冠剑卡池 @@ -111,9 +111,10 @@ - [x] PIX相关操作 - [x] 检查更新真寻 - [x] 重启 -- [x] 添加/删除群白名单 +- [x] 添加/删除/查看群白名单 - [x] 功能开关(更多设置) - [x] 功能状态 +- [x] b了 #### 超级用户的被动技能 - [x] 邀请入群提醒(别人邀请真寻入群) @@ -123,7 +124,7 @@ - [x] 进群欢迎消息 - [x] 群早晚安 - [x] 每日开箱重置提醒 -- [x] b站转发解析(解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv) +- [x] b站转发解析(解析b站分享信息,支持bv,bilibili链接,b站手机端转发卡片,cv,b23.tv),且5分钟内不解析相同url - [x] 丢人爬(爬表情包) - [x] epic通知(每日发送epic免费游戏链接) - [x] 原神黄历提醒 @@ -201,6 +202,8 @@ | 添加pix关键词/uid/pid | 添加pix关键词/uid/pid *[关键词/uid/pid]| 添加关键词或uid或pid用于下次搜索,关键词搜索相关tag,uid会收录作者下收藏符合标准的作品,pid收录单张作品
示例:添加pix关键词 原神
添加pixuid 123441
添加pixpid 2748937| | 查看pix图库 | 查看pix图库 [tags] | 查看已收录的tag相关图片数量
示例:查看pix图库 原神 莫娜 |显示pix关键词 | 显示pix关键词 | 查看已收录的所有关键词/UID/PID +| b了 | b了 [at] | 使真寻完全忽略一个用户的所有信息 +| B站订阅 | 添加订阅 [主播/UP/番剧] [id/链接/番名] / 删除订阅 [id] / 查看订阅 | 可以通过直接间链接或主播间id添加订阅主播开播提醒,动态和投稿,可以通过番名,番剧的id,或者番剧链接添加番剧订阅(是番剧id,md开头,不是集数id,ep开头的)更新, 可以通过UP个人id或UP主页链接订阅UP动态和投稿 ### 管理员功能 @@ -218,6 +221,7 @@ | 上传图片 | 6 | 上传图片 \[图库] \[图片]... | 上传图片至指定图库,虽然并不打算开放给群员,但还是写了,支持批量图片
示例:上传图片 美图 \[图片].. | 删除图片 | 6 | 删除图片 [图库] \[图片id] | 通过指定本地图片id来删除指定图库的图片
示例:删除图片 美图 1 | 移动图片 | 6 | 移动图片 \[移出的图库] \[移入的图库] \[图片id] | 移动指定图库中的图片到指定的新图库中,移入的图片id更改为移入图库的最后一位,移除的图库中原本图片的id又最后一位图片替代
示例:移动图片 美图 萝莉 22 +|B站订阅 | 5 | 功能同上,就是在群中有权限限制 | 略 ### 超级用户功能 @@ -250,225 +254,12 @@ |pix检测更新 | pix检测更新 [update]? | 检测从未更新过的pid或uid,-update参数将在检测后直接更新未更新过的pid或uid
示例:pix检测更新 update | 检查更新真寻 | 检查更新真寻 | 不再需要麻烦的clone第一步 | 关闭功能 | 关闭[功能] [group/private]/*[群号]? | 关闭色图:维护功能(关闭总开关)
关闭色图 g:在群内限制功能
关闭色图 p:在私聊限制功能
关闭色图 1234678:禁用12345678的色图功能 -| 群白名单 | 添加/删除群白名单 *[群号] | 白名单内的群不会受到 维护 限制 +| 群白名单 | 添加/删除群白名单 *[群号] / 查看群白名单 | 白名单内的群不会受到 维护 限制 | 功能状态 | 功能状态 | 查看功能开关状态 +| pix | pix [-s/-r] [keyword] | 可以通过pix -s 查看图库的涩图,pix -r查看图库的r18图,支持搜索,当然,pix图库只区分了r18和非r18,如果-s查询到不色的图也问题不大 -## 部分功能展示 -
-部分功能展示及说明 - -### 帮助以及开关(功能控制) - -群帮助将会在功能左侧展示该功能的开关,带有√或×的功能代表可以开关
-此插件使用 [nonebot_plugin_manager](https://github.com/Jigsaw111/nonebot_plugin_manager) 并魔改一点实现 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/help.PNG) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/kg1.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/kg3.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/kg2.png) - -
-如果你希望某功能暂时停用
-私聊发送 npm block xx (xx=功能名)来锁定
-使用npm unblock xx 进行解锁 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/ocgn.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/ocgn2.png) - -### 签到 -普普通通的签到,设置影响开箱次数和涩图触发成功的概率(可配置)
-开箱次数 = 初始开箱数量 + 好感度 / 3
-金币 = random.randint(100) + random.randint(好感度)【好感度获取的金币不会超过200】 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/sign.png) - -### 黑白草图 - -整活生成器(从未设想的道路) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/w2b.png) - -### 发送文件夹下随机图片 - -提供了 美图589(获取该图库下文件名589.jpg的图片)方法,图库内图片名称需要有序(如:0.jpg,1.jpg....) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/send_img.png) - -### 色图 - -略,send_setu/check_setu_hash.py文件用于记录涩图hash和检测文件名是否连贯(例如:0.jpg, 1.jpg....) - -### 开箱(csgo模拟开箱) - -我的开箱/群开箱统计/我的金色 功能是对开箱数据的统计展示
- -目前支持的武器箱(数据已备好): -* 狂牙大行动武器箱 -* 突围大行动武器箱 -* 命悬一线武器箱 -* 裂空武器箱 -* 光谱武器箱 -
- BUFF账号可能会因为短时间内访问api次数过多被禁止访问api!! - 如果是第一次启动请先使用命令 “更新价格”, “更新图片” (需要配置cookie!!如果经常超时请设置代理,配置文件中的 buff_proxy!)
- 如果需要配置新的箱子,请在.config.py中配置好该箱子中的皮肤,且列表名是箱子名称的大写拼音
- 示例:光谱武器箱 GUANGPU_CASE_KNIFE,GUANGPU_CASE_RED...后面的颜色代表皮肤品质 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/kaixiang.png) - - -### BUFF皮肤底价查询 - -需要配置cookie!!!!!!!!
-如果经常超时请设置代理,配置文件中的 buff_proxy! - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/buff.png) - - -### coser - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/coser.png) - -### 鸡汤/语录 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/jitang.png) - -### 骂我 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/mawo.png) - -### 鲁迅说 - -此插件使用 [nonebot2_luxun_says](https://github.com/NothAmor/nonebot2_luxun_says) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/luxun.png) - -### 假消息 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/jiaxiaoxi.png) - -### 商店系统 - -商店内的道具支持自定义,但需要写触发后的效果...整不出活,到头来也就增加好感度概率的商品 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/shop.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/daoju.png) - - -### 昵称系统 - -养成方法第一步,让可爱的小真寻叫自己昵称!(替换ai中的'你'等等) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/nicheng1.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/nicheng2.png) - -### 抽卡(8种手游的抽卡) - -已单独分离并上传至nb2商店,不再放图片了,项目地址:[nonebot_plugin_gamedraw](https://github.com/HibiKier/nonebot_plugin_gamedraw) - -### 我有一个朋友... - -使用大佬的插件 [cappuccilo_plugins](https://github.com/pcrbot/cappuccilo_plugins#%E7%94%9F%E6%88%90%E5%99%A8%E6%8F%92%E4%BB%B6) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/one_firend.png) - - -### 原神黄历/今日素材/丘丘语翻译/地图资源查询 - -使用大佬的插件 [Genshin_Impact_bot](https://github.com/H-K-Y/Genshin_Impact_bot) - -### 对图片的操作 - -只是一些简单对图片操作(娱乐整活) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/tupian.png) - -### 识番 - -使用大佬的插件 [XUN_Langskip](https://github.com/Angel-Hair/XUN_Bot) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/shifan.png) - - -### 识图 - -使用nb2商店插件 [nonebot_plugin_picsearcher](https://github.com/synodriver/nonebot_plugin_picsearcher) (可配置图片返回的最大数量) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/shitu.png) - -### epic免费游戏 - -访问rsshub获取数据解析
可以不玩,不能没有 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/epic.png) - - -### P站排行/搜图 - -访问rsshub获取数据解析 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/p_rank.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/p_sou.png) - - -### 翻译 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/fanyi.png) - -### 自定义群欢迎消息 - -关键字 [at] 判断是否艾特入群用户 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/qhyxx.png) - -### 查看当前群欢迎消息 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/qunhuanying.png) - -### 自检 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/check.png) - -### .ban/.unban - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/ban.png) - -### 查看被动技能(被动技能除复读外都提供了开关) - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/beidong.png) - -### 自我介绍 - -只是一段简单自我介绍,但是,还是想放上来 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/jieshao.png) - -### 我的信息/我的权限 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/info.png) - -### 金币红包 - -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/redbag0.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/redbag1.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/redbag2.png) - -### 俄罗斯轮盘 -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/russian0.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/russian1.png) -![](https://raw.githubusercontent.com/HibiKier/zhenxun_bot/main/docs/russian2.png) - -
- -### 其他 - -点歌:使用 [nonebot_plugin_songpicker2](https://github.com/maxesisn/nonebot_plugin_songpicker2) 插件
-

-## 其他功能请自己试一试 ) - -
## 部署 @@ -672,6 +463,36 @@ python bot.py ## 更新 +### 2021/9/5 +* 添加配置PIX_IMAGE_SIZE,调整PIX下载图片大小,当设置的图片404时,改为原图 +* 新增配置DEFAULT_GROUP_LEVEL,默认群等级 +* 新增超级用户功能 super ban,将屏蔽被ban用户的所有消息,指令:b了 +* b站转发解析支持纯BV号解析,且五分钟内不会解析相同url +* 俄罗斯轮盘新增 连胜/最高连胜/连败/最高连败 纪录,新增 最高连胜排行榜/最高连败排行榜 +* 增加扩展图库 OmegaPixivIllusts,不想自己找图的人福音([Ailitonia](https://github.com/Ailitonia) 佬的高质量精品手筛图库)([传送门](https://github.com/Ailitonia/omega-miya/blob/master/archive_data/db_pixiv.7z) ),可以手动导入图库,也可以将解压文件放在bot.py同级目录重启bot +* 增加配置PIX_OMEGA_PIXIV_RATIO,PIX功能发送PIX图库和扩展图库OmegaPixivIllusts图片的比例,如果没有使用扩展图库OmegaPixivIllusts,请设置为(10, 0) +* 增加配置WITHDRAW_PIX_TIME,用于配置在开关PIX图片在群私聊的自动撤回 +* 上传图库cases, 开箱 也可以连抽(未更新过没有价格) +* 新增命令 查看群白名单 +* plugins2info_dict新增键"default_status",设置加入新群时功能的默认开关状态 +* 增加配置plugins2exists_dict,可自定义是否阻塞某命令同时触发多次 +* 增加配置plugins2cd_dict,可自定义为命令添加cd +* 新增B站订阅(直播/番剧/UP)[测试],提供命令:添加订阅 [主播/UP/番剧] [id/链接/番名],删除订阅 [id],查看订阅 +* 优化pix和色图的数据库查询 +* 触发已关闭的功能的正则时不再触发ai +* 更换coser API +* PIX搜索pid功能在群聊无法搜索PIX图库的r18和OmegaPixivIllusts的r15以及r18,超级用户除外 +* PIX单次搜索的图片张数超级用户限制为至多30张,普通用户10张 +* PIX超级用户新增-s,-r,可以通过pix -s 查看图库的涩图,pix -r查看图库的r18图,支持搜索,当然,pix图库只区分了r18和非r18,如果-s查询到不色的图也问题不大 +* 优化P站排行和搜图,现在需要艾特,改为使用HIBIAPI,在群内时将使用合并消息(群聊搜图会屏蔽R-18) +* win10下playwright相关功能无法使用,但是不再需要删除文件 +* 签到大改,优化签到方式与逻辑,改为图片形式发送,有概率额外获得随机道具(好感度有加成) +* 修改撤回功能,改为回复撤回,回复发送撤回 +* 更改logging为loguru +* 删除了 发送图片 中的 [N]张图[keyword] 功能 +* 修复私聊 关闭[功能] 默认不为 全部 而要添加参数 ‘a’ +* 修复0权限用户可以修改禁言检测相关设置 + ### 2021/8/17 * 新增配置CHECK_NOTICE_INFO_CD,修改群权限,个人权限检测各种检测的提示消息cd @@ -859,3 +680,4 @@ python bot.py [Genshin_Impact_bot](https://github.com/H-K-Y/Genshin_Impact_bot) [nonebot2_luxun_says](https://github.com/NothAmor/nonebot2_luxun_says) [AnimeThesaurus](https://github.com/Kyomotoi/AnimeThesaurus) +[omega-miya](https://github.com/Ailitonia/omega-miya) diff --git a/__version__ b/__version__ new file mode 100644 index 00000000..f56c9dbb --- /dev/null +++ b/__version__ @@ -0,0 +1 @@ +__version__: v0.0.4.0 \ No newline at end of file diff --git a/configs/config.py b/configs/config.py index 19ff4aeb..906a99ba 100644 --- a/configs/config.py +++ b/configs/config.py @@ -1,5 +1,7 @@ from typing import List, Optional, Tuple from services.service_config import TL_M_KEY, SYSTEM_M_PROXY, ALAPI_M_TOKEN +from .path_config import DATA_PATH +from pathlib import Path try: import ujson as json @@ -11,7 +13,7 @@ except ModuleNotFoundError: USE_CONFIG_FILE: bool = False # 回复消息名称 -NICKNAME: str = '小真寻' +NICKNAME: str = "小真寻" # API KEY(必要) RSSHUBAPP: str = "https://rsshub.app" # rsshub @@ -41,6 +43,9 @@ IMAGE_DIR_LIST: List[str] = ["美图", "萝莉", "壁纸"] # 对被ban用户发送的消息 BAN_RESULT: str = "才不会给你发消息." +# PIX图库下载的画质 可能的值:original:原图,master:缩略图(加快发送速度) +PIX_IMAGE_SIZE: str = "master" + # 插件配置 MAXINFO_REIMU: int = 7 # 上车(reimu)功能查找目的地的最大数 @@ -51,6 +56,11 @@ MAXINFO_GROUP_ANIME: int = 5 # 群搜索动漫返回的最大数量 MAX_FIND_IMG_COUNT: int = 3 # 识图最大返回数 # 参1:延迟撤回色图时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊) WITHDRAW_SETU_TIME: Tuple[int, int] = (0, 1) +# 参1:延迟撤回PIX图片时间(秒),0 为关闭 | 参2:监控聊天类型,0(私聊) 1(群聊) 2(群聊+私聊) +WITHDRAW_PIX_TIME: Tuple[int, int] = (0, 1) + +# PIX图库 与 额外图库OmegaPixivIllusts 混合搜索的比例 参1:PIX图库 参2:OmegaPixivIllusts扩展图库(没有此图库请设置为0) +PIX_OMEGA_PIXIV_RATIO: Tuple[int, int] = (10, 0) # 各种卡池的开关 PRTS_FLAG = True # 明日方舟 @@ -68,7 +78,7 @@ SEMAPHORE = 5 # 限制碧蓝航线和FGO并发数 ADMIN_DEFAULT_AUTH: int = 5 # 默认群管理员权限 MAX_SIGN_GOLD: int = 200 # 签到好感度加成额外获得的最大金币数 -MAX_RUSSIAN_BET_GOLD: int = 1000 # 俄罗斯轮盘最大赌注金额 +MAX_RUSSIAN_BET_GOLD: int = 1000 # 俄罗斯轮盘最大赌注金额 INITIAL_SETU_PROBABILITY: float = 0.7 # 色图概率 FUDU_PROBABILITY: float = 0.7 # 复读概率 @@ -78,7 +88,7 @@ MUTE_DEFAULT_COUNT: int = 10 # 刷屏禁言默认检测次数 MUTE_DEFAULT_TIME: int = 7 # 刷屏检测默认规定时间 MUTE_DEFAULT_DURATION: int = 10 # 刷屏检测默禁言时长(分钟) -CHECK_NOTICE_INFO_CD = 300 # 群检测,个人权限检测等各种检测提示信息cd +CHECK_NOTICE_INFO_CD = 300 # 群检测,个人权限检测等各种检测提示信息cd # 注:即在 MALICIOUS_CHECK_TIME 时间内触发相同命令 MALICIOUS_BAN_COUNT 将被ban MALICIOUS_BAN_TIME 分钟 MALICIOUS_BAN_TIME: int = 30 # 恶意命令触发检测触发后ban的时长(分钟) @@ -92,7 +102,9 @@ UPLOAD_LEVEL: int = 6 # 上传图片权限 BAN_LEVEL: int = 5 # BAN权限 OC_LEVEL: int = 2 # 开关群功能权限 MUTE_LEVEL: int = 5 # 更改禁言设置权限 -MEMBER_ACTIVITY_LEVEL = 5 # 群员活跃检测设置权限 +GROUP_BILIBILI_SUB_LEVEL = 5 # 群内bilibili订阅需要的权限 + +DEFAULT_GROUP_LEVEL = 5 # 默认群等级 # 是否开启HIBIAPI搜图功能(该功能会搜索群友提交的xp) HIBIAPI_FLAG: bool = True @@ -128,79 +140,159 @@ admin_plugins_auth = { "upload_img": UPLOAD_LEVEL, "admin_help": 1, "mute": MUTE_LEVEL, - "member_activity_handle": MEMBER_ACTIVITY_LEVEL, } +# 需要cd的功能(方便管理)[秒] +# 自定义的功能需要cd也可以在此配置 +# key:模块名称 +# cd:cd 时长(秒) +# status:此限制的开关状态 +# check_type:'private'/'group'/'all',限制私聊/群聊/全部 +# limit_type:监听对象,以user_id或group_id作为键来限制,'user':用户id,'group':群id +# 示例:'user':用户N秒内触发1次,'group':群N秒内触发1次 +# rst:回复的话,可以添加[at],[uname],[nickname]来对应艾特,用户群名称,昵称系统昵称 +# rst 为 "" 或 None 时则不回复 +# rst示例:"[uname]你冲的太快了,[nickname]先生,请稍后再冲[at]" +# rst回复:"老色批你冲的太快了,欧尼酱先生,请稍后再冲@老色批" +# 用户昵称↑ 昵称系统的昵称↑ 艾特用户↑ +plugins2cd_dict = { + "open_cases": { + "cd": 5, + "status": True, + "check_type": "all", + "limit_type": "user", + "rst": "着什么急啊,慢慢来!", + }, + "send_setu": { + "cd": 5, + "status": True, + "check_type": "all", + "limit_type": "user", + "rst": "您冲得太快了,请稍候再冲", + }, + "sign_in": { + "cd": 5, + "status": True, + "check_type": "group", + "limit_type": "user", + "rst": None, + } +} + +# 用户调用阻塞(方便管理) +# 即 当用户调用此功能还未结束时 +# 用发送消息阻止用户重复调用此命令直到该命令结束 +# 参数同上 plugin2cd_dict +plugins2exists_dict = { + "send_setu": { + "status": False, + "check_type": "all", + "limit_type": "user", + "rst": "您有色图正在处理,请稍等.....", + }, + "pixiv": { + "status": True, + "check_type": "all", + "limit_type": "user", + "rst": "P站排行榜或搜图正在搜索,请不要重复触发命令...", + }, + "pix": { + "status": True, + "check_type": "all", + "limit_type": "user", + "rst": "您有PIX图片正在处理,请稍等...", + } +} # 模块与对应命令和对应群权限 # 用于生成帮助图片 和 开关功能 +# key:模块名称 +# level:需要的群等级 +# default_status:加入群时功能的默认开关状态 +# cmd:关闭[cmd] 都会触发命令 关闭对应功能,cmd列表第一个词为统计的功能名称 plugins2info_dict = { - "sign_in": {"level": 5, "cmd": ["签到"]}, - "send_img": {"level": 5, "cmd": ["发送图片", "发图", "萝莉", "美图", "壁纸"]}, - "send_setu": {"level": 9, "cmd": ["色图", "涩图", "瑟图", "查色图"]}, - "white2black": {"level": 5, "cmd": ["黑白图", "黑白草图"]}, - "coser": {"level": 9, "cmd": ["coser", "cos"]}, - "quotations": {"level": 5, "cmd": ["语录"]}, - "jitang": {"level": 5, "cmd": ["鸡汤"]}, - "send_dinggong_voice": {"level": 5, "cmd": ["骂我", "骂老子", "骂劳资"]}, - "open_cases": {"level": 5, "cmd": ["开箱", "我的开箱", "群开箱统计", "我的金色"]}, - "luxun": {"level": 5, "cmd": ["鲁迅说", "鲁迅说过"]}, - "fake_msg": {"level": 5, "cmd": ["假消息"]}, - "buy": {"level": 5, "cmd": ["购买", "购买道具"]}, - "my_gold": {"level": 5, "cmd": ["我的金币"]}, - "my_props": {"level": 5, "cmd": ["我的道具"]}, - "shop_handle": {"level": 5, "cmd": ["商店"]}, - "update_pic": {"level": 5, "cmd": ["图片", "操作图片", "修改图片"]}, - "search_buff_skin_price": {"level": 5, "cmd": ["查询皮肤"]}, - "weather": {"level": 5, "cmd": ["天气", "查询天气", "天气查询"]}, - "yiqing": {"level": 5, "cmd": ["疫情", "疫情查询", "查询疫情"]}, - "what_anime": {"level": 5, "cmd": ["识番"]}, - "search_anime": {"level": 5, "cmd": ["搜番"]}, - "songpicker2": {"level": 5, "cmd": ["点歌"]}, - "epic": {"level": 5, "cmd": ["epic"]}, - "pixiv": {"level": 9, "cmd": ["pixiv", "p站排行", "搜图"]}, - "poke": {"level": 5, "cmd": ["戳一戳", "拍一拍"]}, + "sign_in": {"level": 5, "default_status": True, "cmd": ["签到"]}, + "send_img": { + "level": 5, + "default_status": True, + "cmd": ["发送图片", "发图", "萝莉", "美图", "壁纸"], + }, + "send_setu": {"level": 9, "default_status": True, "cmd": ["色图", "涩图", "瑟图", "查色图"]}, + "white2black": {"level": 5, "default_status": True, "cmd": ["黑白图", "黑白草图"]}, + "coser": {"level": 9, "default_status": True, "cmd": ["coser", "cos"]}, + "quotations": {"level": 5, "default_status": True, "cmd": ["语录"]}, + "jitang": {"level": 5, "default_status": True, "cmd": ["鸡汤"]}, + "send_dinggong_voice": { + "level": 5, + "default_status": True, + "cmd": ["骂我", "骂老子", "骂劳资"], + }, + "open_cases": { + "level": 5, + "default_status": True, + "cmd": ["开箱", "我的开箱", "群开箱统计", "我的金色"], + }, + "luxun": {"level": 5, "default_status": True, "cmd": ["鲁迅说", "鲁迅说过"]}, + "fake_msg": {"level": 5, "default_status": True, "cmd": ["假消息"]}, + "buy": {"level": 5, "default_status": True, "cmd": ["购买", "购买道具"]}, + "my_gold": {"level": 5, "default_status": True, "cmd": ["我的金币"]}, + "my_props": {"level": 5, "default_status": True, "cmd": ["我的道具"]}, + "shop_handle": {"level": 5, "default_status": True, "cmd": ["商店"]}, + "update_pic": {"level": 5, "default_status": True, "cmd": ["图片", "操作图片", "修改图片"]}, + "search_buff_skin_price": {"level": 5, "default_status": True, "cmd": ["查询皮肤"]}, + "weather": {"level": 5, "default_status": True, "cmd": ["天气", "查询天气", "天气查询"]}, + "yiqing": {"level": 5, "default_status": True, "cmd": ["疫情", "疫情查询", "查询疫情"]}, + "what_anime": {"level": 5, "default_status": True, "cmd": ["识番"]}, + "search_anime": {"level": 5, "default_status": True, "cmd": ["搜番"]}, + "songpicker2": {"level": 5, "default_status": True, "cmd": ["点歌"]}, + "epic": {"level": 5, "default_status": True, "cmd": ["epic"]}, + "pixiv": {"level": 9, "default_status": True, "cmd": ["pixiv", "p站排行", "搜图"]}, + "poke": {"level": 5, "default_status": True, "cmd": ["戳一戳", "拍一拍"]}, "draw_card": { "level": 5, + "default_status": True, "cmd": [ "抽卡", "游戏抽卡", - "原神抽卡", - "方舟抽卡", - "坎公骑冠剑抽卡", - "pcr抽卡", - "fgo抽卡", - "碧蓝抽卡", - "碧蓝航线抽卡", - "阴阳师抽卡", ], }, - "ai": {"level": 5, "cmd": ["ai", "Ai", "AI", "aI"]}, - "one_friend": {"level": 5, "cmd": ["我有一个朋友", "我有一个朋友想问问"]}, - "translate": {"level": 5, "cmd": ["翻译", "英翻", "翻英", "日翻", "翻日", "韩翻", "翻韩"]}, - "nonebot_plugin_picsearcher": {"level": 5, "cmd": ["识图"]}, - "almanac": {"level": 5, "cmd": ["原神黄历", "黄历"]}, - "material_remind": {"level": 5, "cmd": ["今日素材", "天赋材料"]}, - "qiu_qiu_translation": {"level": 5, "cmd": ["丘丘翻译", "丘丘一下", "丘丘语翻译"]}, - "query_resource_points": {"level": 5, "cmd": ["原神资源查询", "原神资源列表"]}, - "russian": {"level": 5, "cmd": ["俄罗斯轮盘", "俄罗斯转盘", "装弹"]}, - "gold_redbag": {"level": 5, "cmd": ["塞红包", "红包", "抢红包"]}, - "poetry": {"level": 5, "cmd": ["念诗", "来首诗", "念首诗"]}, - "comments_163": {"level": 5, "cmd": ["到点了", "12点了", "网易云热评", "网易云评论"]}, - "cover": {"level": 5, "cmd": ["b封面", "B封面"]}, - "pid_search": {"level": 9, "cmd": ["p搜", "P搜"]}, + "ai": {"level": 5, "default_status": True, "cmd": ["ai", "Ai", "AI", "aI"]}, + "one_friend": {"level": 5, "default_status": True, "cmd": ["我有一个朋友", "我有一个朋友想问问"]}, + "translate": { + "level": 5, + "default_status": True, + "cmd": ["翻译", "英翻", "翻英", "日翻", "翻日", "韩翻", "翻韩"], + }, + "nonebot_plugin_picsearcher": {"level": 5, "default_status": True, "cmd": ["识图"]}, + "almanac": {"level": 5, "default_status": True, "cmd": ["原神黄历", "黄历"]}, + "material_remind": {"level": 5, "default_status": True, "cmd": ["今日素材", "天赋材料"]}, + "qiu_qiu_translation": { + "level": 5, + "default_status": True, + "cmd": ["丘丘翻译", "丘丘一下", "丘丘语翻译"], + }, + "query_resource_points": { + "level": 5, + "default_status": True, + "cmd": ["原神资源查询", "原神资源列表"], + }, + "russian": {"level": 5, "default_status": True, "cmd": ["俄罗斯轮盘", "俄罗斯转盘", "装弹"]}, + "gold_redbag": {"level": 5, "default_status": True, "cmd": ["塞红包", "红包", "抢红包"]}, + "poetry": {"level": 5, "default_status": True, "cmd": ["念诗", "来首诗", "念首诗"]}, + "comments_163": { + "level": 5, + "default_status": True, + "cmd": ["到点了", "12点了", "网易云热评", "网易云评论"], + }, + "cover": {"level": 5, "default_status": True, "cmd": ["b封面", "B封面"]}, + "pid_search": {"level": 9, "default_status": True, "cmd": ["p搜", "P搜"]}, "pix": { "level": 5, + "default_status": True, "cmd": ["pix", "PIX", "pIX", "Pix", "PIx"], }, - "wbtop": { - "level": 5, - "cmd": ['微博热搜', "微博", "wbtop"] - }, - "update_info": { - "level": 5, - "cmd": ['更新信息', '更新日志'] - } + "wbtop": {"level": 5, "default_status": True, "cmd": ["微博热搜", "微博", "wbtop"]}, + "update_info": {"level": 5, "default_status": True, "cmd": ["更新信息", "更新日志"]}, } if TL_M_KEY: @@ -214,6 +306,12 @@ if ALAPI_M_TOKEN: HIBIAPI = HIBIAPI[:-1] if HIBIAPI[-1] == "/" else HIBIAPI RSSHUBAPP = RSSHUBAPP[:-1] if RSSHUBAPP[-1] == "/" else RSSHUBAPP + +# plugins2info_file = Path(DATA_PATH) / 'configs' / 'plugins2info.json' +# plugins2info_file.parent.mkdir(exist_ok=True, parents=True) +# +# with open(f'{DATA_PATH}/configs/') + # 配置文件应用 # if USE_CONFIG_FILE: # config = get_config_data() diff --git a/models/ban_user.py b/models/ban_user.py index 8c9243b1..53f488dd 100644 --- a/models/ban_user.py +++ b/models/ban_user.py @@ -60,6 +60,20 @@ class BanUser(db.Model): await cls.unban(user_qq) return False + @classmethod + async def is_super_ban(cls, user_qq: int) -> bool: + """ + 说明: + 判断用户是否被ban + 参数: + :param user_qq: qq号 + """ + user = await cls.query.where((cls.user_qq == user_qq)).gino.first() + if not user: + return False + if user.ban_level == 10: + return True + @classmethod async def ban(cls, user_qq: int, ban_level: int, duration: int) -> bool: """ diff --git a/models/bilibili_sub.py b/models/bilibili_sub.py new file mode 100644 index 00000000..f201c575 --- /dev/null +++ b/models/bilibili_sub.py @@ -0,0 +1,249 @@ +from services.log import logger +from services.db_context import db +from datetime import datetime +from typing import Optional, List + + +class BilibiliSub(db.Model): + __tablename__ = "bilibili_sub" + + id = db.Column(db.Integer(), primary_key=True) + sub_id = db.Column(db.Integer(), nullable=False) + sub_type = db.Column(db.String(), nullable=False) + # 订阅用户 + sub_users = db.Column(db.String(), nullable=False) + # 直播 + live_short_id = db.Column(db.Integer()) + live_status = db.Column(db.Integer) + # 主播/UP + uid = db.Column(db.BigInteger()) + uname = db.Column(db.String()) + latest_video_created = db.Column(db.BigInteger()) # 视频上传时间 + dynamic_upload_time = db.Column(db.BigInteger(), default=0) # 动态发布时间 + # 番剧 + season_name = db.Column(db.String()) + season_id = db.Column(db.Integer()) + season_current_episode = db.Column(db.String()) + season_update_time = db.Column(db.DateTime()) + + _idx1 = db.Index("bilibili_sub_idx1", "sub_id", "sub_type", unique=True) + + @classmethod + async def add_bilibili_sub( + cls, + sub_id: int, + sub_type: str, + sub_user: str, + *, + live_short_id: Optional[int] = None, + live_status: Optional[int] = None, + dynamic_upload_time: Optional[int] = None, + uid: Optional[int] = None, + uname: Optional[str] = None, + latest_video_created: Optional[int] = None, + season_name: Optional[str] = None, + season_id: Optional[int] = None, + season_current_episode: Optional[str] = None, + season_update_time: Optional[datetime] = None, + ) -> bool: + """ + 说明: + 添加订阅 + 参数: + :param sub_id: 订阅名称,房间号,番剧号等 + :param sub_type: 订阅类型 + :param sub_user: 订阅此条目的用户 + :param live_short_id: 直接短 id + :param live_status: 主播开播状态 + :param dynamic_upload_time: 主播/UP最新动态时间 + :param uid: 主播/UP uid + :param uname: 用户名称 + :param latest_video_created: 最新视频上传时间 + :param season_name: 番剧名称 + :param season_id: 番剧 season_id + :param season_current_episode: 番剧最新集数 + :param season_update_time: 番剧更新时间 + """ + try: + async with db.transaction(): + query = ( + await cls.query.where(cls.sub_id == sub_id) + .with_for_update() + .gino.first() + ) + sub_user = sub_user if sub_user[-1] == "," else f"{sub_user}," + if query: + if sub_user not in query.sub_users: + sub_users = query.sub_users + sub_user + await query.update(sub_users=sub_users).apply() + else: + sub = await cls.create( + sub_id=sub_id, sub_type=sub_type, sub_users=sub_user + ) + await sub.update( + live_short_id=live_short_id + if live_short_id + else sub.live_short_id, + live_status=live_status if live_status else sub.live_status, + dynamic_upload_time=dynamic_upload_time + if dynamic_upload_time + else sub.dynamic_upload_time, + uid=uid if uid else sub.uid, + uname=uname if uname else sub.uname, + latest_video_created=latest_video_created + if latest_video_created + else sub.latest_video_created, + season_update_time=season_update_time + if season_update_time + else sub.season_update_time, + season_current_episode=season_current_episode + if season_current_episode + else sub.season_current_episode, + season_id=season_id if season_id else sub.season_id, + season_name=season_name if season_name else sub.season_name, + ).apply() + return True + except Exception as e: + logger.info(f"bilibili_sub 添加订阅错误 {type(e)}: {e}") + return False + + @classmethod + async def delete_bilibili_sub(cls, sub_id: int, sub_user: str) -> bool: + """ + 说明: + 删除订阅 + 参数: + :param sub_id: 订阅名称 + :param sub_user: 删除此条目的用户 + """ + try: + async with db.transaction(): + query = ( + await cls.query.where( + (cls.sub_id == sub_id) & (cls.sub_users.contains(sub_user)) + ) + .with_for_update() + .gino.first() + ) + if not query: + return False + await query.update( + sub_users=query.sub_users.replace(f"{sub_user},", "") + ).apply() + if not query.sub_users.strip(): + await query.delete() + return True + except Exception as e: + logger.info(f"bilibili_sub 删除订阅错误 {type(e)}: {e}") + return False + + @classmethod + async def get_sub(cls, sub_id: int) -> Optional["BilibiliSub"]: + """ + 说明: + 获取订阅对象 + 参数: + :param sub_id: 订阅 id + """ + return await cls.query.where(cls.sub_id == sub_id).gino.first() + + @classmethod + async def get_sub_data(cls, id_: str) -> List["BilibiliSub"]: + """ + 获取 id_ 订阅的所有内容 + :param id_: id + """ + query = cls.query.where(cls.sub_users.contains(id_)) + return await query.gino.all() + + @classmethod + async def update_sub_info( + cls, + sub_id: int, + *, + live_short_id: Optional[int] = None, + live_status: Optional[int] = None, + dynamic_upload_time: Optional[int] = None, + uid: Optional[int] = None, + uname: Optional[str] = None, + latest_video_created: Optional[int] = None, + season_name: Optional[str] = None, + season_id: Optional[int] = None, + season_current_episode: Optional[str] = None, + season_update_time: Optional[datetime] = None, + ) -> bool: + """ + 说明: + 更新订阅信息 + 参数: + :param sub_id: 订阅名称,房间号,番剧号等 + :param live_short_id: 直接短 id + :param live_status: 主播开播状态 + :param dynamic_upload_time: 主播/UP最新动态时间 + :param uid: 主播/UP uid + :param uname: 用户名称 + :param latest_video_created: 最新视频上传时间 + :param season_name: 番剧名称 + :param season_id: 番剧 season_id + :param season_current_episode: 番剧最新集数 + :param season_update_time: 番剧更新时间 + """ + try: + async with db.transaction(): + sub = ( + await cls.query.where(cls.sub_id == sub_id) + .with_for_update() + .gino.first() + ) + if sub: + await sub.update( + live_short_id=live_short_id + if live_short_id is not None + else sub.live_short_id, + live_status=live_status + if live_status is not None + else sub.live_status, + dynamic_upload_time=dynamic_upload_time + if dynamic_upload_time is not None + else sub.dynamic_upload_time, + uid=uid if uid is not None else sub.uid, + uname=uname if uname is not None else sub.uname, + latest_video_created=latest_video_created + if latest_video_created is not None + else sub.latest_video_created, + season_update_time=season_update_time + if season_update_time is not None + else sub.season_update_time, + season_current_episode=season_current_episode + if season_current_episode is not None + else sub.season_current_episode, + season_id=season_id if season_id is not None else sub.season_id, + season_name=season_name + if season_name is not None + else sub.season_name, + ).apply() + return True + except Exception as e: + logger.info(f"bilibili_sub 更新订阅错误 {type(e)}: {e}") + return False + + @classmethod + async def get_all_sub_data( + cls, + ) -> "List[BilibiliSub], List[BilibiliSub], List[BilibiliSub]": + """ + 说明: + 分类获取所有数据 + """ + live_data = [] + up_data = [] + season_data = [] + query = await cls.query.gino.all() + for x in query: + if x.sub_type == "live": + live_data.append(x) + if x.sub_type == "up": + up_data.append(x) + if x.sub_type == "season": + season_data.append(x) + return live_data, up_data, season_data diff --git a/models/group_member_info.py b/models/group_member_info.py index 14c20ef4..a2ef57ac 100644 --- a/models/group_member_info.py +++ b/models/group_member_info.py @@ -1,7 +1,7 @@ from datetime import datetime from services.db_context import db -from typing import List +from typing import List, Optional class GroupInfoUser(db.Model): @@ -13,6 +13,7 @@ class GroupInfoUser(db.Model): belonging_group = db.Column(db.BigInteger(), nullable=False) user_join_time = db.Column(db.DateTime(), nullable=False) nickname = db.Column(db.Unicode()) + uid = db.Column(db.BigInteger()) _idx1 = db.Index("info_group_users_idx1", "user_qq", "belonging_group", unique=True) @@ -141,3 +142,33 @@ class GroupInfoUser(db.Model): if user.nickname: return user.nickname return "" + + @classmethod + async def get_group_member_uid(cls, user_qq: int, belonging_group: int) -> Optional[str]: + query = cls.query.where( + (cls.user_qq == user_qq) & (cls.belonging_group == belonging_group) + ) + user = await query.gino.first() + _max_uid = cls.query.where((cls.user_qq == 114514) & (cls.belonging_group == 114514)).with_for_update() + _max_uid_user = await _max_uid.gino.first() + _max_uid = _max_uid_user.uid + if not user or not user.uid: + all_user = await cls.query.where(cls.user_qq == user_qq).gino.all() + for x in all_user: + if x.uid: + return x.uid + else: + if not user: + await GroupInfoUser.add_member_info(user_qq, belonging_group, '', datetime.min) + user = await cls.query.where( + (cls.user_qq == user_qq) & (cls.belonging_group == belonging_group) + ).gino.first() + await user.update( + uid=_max_uid + 1, + ).apply() + await _max_uid_user.update( + uid=_max_uid + 1, + ).apply() + + return user.uid if user and user.uid else None + diff --git a/models/omega_pixiv_illusts.py b/models/omega_pixiv_illusts.py new file mode 100644 index 00000000..13ba0135 --- /dev/null +++ b/models/omega_pixiv_illusts.py @@ -0,0 +1,138 @@ +from typing import Optional, List +from datetime import datetime +from services.db_context import db + + +class OmegaPixivIllusts(db.Model): + __tablename__ = "omega_pixiv_illusts" + id = db.Column(db.Integer(), primary_key=True) + pid = db.Column(db.BigInteger(), nullable=False) + uid = db.Column(db.BigInteger(), nullable=False) + title = db.Column(db.String(), nullable=False) + uname = db.Column(db.String(), nullable=False) + nsfw_tag = db.Column(db.Integer(), nullable=False) + width = db.Column(db.Integer(), nullable=False) + height = db.Column(db.Integer(), nullable=False) + tags = db.Column(db.String(), nullable=False) + url = db.Column(db.String(), nullable=False) + created_at = db.Column(db.DateTime(timezone=True)) + updated_at = db.Column(db.DateTime(timezone=True)) + + _idx1 = db.Index("omega_pixiv_illusts_idx1", "pid", "url", unique=True) + + @classmethod + async def add_image_data( + cls, + pid: int, + title: str, + width: int, + height: int, + url: str, + uid: int, + uname: str, + nsfw_tag: int, + tags: str, + created_at: datetime, + updated_at: datetime, + ): + """ + 说明: + 添加图片信息 + 参数: + :param pid: pid + :param title: 标题 + :param width: 宽度 + :param height: 长度 + :param url: url链接 + :param uid: 作者uid + :param uname: 作者名称 + :param nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 + :param tags: 相关tag + :param created_at: 创建日期 + :param updated_at: 更新日期 + """ + if not await cls.check_exists(pid): + await cls.create( + pid=pid, + title=title, + width=width, + height=height, + url=url, + uid=uid, + uname=uname, + nsfw_tag=nsfw_tag, + tags=tags, + ) + return True + return False + + @classmethod + async def query_images( + cls, + keywords: Optional[List[str]] = None, + uid: Optional[int] = None, + pid: Optional[int] = None, + nsfw_tag: Optional[int] = 0, + num: int = 100 + ) -> List[Optional["OmegaPixivIllusts"]]: + """ + 说明: + 查找符合条件的图片 + 参数: + :param keywords: 关键词 + :param uid: 画师uid + :param pid: 图片pid + :param nsfw_tag: nsfw标签, 0=safe, 1=setu. 2=r18 + :param num: 获取图片数量 + """ + if nsfw_tag is not None: + query = cls.query.where(cls.nsfw_tag == nsfw_tag) + else: + query = cls.query + if keywords: + for keyword in keywords: + query = query.where(cls.tags.contains(keyword)) + elif uid: + query = query.where(cls.uid == uid) + elif pid: + query = query.where(cls.uid == pid) + query = query.order_by(db.func.random()).limit(num) + return await query.gino.all() + + @classmethod + async def check_exists(cls, pid: int) -> bool: + """ + 说明: + 检测pid是否已存在 + 参数: + :param pid: 图片PID + """ + query = await cls.query.where(cls.pid == pid).gino.all() + return bool(query) + + @classmethod + async def get_keyword_num(cls, tags: List[str] = None) -> "int, int, int": + """ + 说明: + 获取相关关键词(keyword, tag)在图库中的数量 + 参数: + :param tags: 关键词/Tag + """ + query = cls.query + if tags: + for tag in tags: + query = query.where(cls.tags.contains(tag)) + count = len(await query.where(cls.nsfw_tag == 0).gino.all()) + setu_count = len(await query.where(cls.nsfw_tag == 1).gino.all()) + r18_count = len(await query.where(cls.nsfw_tag == 2).gino.all()) + return count, setu_count, r18_count + + @classmethod + async def get_all_pid(cls) -> List[int]: + """ + 说明: + 获取所有图片PID + """ + data = await cls.query.gino.all() + return [x.pid for x in data] + diff --git a/models/pixiv.py b/models/pixiv.py index 28e023a3..328be221 100644 --- a/models/pixiv.py +++ b/models/pixiv.py @@ -1,6 +1,5 @@ from typing import Optional, List from services.db_context import db -import asyncio class Pixiv(db.Model): @@ -24,18 +23,19 @@ class Pixiv(db.Model): @classmethod async def add_image_data( - cls, - pid: int, - title: str, - width: int, - height: int, - view: int, - bookmarks: int, - img_url: str, - img_p: str, - uid: int, - author: str, - tags: str, + cls, + pid: int, + title: str, + width: int, + height: int, + view: int, + bookmarks: int, + img_url: str, + img_p: str, + uid: int, + author: str, + tags: str, + nws ): """ 说明: @@ -65,7 +65,7 @@ class Pixiv(db.Model): img_p=img_p, uid=uid, author=author, - is_r18=True if 'R-18' in tags else False, + is_r18=True if "R-18" in tags else False, tags=tags, ) return True @@ -97,44 +97,45 @@ class Pixiv(db.Model): 说明: 获取所有PID """ - pid = [] - query = await cls.query.gino.all() - for image in query: - if image.pid not in pid: - pid.append(image.pid) - return pid + query = await cls.query.select("pid").gino.first() + pid = [x[0] for x in query] + return list(set(pid)) # 0:非r18 1:r18 2:混合 @classmethod async def query_images( cls, - keyword: Optional[List[str]] = None, + keywords: Optional[List[str]] = None, uid: Optional[int] = None, pid: Optional[int] = None, - r18: int = 0, + r18: Optional[int] = 0, + num: int = 100 ) -> List[Optional["Pixiv"]]: """ 说明: 查找符合条件的图片 参数: - :param keyword: 关键词 + :param keywords: 关键词 :param uid: 画师uid :param pid: 图片pid :param r18: 是否r18,0:非r18 1:r18 2:混合 + :param num: 查找图片的数量 """ if r18 == 0: - query = await cls.query.where(cls.is_r18 == False).gino.all() + query = cls.query.where(cls.is_r18 == False) elif r18 == 1: - query = await cls.query.where(cls.is_r18 == True).gino.all() + query = cls.query.where(cls.is_r18 == True) else: - query = await cls.query.gino.all() - if keyword: - query = [x for x in query if set(x.tags.split(',')) > set(keyword)] + query = cls.query + if keywords: + for keyword in keywords: + query = query.where(cls.tags.contains(keyword)) elif uid: - query = [x for x in query if x.uid == uid] + query = query.where(cls.uid == uid) elif pid: - query = [x for x in query if x.pid == pid] - return query + query = query.where(cls.uid == pid) + query = query.order_by(db.func.random()).limit(num) + return await query.gino.all() @classmethod async def check_exists(cls, pid: int, img_p: str) -> bool: @@ -151,41 +152,18 @@ class Pixiv(db.Model): return bool(query) @classmethod - async def get_keyword_num(cls, keyword: List[str]) -> "int, int": + async def get_keyword_num(cls, tags: List[str] = None) -> "int, int": """ 说明: 获取相关关键词(keyword, tag)在图库中的数量 参数: - :param keyword: 关键词/Tag + :param tags: 关键词/Tag """ - query = await cls.query.gino.all() - i = int(len(query) / 200) - mod = len(query) % 200 - tasks = [] - start = 0 - end = 200 - count = 0 - r18_count = 0 - for _ in range(i): - tasks.append(asyncio.ensure_future(split_query_list(query[start: end], keyword))) - start += 200 - end += 200 - if mod: - tasks.append(asyncio.ensure_future(split_query_list(query[end:], keyword))) - result = await asyncio.gather(*tasks) - for x, j in result: - count += x - r18_count += j - # query = [x for x in query if set(x.tags.split(',')) > set(keyword)] - # r18_count = len([x for x in query if x.is_r18]) + query = cls.query + if tags: + for tag in tags: + query = cls.query.where(cls.tags.contains(tag)) + count = len(await query.where(cls.is_r18 == False).gino.all()) + r18_count = len(await query.where(cls.is_r18 == True).gino.all()) return count, r18_count - -async def split_query_list(query: List['Pixiv'], keyword: List[str]) -> 'int, int': - return await asyncio.get_event_loop().run_in_executor(None, _split_query_list, query, keyword) - - -def _split_query_list(query: List['Pixiv'], keyword: List[str]) -> 'int, int': - query = [x for x in query if set(x.tags.split(',')) > set(keyword)] - r18_count = len([x for x in query if x.is_r18]) - return len(query) - r18_count, r18_count diff --git a/models/russian_user.py b/models/russian_user.py index 0de17014..26e5a207 100644 --- a/models/russian_user.py +++ b/models/russian_user.py @@ -12,6 +12,10 @@ class RussianUser(db.Model): fail_count = db.Column(db.Integer(), default=0) make_money = db.Column(db.Integer(), default=0) lose_money = db.Column(db.Integer(), default=0) + winning_streak = db.Column(db.Integer(), default=0) + losing_streak = db.Column(db.Integer(), default=0) + max_winning_streak = db.Column(db.Integer(), default=0) + max_losing_streak = db.Column(db.Integer(), default=0) _idx1 = db.Index("russian_group_users_idx1", "user_qq", "group_id", unique=True) @@ -52,12 +56,28 @@ class RussianUser(db.Model): if not user: user = await cls.create(user_qq=user_qq, group_id=group_id) if itype == "win": + _max = ( + user.max_winning_streak + if user.max_winning_streak > user.winning_streak + 1 + else user.winning_streak + 1 + ) await user.update( win_count=user.win_count + 1, + winning_streak=user.winning_streak + 1, + losing_streak=0, + max_winning_streak=_max ).apply() elif itype == "lose": + _max = ( + user.max_losing_streak + if user.max_losing_streak > user.losing_streak + 1 + else user.losing_streak + 1 + ) await user.update( fail_count=user.fail_count + 1, + losing_streak=user.losing_streak + 1, + winning_streak=0, + max_losing_streak=_max, ).apply() return True except Exception: diff --git a/models/setu.py b/models/setu.py index 23fb31c6..48c50b35 100644 --- a/models/setu.py +++ b/models/setu.py @@ -72,17 +72,19 @@ class Setu(db.Model): (cls.local_id == local_id) & (cls.is_r18 == flag) ).gino.first() if r18 == 0: - query = await cls.query.where(cls.is_r18 == False).gino.all() + query = cls.query.where(cls.is_r18 == False) elif r18 == 1: - query = await cls.query.where(cls.is_r18 == True).gino.all() + query = cls.query.where(cls.is_r18 == True) else: - query = await cls.query.gino.all() + query = cls.query if tags: - query = [x for x in query if set(x.tags.split(",")) > set(tags)] - return query + for tag in tags: + query = query.where(cls.tags.contains(tag) | cls.title.contains(tag) | cls.author.contains(tag)) + query = query.order_by(db.func.random()).limit(50) + return await query.gino.all() @classmethod - async def get_image_count(cls, r18: int = 0): + async def get_image_count(cls, r18: int = 0) -> int: """ 说明: 查询图片数量 diff --git a/models/sigin_group_user.py b/models/sign_group_user.py similarity index 84% rename from models/sigin_group_user.py rename to models/sign_group_user.py index 34ab0fbe..d605c164 100644 --- a/models/sigin_group_user.py +++ b/models/sign_group_user.py @@ -41,7 +41,17 @@ class SignGroupUser(db.Model): ) @classmethod - async def get_all_impression(cls, belonging_group: int) -> "list, list": + async def sign(cls, user: "SignGroupUser", impression: float, checkin_time_last: datetime): + await user.update( + checkin_count=user.checkin_count + 1, + checkin_time_last=checkin_time_last, + impression=user.impression + impression, + add_probability=0, + specify_probability=0, + ).apply() + + @classmethod + async def get_all_impression(cls, belonging_group: int) -> "list, list, list": """ 说明: 获取该群所有用户 id 及对应 好感度 diff --git a/plugins/admin_bot_manage/data_source.py b/plugins/admin_bot_manage/data_source.py index c73f8d10..5d08e24b 100644 --- a/plugins/admin_bot_manage/data_source.py +++ b/plugins/admin_bot_manage/data_source.py @@ -1,22 +1,22 @@ from models.group_remind import GroupRemind from services.log import logger from configs.path_config import DATA_PATH -import os -import aiofiles -import aiohttp from utils.message_builder import image from utils.utils import get_local_proxy, get_bot from pathlib import Path -from configs.config import plugins2info_dict from models.group_member_info import GroupInfoUser -import time from datetime import datetime from services.db_context import db from models.level_user import LevelUser from configs.config import ADMIN_DEFAULT_AUTH from utils.static_data import group_manager from utils.image_utils import CreateImg +from configs.config import plugins2info_dict +import aiofiles +import aiohttp import asyncio +import time +import os try: import ujson as json @@ -267,3 +267,7 @@ async def update_member_info(group_id: int) -> bool: user_id=int(list(bot.config.superusers)[0]), message=result[:-1] ) return True + + + + diff --git a/plugins/admin_bot_manage/switch_rule.py b/plugins/admin_bot_manage/switch_rule.py index 88d7d23a..80d6c5ac 100644 --- a/plugins/admin_bot_manage/switch_rule.py +++ b/plugins/admin_bot_manage/switch_rule.py @@ -46,6 +46,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): else: if str(event.user_id) in bot.config.superusers: block_type = get_message_text(event.json()) + block_type = block_type if block_type else 'a' _cmd = state["_prefix"]["raw_command"].strip() if is_number(block_type): if not int(block_type) in [g["group_id"] for g in await bot.get_group_list(self_id=int(bot.self_id))]: diff --git a/plugins/admin_help/__init__.py b/plugins/admin_help/__init__.py index 628fb1b1..37f15767 100644 --- a/plugins/admin_help/__init__.py +++ b/plugins/admin_help/__init__.py @@ -9,17 +9,19 @@ from configs.config import NICKNAME __plugin_name__ = '管理帮助 [Hidden]' -__plugin_usage__ = f'''管理帮助(权限等级): - 1.更新群组成员列表(1) - 2.功能开关 --> 指令:开启/关闭xx功能(2) - 3.查看群被动技能 --> 指令:群通知状态(2) - 4.自定义群欢迎 --> 指令:自定义进群欢迎消息(2) - 5.将用户拉入{NICKNAME}黑名单 --> .ban/.unban(5) - 6.刷屏禁言相关 --> 指令:刷屏检测设置/设置检测时间 - \t\t/设置检测次数/设置禁言时长(5) - 8.上传图片/连续上传图片(6) - 9.移动图片(7) - 10.删除图片(7) +__plugin_usage__ = f'''[权限等级]管理帮助: + [1]1.更新群组成员列表 + [2]2.功能开关 --> 指令:开启/关闭xx功能 + [2]3.查看群被动技能 --> 指令:群通知状态 + [2]4.自定义群欢迎 --> 指令:自定义进群欢迎消息 + [5]5.将用户拉入{NICKNAME}黑名单 --> .ban/.unban + [5]6.刷屏禁言相关 -> 指令:刷屏检测设置/设置检测时间 + \t\t/设置检测次数/设置禁言时长 + [5]7.群订阅相关 -> 指令:添加订阅 [主播/up/番剧] [id/番名/链接] + \t\t/删除订阅 [id]/ 查看订阅 + [6]8.上传图片/连续上传图片(6) + [7]9.移动图片(7) + [7]10.删除图片(7) 对我说 “{NICKNAME}帮助 指令” 获取对应详细帮助 群主与管理员默认 5 级权限 ''' @@ -37,11 +39,11 @@ passive_help = '''【被动技能开关(2): admin_help = on_command("管理员帮助", aliases={"管理帮助"}, priority=5, block=True) -admin_help_img = CreateImg(1000, 600, font_size=24) +admin_help_img = CreateImg(1200, 600, font_size=24) admin_help_img.text((10, 10), __plugin_usage__) text_img = CreateImg(450, 600, font_size=24) text_img.text((0, 0), passive_help) -admin_help_img.paste(text_img, (650, 50)) +admin_help_img.paste(text_img, (850, 50)) admin_help_img.save(IMAGE_PATH + 'admin_help_img.png') diff --git a/plugins/ai/__init__.py b/plugins/ai/__init__.py index a08a5823..7781e929 100644 --- a/plugins/ai/__init__.py +++ b/plugins/ai/__init__.py @@ -52,7 +52,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): result = await get_chat_result(msg, img, event.user_id, nickname) logger.info( f"USER {event.user_id} GROUP {event.group_id if not isinstance(event, PrivateMessageEvent) else ''} " - f"问题:{msg}\n回答:{result}" + f"问题:{msg} ---- 回答:{result}" ) if result: await ai.finish(Message(result)) diff --git a/plugins/ai/data_source.py b/plugins/ai/data_source.py index f4a03247..f4e07af3 100644 --- a/plugins/ai/data_source.py +++ b/plugins/ai/data_source.py @@ -23,7 +23,6 @@ index = 0 anime_data = json.load(open(DATA_PATH + "anime.json", "r", encoding="utf8")) -# 图灵AI async def get_chat_result(text: str, img_url: str, user_id: int, nickname: str) -> str: global index if index == 5: @@ -104,7 +103,9 @@ async def xie_ai(text: str, sess: ClientSession): if data["result"] == 0: content = data["content"] if "菲菲" in content: - content = content.replace("菲菲", f"{NICKNAME}") + content = content.replace("菲菲", NICKNAME) + if '艳儿' in content: + content = content.replace('艳儿', NICKNAME) if "公众号" in content: content = "" if "{br}" in content: diff --git a/plugins/alapi/wbtop.py b/plugins/alapi/wbtop.py index d3a83691..817e3a61 100644 --- a/plugins/alapi/wbtop.py +++ b/plugins/alapi/wbtop.py @@ -34,11 +34,12 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): if code != 200: await wbtop.finish(data, at_sender=True) wbtop_data = data['data'] - img = await asyncio.get_event_loop().run_in_executor(None, gen_wbtop_pic, wbtop_data) - await wbtop.send(img) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查询微博热搜") + if not msg: + img = await asyncio.get_event_loop().run_in_executor(None, gen_wbtop_pic, wbtop_data) + await wbtop.send(img) + logger.info( + f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 查询微博热搜") if is_number(msg) and 0 < int(msg) <= 50: url = wbtop_data[int(msg) - 1]['url'] browser = await get_browser() diff --git a/plugins/ban/__init__.py b/plugins/ban/__init__.py index af4d70c7..6a4c7eac 100644 --- a/plugins/ban/__init__.py +++ b/plugins/ban/__init__.py @@ -4,9 +4,9 @@ from models.level_user import LevelUser from nonebot.typing import T_State from nonebot.adapters.cqhttp import Bot from nonebot.adapters.cqhttp import GroupMessageEvent, PrivateMessageEvent -from nonebot.adapters.cqhttp.permission import GROUP from utils.utils import get_message_at, get_message_text, is_number from configs.config import NICKNAME +from nonebot.permission import SUPERUSER from services.log import logger @@ -29,6 +29,8 @@ ban = on_command( block=True, ) +super_ban = on_command('b了', permission=SUPERUSER, priority=5, block=True) + @ban.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): @@ -155,5 +157,17 @@ async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): await ban.finish('qq号必须是数字!\n格式:.ban [qq] [hour]? [minute]?', at_sender=True) - +@super_ban.handle() +async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + qq = get_message_at(event.json()) + if qq: + qq = qq[0] + user = await bot.get_group_member_info(group_id=event.group_id, user_id=qq) + user_name = user['card'] if user['card'] else user['nickname'] + if not await BanUser.ban(qq, 10, 99999999): + await BanUser.unban(qq) + await BanUser.ban(qq, 10, 99999999) + await ban.send(f"{user_name} 已在黑名单!预计不解封了..") + else: + await super_ban.send('需要艾特被super ban的对象..') diff --git a/plugins/bilibili_sub/__init__.py b/plugins/bilibili_sub/__init__.py new file mode 100644 index 00000000..f6491c0a --- /dev/null +++ b/plugins/bilibili_sub/__init__.py @@ -0,0 +1,198 @@ +from nonebot import on_command +from nonebot.typing import T_State +from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent, Message +from .data_source import ( + add_live_sub, + delete_sub, + add_up_sub, + add_season_sub, + get_media_id, + get_sub_status, + SubManager, +) +from models.level_user import LevelUser +from configs.config import GROUP_BILIBILI_SUB_LEVEL +from utils.utils import get_message_text, is_number, scheduler, get_bot +from models.bilibili_sub import BilibiliSub +from typing import Optional +from services.log import logger +from nonebot import Driver +import nonebot + +__plugin_name__ = "B站订阅" + +__plugin_usage__ = """B站订阅帮助: + 添加订阅 [主播/UP/番剧] [id/链接/番名] + 删除订阅 [id] + 查看订阅""" + +add_sub = on_command("添加订阅", priority=5, block=True) +del_sub = on_command("删除订阅", priority=5, block=True) +show_sub_info = on_command('查看订阅', priority=5, block=True) + +driver: Driver = nonebot.get_driver() + + +sub_manager: Optional[SubManager] = None + + +@driver.on_startup +async def _(): + global sub_manager + sub_manager = SubManager() + + +@add_sub.args_parser +async def _(bot: Bot, event: MessageEvent, state: T_State): + season_data = state["season_data"] + msg = get_message_text(event.json()) + if not is_number(msg) or int(msg) < 1 or int(msg) > len(season_data): + await add_sub.reject("Id必须为数字且在范围内!请重新输入...") + state["id"] = season_data[int(msg) - 1]["media_id"] + + +@add_sub.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): + msg = get_message_text(event.json()).split() + if len(msg) < 2: + await add_sub.finish("参数不完全,请查看订阅帮助...") + sub_type = msg[0] + id_ = "" + if isinstance(event, GroupMessageEvent): + if not await LevelUser.check_level( + event.user_id, event.group_id, GROUP_BILIBILI_SUB_LEVEL + ): + await add_sub.finish( + f"您的权限不足,群内订阅的需要 {GROUP_BILIBILI_SUB_LEVEL} 级权限..", at_sender=True + ) + sub_user = f"{event.user_id}:{event.group_id}" + else: + sub_user = f"{event.user_id}" + state["sub_type"] = sub_type + state["sub_user"] = sub_user + if len(msg) > 1: + if "http" in msg[1]: + msg[1] = msg[1].split("?")[0] + msg[1] = msg[1][:-1] if msg[1][-1] == "/" else msg[1] + msg[1] = msg[1].split("/")[-1] + id_ = msg[1][2:] if msg[1].startswith("md") else msg[1] + if not is_number(id_): + if sub_type in ["season", "动漫", "番剧"]: + rst = "*以为您找到以下番剧,请输入Id选择:*\n" + state["season_data"] = await get_media_id(id_) + print(state["season_data"]) + if len(state["season_data"]) == 0: + await add_sub.finish(f"未找到番剧:{msg}") + for i, x in enumerate(state["season_data"]): + rst += f'{i + 1}.{state["season_data"][x]["title"]}\n----------\n' + await add_sub.send("\n".join(rst.split("\n")[:-1])) + else: + await add_sub.finish("Id 必须为全数字!") + else: + state["id"] = int(id_) + + +@add_sub.got("id") +async def _(bot: Bot, event: MessageEvent, state: T_State): + sub_type = state["sub_type"] + sub_user = state["sub_user"] + id_ = state["id"] + if sub_type in ["主播", "直播"]: + await add_sub.send(await add_live_sub(id_, sub_user)) + elif sub_type.lower() in ["up", "用户"]: + await add_sub.send(await add_up_sub(id_, sub_user)) + elif sub_type in ["season", "动漫", "番剧"]: + await add_sub.send(await add_season_sub(id_, sub_user)) + else: + await add_sub.finish("参数错误,第一参数必须为:主播/up/番剧!") + sub_manager.reload_flag = True + logger.info( + f"(USER {event.user_id}, GROUP " + f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 添加订阅:{sub_type} -> {sub_user} -> {id_}" + ) + + +@del_sub.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): + msg = get_message_text(event.json()) + if not is_number(msg): + await del_sub.finish('Id必须为数字!', at_sender=True) + id_ = f'{event.user_id}:{event.group_id}' if isinstance(event, GroupMessageEvent) else f'{event.user_id}' + if await BilibiliSub.delete_bilibili_sub(int(msg), id_): + await del_sub.send(f'删除订阅id:{msg} 成功...') + logger.info( + f"(USER {event.user_id}, GROUP " + f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 删除订阅 {id_}" + ) + else: + await del_sub.send(f'删除订阅id:{msg} 失败...') + + +@show_sub_info.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): + id_ = f'{event.user_id}:{event.group_id}' if isinstance(event, GroupMessageEvent) else f'{event.user_id}' + data = await BilibiliSub.get_sub_data(id_) + live_rst = '' + up_rst = '' + season_rst = '' + for x in data: + if x.sub_type == 'live': + live_rst += f'\t直播间id:{x.sub_id}\n' \ + f'\t名称:{x.uname}\n' \ + f'------------------\n' + if x.sub_type == 'up': + up_rst += f'\tUP:{x.uname}\n' \ + f'\tuid:{x.uid}\n' \ + f'------------------\n' + if x.sub_type == 'season': + season_rst += f'\t番名:{x.season_name}\n' \ + f'\t当前集数:{x.season_current_episode}\n' \ + f'------------------\n' + live_rst = '当前订阅的直播:\n' + live_rst if live_rst else live_rst + up_rst = '当前订阅的UP:\n' if up_rst else up_rst + season_rst = '当前订阅的番剧:\n' if season_rst else season_rst + await show_sub_info.send(live_rst + up_rst + season_rst) + + +# 推送 +@scheduler.scheduled_job( + "interval", + seconds=30, +) +async def _(): + bot = get_bot() + sub = None + if bot: + try: + await sub_manager.reload_sub_data() + sub = await sub_manager.random_sub_data() + if sub: + rst = await get_sub_status(sub.sub_id, sub.sub_type) + await send_sub_msg(rst, sub, bot) + if sub.sub_type == "live": + rst = await get_sub_status(sub.sub_id, "up") + await send_sub_msg(rst, sub, bot) + except Exception as e: + logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id if sub else 0} {type(e)}:{e}") + + +async def send_sub_msg(rst: str, sub: BilibiliSub, bot: Bot): + """ + 推送信息 + :param rst: 回复 + :param sub: BilibiliSub + :param bot: Bot + """ + if rst: + for x in sub.sub_users.split(",")[:-1]: + try: + if ":" in x: + await bot.send_group_msg( + group_id=int(x.split(":")[1]), message=Message(rst) + ) + else: + await bot.send_private_msg(user_id=int(x), message=Message(rst)) + except Exception as e: + logger.error(f"B站订阅推送发生错误 sub_id:{sub.sub_id} {type(e)}:{e}") diff --git a/plugins/bilibili_sub/data_source.py b/plugins/bilibili_sub/data_source.py new file mode 100644 index 00000000..d4032287 --- /dev/null +++ b/plugins/bilibili_sub/data_source.py @@ -0,0 +1,379 @@ +from bilibili_api.exceptions.ResponseCodeException import ResponseCodeException +from asyncio.exceptions import TimeoutError +from models.bilibili_sub import BilibiliSub +from bilibili_api.live import LiveRoom +from bilibili_api import bangumi +from utils.message_builder import image +from bilibili_api.user import User +from bilibili_api import user +from typing import Optional +from pathlib import Path +from configs.path_config import IMAGE_PATH +from datetime import datetime +from utils.browser import get_browser +from services.db_context import db +from services.log import logger +import aiohttp +import random + + +bilibili_search_url = "https://api.bilibili.com/x/web-interface/search/all/v2" + +dynamic_path = Path(IMAGE_PATH) / "bilibili_sub" / "dynamic" +dynamic_path.mkdir(exist_ok=True, parents=True) + + +async def add_live_sub(live_id: int, sub_user: str) -> str: + """ + 添加直播订阅 + :param live_id: 直播房间号 + :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) + :return: + """ + try: + async with db.transaction(): + try: + live = LiveRoom(live_id) + live_info = (await live.get_room_info())["room_info"] + except ResponseCodeException: + return f"未找到房间号Id:{live_id} 的信息,请检查Id是否正确" + uid = live_info["uid"] + room_id = live_info["room_id"] + short_id = live_info["short_id"] + title = live_info["title"] + live_status = live_info["live_status"] + if await BilibiliSub.add_bilibili_sub( + room_id, + "live", + sub_user, + uid=uid, + live_short_id=short_id, + live_status=live_status, + ): + await _get_up_status(live_id) + uname = (await BilibiliSub.get_sub(live_id)).uname + return ( + "已成功订阅主播:\n" + f"\ttitle:{title}\n" + f"\tname: {uname}\n" + f"\tlive_id:{live_id}\n" + f"\tuid:{uid}" + ) + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅主播live_id:{live_id} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def add_up_sub(uid: int, sub_user: str) -> str: + """ + 添加订阅 UP + :param uid: UP uid + :param sub_user: 订阅用户 + """ + try: + async with db.transaction(): + try: + u = user.User(uid) + user_info = await u.get_user_info() + except ResponseCodeException: + return f"未找到UpId:{uid} 的信息,请检查Id是否正确" + uname = user_info["name"] + dynamic_info = await u.get_dynamics(0) + dynamic_upload_time = 0 + if dynamic_info.get("cards"): + dynamic_upload_time = dynamic_info["cards"][0]["desc"]["timestamp"] + video_info = await u.get_videos() + latest_video_created = 0 + if video_info["list"].get("vlist"): + latest_video_created = video_info["list"]["vlist"][0]["created"] + if await BilibiliSub.add_bilibili_sub( + uid, + "up", + sub_user, + uid=uid, + uname=uname, + dynamic_upload_time=dynamic_upload_time, + latest_video_created=latest_video_created, + ): + return "已成功订阅UP:\n" f"\tname: {uname}\n" f"\tuid:{uid}" + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅Up uid:{uid} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def add_season_sub(media_id: int, sub_user: str) -> str: + """ + 添加订阅 UP + :param media_id: 番剧 media_id + :param sub_user: 订阅用户 + """ + try: + async with db.transaction(): + try: + season_info = await bangumi.get_meta(media_id) + except ResponseCodeException: + return f"未找到media_id:{media_id} 的信息,请检查Id是否正确" + season_id = season_info["media"]["season_id"] + season_current_episode = season_info["media"]["new_ep"]["index"] + season_name = season_info["media"]["title"] + if await BilibiliSub.add_bilibili_sub( + media_id, + "season", + sub_user, + season_name=season_name, + season_id=season_id, + season_current_episode=season_current_episode, + ): + return ( + "已成功订阅番剧:\n" + f"\ttitle: {season_name}\n" + f"\tcurrent_episode: {season_current_episode}" + ) + else: + return "添加订阅失败..." + except Exception as e: + logger.error(f"订阅番剧 media_id:{media_id} 发生了错误 {type(e)}:{e}") + return "添加订阅失败..." + + +async def delete_sub(sub_id: str, sub_user: str) -> str: + """ + 删除订阅 + :param sub_id: 订阅 id + :param sub_user: 订阅用户 id # 7384933:private or 7384933:2342344(group) + """ + if await BilibiliSub.delete_bilibili_sub(sub_id, sub_user): + return f"已成功取消订阅:{sub_id}" + else: + return f"取消订阅:{sub_id} 失败,请检查是否订阅过该Id...." + + +async def get_media_id(keyword: str) -> dict: + """ + 获取番剧的 media_id + :param keyword: 番剧名称 + """ + params = {"keyword": keyword} + async with aiohttp.ClientSession() as session: + for _ in range(3): + try: + _season_data = {} + async with session.get( + bilibili_search_url, timeout=5, params=params + ) as response: + if response.status == 200: + data = await response.json() + if data.get("data"): + for item in data["data"]["result"]: + if item["result_type"] == "media_bangumi": + idx = 0 + for x in item["data"]: + _season_data[idx] = { + "media_id": x["media_id"], + "title": x["title"] + .replace('', "") + .replace("", ""), + } + idx += 1 + return _season_data + except TimeoutError: + pass + return {} + + +async def get_sub_status(id_: int, sub_type: str) -> Optional[str]: + """ + 获取订阅状态 + :param id_: 订阅 id + :param sub_type: 订阅类型 + :return: + """ + try: + if sub_type == "live": + return await _get_live_status(id_) + elif sub_type == "up": + return await _get_up_status(id_) + elif sub_type == "season": + return await _get_season_status(id_) + except ResponseCodeException: + return "获取信息失败...请检查订阅Id是否存在或稍后再试..." + # except Exception as e: + # logger.error(f"获取订阅状态发生预料之外的错误 id_:{id_} {type(e)}:{e}") + # return "发生了预料之外的错误..请稍后再试或联系管理员....." + + +async def _get_live_status(id_: int) -> Optional[str]: + """ + 获取直播订阅状态 + :param id_: 直播间 id + """ + live = LiveRoom(id_) + live_info = (await live.get_room_info())["room_info"] + title = live_info["title"] + room_id = live_info["room_id"] + live_status = live_info["live_status"] + cover = live_info["cover"] + sub = await BilibiliSub.get_sub(id_) + if sub.live_status != live_status: + await BilibiliSub.update_sub_info(id_, live_status=live_status) + if sub.live_status == 0 and live_status == 1: + return ( + f"{image(cover)}\n" + f"{sub.uname} 开播啦!\n" + f"标题:{title}\n" + f"直链:https://live.bilibili.com/{room_id}" + ) + return None + + +async def _get_up_status(id_: int) -> Optional[str]: + """ + 获取用户投稿状态 + :param id_: 用户 id + :return: + """ + _user = await BilibiliSub.get_sub(id_) + u = user.User(_user.uid) + user_info = await u.get_user_info() + uname = user_info["name"] + video_info = await u.get_videos() + latest_video_created = 0 + video = None + if _user.uname != uname: + await BilibiliSub.update_sub_info(id_, uname=uname) + dynamic_img, dynamic_upload_time = await get_user_dynamic(u, _user) + if video_info["list"].get("vlist"): + video = video_info["list"]["vlist"][0] + latest_video_created = video["created"] + rst = "" + if dynamic_img: + await BilibiliSub.update_sub_info(id_, dynamic_upload_time=dynamic_upload_time) + rst += f"{uname} 发布了动态!\n" f"{dynamic_img}\n" + if _user.latest_video_created != latest_video_created and video: + rst = rst + "-------------\n" if rst else rst + await BilibiliSub.update_sub_info( + id_, latest_video_created=latest_video_created + ) + rst += ( + f'{image(video["pic"])}\n' + f"{uname} 投稿了新视频啦\n" + f'标题:{video["title"]}\n' + f'Bvid:{video["bvid"]}\n' + f'直链:https://www.bilibili.com/video/{video["bvid"]}' + ) + rst = None if rst == "-------------\n" else rst + return rst + + +async def _get_season_status(id_) -> Optional[str]: + """ + 获取 番剧 更新状态 + :param id_: 番剧 id + """ + season_info = await bangumi.get_meta(id_) + title = season_info["media"]["title"] + _idx = (await BilibiliSub.get_sub(id_)).season_current_episode + new_ep = season_info["media"]["new_ep"]["index"] + if new_ep != _idx: + await BilibiliSub.update_sub_info(id_, season_current_episode=new_ep, season_update_time=datetime.now()) + return ( + f'{image(season_info["media"]["cover"])}\n' f"[{title}]更新啦\n" f"最新集数:{new_ep}" + ) + return None + + +async def get_user_dynamic( + u: User, local_user: BilibiliSub +) -> "Optional[MessageSegment], int": + """ + 获取用户动态 + :param u: 用户类 + :param local_user: 数据库存储的用户数据 + :return: 最新动态截图与时间 + """ + dynamic_info = await u.get_dynamics(0) + browser = await get_browser() + if dynamic_info.get("cards") and browser: + dynamic_upload_time = dynamic_info["cards"][0]["desc"]["timestamp"] + if local_user.dynamic_upload_time != dynamic_upload_time: + page = await browser.new_page() + await page.goto( + f"https://space.bilibili.com/{local_user.uid}/dynamic", + wait_until="networkidle", + timeout=10000, + ) + await page.set_viewport_size({"width": 2560, "height": 1080}) + # 删除置顶 + await page.evaluate( + """ + xs = document.getElementsByClassName('first-card-with-title'); + for (x of xs) { + x.remove(); + } + """ + ) + card = await page.query_selector(".card") + # 截图并保存 + await card.screenshot( + path=dynamic_path / f"{local_user.sub_id}_{dynamic_upload_time}.jpg", + timeout=100000, + ) + await page.close() + return ( + image( + f"{local_user.sub_id}_{dynamic_upload_time}.jpg", + "bilibili_sub/dynamic", + ), + dynamic_upload_time, + ) + return None, None + + +class SubManager: + def __init__(self): + self.reload_flag = True + self.live_data = [] + self.up_data = [] + self.season_data = [] + self.sub_list = [] + + async def reload_sub_data(self): + """ + 重载数据 + """ + if self.reload_flag or not self.sub_list: + ( + self.live_data, + self.up_data, + self.season_data, + ) = await BilibiliSub.get_all_sub_data() + for x, i, j in zip(self.live_data, self.up_data, self.season_data): + self.sub_list.append(x) + self.sub_list.append(i) + self.sub_list.append(j) + self.reload_flag = False + + def append(self, data: BilibiliSub): + """ + 增加新数据 + :param data: 数据 + """ + self.sub_list.append(data) + + async def random_sub_data(self) -> Optional[BilibiliSub]: + """ + 随机获取一条数据 + :return: + """ + if not self.sub_list: + await self.reload_sub_data() + if self.sub_list: + sub = random.choice(self.sub_list) + self.sub_list.remove(sub) + return sub + return None + diff --git a/plugins/bilibili_sub/utils.py b/plugins/bilibili_sub/utils.py new file mode 100644 index 00000000..89325453 --- /dev/null +++ b/plugins/bilibili_sub/utils.py @@ -0,0 +1,72 @@ +from utils.image_utils import CreateImg +from configs.path_config import IMAGE_PATH +from pathlib import Path +from bilibili_api import user +from io import BytesIO +import aiohttp + + +BORDER_PATH = Path(IMAGE_PATH) / 'border' +BORDER_PATH.mkdir(parents=True, exist_ok=True) + + +async def get_pic(url: str) -> bytes: + """ + 获取图像 + :param url: 图像链接 + :return: 图像二进制 + """ + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=2) as response: + return await response.read() + + +async def create_live_des_image(uid: int, title: str, cover: str, tags: str, des: str): + """ + 生成主播简介图片 + :param uid: 主播 uid + :param title: 直播间标题 + :param cover: 直播封面 + :param tags: 直播标签 + :param des: 直播简介 + :return: + """ + u = user.User(uid) + user_info = await u.get_user_info() + name = user_info['name'] + sex = user_info['sex'] + face = user_info['face'] + sign = user_info['sign'] + ava = CreateImg(100, 100, background=BytesIO(await get_pic(face))) + ava.circle() + cover = CreateImg(470, 265, background=BytesIO(await get_pic(cover))) + print() + + +def _create_live_des_image(title: str, cover: CreateImg, tags: str, des: str, user_name: str, sex: str, sign: str, ava: CreateImg): + """ + 生成主播简介图片 + :param title: 直播间标题 + :param cover: 直播封面 + :param tags: 直播标签 + :param des: 直播简介 + :param user_name: 主播名称 + :param sex: 主播性别 + :param sign: 主播签名 + :param ava: 主播头像 + :return: + """ + border = BORDER_PATH / '0.png' + border_img = None + if border.exists(): + border_img = CreateImg(1772, 2657, background=border) + bk = CreateImg(1772, 2657, font_size=30) + bk.paste(cover, (0, 100), center_type='by_width') + + + + + + + + diff --git a/plugins/bt/__init__.py b/plugins/bt/__init__.py index 86431c09..46af7cce 100644 --- a/plugins/bt/__init__.py +++ b/plugins/bt/__init__.py @@ -54,7 +54,7 @@ async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): @bt.got("keyword", prompt="虚空磁力?查什么GKD") async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) keyword = state["keyword"] page = state["page"] await bt.send("开始搜索....", at_sender=True) @@ -72,16 +72,16 @@ async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): ) send_flag = True except TimeoutError: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await bt.finish(f"搜索 {keyword} 超时...") except ServerDisconnectedError: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await bt.finish(f"搜索 {keyword} 连接失败") except Exception as e: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await bt.finish(f"bt 其他未知错误..") logger.error(f"bt 错误 e:{e}") if not send_flag: await bt.send(f"{keyword} 未搜索到...") logger.info(f"USER {event.user_id} BT搜索 {keyword} 第 {page} 页") - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) diff --git a/plugins/check_zhenxun_update/__init__.py b/plugins/check_zhenxun_update/__init__.py index d83ceda5..68564233 100644 --- a/plugins/check_zhenxun_update/__init__.py +++ b/plugins/check_zhenxun_update/__init__.py @@ -44,7 +44,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): @restart.got('flag', prompt='确定是否重启真寻?(重启失败咱们将失去联系,请谨慎!)') async def _(bot: Bot, event: MessageEvent, state: T_State): flag = state['flag'] - if flag.lower() in ['true', '是', '好']: + if flag.lower() in ['true', '是', '好', '确定', '确定是']: await restart.send('开始重启真寻..请稍等...') open('is_restart', 'w') os.system('./restart.sh') diff --git a/plugins/check_zhenxun_update/data_source.py b/plugins/check_zhenxun_update/data_source.py index 9e010abe..284adfc7 100644 --- a/plugins/check_zhenxun_update/data_source.py +++ b/plugins/check_zhenxun_update/data_source.py @@ -1,9 +1,10 @@ from aiohttp.client_exceptions import ClientConnectorError -from nonebot.adapters.cqhttp import Bot +from nonebot.adapters.cqhttp import Bot, Message from utils.user_agent import get_user_agent from utils.utils import get_local_proxy from utils.image_utils import CreateImg from configs.path_config import IMAGE_PATH +from utils.message_builder import image from typing import List from services.log import logger from pathlib import Path @@ -84,13 +85,6 @@ async def check_update(bot: Bot) -> int: logger.info("真寻更新完毕,清理文件完成....") logger.info("开始获取真寻更新日志.....") update_info = data["body"] - await bot.send_private_msg( - user_id=int(list(bot.config.superusers)[0]), - message=f"真寻更新完成,版本:{_version} -> {latest_version}\n" - f"更新日期:{data['created_at']}\n" - f"更新日志:\n" - f"{update_info}", - ) width = 0 height = len(update_info.split('\n')) * 24 for m in update_info.split('\n'): @@ -99,6 +93,13 @@ async def check_update(bot: Bot) -> int: A = CreateImg(width, height, font_size=20) A.text((10, 10), update_info) A.save(f'{IMAGE_PATH}/update_info.png') + await bot.send_private_msg( + user_id=int(list(bot.config.superusers)[0]), + message=Message(f"真寻更新完成,版本:{_version} -> {latest_version}\n" + f"更新日期:{data['created_at']}\n" + f"更新日志:\n" + f"{image('update_info.png')}"), + ) return 200 else: logger.warning(f"下载真寻最新版本失败...版本号:{latest_version}") diff --git a/plugins/coser/__init__.py b/plugins/coser/__init__.py index 2197618c..8afa2fb3 100644 --- a/plugins/coser/__init__.py +++ b/plugins/coser/__init__.py @@ -2,8 +2,12 @@ from nonebot import on_command from nonebot.typing import T_State from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent from services.log import logger +from asyncio.exceptions import TimeoutError from utils.message_builder import image -import requests +from configs.path_config import IMAGE_PATH +import aiohttp +import aiofiles +import re __plugin_name__ = "coser" @@ -15,13 +19,31 @@ coser = on_command( ) -url_2 = "http://api.rosysun.cn/cos" +url = "http://81.70.100.130/api/cosplay.php" @coser.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): - img_url = requests.get(url_2).text - await coser.send(image(img_url)) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 发送COSER" - ) + async with aiohttp.ClientSession() as session: + try: + for _ in range(3): + try: + async with session.get(url, timeout=2) as response: + r = re.search(r'±img=(.*)±', await response.text()) + if r: + async with session.get(r.group(1), timeout=5, verify_ssl=False) as res: + async with aiofiles.open(f'{IMAGE_PATH}/temp/{event.user_id}_coser.jpg', 'wb') as f: + await f.write(await res.read()) + logger.info( + f"(USER {event.user_id}, " + f"GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 发送COSER" + ) + await coser.send(image(f'{event.user_id}_coser.jpg', 'temp')) + break + except TimeoutError: + pass + except Exception as e: + await coser.send('发生了预料之外的错误..请稍后再试或联系管理员修复...') + logger.error(f'coser 发送了未知错误 {type(e)}:{e}') + diff --git a/plugins/database_scripts.py b/plugins/database_scripts.py new file mode 100644 index 00000000..160a31af --- /dev/null +++ b/plugins/database_scripts.py @@ -0,0 +1,29 @@ +from pathlib import Path +from services.db_context import db +from asyncpg.exceptions import DuplicateColumnError +import nonebot + +driver = nonebot.get_driver() + + +@driver.on_startup +async def _init_database(): + file = Path() / 'plugins' / 'database_scripts.py' + if file.exists(): + update_sql = [ + 'ALTER TABLE russian_users ADD winning_streak Integer default 0;', + 'ALTER TABLE russian_users ADD losing_streak Integer default 0;', + 'ALTER TABLE russian_users ADD max_winning_streak Integer default 0;', + 'ALTER TABLE russian_users ADD max_losing_streak Integer default 0;', + 'ALTER TABLE group_info_users ADD uid Integer default 0;' + ] + for sql in update_sql: + try: + query = db.text(sql) + await db.first(query) + except DuplicateColumnError: + pass + file.unlink() + + + diff --git a/plugins/draw_card/announcement.py b/plugins/draw_card/announcement.py index f0fc7c98..819fc8d5 100644 --- a/plugins/draw_card/announcement.py +++ b/plugins/draw_card/announcement.py @@ -1,7 +1,7 @@ import aiohttp from bs4 import BeautifulSoup import re -from datetime import datetime +from datetime import datetime, timedelta from .config import DRAW_PATH from pathlib import Path from asyncio.exceptions import TimeoutError @@ -177,6 +177,11 @@ class GenshinAnnouncement: data[itype]['time'] = trs[1].find('td').text if data[itype]['time'][-1] == '\n': data[itype]['time'] = data[itype]['time'][:-1] + if '版本更新后' in data[itype]['time']: + sp = data[itype]['time'].split('~') + end_time = datetime.strptime(sp[1].strip(), "%Y/%m/%d %H:%M:%S") + start_time = end_time - timedelta(days=20) + data[itype]['time'] = start_time.strftime('%Y/%m/%d') + ' ~ ' + end_time.strftime('%Y/%m/%d') tmp = '' for tm in data[itype]['time'].split('~'): date_time_sp = tm.split('/') diff --git a/plugins/fudu.py b/plugins/fudu.py index a3ac3d5b..c8406722 100644 --- a/plugins/fudu.py +++ b/plugins/fudu.py @@ -9,7 +9,6 @@ from nonebot.typing import T_State from nonebot.adapters.cqhttp import Bot, GroupMessageEvent import aiohttp import aiofiles -from collections import defaultdict from asyncio.exceptions import TimeoutError from configs.config import FUDU_PROBABILITY @@ -20,39 +19,39 @@ class Fudu: def append(self, key, content): self._create(key) - self.data[key]['data'].append(content) + self.data[key]["data"].append(content) def clear(self, key): self._create(key) - self.data[key]['data'] = [] - self.data[key]['is_repeater'] = False + self.data[key]["data"] = [] + self.data[key]["is_repeater"] = False def size(self, key) -> int: self._create(key) - return len(self.data[key]['data']) + return len(self.data[key]["data"]) def check(self, key, content) -> bool: self._create(key) - return self.data[key]['data'][0] == content + return self.data[key]["data"][0] == content def get(self, key): self._create(key) - return self.data[key]['data'][0] + return self.data[key]["data"][0] def is_repeater(self, key): self._create(key) - return self.data[key]['is_repeater'] + return self.data[key]["is_repeater"] def set_repeater(self, key): self._create(key) - self.data[key]['is_repeater'] = True + self.data[key]["is_repeater"] = True def _create(self, key): if self.data.get(key) is None: - self.data[key] = {'is_repeater': False, 'data': []} + self.data[key] = {"is_repeater": False, "data": []} -_fudulist = Fudu() +_fudu_list = Fudu() fudu = on_message(permission=GROUP, priority=9) @@ -74,17 +73,20 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): else: img_hash = "" add_msg = msg + "|-|" + img_hash - if _fudulist.size(event.group_id) == 0: - _fudulist.append(event.group_id, add_msg) - elif _fudulist.check(event.group_id, add_msg): - _fudulist.append(event.group_id, add_msg) + if _fudu_list.size(event.group_id) == 0: + _fudu_list.append(event.group_id, add_msg) + elif _fudu_list.check(event.group_id, add_msg): + _fudu_list.append(event.group_id, add_msg) else: - _fudulist.clear(event.group_id) - _fudulist.append(event.group_id, add_msg) - if _fudulist.size(event.group_id) > 2: - if random.random() < FUDU_PROBABILITY and not _fudulist.is_repeater(event.group_id): + _fudu_list.clear(event.group_id) + _fudu_list.append(event.group_id, add_msg) + if _fudu_list.size(event.group_id) > 2: + if random.random() < FUDU_PROBABILITY and not _fudu_list.is_repeater( + event.group_id + ): if random.random() < 0.2: await fudu.finish("打断施法!") + _fudu_list.set_repeater(event.group_id) if imgs and msg: rst = msg + image(f"compare_{event.group_id}_img.jpg", "temp") elif imgs: @@ -95,7 +97,6 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): rst = "" if rst: await fudu.send(rst) - _fudulist.set_repeater(event.group_id) async def get_fudu_img_hash(url, group_id): @@ -109,4 +110,4 @@ async def get_fudu_img_hash(url, group_id): img_hash = get_img_hash(IMAGE_PATH + f"temp/compare_{group_id}_img.jpg") return str(img_hash) except TimeoutError: - return '' + return "" diff --git a/plugins/genshin/query_resource_points/__init__.py b/plugins/genshin/query_resource_points/__init__.py index 9211954f..caa2d0e3 100644 --- a/plugins/genshin/query_resource_points/__init__.py +++ b/plugins/genshin/query_resource_points/__init__.py @@ -5,6 +5,7 @@ from utils.utils import get_message_text, scheduler from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent, Message from nonebot.typing import T_State from services.log import logger +from configs.config import NICKNAME from nonebot.permission import SUPERUSER import re @@ -58,21 +59,20 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): @qr_lst.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): - mes_list = [] txt = get_resource_type_list() txt_list = txt.split("\n") - if event.message_type == "group": + if isinstance(event, GroupMessageEvent): + mes_list = [] for txt in txt_list: data = { "type": "node", "data": { - "name": f"这里是{list(bot.config.nickname)[0]}酱", + "name": f"这里是{NICKNAME}酱", "uin": f"{bot.self_id}", "content": txt, }, } mes_list.append(data) - if isinstance(event, GroupMessageEvent): await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) else: rst = "" diff --git a/plugins/group_handle/__init__.py b/plugins/group_handle/__init__.py index 64344ab5..310329e5 100644 --- a/plugins/group_handle/__init__.py +++ b/plugins/group_handle/__init__.py @@ -1,8 +1,6 @@ from nonebot import on_notice, on_request from configs.path_config import IMAGE_PATH, DATA_PATH from utils.message_builder import image -import os -import random from models.group_member_info import GroupInfoUser from datetime import datetime from services.log import logger @@ -13,9 +11,12 @@ from nonebot.adapters.cqhttp import ( GroupDecreaseNoticeEvent, ) from nonebot.adapters.cqhttp.exception import ActionFailed -from pathlib import Path +from configs.config import plugins2info_dict +from utils.static_data import group_manager from models.group_info import GroupInfo - +from pathlib import Path +import random +import os try: import ujson as json except ModuleNotFoundError: @@ -37,46 +38,52 @@ add_group = on_request(priority=1, block=False) @group_increase_handle.handle() async def _(bot: Bot, event: GroupIncreaseNoticeEvent, state: dict): - join_time = datetime.now() - user_info = await bot.get_group_member_info( - group_id=event.group_id, user_id=event.user_id - ) - if await GroupInfoUser.add_member_info( - user_info["user_id"], - user_info["group_id"], - user_info["nickname"], - join_time, - ): - logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") + if event.user_id == int(bot.self_id): + if event.group_id not in group_manager['group_manager'].keys(): + for plugin in plugins2info_dict.keys(): + if not plugins2info_dict[plugin]['default_status']: + group_manager.block_plugin(plugin, str(event.group_id)) else: - logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新失败") - - # 群欢迎消息 - if await GroupRemind.get_status(event.group_id, "hy"): - msg = "" - img = "" - at_flag = False - custom_welcome_msg_json = ( - Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" + join_time = datetime.now() + user_info = await bot.get_group_member_info( + group_id=event.group_id, user_id=event.user_id ) - if custom_welcome_msg_json.exists(): - data = json.load(open(custom_welcome_msg_json, "r")) - if data.get(str(event.group_id)): - msg = data[str(event.group_id)] - if msg.find("[at]") != -1: - msg = msg.replace("[at]", "") - at_flag = True - if os.path.exists(DATA_PATH + f"custom_welcome_msg/{event.group_id}.jpg"): - img = image(abspath=DATA_PATH + f"custom_welcome_msg/{event.group_id}.jpg") - if msg or img: - await group_increase_handle.send( - "\n" + msg.strip() + img, at_sender=at_flag - ) + if await GroupInfoUser.add_member_info( + user_info["user_id"], + user_info["group_id"], + user_info["nickname"], + join_time, + ): + logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新成功") else: - await group_increase_handle.send( - "新人快跑啊!!本群现状↓(快使用自定义!)" - + image(random.choice(os.listdir(IMAGE_PATH + "qxz/")), "qxz") + logger.info(f"用户{user_info['user_id']} 所属{user_info['group_id']} 更新失败") + + # 群欢迎消息 + if await GroupRemind.get_status(event.group_id, "hy"): + msg = "" + img = "" + at_flag = False + custom_welcome_msg_json = ( + Path() / "data" / "custom_welcome_msg" / "custom_welcome_msg.json" ) + if custom_welcome_msg_json.exists(): + data = json.load(open(custom_welcome_msg_json, "r")) + if data.get(str(event.group_id)): + msg = data[str(event.group_id)] + if msg.find("[at]") != -1: + msg = msg.replace("[at]", "") + at_flag = True + if os.path.exists(DATA_PATH + f"custom_welcome_msg/{event.group_id}.jpg"): + img = image(abspath=DATA_PATH + f"custom_welcome_msg/{event.group_id}.jpg") + if msg or img: + await group_increase_handle.send( + "\n" + msg.strip() + img, at_sender=at_flag + ) + else: + await group_increase_handle.send( + "新人快跑啊!!本群现状↓(快使用自定义!)" + + image(random.choice(os.listdir(IMAGE_PATH + "qxz/")), "qxz") + ) @group_decrease_handle.handle() diff --git a/plugins/group_manager/__init__.py b/plugins/group_manager/__init__.py index 1ef81650..b294187b 100644 --- a/plugins/group_manager/__init__.py +++ b/plugins/group_manager/__init__.py @@ -33,6 +33,8 @@ manager_group_whitelist = on_command( "添加群白名单", aliases={"删除群白名单"}, priority=1, permission=SUPERUSER, block=True ) +show_group_whitelist = on_command('查看群白名单', priority=1, permission=SUPERUSER, block=True) + @add_group_level.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): @@ -100,3 +102,13 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): ) else: await manager_group_whitelist.send(f"添加失败,请检查{NICKNAME}是否已加入这些群聊或重复添加/删除群白单名") + + +@show_group_whitelist.handle() +async def _(bot: Bot, event: MessageEvent, state: T_State): + x = group_manager.get_group_white_list() + x = [str(g) for g in x] + if x: + await show_group_whitelist.send("目前的群白名单:\n" + '\n'.join(x)) + else: + await show_group_whitelist.send('没有任何群在群白名单...') diff --git a/plugins/help/config.py b/plugins/help/config.py index 6b2c9b9c..08b02c28 100644 --- a/plugins/help/config.py +++ b/plugins/help/config.py @@ -54,7 +54,8 @@ entertainment_help = { "pix_gallery": "偶尔也想看看美图? --> 指令:PIX [关键词/uid/pid:pid] [num]/查看pix图库 [关键词]/显示pix关键词", 'nbnhhsh': "会说话就多说点! --> 指令:nbnhhsh/能不能好好说话 [文本](空格划分)", "roll": f"让{NICKNAME}来帮你决定吧! --> 指令:roll/ roll [文本](空格划分)", - "wbtop": f"刚买完瓜,在吃瓜现场 --> 指令:微博热搜/微博热搜 [数字]" + "wbtop": f"刚买完瓜,在吃瓜现场 --> 指令:微博热搜/微博热搜 [数字]", + "bilibili_sub": f"非常快捷的订阅播报 --> 指令:添加订阅 [主播/up/番剧] [id/番名/链接] / 删除订阅 [id]/ 查看订阅" } # 其他 other_help = [ diff --git a/plugins/help/data_source.py b/plugins/help/data_source.py index f459fcd5..d13c0cb8 100644 --- a/plugins/help/data_source.py +++ b/plugins/help/data_source.py @@ -13,10 +13,6 @@ import nonebot width = 1600 -e_height = 0 -u_height = 950 -o_height = 1500 -# f_height = def create_help_img(): @@ -28,9 +24,9 @@ def create_help_img(): + len(utility_help) * 24 + len(entertainment_help) * 24 + len(other_help) * 24 - ) * 2 + ) + 800 A = CreateImg(width, h - 200, font_size=24) - e = CreateImg(width, len(entertainment_help) * 42, font_size=24) + e = CreateImg(width, len(entertainment_help) * 26 + 100, font_size=24) rst = "" i = 0 for cmd in entertainment_help: @@ -38,7 +34,7 @@ def create_help_img(): i += 1 e.text((10, 10), "娱乐功能:") e.text((40, 40), rst) - u = CreateImg(width, len(utility_help) * 40 + 50, font_size=24, color="black") + u = CreateImg(width, len(utility_help) * 26 + 100, font_size=24, color="black") rst = "" i = 0 for cmd in utility_help: @@ -46,19 +42,18 @@ def create_help_img(): i += 1 u.text((10, 10), "实用功能:", fill=(255, 255, 255)) u.text((40, 40), rst, fill=(255, 255, 255)) - o = CreateImg(width, len(other_help) * 40, font_size=24) + o = CreateImg(width, len(other_help) * 26 + 100, font_size=24) rst = "" - i = 0 for i in range(len(other_help)): rst += f"{i + 1}.{other_help[i]}\n" i += 1 o.text((10, 10), "其他功能:") o.text((40, 40), rst) A.paste(e, (0, 0)) - A.paste(u, (0, u_height)) - A.paste(o, (0, o_height)) + A.paste(u, (0, e.h)) + A.paste(o, (0, e.h + u.h)) A.text( - (10, h * 0.68), + (10, e.h + u.h + o.h + 50), f"大部分交互功能可以通过输入‘取消’,‘算了’来取消当前交互\n对{NICKNAME}说 “{NICKNAME}帮助 指令名” 获取对应详细帮助\n" "可以通过 “滴滴滴- [消息]” 联系管理员(有趣的想法尽管来吧!<还有Bug和建议>)" "\n[群管理员请看 管理员帮助(群主与管理员自带 5 级权限)]\n\n" @@ -75,11 +70,11 @@ def create_group_help_img(group_id: int): + len(utility_help) * 24 + len(entertainment_help) * 24 + len(other_help) * 24 - ) * 2 - A = CreateImg(width, h - 200, font_size=24) - u = CreateImg(width, len(utility_help) * 40, font_size=24, color="black") - o = CreateImg(width, len(other_help) * 40, font_size=24) - e = CreateImg(width, len(entertainment_help) * 42, font_size=24) + ) + 800 + A = CreateImg(width, h, font_size=24) + u = CreateImg(width, len(utility_help) * 26 + 100, font_size=24, color="black") + o = CreateImg(width, len(other_help) * 26 + 100, font_size=24) + e = CreateImg(width, len(entertainment_help) * 26 + 100, font_size=24) rst = "" i = 1 for cmd in entertainment_help.keys(): @@ -107,17 +102,17 @@ def create_group_help_img(group_id: int): o.text((40, 40), rst) A.paste(e, (0, 0)) - A.paste(u, (0, u_height)) - A.paste(o, (0, o_height)) + A.paste(u, (0, e.h)) + A.paste(o, (0, e.h + u.h)) # A.text((width, 10), f'总开关【{"√" if data["总开关"] else "×"}】') A.text( - (10, h * 0.68), + (10, e.h + u.h + o.h + 50), f"大部分交互功能可以通过输入‘取消’,‘算了’来取消当前交互\n对{NICKNAME}说 “{NICKNAME}帮助 指令名” 获取对应详细帮助\n" "可以通过 “滴滴滴- [消息]” 联系管理员(有趣的想法尽管来吧!<还有Bug和建议>)" f"\n[群管理员请看 管理员帮助(群主与管理员自带 {ADMIN_DEFAULT_AUTH} 级权限)]", ) A.text( - (10, h * 0.77), + (10, e.h + u.h + o.h + 250), f"【注】「色图概率:好感度 + {int(INITIAL_SETU_PROBABILITY*100)}%\n" f"\t\t每 3 点好感度 + 1次开箱,初始 {INITIAL_OPEN_CASE_COUNT} 次\n" f"\t\t开启/关闭功能只需输入‘开启/关闭 指令名’(每个功能的第一个指令)」\n" diff --git a/plugins/hook.py b/plugins/hook.py index 1d4a17b4..6aef856e 100644 --- a/plugins/hook.py +++ b/plugins/hook.py @@ -1,13 +1,18 @@ from nonebot.matcher import Matcher -from nonebot.message import run_preprocessor, IgnoredException +from nonebot.message import run_preprocessor, run_postprocessor, IgnoredException from nonebot.adapters.cqhttp.exception import ActionFailed +from models.group_member_info import GroupInfoUser +from models.friend_user import FriendUser +from typing import Optional from nonebot.typing import T_State from nonebot.adapters.cqhttp import ( Bot, + Event, MessageEvent, PrivateMessageEvent, GroupMessageEvent, - PokeNotifyEvent + PokeNotifyEvent, + Message, ) from configs.config import ( BAN_RESULT, @@ -17,24 +22,43 @@ from configs.config import ( MALICIOUS_BAN_COUNT, CHECK_NOTICE_INFO_CD, plugins2info_dict, + plugins2cd_dict, + plugins2exists_dict, ) from models.ban_user import BanUser -from utils.utils import is_number, static_flmt, BanCheckLimiter +from utils.utils import ( + is_number, + static_flmt, + BanCheckLimiter, + FreqLimiter, + UserExistLimiter, +) +from utils.static_data import withdraw_message_id_manager from utils.message_builder import at from services.log import logger from models.level_user import LevelUser from utils.static_data import group_manager -from utils.utils import FreqLimiter +import asyncio try: import ujson as json except ModuleNotFoundError: import json +withdraw_message_id_manager["message_id"] = [] + # 检查是否被ban @run_preprocessor async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + try: + if ( + await BanUser.is_super_ban(event.user_id) + and str(event.user_id) not in bot.config.superusers + ): + raise IgnoredException("用户处于超级黑名单中") + except AttributeError: + pass if not isinstance(event, MessageEvent): return if matcher.type == "message" and matcher.priority not in [1, 9]: @@ -48,9 +72,9 @@ async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): if time < 60: time = str(time) + " 秒" else: - time = str(int(time / 60)) + " 分" + time = str(int(time / 60)) + " 分钟" else: - time = str(time) + " 分" + time = str(time) + " 分钟" if isinstance(event, GroupMessageEvent): if not static_flmt.check(event.user_id): raise IgnoredException("用户处于黑名单中") @@ -61,7 +85,7 @@ async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): group_id=event.group_id, message=at(event.user_id) + BAN_RESULT - + f" 在..在 {time}后才会理你喔", + + f" 在..在 {time} 后才会理你喔", ) except ActionFailed: pass @@ -127,11 +151,16 @@ _flmt = FreqLimiter(CHECK_NOTICE_INFO_CD) _flmt_g = FreqLimiter(CHECK_NOTICE_INFO_CD) _flmt_s = FreqLimiter(CHECK_NOTICE_INFO_CD) _flmt_c = FreqLimiter(CHECK_NOTICE_INFO_CD) +_exists_msg = {} + + +ignore_rst_module = ["ai", "poke"] # 权限检测 @run_preprocessor async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + global _exists_msg if ( (not isinstance(event, MessageEvent) and matcher.module != "poke") or await BanUser.is_ban(event.user_id) @@ -170,54 +199,70 @@ async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): raise IgnoredException("权限不足") if module in plugins2info_dict.keys() and matcher.priority not in [1, 9]: # 戳一戳单独判断 - if isinstance(event, GroupMessageEvent) or (isinstance(event, PokeNotifyEvent) and event.group_id): + if isinstance(event, GroupMessageEvent) or ( + isinstance(event, PokeNotifyEvent) and event.group_id + ): + if _exists_msg.get(event.group_id) is None: + _exists_msg[event.group_id] = False # 群权限 if plugins2info_dict[module]["level"] > group_manager.get_group_level( str(event.group_id) ): try: - if _flmt_g.check(event.user_id): + if _flmt_g.check(event.user_id) and module not in ignore_rst_module: _flmt_g.start_cd(event.user_id) await bot.send_group_msg( group_id=event.group_id, message="群权限不足..." ) except ActionFailed: pass + _exists_msg[event.group_id] = True raise IgnoredException("群权限不足") # 插件状态 if not group_manager.get_plugin_status(module, str(event.group_id)): try: - if module != "poke" and _flmt_s.check(event.group_id): + if module not in ignore_rst_module and _flmt_s.check( + event.group_id + ): _flmt_s.start_cd(event.group_id) await bot.send_group_msg( group_id=event.group_id, message="该群未开启此功能.." ) except ActionFailed: pass + _exists_msg[event.group_id] = True raise IgnoredException("未开启此功能...") # 管理员禁用 if not group_manager.get_plugin_status( f"{module}:super", str(event.group_id) ): try: - if _flmt_s.check(event.group_id): + if ( + _flmt_s.check(event.group_id) + and module not in ignore_rst_module + ): _flmt_s.start_cd(event.group_id) await bot.send_group_msg( group_id=event.group_id, message="管理员禁用了此群该功能..." ) except ActionFailed: pass + _exists_msg[event.group_id] = True raise IgnoredException("管理员禁用了此群该功能...") # 群聊禁用 if not group_manager.get_plugin_status(module, block_type="group"): try: - if _flmt_c.check(event.group_id): + if ( + _flmt_c.check(event.group_id) + and module not in ignore_rst_module + ): _flmt_c.start_cd(event.group_id) await bot.send_group_msg( group_id=event.group_id, message="该功能在群聊中已被禁用..." ) except ActionFailed: pass + _exists_msg[event.group_id] = True raise IgnoredException("该插件在群聊中已被禁用...") else: # 私聊禁用 @@ -238,21 +283,187 @@ async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): ) and group_manager.check_group_is_white(event.group_id): return try: - if _flmt_c.check(event.user_id): - _flmt_c.start_cd(event.user_id) - if isinstance(event, GroupMessageEvent): + if isinstance(event, GroupMessageEvent): + if ( + _flmt_c.check(event.group_id) + and module not in ignore_rst_module + ): + _flmt_c.start_cd(event.group_id) await bot.send_group_msg( group_id=event.group_id, message="此功能正在维护..." ) - else: - await bot.send_private_msg( - user_id=event.user_id, message="此功能正在维护..." - ) + else: + await bot.send_private_msg( + user_id=event.user_id, message="此功能正在维护..." + ) except ActionFailed: pass + if isinstance(event, GroupMessageEvent): + _exists_msg[event.group_id] = True raise IgnoredException("此功能正在维护...") +check_flmt = {} +for plugin in plugins2cd_dict.keys(): + if plugins2cd_dict[plugin]["status"]: + check_flmt[plugin] = FreqLimiter(plugins2cd_dict[plugin]["cd"]) + + +check_elmt = {} +for plugin in plugins2exists_dict.keys(): + if plugins2exists_dict[plugin]["status"]: + check_elmt[plugin] = UserExistLimiter() + + +# 命令cd 和 命令阻塞 +@run_preprocessor +async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + global _exists_msg + if not isinstance(event, MessageEvent) and matcher.module != "poke": + return + module = matcher.module + if isinstance(event, GroupMessageEvent) and _exists_msg.get(event.group_id) is None: + _exists_msg[event.group_id] = False + if module in plugins2cd_dict.keys() and module in check_flmt.keys(): + if ( + ( + isinstance(event, PrivateMessageEvent) + and plugins2cd_dict[module]["check_type"] == "private" + ) + or ( + isinstance(event, GroupMessageEvent) + and plugins2cd_dict[module]["check_type"] == "group" + ) + or plugins2cd_dict[module]["check_type"] == "all" + ) and plugins2cd_dict[module]["cd"] > 0: + cd_type_ = event.user_id + if plugins2cd_dict[module]["limit_type"] == "group" and isinstance( + event, GroupMessageEvent + ): + cd_type_ = event.group_id + if not check_flmt[module].check(cd_type_): + rst = plugins2cd_dict[module]["rst"] + if rst: + rst = await init_rst(rst, event) + await send_msg(rst, bot, event) + raise IgnoredException(f"{module} 正在cd中...") + else: + check_flmt[module].start_cd(cd_type_) + if module in plugins2exists_dict.keys() and module in check_elmt.keys(): + if ( + ( + isinstance(event, PrivateMessageEvent) + and plugins2exists_dict[module]["check_type"] == "private" + ) + or ( + isinstance(event, GroupMessageEvent) + and plugins2exists_dict[module]["check_type"] == "group" + ) + or plugins2exists_dict[module]["check_type"] == "all" + ): + exists_type_ = event.user_id + if plugins2exists_dict[module]["limit_type"] == "group" and isinstance( + event, GroupMessageEvent + ): + exists_type_ = event.group_id + if check_elmt[module].check(exists_type_): + rst = plugins2exists_dict[module]["rst"] + if rst: + rst = await init_rst(rst, event) + await send_msg(rst, bot, event) + raise IgnoredException(f"{event.user_id}正在调用{module}....") + else: + check_elmt[module].set_true(exists_type_) + + +async def send_msg(rst: str, bot: Bot, event: MessageEvent): + """ + 发送信息 + :param rst: pass + :param bot: pass + :param event: pass + """ + global _exists_msg + rst = await init_rst(rst, event) + try: + if isinstance(event, GroupMessageEvent): + _exists_msg[event.group_id] = True + await bot.send_group_msg(group_id=event.group_id, message=Message(rst)) + else: + _exists_msg[event.user_id] = True + await bot.send_private_msg(user_id=event.user_id, message=Message(rst)) + except ActionFailed: + pass + + +@run_postprocessor +async def _( + matcher: Matcher, + exception: Optional[Exception], + bot: Bot, + event: Event, + state: T_State, +): + if not isinstance(event, MessageEvent) and matcher.module != "poke": + return + module = matcher.module + if module in plugins2exists_dict.keys() and module in check_elmt.keys(): + if not ( + ( + isinstance(event, GroupMessageEvent) + and plugins2exists_dict[module]["check_type"] == "private" + ) + or ( + isinstance(event, PrivateMessageEvent) + and plugins2exists_dict[module]["check_type"] == "group" + ) + ): + exists_type_ = event.user_id + if plugins2exists_dict[module]["limit_type"] == "group" and isinstance( + event, GroupMessageEvent + ): + exists_type_ = event.group_id + check_elmt[module].set_false(exists_type_) + + +async def init_rst(rst: str, event: MessageEvent): + if "[uname]" in rst: + uname = event.sender.card if event.sender.card else event.sender.nickname + rst = rst.replace("[uname]", uname) + if "[nickname]" in rst: + if isinstance(event, GroupMessageEvent): + nickname = await GroupInfoUser.get_group_member_nickname( + event.user_id, event.group_id + ) + else: + nickname = await FriendUser.get_friend_nickname(event.user_id) + rst = rst.replace("[nickname]", nickname) + if "[at]" in rst and isinstance(event, GroupMessageEvent): + rst = rst.replace("[at]", str(at(event.user_id))) + return rst + + +# 消息撤回 +@run_postprocessor +async def _( + matcher: Matcher, + exception: Optional[Exception], + bot: Bot, + event: Event, + state: T_State, +): + tasks = [] + for id_, time in withdraw_message_id_manager["message_id"]: + tasks.append(asyncio.ensure_future(_withdraw_message(bot, id_, time))) + withdraw_message_id_manager["message_id"].remove((id_, time)) + await asyncio.gather(*tasks) + + +async def _withdraw_message(bot: Bot, id_: int, time: int): + await asyncio.sleep(time) + await bot.delete_msg(message_id=id_, self_id=int(bot.self_id)) + + # 为什么AI会自己和自己聊天 @run_preprocessor async def _(matcher: Matcher, bot: Bot, event: PrivateMessageEvent, state: T_State): @@ -265,8 +476,19 @@ async def _(matcher: Matcher, bot: Bot, event: PrivateMessageEvent, state: T_Sta # 有命令就别说话了 @run_preprocessor async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): + global _exists_msg if not isinstance(event, MessageEvent): return if matcher.type == "message": - if state["_prefix"]["raw_command"] and matcher.module == "ai": + if matcher.module == "ai": + if ( + isinstance(event, GroupMessageEvent) + and _exists_msg.get(event.group_id) is True + ): + _exists_msg[event.group_id] = False + elif ( + isinstance(event, PrivateMessageEvent) + and _exists_msg.get(event.user_id) is True + ): + _exists_msg[event.user_id] = False raise IgnoredException("有命令就别说话了") diff --git a/plugins/luxun/__init__.py b/plugins/luxun/__init__.py index 51b3b412..d3e989dd 100644 --- a/plugins/luxun/__init__.py +++ b/plugins/luxun/__init__.py @@ -34,9 +34,9 @@ async def handle_event(bot: Bot, event: MessageEvent, state: T_State): content = state["content"].strip() if content.startswith(",") or content.startswith(","): content = content[1:] - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) if len(content) > 20: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await luxun.finish("太长了, 鲁迅说不完!", at_sender=True) else: if len(content) >= 12: @@ -47,7 +47,7 @@ async def handle_event(bot: Bot, event: MessageEvent, state: T_State): f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 鲁迅说过 {content}" ) await luxun.send(img) - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) def process_pic(content, filename) -> str: diff --git a/plugins/mute.py b/plugins/mute.py index 3f124ef0..2ed9f922 100644 --- a/plugins/mute.py +++ b/plugins/mute.py @@ -29,6 +29,7 @@ mute_setting = on_command( aliases={"设置检测时间", "设置检测次数", "设置禁言时长", "刷屏检测设置"}, permission=GROUP, block=True, + priority=5 ) diff --git a/plugins/nonebot_plugin_withdraw/__init__.py b/plugins/nonebot_plugin_withdraw/__init__.py deleted file mode 100644 index f0d4f4a3..00000000 --- a/plugins/nonebot_plugin_withdraw/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -from typing import Any, Dict -from nonebot import get_driver, on_command -from nonebot.adapters.cqhttp import Bot, Event, GroupMessageEvent, PrivateMessageEvent -from nonebot.rule import to_me -from nonebot.typing import T_State, T_CalledAPIHook - -from .config import Config - -global_config = get_driver().config -withdraw_config = Config(**global_config.dict()) - -msg_ids = {} -max_size = withdraw_config.withdraw_max_size - - -__plugin_name__ = "撤回" - -__plugin_usage__ = ( - "用法:撤回 [消息位置](默认0)\n" "示例:\n" "\t撤回0 -> 撤回倒数第一条消息(即最新发送的消息)" "\t撤回1 -> 撤回倒数第2条消息" -) - - -def get_key(msg_type, id): - return f"{msg_type}_{id}" - - -async def save_msg_id( - bot: Bot, e: Exception, api: str, data: Dict[str, Any], result: Any -) -> T_CalledAPIHook: - try: - if api == "send_msg": - msg_type = data["message_type"] - id = data["group_id"] if msg_type == "group" else data["user_id"] - elif api == "send_private_msg": - msg_type = "private" - id = data["user_id"] - elif api == "send_group_msg": - msg_type = "group" - id = data["group_id"] - else: - return - key = get_key(msg_type, id) - msg_id = result["message_id"] - - if key not in msg_ids: - msg_ids[key] = [] - msg_ids[key].append(msg_id) - if len(msg_ids) > max_size: - msg_ids[key].pop(0) - except: - pass - - -Bot._called_api_hook.add(save_msg_id) - - -withdraw = on_command("withdraw", aliases={"撤回"}, rule=to_me(), priority=1, block=True) - - -@withdraw.handle() -async def _(bot: Bot, event: Event, state: T_State): - if isinstance(event, GroupMessageEvent): - msg_type = "group" - id = event.group_id - elif isinstance(event, PrivateMessageEvent): - msg_type = "private" - id = event.user_id - else: - return - key = get_key(msg_type, id) - - num = event.get_plaintext().strip() - if not num: - num = 0 - elif num.isdigit() and 0 <= int(num) < len(msg_ids[key]): - num = int(num) - else: - return - - try: - idx = -num - 1 - await bot.delete_msg(message_id=msg_ids[key][idx]) - msg_ids[key].pop(idx) - except: - await withdraw.finish("撤回失败,可能已超时") diff --git a/plugins/nonebot_plugin_withdraw/config.py b/plugins/nonebot_plugin_withdraw/config.py deleted file mode 100644 index 5e26d25b..00000000 --- a/plugins/nonebot_plugin_withdraw/config.py +++ /dev/null @@ -1,8 +0,0 @@ -from pydantic import BaseSettings - - -class Config(BaseSettings): - withdraw_max_size: int = 50 - - class Config: - extra = 'ignore' diff --git a/plugins/one_friend/__init__.py b/plugins/one_friend/__init__.py index 60a9217d..8f798f55 100644 --- a/plugins/one_friend/__init__.py +++ b/plugins/one_friend/__init__.py @@ -1,11 +1,10 @@ import aiohttp -from utils.user_agent import get_user_agent from io import BytesIO from random import choice from nonebot import on_regex from nonebot.typing import T_State from nonebot.adapters.cqhttp import Bot, GroupMessageEvent -from utils.utils import get_message_text, get_local_proxy, get_message_at +from utils.utils import get_message_text, get_message_at from utils.message_builder import image import re from utils.image_utils import CreateImg @@ -16,15 +15,15 @@ __plugin_usage__ = "用法:我有一个朋友说/问 [消息] [at](不艾特 one_friend = on_regex( "^我.*?朋友.*?(想问问|说|让我问问|想问|让我问|想知道|让我帮他问问|让我帮他问|让我帮忙问|让我帮忙问问|问).*", - priority=5, + priority=4, block=True, ) async def get_pic(qq): url = f"http://q1.qlogo.cn/g?b=qq&nk={qq}&s=100" - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - async with session.get(url, proxy=get_local_proxy(), timeout=5) as response: + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as response: return await response.read() @@ -59,7 +58,7 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): text.text((0, 0), user_name) A = CreateImg(700, 150, font_size=25, color="white") A.paste(ava, (30, 25), True) - A.paste(text, (150, 40)) + A.paste(text, (150, 38)) A.text((150, 85), msg, (125, 125, 125)) await one_friend.send(image(b64=A.pic2bs4())) diff --git a/plugins/open_cases/__init__.py b/plugins/open_cases/__init__.py index 4a8a1ee7..f3c1d083 100644 --- a/plugins/open_cases/__init__.py +++ b/plugins/open_cases/__init__.py @@ -1,5 +1,5 @@ from nonebot import on_command -from utils.utils import FreqLimiter, scheduler, get_message_text, is_number +from utils.utils import scheduler, get_message_text, is_number from nonebot.adapters.cqhttp.permission import GROUP from nonebot.typing import T_State from nonebot.adapters.cqhttp import Bot, GroupMessageEvent, MessageEvent @@ -33,8 +33,6 @@ __plugin_usage__ = ( "示例:我的金色" ) -_flmt = FreqLimiter(3) - cases_name = ["狂牙大行动", "突围大行动", "命悬一线", "裂空", "光谱"] cases_matcher_group = MatcherGroup(priority=5, permission=GROUP, block=True) @@ -47,9 +45,9 @@ k_open_case = cases_matcher_group.on_command("开箱") async def _(bot: Bot, event: GroupMessageEvent, state: T_State): if str(event.get_message()).strip() in ["帮助"]: await k_open_case.finish(__plugin_usage__) - if not _flmt.check(event.user_id): - await k_open_case.finish("着什么急啊,慢慢来!", at_sender=True) - _flmt.start_cd(event.user_id) + # if not _flmt.check(event.user_id): + # await k_open_case.finish("着什么急啊,慢慢来!", at_sender=True) + # _flmt.start_cd(event.user_id) case_name = get_message_text(event.json()) if case_name: result = await open_case(event.user_id, event.group_id, case_name) @@ -98,7 +96,7 @@ open_shilian = cases_matcher_group.on_regex(".*连开箱") async def _(bot: Bot, event: GroupMessageEvent, state: T_State): # if not _flmt.check(event.user_id): # await k_open_case.finish('着什么急啊,慢慢来!', at_sender=True) - _flmt.start_cd(event.user_id) + # _flmt.start_cd(event.user_id) msg = get_message_text(event.json()) rs = re.search(r"(.*)连开箱(.*)", msg) if rs: diff --git a/plugins/open_cases/open_cases_c.py b/plugins/open_cases/open_cases_c.py index e665540e..20de162f 100644 --- a/plugins/open_cases/open_cases_c.py +++ b/plugins/open_cases/open_cases_c.py @@ -3,14 +3,14 @@ from .config import * from services.log import logger from services.db_context import db from models.open_cases_user import OpenCasesUser -from models.sigin_group_user import SignGroupUser +from models.sign_group_user import SignGroupUser from utils.message_builder import image import pypinyin import random from .utils import get_price from models.buff_price import BuffPrice from PIL import Image -from utils.image_utils import alpha2white_PIL, CreateImg +from utils.image_utils import alpha2white_pil, CreateImg from configs.path_config import IMAGE_PATH import asyncio from utils.utils import cn2py @@ -221,7 +221,7 @@ async def open_shilian_case(user_qq: int, group: int, case_name: str, num: int = skin_name += ''.join(i) # img = image(skin_name, "cases/" + case, "png") wImg = CreateImg(200, 270, 200, 200) - wImg.paste(alpha2white_PIL(Image.open(IMAGE_PATH + f'cases/{case}/{skin_name}.png').resize((200, 200), Image.ANTIALIAS)), (0, 0)) + wImg.paste(alpha2white_pil(Image.open(IMAGE_PATH + f'cases/{case}/{skin_name}.png').resize((200, 200), Image.ANTIALIAS)), (0, 0)) wImg.text((5, 200), skin) wImg.text((5, 220), f'磨损:{str(mosun)[:9]}') wImg.text((5, 240), f'价格:{price_result}') @@ -370,7 +370,7 @@ def _pst_my_knife(w, h, knifes_list): style=pypinyin.NORMAL): skin_name += ''.join(i) knife_img = CreateImg(470, 600, 470, 470, font_size=20) - knife_img.paste(alpha2white_PIL( + knife_img.paste(alpha2white_pil( Image.open(IMAGE_PATH + f'cases/{case}/{skin_name}.png').resize((470, 470), Image.ANTIALIAS)), (0, 0)) knife_img.text((5, 500), f'\t{name}({itype})') knife_img.text((5, 530), f'\t磨损:{mosun}') diff --git a/plugins/parse_bilibili_json.py b/plugins/parse_bilibili_json.py index ff02bfe5..f59ea9c7 100644 --- a/plugins/parse_bilibili_json.py +++ b/plugins/parse_bilibili_json.py @@ -2,7 +2,7 @@ from nonebot import on_message from services.log import logger from nonebot.adapters.cqhttp import Bot, GroupMessageEvent from nonebot.typing import T_State -from utils.utils import get_message_json, get_local_proxy, get_message_text +from utils.utils import get_message_json, get_local_proxy, get_message_text, is_number from utils.user_agent import get_user_agent from nonebot.adapters.cqhttp.permission import GROUP from bilibili_api import video @@ -23,11 +23,14 @@ if get_local_proxy(): parse_bilibili_json = on_message(priority=1, permission=GROUP, block=False) +_tmp = {} + @parse_bilibili_json.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): if await GroupRemind.get_status(event.group_id, "blpar"): vd_info = None + url = None if get_message_json(event.json()): try: data = json.loads(get_message_json(event.json())[0]["data"]) @@ -66,7 +69,6 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): await page.goto(url, wait_until="networkidle", timeout=10000) await page.set_viewport_size({"width": 2560, "height": 1080}) await page.click("#app > div") - # await page.click("text=继续阅读全文", timeout=3000) div = await page.query_selector("#app > div") await div.screenshot( path=f"{IMAGE_PATH}/temp/cv_{event.user_id}.png", @@ -90,7 +92,20 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): # BV if get_message_text(event.json()): msg = get_message_text(event.json()) - if "https://b23.tv" in msg: + if "BV" in msg: + index = msg.find('BV') + if len(msg[index + 2:]) >= 10: + msg = msg[index: index + 12] + url = f'https://www.bilibili.com/video/{msg}' + vd_info = await video.Video(bvid=msg).get_info() + elif 'av' in msg: + index = msg.find('av') + if len(msg[index + 2:]) >= 9: + msg = msg[index + 2: index + 11] + if is_number(msg): + url = f'https://www.bilibili.com/video/{msg}' + vd_info = await video.Video(aid=int(msg)).get_info() + elif "https://b23.tv" in msg: url = "https://" + msg[msg.find("b23.tv") : msg.find("b23.tv") + 13] async with aiohttp.ClientSession(headers=get_user_agent()) as session: async with session.get( @@ -101,33 +116,32 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): url = str(response.url).split("?")[0] bvid = url.split("/")[-1] vd_info = await video.Video(bvid=bvid).get_info() - if "https://www.bilibili.com/video" in msg: - url = msg.split("?")[0] - msg = url.split("/")[-1] - vd_info = await video.Video(bvid=msg).get_info() if vd_info: - aid = vd_info["aid"] - title = vd_info["title"] - author = vd_info["owner"]["name"] - reply = vd_info["stat"]["reply"] # 回复 - favorite = vd_info["stat"]["favorite"] # 收藏 - coin = vd_info["stat"]["coin"] # 投币 - # like = vd_info['stat']['like'] # 点赞 - # danmu = vd_info['stat']['danmaku'] # 弹幕 - date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) - try: - await parse_bilibili_json.send( - image(vd_info["pic"]) + f"\nav{aid}\n标题:{title}\n" - f"UP:{author}\n" - f"上传日期:{date}\n" - f"回复:{reply},收藏:{favorite},投币:{coin}\n" - f"{url}" - ) - except ActionFailed: - logger.warning(f"{event.group_id} 发送bilibili解析失败") - logger.info( - f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" - ) + if (url in _tmp.keys() and time.time() - _tmp[url] > 30) or url not in _tmp.keys(): + _tmp[url] = time.time() + aid = vd_info["aid"] + title = vd_info["title"] + author = vd_info["owner"]["name"] + reply = vd_info["stat"]["reply"] # 回复 + favorite = vd_info["stat"]["favorite"] # 收藏 + coin = vd_info["stat"]["coin"] # 投币 + # like = vd_info['stat']['like'] # 点赞 + # danmu = vd_info['stat']['danmaku'] # 弹幕 + date = time.strftime("%Y-%m-%d", time.localtime(vd_info["ctime"])) + try: + await parse_bilibili_json.send( + image(vd_info["pic"]) + f"\nav{aid}\n标题:{title}\n" + f"UP:{author}\n" + f"上传日期:{date}\n" + f"回复:{reply},收藏:{favorite},投币:{coin}\n" + f"{url}" + ) + except ActionFailed: + logger.warning(f"{event.group_id} 发送bilibili解析失败") + else: + logger.info( + f"USER {event.user_id} GROUP {event.group_id} 解析bilibili转发 {url}" + ) def resize(path: str): diff --git a/plugins/pix_gallery/__init__.py b/plugins/pix_gallery/__init__.py index 5cf5d3dc..f38ef7ec 100644 --- a/plugins/pix_gallery/__init__.py +++ b/plugins/pix_gallery/__init__.py @@ -1,14 +1,76 @@ +from configs.config import HIBIAPI +from services.log import logger +from models.omega_pixiv_illusts import OmegaPixivIllusts +from pathlib import Path +from nonebot import Driver +from typing import List +from datetime import datetime +import nonebot +import asyncio +import os + +driver: Driver = nonebot.get_driver() + +illust_url = f"{HIBIAPI}/api/pixiv/illust" +@driver.on_startup +async def _init_omega_pixiv_illusts(): + omega_pixiv_illusts = None + for file in os.listdir("."): + if "omega_pixiv_illusts" in file and ".sql" in file: + omega_pixiv_illusts = Path() / file + if omega_pixiv_illusts: + with open(omega_pixiv_illusts, "r", encoding="utf8") as f: + lines = f.readlines() + tasks = [] + length = len([x for x in lines if "INSERT INTO" in x.upper()]) + all_pid = await OmegaPixivIllusts.get_all_pid() + index = 0 + logger.info('检测到OmegaPixivIllusts数据库,准备开始更新....') + for line in lines: + if "INSERT INTO" in line.upper(): + index += 1 + tasks.append(asyncio.ensure_future(_tasks(line, all_pid, length, index))) + await asyncio.gather(*tasks) + omega_pixiv_illusts.unlink() - - - - - - - - - +async def _tasks(line: str, all_pid: List[int], length: int, index: int): + data = line.split("VALUES", maxsplit=1)[-1].strip() + if data.startswith("("): + data = data[1:] + if data.endswith(");"): + data = data[:-2] + x = data.split(maxsplit=3) + pid = int(x[1][:-1].strip()) + if pid in all_pid: + logger.info(f'添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}') + return + uid = int(x[2][:-1].strip()) + x = x[3].split(", '") + title = x[0].strip()[1:-1] + tmp = x[1].split(', ') + author = tmp[0].strip()[:-1] + nsfw_tag = int(tmp[1]) + width = int(tmp[2]) + height = int(tmp[3]) + tags = x[2][:-1] + url = x[3][:-1] + if await OmegaPixivIllusts.add_image_data( + pid, + title, + width, + height, + url, + uid, + author, + nsfw_tag, + tags, + datetime.min, + datetime.min + ): + logger.info(f"成功添加OmegaPixivIllusts图库数据 pid:{pid} 本次预计存储 {length} 张,已更新第 {index} 张") + else: + logger.info(f"添加OmegaPixivIllusts图库数据已存在 ---> pid:{pid}") diff --git a/plugins/pix_gallery/data_source.py b/plugins/pix_gallery/data_source.py index 76796c0c..17941f45 100644 --- a/plugins/pix_gallery/data_source.py +++ b/plugins/pix_gallery/data_source.py @@ -4,16 +4,17 @@ from aiohttp.client_exceptions import ( ClientConnectorError, ) from asyncpg.exceptions import UniqueViolationError +from models.omega_pixiv_illusts import OmegaPixivIllusts from asyncio.locks import Semaphore from aiohttp import ClientPayloadError from aiohttp.client import ClientSession from asyncio.exceptions import TimeoutError from models.pixiv import Pixiv from typing import List -from utils.utils import get_local_proxy +from utils.utils import get_local_proxy, change_picture_links from utils.image_utils import CreateImg from services.log import logger -from configs.config import HIBIAPI_BOOKMARKS, HIBIAPI +from configs.config import HIBIAPI_BOOKMARKS, HIBIAPI, PIX_IMAGE_SIZE from configs.path_config import TEMP_PATH import platform import aiohttp @@ -215,19 +216,45 @@ async def download_image(img_url: str, session: ClientSession, _count: int = 1): async def get_image(img_url: str, user_id: int) -> str: - try: - async with aiohttp.ClientSession(headers=headers) as session: - async with session.get( - img_url, - proxy=get_local_proxy(), - ) as response: - async with aiofiles.open( - f"{TEMP_PATH}/pix_{user_id}_{img_url[-10:-4]}.jpg", "wb" - ) as f: - await f.write(await response.read()) - return f"pix_{user_id}_{img_url[-10:-4]}.jpg" - except (ClientConnectorError, TimeoutError): - return await get_image(img_url, user_id) + async with aiohttp.ClientSession(headers=headers) as session: + if 'https://www.pixiv.net/artworks' in img_url: + pid = img_url.rsplit('/', maxsplit=1)[-1] + params = {"id": pid} + for _ in range(3): + try: + async with session.get( + illust_url, + params=params, + proxy=get_local_proxy(), + ) as response: + if response.status == 200: + data = await response.json() + if data.get('illust'): + if data['illust']['page_count'] == 1: + img_url = data['illust']['meta_single_page']['original_image_url'] + else: + img_url = data['illust']["meta_pages"][0]["image_urls"]["original"] + break + except (ClientConnectorError, TimeoutError): + pass + old_img_url = img_url + img_url = change_picture_links(img_url, PIX_IMAGE_SIZE) + for _ in range(3): + try: + async with session.get( + img_url, + proxy=get_local_proxy(), + ) as response: + if response.status == 404: + img_url = old_img_url + continue + async with aiofiles.open( + f"{TEMP_PATH}/pix_{user_id}_{img_url[-10:-4]}.jpg", "wb" + ) as f: + await f.write(await response.read()) + return f"pix_{user_id}_{img_url[-10:-4]}.jpg" + except (ClientConnectorError, TimeoutError): + pass # 检测UID或PID是否有效 @@ -248,7 +275,9 @@ async def uid_pid_exists(id_: str) -> bool: async def get_keyword_num(keyword: str) -> "int , int": - return await Pixiv.get_keyword_num(keyword.split()) + count, r18_count = await Pixiv.get_keyword_num(keyword.split()) + count_, setu_count, r18_count_ = await OmegaPixivIllusts.get_keyword_num(keyword.split()) + return count, r18_count, count_, setu_count, r18_count_ async def remove_image(pid: int, img_p: str) -> bool: diff --git a/plugins/pix_gallery/pix.py b/plugins/pix_gallery/pix.py index 46860f10..6d995c9f 100644 --- a/plugins/pix_gallery/pix.py +++ b/plugins/pix_gallery/pix.py @@ -1,17 +1,30 @@ -from nonebot import on_command -from nonebot.adapters.cqhttp.event import GroupMessageEvent from nonebot.adapters.cqhttp.message import Message from utils.utils import get_message_text, is_number +from configs.config import PIX_OMEGA_PIXIV_RATIO, WITHDRAW_PIX_TIME +from models.omega_pixiv_illusts import OmegaPixivIllusts from utils.message_builder import image from services.log import logger -from nonebot.adapters.cqhttp import Bot, MessageEvent +from nonebot.adapters.cqhttp import ( + Bot, + MessageEvent, + GroupMessageEvent, + PrivateMessageEvent, +) +from utils.static_data import withdraw_message_id_manager from nonebot.typing import T_State from .data_source import get_image from models.pixiv import Pixiv +from nonebot import on_command import random -pix = on_command("pix", aliases={"PIX"}, priority=5, block=True) +pix = on_command("pix", aliases={"PIX", "Pix"}, priority=5, block=True) + + +PIX_RATIO = PIX_OMEGA_PIXIV_RATIO[0] / ( + PIX_OMEGA_PIXIV_RATIO[0] + PIX_OMEGA_PIXIV_RATIO[1] +) +OMEGA_RATIO = 1 - PIX_RATIO @pix.handle() @@ -24,40 +37,97 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): pid = keyword.replace("pid", "").replace(":", "") if not is_number(pid): await pix.finish("PID必须是数字...", at_sender=True) - all_image = await Pixiv.query_images(pid=int(pid)) + isr18 = 0 if isinstance(event, GroupMessageEvent) else 2 + nsfw_tag = 0 if isinstance(event, GroupMessageEvent) else None + all_image = await Pixiv.query_images(pid=int(pid), r18=isr18) + if not all_image: + all_image = await OmegaPixivIllusts.query_images( + pid=int(pid), nsfw_tag=nsfw_tag + ) else: x = keyword.split() + if '-s' in x: + x.remove('-s') + nsfw_tag = 1 + elif '-r' in x: + x.remove('-r') + nsfw_tag = 2 + else: + nsfw_tag = 0 + if nsfw_tag != 0 and str(event.user_id) not in bot.config.superusers: + await pix.finish('你不能看这些噢,这些都是是留给管理员看的...') if len(x) > 1: if is_number(x[-1]): num = int(x[-1]) - if num > 11: - num = random.randint(1, 10) - await pix.send(f"太贪心了,就给你发 {num}张 好了") + if num > 10: + if str(event.user_id) not in bot.config.superusers or ( + str(event.user_id) in bot.config.superusers and num > 30 + ): + num = random.randint(1, 10) + await pix.send(f"太贪心了,就给你发 {num}张 好了") x = x[:-1] keyword = " ".join(x) - all_image = await Pixiv.query_images(x) + pix_num = int(num * PIX_RATIO) + 15 if PIX_RATIO != 0 else 0 + omega_num = num - pix_num + 15 + tmp = await Pixiv.query_images( + x, r18=1 if nsfw_tag == 2 else 0, num=pix_num + ) + await OmegaPixivIllusts.query_images(x, nsfw_tag=nsfw_tag, num=omega_num) + tmp_ = [] + all_image = [] + for x in tmp: + if x.pid not in tmp_: + all_image.append(x) + tmp_.append(x.pid) if not all_image: await pix.finish(f"未在图库中找到与 {keyword} 相关Tag/UID/PID的图片...", at_sender=True) for _ in range(num): + img_url = None + author = None if not all_image: await pix.finish("坏了...发完了,没图了...") img = random.choice(all_image) all_image.remove(img) - img_url = img.img_url + if isinstance(img, OmegaPixivIllusts): + img_url = img.url + author = img.uname + print(img.nsfw_tag) + elif isinstance(img, Pixiv): + img_url = img.img_url + author = img.author pid = img.pid title = img.title - author = img.author uid = img.uid # tags = img.tags - await pix.send( - Message( - f"title:{title}\n" - f"author:{author}\n" - f"PID:{pid}\nUID:{uid}\n" - f"{image(await get_image(img_url, event.user_id), 'temp')}" + _img = await get_image(img_url, event.user_id) + if _img: + msg_id = await pix.send( + Message( + f"title:{title}\n" + f"author:{author}\n" + f"PID:{pid}\nUID:{uid}\n" + f"{image(_img, 'temp')}" + ) + ) + logger.info( + f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 查看PIX图库PID: {pid}" + ) + else: + msg_id = await pix.send(f"下载图片似乎出了一点问题,PID:{pid}") + logger.info( + f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 查看PIX图库PID: {pid},下载图片出错" + ) + withdraw_message(event, msg_id["message_id"]) + + +def withdraw_message(event: MessageEvent, id_: int): + if WITHDRAW_PIX_TIME[0]: + if ( + (WITHDRAW_PIX_TIME[1] == 0 and isinstance(event, PrivateMessageEvent)) + or (WITHDRAW_PIX_TIME[1] == 1 and isinstance(event, GroupMessageEvent)) + or WITHDRAW_PIX_TIME[1] == 2 + ): + withdraw_message_id_manager["message_id"].append( + (id_, WITHDRAW_PIX_TIME[0]) ) - ) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看PIX图库PID: {pid}" - ) diff --git a/plugins/pix_gallery/pix_show_info.py b/plugins/pix_gallery/pix_show_info.py index e779f08d..9d9c537d 100644 --- a/plugins/pix_gallery/pix_show_info.py +++ b/plugins/pix_gallery/pix_show_info.py @@ -56,10 +56,16 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): @show_pix.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): keyword = get_message_text(event.json()) - count, r18_count = await get_keyword_num(keyword) + count, r18_count, count_, setu_count, r18_count_ = await get_keyword_num(keyword) await show_pix.send( f"PIX图库:{keyword}\n" f"总数:{count + r18_count}\n" f"美图:{count}\n" - f"r18:{r18_count}" + f"R18:{r18_count}\n" + f"---------------\n" + f"Omega图库:{keyword}\n" + f"总数:{count_ + setu_count + r18_count_}\n" + f"美图:{count_}\n" + f"色图:{setu_count}\n" + f"R18:{r18_count_}" ) diff --git a/plugins/pixiv/__init__.py b/plugins/pixiv/__init__.py index 7b55d199..35916d80 100644 --- a/plugins/pixiv/__init__.py +++ b/plugins/pixiv/__init__.py @@ -1,17 +1,16 @@ from nonebot.typing import T_State -from nonebot.adapters.cqhttp import Bot, MessageEvent, Event, GroupMessageEvent, Message +from nonebot.adapters.cqhttp import Bot, MessageEvent, GroupMessageEvent from nonebot.matcher import Matcher from nonebot import on_command -from utils.utils import get_message_text, UserExistLimiter, is_number +from utils.utils import get_message_text, is_number from .data_source import get_pixiv_urls, download_pixiv_imgs, search_pixiv_urls -from utils.message_builder import at from services.log import logger from nonebot.adapters.cqhttp.exception import NetworkError from asyncio.exceptions import TimeoutError from aiohttp.client_exceptions import ClientConnectorError -from nonebot.exception import IgnoredException -from nonebot.message import run_preprocessor, run_postprocessor -from typing import Optional +from configs.config import NICKNAME +from typing import Type +from nonebot.rule import to_me import time __plugin_name__ = "P站" @@ -38,17 +37,13 @@ p站排行榜 [参数] [数量](可选) [日期](可选) 【注意空格!!】【在线搜索会较慢】 --------------------------------- 'P站搜图帮助: - 可选参数: - 1.热度排序 - 2.时间排序 - 【使用时选择参数序号即可,R18仅可私聊】 - 搜图 [关键词] [数量](可选) [排序方式](可选) [r18](可选) + 搜图 [关键词] [数量](可选) [页数](可选默认1) [r18](不屏蔽R-18,可选) 示例: 搜图 樱岛麻衣 - 搜图 樱岛麻衣 5 1 - 搜图 樱岛麻衣 5 2 r18 + 搜图 樱岛麻衣 5 + 搜图 樱岛麻衣 5 r18 【默认为 热度排序】 - 【注意空格!!】【在线搜索会较慢】【数量可能不符】 + 【注意空格!!】【在线搜索会较慢】【数量可能不符?可能该页数量不够,也可能被R-18屏蔽】 """ rank_dict = { @@ -63,142 +58,84 @@ rank_dict = { "9": "week_r18g", } -_ulmt = UserExistLimiter() - - -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: Event, state: T_State): - if isinstance(event, MessageEvent): - if matcher.module == "pixiv": - if _ulmt.check(event.user_id): - if isinstance(event, GroupMessageEvent): - await bot.send_group_msg( - group_id=event.group_id, - message=Message(f"{at(event.user_id)}P站排行榜或搜图正在搜索噢,不要重复触发命令呀"), - ) - else: - await bot.send_private_msg( - user_id=event.user_id, message=f"P站排行榜或搜图正在搜索噢,不要重复触发命令呀" - ) - raise IgnoredException("pixiv插件正在访问!") - - -@run_postprocessor -async def do_something( - matcher: Matcher, - exception: Optional[Exception], - bot: Bot, - event: Event, - state: T_State, -): - if isinstance(event, MessageEvent): - if matcher.module == "pixiv": - _ulmt.set_False(event.user_id) - pixiv_rank = on_command( - "p站排行", aliases={"P站排行榜", "p站排行榜", "P站排行榜"}, priority=5, block=True + "p站排行", aliases={"P站排行榜", "p站排行榜", "P站排行榜", "P站排行"}, priority=5, block=True, rule=to_me() ) -pixiv_keyword = on_command("搜图", priority=5, block=True) +pixiv_keyword = on_command("搜图", priority=5, block=True, rule=to_me()) @pixiv_rank.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): msg = get_message_text(event.json()).strip() - _ulmt.set_True(event.user_id) msg = msg.split(" ") msg = [m for m in msg if m] + code = 0 + info_list = [] if not msg: msg = ["1"] if msg[0] in ["6", "7", "8", "9"]: if event.message_type == "group": await pixiv_rank.finish("羞羞脸!私聊里自己看!", at_sender=True) - if len(msg) == 0 or msg[0] == "": - text_list, urls, code = await get_pixiv_urls(rank_dict.get("1")) - elif len(msg) == 1: + if (n := len(msg)) == 0 or msg[0] == "": + info_list, code = await get_pixiv_urls(rank_dict.get("1")) + elif n == 1: if msg[0] not in ["1", "2", "3", "4", "5", "6", "7", "8", "9"]: await pixiv_rank.finish("要好好输入要看什么类型的排行榜呀!", at_sender=True) - text_list, urls, code = await get_pixiv_urls(rank_dict.get(msg[0])) - elif len(msg) == 2: - text_list, urls, code = await get_pixiv_urls(rank_dict.get(msg[0]), int(msg[1])) - elif len(msg) == 3: + info_list, code = await get_pixiv_urls(rank_dict.get(msg[0])) + elif n == 2: + info_list, code = await get_pixiv_urls(rank_dict.get(msg[0]), int(msg[1])) + elif n == 3: if not check_date(msg[2]): await pixiv_rank.finish("日期格式错误了", at_sender=True) - text_list, urls, code = await get_pixiv_urls( - rank_dict.get(msg[0]), int(msg[1]), msg[2] + info_list, code = await get_pixiv_urls( + rank_dict.get(msg[0]), int(msg[1]), date=msg[2] ) else: - await pixiv_rank.finish("格式错了噢,看看帮助?", at_sender=True) - if code != 200: - await pixiv_keyword.finish(text_list[0]) - else: - if not text_list or not urls: - await pixiv_rank.finish("没有找到啊,等等再试试吧~V", at_sender=True) - for i in range(len(text_list)): - try: - await pixiv_rank.send( - text_list[i] + await download_pixiv_imgs(urls[i], event.user_id) - ) - except (NetworkError, TimeoutError, ClientConnectorError): - await pixiv_keyword.send("这张图网络炸了!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 查看了P站排行榜 code:{msg[0]}" - ) + await pixiv_rank.finish("格式错了噢,参数不够?看看帮助?", at_sender=True) + if code != 200 and info_list: + await pixiv_rank.finish(info_list[0]) + if not info_list: + await pixiv_rank.finish("没有找到啊,等等再试试吧~V", at_sender=True) + await send_image(info_list, pixiv_rank, bot, event) + logger.info( + f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 查看了P站排行榜 code:{msg[0]}" + ) @pixiv_keyword.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): - msg = get_message_text(event.json()).strip() - _ulmt.set_True(event.user_id) - if event.message_type == "group": - if msg.find("r18") != -1: + msg = get_message_text(event.json()) + if isinstance(event, GroupMessageEvent): + if 'r18' in msg.lower(): await pixiv_keyword.finish("(脸红#) 你不会害羞的 八嘎!", at_sender=True) - if msg.find("r18") == -1: - r18 = 1 - else: - r18 = 2 - msg = msg.replace("r18", "").strip() - msg = msg.split(" ") - msg = [m for m in msg if m] - if len(msg) == 1: - keyword = msg[0].strip() - num = 5 - order = "popular" - elif len(msg) == 2: - keyword = msg[0].strip() - if not is_number(msg[1].strip()): + r18 = 0 if 'r18' in msg else 1 + msg = msg.replace("r18", "").strip().split() + msg = [m.strip() for m in msg if m] + keyword = None + info_list = None + num = 10 + page = 1 + if (n := len(msg)) == 1: + keyword = msg[0] + if n > 1: + if not is_number(msg[1]): await pixiv_keyword.finish("图片数量必须是数字!", at_sender=True) - num = int(msg[1].strip()) - order = "popular" - elif len(msg) == 3: - keyword = msg[0].strip() - if not is_number(msg[1].strip()): - await pixiv_keyword.finish("图片数量必须是数字!", at_sender=True) - num = int(msg[1].strip()) - if not is_number(msg[2].strip()): - await pixiv_keyword.finish("排序方式必须是数字!", at_sender=True) - if msg[2].strip() == "1": - order = "popular" - else: - order = "xxx" - else: - await pixiv_keyword.finish("参数不正确,一定要好好看看帮助啊!", at_sender=True) - text_list, urls, code = await search_pixiv_urls(keyword, num, order, r18) - if code != 200: - await pixiv_keyword.finish(text_list[0]) - else: - for i in range(len(text_list)): - try: - await pixiv_keyword.send( - text_list[i] + await download_pixiv_imgs(urls[i], event.user_id) - ) - except (NetworkError, TimeoutError, ClientConnectorError): - await pixiv_keyword.send("这张图网络炸了!", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if event.message_type != 'private' else 'private'})" - f" 查看了搜索 {keyword} R18:{r18}" - ) + num = int(msg[1]) + if n > 2: + if not is_number(msg[2]): + await pixiv_keyword.finish("页数数量必须是数字!", at_sender=True) + page = int(msg[2]) + if keyword: + info_list, code = await search_pixiv_urls(keyword, num, page, r18) + if code != 200: + await pixiv_keyword.finish(info_list[0]) + await send_image(info_list, pixiv_keyword, bot, event) + logger.info( + f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" + f" 查看了搜索 {keyword} R18:{r18}" + ) def check_date(date): @@ -207,3 +144,30 @@ def check_date(date): return True except: return False + + +async def send_image(info_list: list, matcher: Type[Matcher], bot: Bot, event: MessageEvent): + if isinstance(event, GroupMessageEvent): + await pixiv_rank.send('开始下载整理数据...') + idx = 0 + mes_list = [] + for title, author, urls in info_list: + _message = f'title: {title}\nauthor: {author}\n' + await download_pixiv_imgs(urls, event.user_id, idx) + data = { + "type": "node", + "data": { + "name": f"这里是{NICKNAME}酱", + "uin": f"{bot.self_id}", + "content": _message, + }, + } + mes_list.append(data) + idx += 1 + await bot.send_group_forward_msg(group_id=event.group_id, messages=mes_list) + else: + for title, author, urls in info_list: + try: + await matcher.send(f'title: {title}\n' + f'author: {author}\n' + await download_pixiv_imgs(urls, event.user_id)) + except (NetworkError, TimeoutError, ClientConnectorError): + await matcher.send("这张图网络直接炸掉了!", at_sender=True) diff --git a/plugins/pixiv/data_source.py b/plugins/pixiv/data_source.py index b3e7ca3c..ab295199 100644 --- a/plugins/pixiv/data_source.py +++ b/plugins/pixiv/data_source.py @@ -1,14 +1,13 @@ -import aiohttp -import aiofiles from configs.path_config import IMAGE_PATH from utils.utils import get_local_proxy -from utils.user_agent import get_user_agent -from bs4 import BeautifulSoup -import feedparser from utils.message_builder import image from asyncio.exceptions import TimeoutError -from configs.config import RSSHUBAPP +from configs.config import HIBIAPI from aiohttp.client_exceptions import ClientConnectorError +from typing import Optional +from pathlib import Path +import aiohttp +import aiofiles import platform if platform.system() == "Windows": @@ -17,82 +16,115 @@ if platform.system() == "Windows": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -async def get_pixiv_urls(mode: str, num: int = 5, date: str = "") -> "list, list, int": - url = f"{RSSHUBAPP}/pixiv/ranking/{mode}" +rank_url = f"{HIBIAPI}/api/pixiv/rank" +search_url = f"{HIBIAPI}/api/pixiv/search" + + +headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6;" + " rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Referer": "https://www.pixiv.net", +} + + +async def get_pixiv_urls( + mode: str, num: int = 10, page: int = 1, date: Optional[str] = None +) -> "list, int": + params = {"mode": mode, "page": page} if date: - url += f"/{date}" - try: - return await parser_data(url, num) - except ClientConnectorError: - return await get_pixiv_urls(mode, num, date) - - -async def download_pixiv_imgs(urls: list, user_id: int) -> str: - result = "" - index = 0 - for img in urls: - async with aiohttp.ClientSession(headers=get_user_agent()) as session: - for _ in range(3): - async with session.get( - img, proxy=get_local_proxy(), timeout=2 - ) as response: - async with aiofiles.open( - IMAGE_PATH + f"temp/{user_id}_{index}_pixiv.jpg", "wb" - ) as f: - try: - await f.write(await response.read()) - result += image(f"{user_id}_{index}_pixiv.jpg", "temp") - index += 1 - break - except TimeoutError: - # result += '\n这张图下载失败了..\n' - pass - else: - result += "\n这张图下载失败了..\n" - return result + params["date"] = date + return await parser_data(rank_url, num, params, 'rank') async def search_pixiv_urls( - keyword: str, num: int, order: str, r18: int + keyword: str, num: int, page: int, r18: int ) -> "list, list": - url = f"{RSSHUBAPP}/pixiv/search/{keyword}/{order}/{r18}" - return await parser_data(url, num) + params = {"word": keyword, 'page': page} + return await parser_data(search_url, num, params, 'search', r18) -async def parser_data(url: str, num: int) -> "list, list, int": - text_list = [] - urls = [] +async def parser_data(url: str, num: int, params: dict, type_: str, r18: int = 0) -> "list, int": + info_list = [] async with aiohttp.ClientSession() as session: for _ in range(3): try: async with session.get( - url, proxy=get_local_proxy(), timeout=2 + url, params=params, proxy=get_local_proxy(), timeout=5 ) as response: - data = feedparser.parse(await response.text())["entries"] - break - except TimeoutError: + if response.status == 200: + data = await response.json() + if data.get("illusts"): + data = data["illusts"] + break + except (TimeoutError, ClientConnectorError): pass else: - return ["网络不太好,也许过一会就好了"], [], 998 - try: - if len(data) == 0: - return ["没有搜索到喔"], [], 997 - if num > len(data): - num = len(data) - data = data[:num] - for data in data: - soup = BeautifulSoup(data["summary"], "lxml") - title = "标题:" + data["title"] - pl = soup.find_all("p") - author = pl[0].text.split("-")[0].strip() - imgs = [] - text_list.append(f"{title}\n{author}\n") - for p in pl[1:]: - imgs.append(p.find("img").get("src")) - urls.append(imgs) - except ValueError: - return ["是网站坏了啊,也许过一会就好了"], [], 999 - return text_list, urls, 200 + return ["网络不太好?没有该页数?也许过一会就好了..."], 998 + num = num if num < 30 else 30 + data = data[:num] + for x in data: + if type_ == 'search' and r18 == 1: + if 'R-18' in str(x['tags']): + continue + title = x["title"] + author = x["user"]["name"] + urls = [] + if x["page_count"] == 1: + urls.append(x["image_urls"]["large"]) + else: + for j in x["meta_pages"]: + urls.append(j["image_urls"]["large"]) + info_list.append((title, author, urls)) + return info_list, 200 -# asyncio.get_event_loop().run_until_complete(get_pixiv_urls('day')) +async def download_pixiv_imgs( + urls: list, user_id: int, forward_msg_index: int = None +) -> str: + result = "" + index = 0 + for url in urls: + async with aiohttp.ClientSession(headers=headers) as session: + for _ in range(3): + try: + async with session.get( + url, proxy=get_local_proxy(), timeout=3 + ) as response: + if response.status == 200: + try: + file = ( + f"{IMAGE_PATH}/temp/{user_id}_{forward_msg_index}_{index}_pixiv.jpg" + if forward_msg_index is not None + else f"{IMAGE_PATH}/temp/{user_id}_{index}_pixiv.jpg" + ) + file = Path(file) + if forward_msg_index is not None: + async with aiofiles.open( + file, + "wb", + ) as f: + await f.write(await response.read()) + result += image( + f"{user_id}_{forward_msg_index}_{index}_pixiv.jpg", + "temp", + ) + else: + async with aiofiles.open( + file, + "wb", + ) as f: + await f.write(await response.read()) + result += image( + f"{user_id}_{index}_pixiv.jpg", "temp" + ) + index += 1 + break + except OSError: + file.unlink() + except (TimeoutError, ClientConnectorError): + # result += '\n这张图下载失败了..\n' + pass + else: + result += "\n这张图下载失败了..\n" + return result + diff --git a/plugins/reimu/__init__.py b/plugins/reimu/__init__.py index 26b7b46b..dad7b5e9 100644 --- a/plugins/reimu/__init__.py +++ b/plugins/reimu/__init__.py @@ -43,7 +43,7 @@ async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): await reimu.finish("今天已经没车了,请明天再来...", at_sender=True) if _ulmt.check(event.user_id): await reimu.finish("您还没下车呢,请稍等...", at_sender=True) - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) msg = get_message_text(event.json()) args = msg.split(" ") if msg in ["!", "!", "?", "?", ",", ",", ".", "。"]: @@ -70,9 +70,9 @@ async def _(bot: Bot, event: PrivateMessageEvent, state: T_State): else: logger.error("Not found reimuInfo") await reimu.send("没找着") - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) except: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) @scheduler.scheduled_job( diff --git a/plugins/russian/__init__.py b/plugins/russian/__init__.py index c9b68326..d4ba9bed 100644 --- a/plugins/russian/__init__.py +++ b/plugins/russian/__init__.py @@ -13,9 +13,9 @@ import time from .data_source import rank from configs.config import MAX_RUSSIAN_BET_GOLD, NICKNAME -__plugin_name__ = '俄罗斯轮盘' +__plugin_name__ = "俄罗斯轮盘" -__plugin_usage__ = '''俄罗斯轮盘帮助: +__plugin_usage__ = """俄罗斯轮盘帮助: 开启游戏:装弹 [子弹数] [金额](默认200金币) [at](指定决斗对象,为空则所有群友都可接受决斗) 示例:装弹 1 10 接受对决:接受对决/拒绝决斗 @@ -24,24 +24,37 @@ __plugin_usage__ = '''俄罗斯轮盘帮助: 我的战绩:我的战绩 排行榜:胜场排行/败场排行/欧洲人排行/慈善家排行 【注:同一时间群内只能有一场对决】 -''' +""" rs_player = {} -rssian = on_command('俄罗斯轮盘', aliases={'装弹', '俄罗斯转盘'}, permission=GROUP, priority=5, block=True) +rssian = on_command( + "俄罗斯轮盘", aliases={"装弹", "俄罗斯转盘"}, permission=GROUP, priority=5, block=True +) -accept = on_command('接受对决', aliases={'接受决斗', '接受挑战'}, permission=GROUP, priority=5, block=True) +accept = on_command( + "接受对决", aliases={"接受决斗", "接受挑战"}, permission=GROUP, priority=5, block=True +) -refuse = on_command('拒绝对决', aliases={'拒绝决斗', '拒绝挑战'}, permission=GROUP, priority=5, block=True) +refuse = on_command( + "拒绝对决", aliases={"拒绝决斗", "拒绝挑战"}, permission=GROUP, priority=5, block=True +) -shot = on_command('开枪', aliases={'咔', '嘭', '嘣'}, permission=GROUP, priority=5, block=True) +shot = on_command( + "开枪", aliases={"咔", "嘭", "嘣"}, permission=GROUP, priority=5, block=True +) -settlement = on_command('结算', permission=GROUP, priority=5, block=True) +settlement = on_command("结算", permission=GROUP, priority=5, block=True) -record = on_command('我的战绩', permission=GROUP, priority=5, block=True) +record = on_command("我的战绩", permission=GROUP, priority=5, block=True) -rssian_rank = on_command('胜场排行', aliases={'胜利排行', '败场排行', '失败排行', - '欧洲人排行', '慈善家排行'}, permission=GROUP, priority=5, block=True) +rssian_rank = on_command( + "胜场排行", + aliases={"胜利排行", "败场排行", "失败排行", "欧洲人排行", "慈善家排行", "最高连胜排行", "最高连败排行"}, + permission=GROUP, + priority=5, + block=True, +) @accept.handle() @@ -49,38 +62,51 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player try: if rs_player[event.group_id][1] == 0: - await accept.finish('目前没有发起对决,你接受个啥?速速装弹!', at_sender=True) + await accept.finish("目前没有发起对决,你接受个啥?速速装弹!", at_sender=True) except KeyError: - await accept.finish('目前没有进行的决斗,请发送 装弹 开启决斗吧!', at_sender=True) + await accept.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) if rs_player[event.group_id][2] != 0: - if rs_player[event.group_id][1] == event.user_id or rs_player[event.group_id][2] == event.user_id: - await accept.finish(f'你已经身处决斗之中了啊,给我认真一点啊!', at_sender=True) + if ( + rs_player[event.group_id][1] == event.user_id + or rs_player[event.group_id][2] == event.user_id + ): + await accept.finish(f"你已经身处决斗之中了啊,给我认真一点啊!", at_sender=True) else: - await accept.finish('已经有人接受对决了,你还是乖乖等待下一场吧!', at_sender=True) + await accept.finish("已经有人接受对决了,你还是乖乖等待下一场吧!", at_sender=True) if rs_player[event.group_id][1] == event.user_id: - await accept.finish('请不要自己枪毙自己!换人来接受对决...', at_sender=True) - if rs_player[event.group_id]['at'] != 0 and rs_player[event.group_id]['at'] != event.user_id: - await accept.finish(Message(f'这场对决是邀请 {at(rs_player[event.group_id]["at"])}的,不要捣乱!'), at_sender=True) - if time.time() - rs_player[event.group_id]['time'] > 30: + await accept.finish("请不要自己枪毙自己!换人来接受对决...", at_sender=True) + if ( + rs_player[event.group_id]["at"] != 0 + and rs_player[event.group_id]["at"] != event.user_id + ): + await accept.finish( + Message(f'这场对决是邀请 {at(rs_player[event.group_id]["at"])}的,不要捣乱!'), + at_sender=True, + ) + if time.time() - rs_player[event.group_id]["time"] > 30: rs_player[event.group_id] = {} - await accept.finish('这场对决邀请已经过时了,请重新发起决斗...', at_sender=True) + await accept.finish("这场对决邀请已经过时了,请重新发起决斗...", at_sender=True) user_money = await BagUser.get_gold(event.user_id, event.group_id) - if user_money < rs_player[event.group_id]['money']: - if rs_player[event.group_id]['at'] != 0 and rs_player[event.group_id]['at'] == event.user_id: + if user_money < rs_player[event.group_id]["money"]: + if ( + rs_player[event.group_id]["at"] != 0 + and rs_player[event.group_id]["at"] == event.user_id + ): rs_player[event.group_id] = {} - await accept.finish('你的金币不足以接受这场对决!对决还未开始便结束了,请重新装弹!', at_sender=True) + await accept.finish("你的金币不足以接受这场对决!对决还未开始便结束了,请重新装弹!", at_sender=True) else: - await accept.finish('你的金币不足以接受这场对决!', at_sender=True) + await accept.finish("你的金币不足以接受这场对决!", at_sender=True) player2_name = event.sender.card if event.sender.card else event.sender.nickname rs_player[event.group_id][2] = event.user_id - rs_player[event.group_id]['player2'] = player2_name - rs_player[event.group_id]['time'] = time.time() + rs_player[event.group_id]["player2"] = player2_name + rs_player[event.group_id]["time"] = time.time() - await accept.send(Message(f'{player2_name}接受了对决!\n' - f'请{at(rs_player[event.group_id][1])}先开枪!')) + await accept.send( + Message(f"{player2_name}接受了对决!\n" f"请{at(rs_player[event.group_id][1])}先开枪!") + ) @refuse.handle() @@ -88,74 +114,107 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player try: if rs_player[event.group_id][1] == 0: - await accept.finish('你要拒绝啥?明明都没有人发起对决的说!', at_sender=True) + await accept.finish("你要拒绝啥?明明都没有人发起对决的说!", at_sender=True) except KeyError: - await refuse.finish('目前没有进行的决斗,请发送 装弹 开启决斗吧!', at_sender=True) - if rs_player[event.group_id]['at'] != 0 and event.user_id != rs_player[event.group_id]['at']: - await accept.finish('又不是找你决斗,你拒绝什么啊!气!', at_sender=True) - if rs_player[event.group_id]['at'] == event.user_id: - at_player_name = (await GroupInfoUser.get_member_info(event.user_id, event.group_id)).user_name - await accept.send(Message(f'{at(rs_player[event.group_id][1])}\n' - f'{at_player_name}拒绝了你的对决!')) + await refuse.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) + if ( + rs_player[event.group_id]["at"] != 0 + and event.user_id != rs_player[event.group_id]["at"] + ): + await accept.finish("又不是找你决斗,你拒绝什么啊!气!", at_sender=True) + if rs_player[event.group_id]["at"] == event.user_id: + at_player_name = ( + await GroupInfoUser.get_member_info(event.user_id, event.group_id) + ).user_name + await accept.send( + Message(f"{at(rs_player[event.group_id][1])}\n" f"{at_player_name}拒绝了你的对决!") + ) rs_player[event.group_id] = {} @settlement.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player - if not rs_player.get(event.group_id) or rs_player[event.group_id][1] == 0 or rs_player[event.group_id][2] == 0: - await settlement.finish('比赛并没有开始...无法结算...', at_sender=True) - if event.user_id != rs_player[event.group_id][1] and event.user_id != rs_player[event.group_id][2]: - await settlement.finish('吃瓜群众不要捣乱!黄牌警告!', at_sender=True) - if time.time() - rs_player[event.group_id]['time'] <= 30: - await settlement.finish(f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]} 比赛并未超时,请继续比赛...') - win_name = rs_player[event.group_id]["player1"] if \ - rs_player[event.group_id][2] == rs_player[event.group_id]['next'] else \ - rs_player[event.group_id]["player2"] - await settlement.send(f'这场对决是 {win_name} 胜利了') + if ( + not rs_player.get(event.group_id) + or rs_player[event.group_id][1] == 0 + or rs_player[event.group_id][2] == 0 + ): + await settlement.finish("比赛并没有开始...无法结算...", at_sender=True) + if ( + event.user_id != rs_player[event.group_id][1] + and event.user_id != rs_player[event.group_id][2] + ): + await settlement.finish("吃瓜群众不要捣乱!黄牌警告!", at_sender=True) + if time.time() - rs_player[event.group_id]["time"] <= 30: + await settlement.finish( + f'{rs_player[event.group_id]["player1"]} 和' + f' {rs_player[event.group_id]["player2"]} 比赛并未超时,请继续比赛...' + ) + win_name = ( + rs_player[event.group_id]["player1"] + if rs_player[event.group_id][2] == rs_player[event.group_id]["next"] + else rs_player[event.group_id]["player2"] + ) + await settlement.send(f"这场对决是 {win_name} 胜利了") await end_game(bot, event) @rssian.args_parser async def _(bot: Bot, event: GroupMessageEvent, state: T_State): msg = get_message_text(event.json()) - if msg in ['取消', '算了']: - await rssian.finish('已取消操作...') + if msg in ["取消", "算了"]: + await rssian.finish("已取消操作...") try: if rs_player[event.group_id][1] != 0: - await rssian.finish('决斗已开始...', at_sender=True) + await rssian.finish("决斗已开始...", at_sender=True) except KeyError: pass if not is_number(msg): - await rssian.reject('输入子弹数量必须是数字啊喂!') + await rssian.reject("输入子弹数量必须是数字啊喂!") if int(msg) < 1 or int(msg) > 6: - await rssian.reject('子弹数量必须大于0小于7!') - state['bullet_num'] = int(msg) + await rssian.reject("子弹数量必须大于0小于7!") + state["bullet_num"] = int(msg) @rssian.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player msg = get_message_text(event.json()) - if msg == '帮助': + if msg == "帮助": await rssian.finish(__plugin_usage__) try: - if rs_player[event.group_id][1] and not rs_player[event.group_id][2] and \ - time.time() - rs_player[event.group_id]['time'] <= 30: - await rssian.finish(f'现在是 {rs_player[event.group_id]["player1"]} 发起的对决\n请等待比赛结束后再开始下一轮...') - if rs_player[event.group_id][1] and rs_player[event.group_id][2] and\ - time.time() - rs_player[event.group_id]['time'] <= 30: - await rssian.finish(f'{rs_player[event.group_id]["player1"]} 和' - f' {rs_player[event.group_id]["player2"]}的对决还未结束!') - if rs_player[event.group_id][1] and rs_player[event.group_id][2] and\ - time.time() - rs_player[event.group_id]['time'] > 30: - await shot.send('决斗已过时,强行结算...') + if ( + rs_player[event.group_id][1] + and not rs_player[event.group_id][2] + and time.time() - rs_player[event.group_id]["time"] <= 30 + ): + await rssian.finish( + f'现在是 {rs_player[event.group_id]["player1"]} 发起的对决\n请等待比赛结束后再开始下一轮...' + ) + if ( + rs_player[event.group_id][1] + and rs_player[event.group_id][2] + and time.time() - rs_player[event.group_id]["time"] <= 30 + ): + await rssian.finish( + f'{rs_player[event.group_id]["player1"]} 和' + f' {rs_player[event.group_id]["player2"]}的对决还未结束!' + ) + if ( + rs_player[event.group_id][1] + and rs_player[event.group_id][2] + and time.time() - rs_player[event.group_id]["time"] > 30 + ): + await shot.send("决斗已过时,强行结算...") await end_game(bot, event) - if not rs_player[event.group_id][2] and time.time() - rs_player[event.group_id]['time'] > 30: + if ( + not rs_player[event.group_id][2] + and time.time() - rs_player[event.group_id]["time"] > 30 + ): rs_player[event.group_id][1] = 0 rs_player[event.group_id][2] = 0 - rs_player[event.group_id]['at'] = 0 + rs_player[event.group_id]["at"] = 0 except KeyError: pass if msg: @@ -163,126 +222,167 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): if len(msg) == 1: msg = msg[0] if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state['bullet_num'] = int(msg) + state["bullet_num"] = int(msg) else: money = msg[1].strip() msg = msg[0].strip() if is_number(msg) and not (int(msg) < 1 or int(msg) > 6): - state['bullet_num'] = int(msg) + state["bullet_num"] = int(msg) if is_number(money) and 0 < int(money) <= MAX_RUSSIAN_BET_GOLD: - state['money'] = int(money) + state["money"] = int(money) else: - state['money'] = 200 - await rssian.send(f'赌注金额超过限制(MAX_RUSSIAN_BET_GOLD),已改为200(默认)') - state['at'] = get_message_at(event.json()) + state["money"] = 200 + await rssian.send(f"赌注金额超过限制(MAX_RUSSIAN_BET_GOLD),已改为200(默认)") + state["at"] = get_message_at(event.json()) -@rssian.got("bullet_num", prompt='请输入装填子弹的数量!(最多6颗)') +@rssian.got("bullet_num", prompt="请输入装填子弹的数量!(最多6颗)") async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player - bullet_num = state['bullet_num'] - at_ = state['at'] if state.get('at') else [] - money = state['money'] if state.get('money') else 200 + bullet_num = state["bullet_num"] + at_ = state["at"] if state.get("at") else [] + money = state["money"] if state.get("money") else 200 user_money = await BagUser.get_gold(event.user_id, event.group_id) if bullet_num < 0 or bullet_num > 6: - await rssian.reject('子弹数量必须大于0小于7!速速重新装弹!') + await rssian.reject("子弹数量必须大于0小于7!速速重新装弹!") if money > MAX_RUSSIAN_BET_GOLD: - await rssian.finish(f'太多了!单次金额不能超过{MAX_RUSSIAN_BET_GOLD}!', at_sender=True) + await rssian.finish(f"太多了!单次金额不能超过{MAX_RUSSIAN_BET_GOLD}!", at_sender=True) if money > user_money: - await rssian.finish('你没有足够的钱支撑起这场挑战', at_sender=True) + await rssian.finish("你没有足够的钱支撑起这场挑战", at_sender=True) player1_name = event.sender.card if event.sender.card else event.sender.nickname if at_: at_ = at_[0] try: - at_player_name = (await GroupInfoUser.get_member_info(at_, event.group_id)).user_name + at_player_name = ( + await GroupInfoUser.get_member_info(at_, event.group_id) + ).user_name except AttributeError: at_player_name = at(at_) - msg = f'{player1_name} 向 {at(at_)} 发起了决斗!请 {at_player_name} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!' + msg = f"{player1_name} 向 {at(at_)} 发起了决斗!请 {at_player_name} 在30秒内回复‘接受对决’ or ‘拒绝对决’,超时此次决斗作废!" else: at_ = 0 - msg = '若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】' + msg = "若30秒内无人接受挑战则此次对决作废【首次游玩请发送 ’俄罗斯轮盘帮助‘ 来查看命令】" - rs_player[event.group_id] = {1: event.user_id, - 'player1': player1_name, - 2: 0, - 'player2': '', - 'at': at_, - 'next': event.user_id, - 'money': money, - 'bullet': random_bullet(bullet_num), - 'bullet_num': bullet_num, - 'null_bullet_num': 7 - bullet_num, - 'index': 0, - 'time': time.time()} + rs_player[event.group_id] = { + 1: event.user_id, + "player1": player1_name, + 2: 0, + "player2": "", + "at": at_, + "next": event.user_id, + "money": money, + "bullet": random_bullet(bullet_num), + "bullet_num": bullet_num, + "null_bullet_num": 7 - bullet_num, + "index": 0, + "time": time.time(), + } - await rssian.send(Message(('咔 ' * bullet_num)[:-1] + f',装填完毕\n挑战金额:{money}\n' - f'第一枪的概率为:{str(float(bullet_num) / 7.0 * 100)[:5]}%\n' - f'{msg}')) + await rssian.send( + Message( + ("咔 " * bullet_num)[:-1] + f",装填完毕\n挑战金额:{money}\n" + f"第一枪的概率为:{str(float(bullet_num) / 7.0 * 100)[:5]}%\n" + f"{msg}" + ) + ) @shot.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): global rs_player try: - if time.time() - rs_player[event.group_id]['time'] > 30: + if time.time() - rs_player[event.group_id]["time"] > 30: if rs_player[event.group_id][2] == 0: rs_player[event.group_id][1] = 0 - await shot.finish('这场对决已经过时了,请重新装弹吧!', at_sender=True) + await shot.finish("这场对决已经过时了,请重新装弹吧!", at_sender=True) else: - await shot.send('决斗已过时,强行结算...') + await shot.send("决斗已过时,强行结算...") await end_game(bot, event) return except KeyError: - await shot.finish('目前没有进行的决斗,请发送 装弹 开启决斗吧!', at_sender=True) + await shot.finish("目前没有进行的决斗,请发送 装弹 开启决斗吧!", at_sender=True) if rs_player[event.group_id][1] == 0: - await shot.finish('没有对决,也还没装弹呢,请先输入 装弹 吧!', at_sender=True) - if rs_player[event.group_id][1] == event.user_id and rs_player[event.group_id][2] == 0: - await shot.finish('baka,你是要枪毙自己嘛笨蛋!', at_sender=True) + await shot.finish("没有对决,也还没装弹呢,请先输入 装弹 吧!", at_sender=True) + if ( + rs_player[event.group_id][1] == event.user_id + and rs_player[event.group_id][2] == 0 + ): + await shot.finish("baka,你是要枪毙自己嘛笨蛋!", at_sender=True) if rs_player[event.group_id][2] == 0: - await shot.finish('请这位勇士先发送 接受对决 来站上擂台...', at_sender=True) - player1_name = rs_player[event.group_id]['player1'] - player2_name = rs_player[event.group_id]['player2'] - if rs_player[event.group_id]['next'] != event.user_id: - if event.user_id != rs_player[event.group_id][1] and event.user_id != rs_player[event.group_id][2]: - await shot.finish(random.choice([ - f'不要打扰 {player1_name} 和 {player2_name} 的决斗啊!', - f'给我好好做好一个观众!不然{NICKNAME}就要生气了', - f'不要捣乱啊baka{(await GroupInfoUser.get_member_info(event.user_id, event.group_id)).user_name}!' - ]), at_sender=True) - await shot.finish(f'你的左轮不是连发的!该 ' - f'{(await GroupInfoUser.get_member_info(int(rs_player[event.group_id]["next"]), event.group_id)).user_name} 开枪了') - if rs_player[event.group_id]['bullet'][rs_player[event.group_id]['index']] != 1: - await shot.send(Message(random.choice([ - '呼呼,没有爆裂的声响,你活了下来', - '虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了', - '\"咔\",你没死,看来运气不错', - ]) + f'\n下一枪中弹的概率' - f':{str(float((rs_player[event.group_id]["bullet_num"])) / float(rs_player[event.group_id]["null_bullet_num"] - 1 + rs_player[event.group_id]["bullet_num"]) * 100)[:5]}%\n' - f'轮到 {at(rs_player[event.group_id][1] if event.user_id == rs_player[event.group_id][2] else rs_player[event.group_id][2])}了')) + await shot.finish("请这位勇士先发送 接受对决 来站上擂台...", at_sender=True) + player1_name = rs_player[event.group_id]["player1"] + player2_name = rs_player[event.group_id]["player2"] + if rs_player[event.group_id]["next"] != event.user_id: + if ( + event.user_id != rs_player[event.group_id][1] + and event.user_id != rs_player[event.group_id][2] + ): + await shot.finish( + random.choice( + [ + f"不要打扰 {player1_name} 和 {player2_name} 的决斗啊!", + f"给我好好做好一个观众!不然{NICKNAME}就要生气了", + f"不要捣乱啊baka{(await GroupInfoUser.get_member_info(event.user_id, event.group_id)).user_name}!", + ] + ), + at_sender=True, + ) + await shot.finish( + f"你的左轮不是连发的!该 " + f'{(await GroupInfoUser.get_member_info(int(rs_player[event.group_id]["next"]), event.group_id)).user_name} 开枪了' + ) + if rs_player[event.group_id]["bullet"][rs_player[event.group_id]["index"]] != 1: + await shot.send( + Message( + random.choice( + [ + "呼呼,没有爆裂的声响,你活了下来", + "虽然黑洞洞的枪口很恐怖,但好在没有子弹射出来,你活下来了", + '"咔",你没死,看来运气不错', + ] + ) + + f"\n下一枪中弹的概率" + f':{str(float((rs_player[event.group_id]["bullet_num"])) / float(rs_player[event.group_id]["null_bullet_num"] - 1 + rs_player[event.group_id]["bullet_num"]) * 100)[:5]}%\n' + f"轮到 {at(rs_player[event.group_id][1] if event.user_id == rs_player[event.group_id][2] else rs_player[event.group_id][2])}了" + ) + ) rs_player[event.group_id]["null_bullet_num"] -= 1 - rs_player[event.group_id]['next'] = rs_player[event.group_id][1] if \ - event.user_id == rs_player[event.group_id][2] else rs_player[event.group_id][2] - rs_player[event.group_id]['time'] = time.time() - rs_player[event.group_id]['index'] += 1 + rs_player[event.group_id]["next"] = ( + rs_player[event.group_id][1] + if event.user_id == rs_player[event.group_id][2] + else rs_player[event.group_id][2] + ) + rs_player[event.group_id]["time"] = time.time() + rs_player[event.group_id]["index"] += 1 else: - await shot.send(random.choice([ - '\"嘭!\",你直接去世了', - '眼前一黑,你直接穿越到了异世界...(死亡)', - '终究还是你先走一步...', - ]) + f'\n第 {rs_player[event.group_id]["index"] + 1} 发子弹送走了你...', at_sender=True) - win_name = player1_name if event.user_id == rs_player[event.group_id][2] else player2_name + await shot.send( + random.choice( + [ + '"嘭!",你直接去世了', + "眼前一黑,你直接穿越到了异世界...(死亡)", + "终究还是你先走一步...", + ] + ) + + f'\n第 {rs_player[event.group_id]["index"] + 1} 发子弹送走了你...', + at_sender=True, + ) + win_name = ( + player1_name + if event.user_id == rs_player[event.group_id][2] + else player2_name + ) await asyncio.sleep(0.5) - await shot.send(f'这场对决是 {win_name} 胜利了') + await shot.send(f"这场对决是 {win_name} 胜利了") await end_game(bot, event) async def end_game(bot: Bot, event: GroupMessageEvent): global rs_player - player1_name = rs_player[event.group_id]['player1'] - player2_name = rs_player[event.group_id]['player2'] - if rs_player[event.group_id]['next'] == rs_player[event.group_id][1]: + player1_name = rs_player[event.group_id]["player1"] + player2_name = rs_player[event.group_id]["player2"] + if rs_player[event.group_id]["next"] == rs_player[event.group_id][1]: win_user_id = rs_player[event.group_id][2] lose_user_id = rs_player[event.group_id][1] win_name = player2_name @@ -293,60 +393,74 @@ async def end_game(bot: Bot, event: GroupMessageEvent): win_name = player1_name lose_name = player2_name rand = random.randint(0, 5) - money = rs_player[event.group_id]['money'] + money = rs_player[event.group_id]["money"] if money > 10: fee = int(money * float(rand) / 100) fee = 1 if fee < 1 and rand != 0 else fee else: fee = 0 - await RussianUser.add_count(win_user_id, event.group_id, 'win') - await RussianUser.add_count(lose_user_id, event.group_id, 'lose') - await RussianUser.money(win_user_id, event.group_id, 'win', money - fee) - await RussianUser.money(lose_user_id, event.group_id, 'lose', money) + await RussianUser.add_count(win_user_id, event.group_id, "win") + await RussianUser.add_count(lose_user_id, event.group_id, "lose") + await RussianUser.money(win_user_id, event.group_id, "win", money - fee) + await RussianUser.money(lose_user_id, event.group_id, "lose", money) await BagUser.add_gold(win_user_id, event.group_id, money - fee) await BagUser.spend_gold(lose_user_id, event.group_id, money) win_user = await RussianUser.ensure(win_user_id, event.group_id) lose_user = await RussianUser.ensure(lose_user_id, event.group_id) - bullet_str = '' - for x in rs_player[event.group_id]['bullet']: - bullet_str += '__ ' if x == 0 else '| ' - logger.info(f'俄罗斯轮盘:胜者:{win_name} - 败者:{lose_name} - 金币:{money}') - await bot.send(event, message=f'结算:\n' - f'\t胜者:{win_name}\n' - f'\t赢取金币:{money - fee}\n' - f'\t累计胜场:{win_user.win_count}\n' - f'\t累计赚取金币:{win_user.make_money}\n' - f'-------------------\n' - f'\t败者:{lose_name}\n' - f'\t输掉金币:{money}\n' - f'\t累计败场:{lose_user.fail_count}\n' - f'\t累计输掉金币:{lose_user.lose_money}\n' - f'-------------------\n' - f'哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n' - f'子弹排列:{bullet_str[:-1]}') + bullet_str = "" + for x in rs_player[event.group_id]["bullet"]: + bullet_str += "__ " if x == 0 else "| " + logger.info(f"俄罗斯轮盘:胜者:{win_name} - 败者:{lose_name} - 金币:{money}") + await bot.send( + event, + message=f"结算:\n" + f"\t胜者:{win_name}\n" + f"\t赢取金币:{money - fee}\n" + f"\t累计胜场:{win_user.win_count}\n" + f"\t累计赚取金币:{win_user.make_money}\n" + f"-------------------\n" + f"\t败者:{lose_name}\n" + f"\t输掉金币:{money}\n" + f"\t累计败场:{lose_user.fail_count}\n" + f"\t累计输掉金币:{lose_user.lose_money}\n" + f"-------------------\n" + f"哼哼,{NICKNAME}从中收取了 {float(rand)}%({fee}金币) 作为手续费!\n" + f"子弹排列:{bullet_str[:-1]}", + ) rs_player[event.group_id] = {} @record.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): user = await RussianUser.ensure(event.user_id, event.group_id) - await record.send(f'俄罗斯轮盘\n' - f'胜利场次:{user.win_count}\n' - f'失败场次:{user.fail_count}\n' - f'赚取金币:{user.make_money}\n' - f'输掉金币:{user.lose_money}', at_sender=True) + await record.send( + f"俄罗斯轮盘\n" + f"总胜利场次:{user.win_count}\n" + f"当前连胜:{user.winning_streak}\n" + f"最高连胜:{user.max_winning_streak}\n" + f"总失败场次:{user.fail_count}\n" + f"当前连败:{user.losing_streak}\n" + f"最高连败:{user.max_losing_streak}\n" + f"赚取金币:{user.make_money}\n" + f"输掉金币:{user.lose_money}", + at_sender=True, + ) @rssian_rank.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): - if state["_prefix"]["raw_command"] in ['胜场排行', '胜利排行']: - await rssian_rank.finish(await rank(event.group_id, 'win_rank')) - if state["_prefix"]["raw_command"] in ['败场排行', '失败排行']: - await rssian_rank.finish(await rank(event.group_id, 'lose_rank')) - if state["_prefix"]["raw_command"] == '欧洲人排行': - await rssian_rank.finish(await rank(event.group_id, 'make_money')) - if state["_prefix"]["raw_command"] == '慈善家排行': - await rssian_rank.finish(await rank(event.group_id, 'spend_money')) + if state["_prefix"]["raw_command"] in ["胜场排行", "胜利排行"]: + await rssian_rank.finish(await rank(event.group_id, "win_rank")) + if state["_prefix"]["raw_command"] in ["败场排行", "失败排行"]: + await rssian_rank.finish(await rank(event.group_id, "lose_rank")) + if state["_prefix"]["raw_command"] == "欧洲人排行": + await rssian_rank.finish(await rank(event.group_id, "make_money")) + if state["_prefix"]["raw_command"] == "慈善家排行": + await rssian_rank.finish(await rank(event.group_id, "spend_money")) + if state["_prefix"]["raw_command"] == "最高连胜排行": + await rssian_rank.finish(await rank(event.group_id, "max_winning_streak")) + if state["_prefix"]["raw_command"] == "最高连败排行": + await rssian_rank.finish(await rank(event.group_id, "max_losing_streak")) # 随机子弹排列 diff --git a/plugins/russian/data_source.py b/plugins/russian/data_source.py index 21041596..9c793e6d 100644 --- a/plugins/russian/data_source.py +++ b/plugins/russian/data_source.py @@ -14,9 +14,15 @@ async def rank(group_id: int, itype) -> str: elif itype == 'make_money': rank_name = '\t赢取金币排行榜\n' all_user_data = [user.make_money for user in all_users] - else: + elif itype == 'spend_money': rank_name = '\t输掉金币排行榜\n' all_user_data = [user.lose_money for user in all_users] + elif itype == 'max_winning_streak': + rank_name = '\t最高连胜排行榜\n' + all_user_data = [user.max_winning_streak for user in all_users] + else: + rank_name = '\t最高连败排行榜\n' + all_user_data = [user.max_losing_streak for user in all_users] rst = '' if all_users: rst = await init_rank(all_user_id, all_user_data, group_id) diff --git a/plugins/search_anime/__init__.py b/plugins/search_anime/__init__.py index 6d62f3a8..04aa0f4e 100644 --- a/plugins/search_anime/__init__.py +++ b/plugins/search_anime/__init__.py @@ -31,7 +31,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): async def _(bot: Bot, event: MessageEvent, state: T_State): if _ulmt.check(event.user_id): await search_anime.finish("您有动漫正在搜索,请稍等...", at_sender=True) - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) if get_message_text(event.json()): state["anime"] = get_message_text(event.json()) @@ -55,4 +55,4 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): else: logger.warning(f"未找到番剧 {key_word}") await search_anime.send(f"未找到番剧 {key_word}(也有可能是超时,再尝试一下?)") - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) diff --git a/plugins/search_buff_skin_price/__init__.py b/plugins/search_buff_skin_price/__init__.py index b5d0890c..aaa2291e 100644 --- a/plugins/search_buff_skin_price/__init__.py +++ b/plugins/search_buff_skin_price/__init__.py @@ -44,7 +44,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): @search_skin.got("skin", prompt="要查询该武器的什么皮肤呢?") async def arg_handle(bot: Bot, event: MessageEvent, state: T_State): result = "" - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) if state["name"] in ["ak", "ak47"]: state["name"] = "ak-47" name = state["name"] + " | " + state["skin"] @@ -53,7 +53,7 @@ async def arg_handle(bot: Bot, event: MessageEvent, state: T_State): except FileNotFoundError: await search_skin.finish(F'请先对{NICKNAME}说"设置cookie"来设置cookie!') if status_code in [996, 997, 998]: - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await search_skin.finish(result) if result: logger.info( @@ -61,7 +61,7 @@ async def arg_handle(bot: Bot, event: MessageEvent, state: T_State): f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}) 查询皮肤:" + name ) - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await search_skin.finish(result) else: logger.info( @@ -69,7 +69,7 @@ async def arg_handle(bot: Bot, event: MessageEvent, state: T_State): f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'}" f" 查询皮肤:{name} 没有查询到" ) - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) await search_skin.finish("没有查询到哦,请检查格式吧") diff --git a/plugins/send_img/__init__.py b/plugins/send_img/__init__.py index 39b0757d..089aff9e 100644 --- a/plugins/send_img/__init__.py +++ b/plugins/send_img/__init__.py @@ -37,7 +37,6 @@ cmd = set(IMAGE_DIR_LIST) send_img = on_command("img", aliases=cmd, priority=5, block=True) pa = on_keyword({"丢人爬", "爪巴"}, priority=5, block=True) pa_reg = on_regex("^爬$", priority=5, block=True) -search_img = on_regex(".*[份|发|张|个|次|点]图.*?", priority=6, block=True) search_url = "https://api.fantasyzone.cc/tu/search.php" @@ -112,70 +111,3 @@ num_key = { "九": 9, } - -@search_img.handle() -async def _(bot: Bot, event: MessageEvent, state: T_State): - msg = get_message_text(event.json()) - r = re.search("[来要]?(.*)[份发张个次点]图(.*)", msg) - num = r.group(1) - if num in num_key.keys(): - num = num_key[num] - elif is_number(num): - num = int(num) - else: - return - keyword = r.group(2) - params = {"search": keyword, "r18": 0} - async with aiohttp.ClientSession() as session: - exists_id = [] - for _ in range(num): - for _ in range(10): - try: - async with session.get( - search_url, timeout=5, params=params - ) as response: - data = json.loads(await response.text()) - except TimeoutError: - pass - else: - if data["id"] == "null": - await send_img.finish(f"没有搜索到 {keyword} 的图片...", at_sender=True) - if data["id"] in exists_id: - continue - title = data["title"] - author = data["userName"] - pid = data["id"] - img_url = data["url"] - exists_id.append(pid) - for _ in range(5): - try: - await download_pic(img_url, event.user_id, session) - except TimeoutError: - pass - else: - break - else: - await search_img.finish("图片下载失败...", at_sender=True) - await search_img.send( - Message( - f"title:{title}\n" - f"pid:{pid}\n" - f"author:{author}\n" - f'{image(f"send_img_{event.user_id}.png", "temp")}' - ) - ) - break - else: - await search_img.send("图片下载惜败了....", at_sender=True) - logger.info( - f"(USER {event.user_id}, GROUP {event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" - f" 发送搜索了 {num} 张 {keyword} 的图片" - ) - - -async def download_pic(img_url: str, user_id: int, session): - async with session.get(img_url, timeout=2) as res: - async with aiofiles.open( - f"{IMAGE_PATH}/temp/send_img_{user_id}.png", "wb" - ) as f: - await f.write(await res.read()) diff --git a/plugins/send_setu/__init__.py b/plugins/send_setu/__init__.py index 0879efea..3d1b2c09 100644 --- a/plugins/send_setu/__init__.py +++ b/plugins/send_setu/__init__.py @@ -1,15 +1,12 @@ import random from nonebot import on_command, on_regex from services.log import logger -from models.sigin_group_user import SignGroupUser -from nonebot.exception import IgnoredException -from nonebot.message import run_preprocessor, run_postprocessor +from models.sign_group_user import SignGroupUser +from nonebot.message import run_postprocessor from nonebot.matcher import Matcher from typing import Optional, Type from gino.exceptions import UninitializedError from utils.utils import ( - FreqLimiter, - UserExistLimiter, is_number, get_message_text, get_message_imgs, @@ -35,9 +32,8 @@ from .data_source import ( ) from nonebot.adapters.cqhttp.exception import ActionFailed from configs.config import ONLY_USE_LOCAL_SETU, WITHDRAW_SETU_TIME, NICKNAME -from utils.message_builder import at +from utils.static_data import withdraw_message_id_manager import re -import asyncio try: import ujson as json @@ -65,27 +61,7 @@ __plugin_usage__ = f"""示例: 色图 萝莉|少女 白丝|黑丝 色图 萝莉 猫娘""" -_flmt = FreqLimiter(5) -_ulmt = UserExistLimiter() setu_data_list = [] -withdraw_message_id = [] - - -@run_preprocessor -async def _(matcher: Matcher, bot: Bot, event: MessageEvent, state: T_State): - if isinstance(event, MessageEvent): - if matcher.module == "send_setu": - if _ulmt.check(event.user_id): - if isinstance(event, GroupMessageEvent): - await bot.send_group_msg( - group_id=event.group_id, - message=Message(f"{at(event.user_id)}您有色图正在处理,请稍等....."), - ) - else: - await bot.send_private_msg( - user_id=event.user_id, message=f"您有色图正在处理,请稍等....." - ) - raise IgnoredException("色图正在处理!") @run_postprocessor @@ -96,17 +72,9 @@ async def do_something( event: Event, state: T_State, ): - global setu_data_list, withdraw_message_id + global setu_data_list if isinstance(event, MessageEvent): if matcher.module == "send_setu": - # 解除占用 - _ulmt.set_False(event.user_id) - tasks = [] - # 撤回色图 - for id_ in withdraw_message_id[:]: - tasks.append(asyncio.ensure_future(withdraw_message(bot, event, id_))) - withdraw_message_id.remove(id_) - await asyncio.gather(*tasks) # 添加数据至数据库 try: await add_data_to_database(setu_data_list) @@ -127,7 +95,6 @@ find_setu = on_command("查色图", priority=5, block=True) @setu.handle() async def _(bot: Bot, event: MessageEvent, state: T_State): - global withdraw_message_id msg = get_message_text(event.json()) if isinstance(event, GroupMessageEvent): impression = ( @@ -136,10 +103,6 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): luox = get_luoxiang(impression) if luox: await setu.finish(luox) - _ulmt.set_True(event.user_id) - if not _flmt.check(event.user_id): - await setu.finish("您冲得太快了,请稍候再冲", at_sender=True) - _flmt.start_cd(event.user_id) r18 = 0 num = 1 # 是否看r18 @@ -166,7 +129,8 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" f" 发送色图 {setu_list[0].local_id}.png" ) - withdraw_message_id.append(msg_id["message_id"]) + if msg_id: + withdraw_message(event, msg_id["message_id"]) return await send_setu_handle(setu, event, state["_prefix"]["raw_command"], msg, num, r18) @@ -195,9 +159,6 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): luox = get_luoxiang(impression) if luox: await setu.finish(luox, at_sender=True) - if not _flmt.check(event.user_id): - await setu.finish("您冲得太快了,请稍候再冲", at_sender=True) - _flmt.start_cd(event.user_id) msg = get_message_text(event.json()) num = 1 msg = re.search(r"(.*)[份发张个次点](.*)[瑟涩色]图", msg) @@ -251,16 +212,16 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): async def send_setu_handle( matcher: Type[Matcher], event: MessageEvent, command: str, msg: str, num: int, r18: int ): - global setu_data_list, withdraw_message_id - error_info = "" + global setu_data_list # 非 id,在线搜索 tags = msg.split() # 真寻的色图?怎么可能 if f"{NICKNAME}" in tags: await matcher.finish("咳咳咳,虽然我很可爱,但是我木有自己的色图~~~有的话记得发我一份呀") # 本地先拿图,下载失败补上去 - setu_list, code = await get_setu_list(tags=msg.split(), r18=r18) - if not ONLY_USE_LOCAL_SETU and (tags or not setu_list or num > len(setu_list)): + setu_list, code = None, 200 + msg_id = None + if not ONLY_USE_LOCAL_SETU and tags: # 先尝试获取在线图片 urls, text_list, add_databases_list, code = await get_setu_urls(tags, num, r18, command) for x in add_databases_list: @@ -280,8 +241,9 @@ async def send_setu_handle( f" 发送色图 {index}.png" ) msg_id = await matcher.send(Message(f"{text_list[i]}\n{setu_img}")) - withdraw_message_id.append(msg_id["message_id"]) else: + if setu_list is None: + setu_list, _ = await get_setu_list(tags=tags, r18=r18) if setu_list: setu_image = random.choice(setu_list) setu_list.remove(setu_image) @@ -298,17 +260,18 @@ async def send_setu_handle( ) else: msg_id = await matcher.send(text_list[i] + "\n" + setu_img) - withdraw_message_id.append(msg_id["message_id"]) + if msg_id: + withdraw_message(event, msg_id["message_id"]) except ActionFailed: await matcher.finish("坏了,这张图色过头了,我自己看看就行了!", at_sender=True) return # 本地无图 或 超过上下限 - if code != 200 or (not setu_list and ONLY_USE_LOCAL_SETU): - if code == 999: - await matcher.finish('网络连接失败...', at_sender=True) - await matcher.finish(setu_list[0], at_sender=True) - elif not setu_list: - await matcher.finish(error_info, at_sender=True) + if code != 200: + await matcher.finish('网络连接失败...', at_sender=True) + if setu_list is None: + setu_list, code = await get_setu_list(tags=tags, r18=r18) + if code != 200: + await matcher.finish(setu_list[0], at_sender=True) # 开始发图 for _ in range(num): if not setu_list: @@ -322,7 +285,7 @@ async def send_setu_handle( + (await check_local_exists_or_download(setu_image))[0] ) ) - withdraw_message_id.append(msg_id["message_id"]) + withdraw_message(event, msg_id["message_id"]) logger.info( f"(USER {event.user_id}, GROUP " f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'})" @@ -333,13 +296,11 @@ async def send_setu_handle( # 撤回图片 -async def withdraw_message(bot: Bot, event: MessageEvent, id_: int): +def withdraw_message(event: MessageEvent, id_: int): if WITHDRAW_SETU_TIME[0]: if ( (WITHDRAW_SETU_TIME[1] == 0 and isinstance(event, PrivateMessageEvent)) or (WITHDRAW_SETU_TIME[1] == 1 and isinstance(event, GroupMessageEvent)) or WITHDRAW_SETU_TIME[1] == 2 ): - await asyncio.sleep(WITHDRAW_SETU_TIME[0]) - await bot.delete_msg(message_id=id_, self_id=int(bot.self_id)) - logger.info(f"自动撤回色图 消息id:{id_}") + withdraw_message_id_manager['message_id'].append((id_, WITHDRAW_SETU_TIME[0])) diff --git a/plugins/send_setu/data_source.py b/plugins/send_setu/data_source.py index c44c5fb0..1184eee3 100644 --- a/plugins/send_setu/data_source.py +++ b/plugins/send_setu/data_source.py @@ -62,6 +62,8 @@ async def get_setu_urls( for x in random_idx: x_urls.append(urls[x]) x_text_lst.append(text_list[x]) + if not x_urls: + return ["没找到符合条件的色图..."], [], [], 401 return x_urls, x_text_lst, add_databases_list, 200 else: return ["没找到符合条件的色图..."], [], [], 401 diff --git a/plugins/shop/gold_redbag/__init__.py b/plugins/shop/gold_redbag/__init__.py index 2b78f3a3..7f9de5c1 100644 --- a/plugins/shop/gold_redbag/__init__.py +++ b/plugins/shop/gold_redbag/__init__.py @@ -23,6 +23,7 @@ from nonebot.permission import SUPERUSER from nonebot.rule import to_me from datetime import datetime, timedelta from configs.config import NICKNAME +from apscheduler.jobstores.base import ConflictingIdError import random import time diff --git a/plugins/shop/use/data_source.py b/plugins/shop/use/data_source.py index 72095b4b..4d628799 100644 --- a/plugins/shop/use/data_source.py +++ b/plugins/shop/use/data_source.py @@ -1,15 +1,14 @@ -from models.sigin_group_user import SignGroupUser +from models.sign_group_user import SignGroupUser async def effect(user_id: int, group_id: int, name: str) -> bool: - if name == "好感双倍加持卡Ⅰ": + if name in ["好感双倍加持卡Ⅰ", "好感度双倍加持卡Ⅰ"]: user = await SignGroupUser.ensure(user_id, group_id) await user.update(add_probability=0.1).apply() - if name == "好感双倍加持卡Ⅱ": + if name in ["好感双倍加持卡Ⅱ", "好感度双倍加持卡Ⅱ"]: user = await SignGroupUser.ensure(user_id, group_id) await user.update(add_probability=0.2).apply() - if name == "好感双倍加持卡Ⅲ": + if name in ["好感双倍加持卡Ⅲ", "好感度双倍加持卡Ⅲ"]: user = await SignGroupUser.ensure(user_id, group_id) - print(user.user_qq) await user.update(add_probability=0.3).apply() return True diff --git a/plugins/sign_in/__init__.py b/plugins/sign_in/__init__.py index f4ae9b92..6ff29d26 100644 --- a/plugins/sign_in/__init__.py +++ b/plugins/sign_in/__init__.py @@ -9,9 +9,11 @@ from nonebot.adapters.cqhttp import Bot, GroupMessageEvent from nonebot.adapters.cqhttp.permission import GROUP from utils.message_builder import image from nonebot import on_command -from utils.utils import get_message_text +from utils.utils import get_message_text, scheduler from pathlib import Path from configs.path_config import DATA_PATH +from services.log import logger +from .utils import clear_sign_data_pic try: import ujson as json @@ -52,16 +54,18 @@ total_sign_rank = on_command( @sign.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + nickname = event.sender.card if event.sender.card else event.sender.nickname await sign.send( - await group_user_check_in(event.user_id, event.group_id), + await group_user_check_in(nickname, event.user_id, event.group_id), at_sender=True, ) @my_sign.handle() async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + nickname = event.sender.card if event.sender.card else event.sender.nickname await my_sign.send( - await group_user_check(event.user_id, event.group_id), + await group_user_check(nickname, event.user_id, event.group_id), at_sender=True, ) @@ -89,3 +93,15 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): await total_sign_rank.send("设置成功,签到总榜将会显示您的头像名称以及好感度!", at_sender=True) with open(_file, "w", encoding="utf8") as f: json.dump(data, f, ensure_ascii=False, indent=4) + + +@scheduler.scheduled_job( + 'interval', + hours=1, +) +async def _(): + try: + clear_sign_data_pic() + logger.info('清理日常签到图片数据数据完成....') + except Exception as e: + logger.error(f'清理日常签到图片数据数据失败..{type(e)}: {e}') diff --git a/plugins/sign_in/config.py b/plugins/sign_in/config.py new file mode 100644 index 00000000..5c062d1e --- /dev/null +++ b/plugins/sign_in/config.py @@ -0,0 +1,76 @@ +from configs.path_config import IMAGE_PATH +from pathlib import Path + + +SIGN_RESOURCE_PATH = Path(IMAGE_PATH) / 'sign' / 'sign_res' +SIGN_TODAY_CARD_PATH = Path(IMAGE_PATH) / 'sign' / 'today_card' +SIGN_BORDER_PATH = Path(SIGN_RESOURCE_PATH) / 'border' +SIGN_BACKGROUND_PATH = Path(SIGN_RESOURCE_PATH) / 'background' + +SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True) +SIGN_BORDER_PATH.mkdir(exist_ok=True, parents=True) +SIGN_BACKGROUND_PATH.mkdir(exist_ok=True, parents=True) + +SIGN_CARD1_PROB = 0.2 # 好感度双倍加持卡Ⅰ +SIGN_CARD2_PROB = 0.09 # 好感度双倍加持卡Ⅱ +SIGN_CARD3_PROB = 0.05 # 好感度双倍加持卡Ⅲ + +PROB_DATA = { + SIGN_CARD3_PROB: '好感度双倍加持卡Ⅲ', + SIGN_CARD2_PROB: '好感度双倍加持卡Ⅱ', + SIGN_CARD1_PROB: '好感度双倍加持卡Ⅰ' +} + + +lik2relation = { + '0': '路人', + '1': '陌生', + '2': '初识', + '3': '普通', + '4': '熟悉', + '5': '信赖', + '6': '相知', + '7': '厚谊', + '8': '亲密' +} + +level2attitude = { + '0': '排斥', + '1': '警惕', + '2': '可以交流', + '3': '一般', + '4': '是个好人', + '5': '好朋友', + '6': '可以分享小秘密', + '7': '喜欢', + '8': '恋人' +} + +weekdays = { + 1: 'Mon', + 2: 'Tue', + 3: 'Wed', + 4: 'Thu', + 5: 'Fri', + 6: 'Sat', + 7: 'Sun' +} + +lik2level = { + 9999: '9', + 400: '8', + 270: '7', + 200: '6', + 140: '5', + 90: '4', + 50: '3', + 25: '2', + 10: '1', + 0: '0' +} + + + + + + diff --git a/plugins/sign_in/group_user_checkin.py b/plugins/sign_in/group_user_checkin.py index 4fa4f75b..874c0be2 100644 --- a/plugins/sign_in/group_user_checkin.py +++ b/plugins/sign_in/group_user_checkin.py @@ -1,39 +1,47 @@ -import random from datetime import datetime, timedelta -from io import BytesIO -from services.log import logger -from services.db_context import db -from models.sigin_group_user import SignGroupUser +from models.sign_group_user import SignGroupUser from models.group_member_info import GroupInfoUser from models.bag_user import BagUser -from configs.config import MAX_SIGN_GOLD, NICKNAME -from utils.image_utils import CreateImg -import aiohttp +from configs.config import NICKNAME +from nonebot.adapters.cqhttp import MessageSegment from asyncio.exceptions import TimeoutError +from utils.image_utils import CreateImg +from services.db_context import db +from .utils import get_card, SIGN_TODAY_CARD_PATH +from services.log import logger +from .random_event import random_event +from io import BytesIO +import random +import aiohttp import math import asyncio +import os -async def group_user_check_in(user_qq: int, group: int) -> str: +async def group_user_check_in( + nickname: str, user_qq: int, group: int +) -> MessageSegment: "Returns string describing the result of checking in" present = datetime.now() async with db.transaction(): # 取得相应用户 user = await SignGroupUser.ensure(user_qq, group, for_update=True) # 如果同一天签到过,特殊处理 - if user.checkin_time_last.date() == present.date(): - return _handle_already_checked_in(user) - return await _handle_check_in(user_qq, group, present) # ok + # if ( + # user.checkin_time_last + timedelta(hours=8) + # ).date() >= present.date() or f"{user}_{group}_sign_{datetime.now().date()}" in os.listdir( + # SIGN_TODAY_CARD_PATH + # ): + # gold = await BagUser.get_gold(user_qq, group) + # return await get_card(user, nickname, -1, gold, "") + return await _handle_check_in(nickname, user_qq, group, present) # ok -def _handle_already_checked_in(user: SignGroupUser) -> str: - return f"已经签到过啦~ 好感度:{user.impression:.2f}" - - -async def _handle_check_in(user_qq: int, group: int, present: datetime) -> str: +async def _handle_check_in( + nickname: str, user_qq: int, group: int, present: datetime +) -> MessageSegment: user = await SignGroupUser.ensure(user_qq, group, for_update=True) impression_added = random.random() - present = present + timedelta(hours=8) critx2 = random.random() add_probability = user.add_probability specify_probability = user.specify_probability @@ -41,56 +49,36 @@ async def _handle_check_in(user_qq: int, group: int, present: datetime) -> str: impression_added *= 2 elif critx2 < specify_probability: impression_added *= 2 - new_impression = user.impression + impression_added - message = random.choice( - ( - "谢谢,你是个好人!", - "对了,来喝杯茶吗?", - ) - ) - await user.update( - checkin_count=user.checkin_count + 1, - checkin_time_last=present, - impression=new_impression, - add_probability=0, - specify_probability=0, - ).apply() - # glod = await random_glod(user_qq, group, specify_probability) - if user.impression < 1: - impression = 1 - else: - impression = user.impression + await SignGroupUser.sign(user, impression_added, present) gold = random.randint(1, 100) - imgold = random.randint(1, int(impression)) - if imgold > MAX_SIGN_GOLD: - imgold = MAX_SIGN_GOLD - await BagUser.add_gold(user_qq, group, gold + imgold) + gift, gift_type = random_event(user.impression) + if gift_type == "gold": + await BagUser.add_gold(user_qq, group, gold + gift) + gift = f"额外金币 + {gift}" + else: + await BagUser.add_props(user_qq, group, gift) + gift += ' + 1' if critx2 + add_probability > 0.97 or critx2 < specify_probability: logger.info( f"(USER {user.user_qq}, GROUP {user.belonging_group})" - f" CHECKED IN successfully. score: {new_impression:.2f} (+{impression_added * 2:.2f}).获取金币:{gold+imgold}" + f" CHECKED IN successfully. score: {user.impression:.2f} " + f"(+{impression_added * 2:.2f}).获取金币:{gold + gift if gift == 'gold' else gold}" ) - return f"{message} 好感度:{new_impression:.2f} (+{impression_added/2:.2f}×2)!!!\n获取金币:{gold} \n好感度额外获得金币:{imgold}" + return await get_card(user, nickname, impression_added, gold, gift, True) else: logger.info( f"(USER {user.user_qq}, GROUP {user.belonging_group})" - f" CHECKED IN successfully. score: {new_impression:.2f} (+{impression_added:.2f}).获取金币:{gold+imgold}" + f" CHECKED IN successfully. score: {user.impression:.2f} " + f"(+{impression_added:.2f}).获取金币:{gold + gift if gift == 'gold' else gold}" ) - return f"{message} 好感度:{new_impression:.2f} (+{impression_added:.2f})\n获取金币:{gold} \n好感度额外获得金币:{imgold}" + return await get_card(user, nickname, impression_added, gold, gift) -async def group_user_check(user_qq: int, group: int) -> str: +async def group_user_check(nickname: str, user_qq: int, group: int) -> MessageSegment: # heuristic: if users find they have never checked in they are probable to check in user = await SignGroupUser.ensure(user_qq, group) gold = await BagUser.get_gold(user_qq, group) - return "好感度:{:.2f}\n金币:{}\n历史签到数:{}\n上次签到日期:{}".format( - user.impression, - gold, - user.checkin_count, - user.checkin_time_last.date() - if user.checkin_time_last != datetime.min - else "从未", - ) + return await get_card(user, nickname, None, gold, "", is_card_view=True) async def group_impression_rank(group: int) -> str: @@ -108,7 +96,7 @@ async def group_impression_rank(group: int) -> str: user_name = ( await GroupInfoUser.get_member_info(user_qq, group) ).user_name - except Exception as e: + except AttributeError: logger.info(f"USER {user_qq}, GROUP {group} 不在群内") _count += 1 impression_list.remove(impression) diff --git a/plugins/sign_in/random_event.py b/plugins/sign_in/random_event.py new file mode 100644 index 00000000..f1dda044 --- /dev/null +++ b/plugins/sign_in/random_event.py @@ -0,0 +1,24 @@ +from configs.config import MAX_SIGN_GOLD +from typing import Union +from .config import PROB_DATA +import random + + +def random_event(impression: float) -> 'Union[str, int], str': + """ + 签到随机事件 + :param impression: 好感度 + :return: 额外奖励 和 类型 + """ + rand = random.random() - impression / 1000 + for prob in PROB_DATA.keys(): + if rand <= prob: + return PROB_DATA[prob], 'props' + gold = random.randint(1, random.randint(1, int(1 if impression < 1 else impression))) + gold = MAX_SIGN_GOLD if gold > MAX_SIGN_GOLD else gold + return gold, 'gold' + + + + + diff --git a/plugins/sign_in/utils.py b/plugins/sign_in/utils.py new file mode 100644 index 00000000..5ee15e95 --- /dev/null +++ b/plugins/sign_in/utils.py @@ -0,0 +1,330 @@ +from .config import ( + SIGN_RESOURCE_PATH, + SIGN_TODAY_CARD_PATH, + SIGN_BORDER_PATH, + SIGN_BACKGROUND_PATH, + lik2level, + lik2relation, + level2attitude, + weekdays, +) +from models.sign_group_user import SignGroupUser +from models.group_member_info import GroupInfoUser +from nonebot.adapters.cqhttp import MessageSegment +from utils.image_utils import CreateImg +from utils.message_builder import image +from configs.config import NICKNAME +from pathlib import Path +from datetime import datetime +from typing import Optional, List +from nonebot import Driver +from io import BytesIO +import asyncio +import nonebot +import aiohttp +import os + + +driver: Driver = nonebot.get_driver() + + +@driver.on_startup +async def init_image(): + SIGN_RESOURCE_PATH.mkdir(parents=True, exist_ok=True) + await GroupInfoUser.add_member_info(114514, 114514, "", datetime.min) + generate_progress_bar_pic() + clear_sign_data_pic() + + +async def _get_pic(qq): + url = f"http://q1.qlogo.cn/g?b=qq&nk={qq}&s=100" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=5) as response: + return await response.read() + + +async def get_card( + user: "SignGroupUser", + nickname: str, + add_impression: Optional[float], + gold: Optional[int], + gift: str, + is_double: bool = False, + is_card_view: bool = False, +) -> MessageSegment: + user_id = user.user_qq + date = datetime.now().date() + _type = 'view' if is_card_view else 'sign' + card_file = Path(SIGN_TODAY_CARD_PATH) / f"{user_id}_{user.belonging_group}_{_type}_{date}.png" + if card_file.exists(): + return image(f"{user_id}_{user.belonging_group}_{_type}_{date}.png", "sign/today_card") + else: + if add_impression == -1: + card_file = Path(SIGN_TODAY_CARD_PATH) / f"{user_id}_{user.belonging_group}_view_{date}.png" + if card_file.exists(): + return image(f"{user_id}_{user.belonging_group}_view_{date}.png", "sign/today_card") + is_card_view = True + ava = BytesIO(await _get_pic(user_id)) + uid = await GroupInfoUser.get_group_member_uid( + user.user_qq, user.belonging_group + ) + impression_list = None + if is_card_view: + _, impression_list, _ = await SignGroupUser.get_all_impression( + user.belonging_group + ) + return await asyncio.get_event_loop().run_in_executor( + None, + _generate_card, + user, + nickname, + user_id, + add_impression, + gold, + gift, + uid, + ava, + impression_list, + is_double, + is_card_view, + ) + + +def _generate_card( + user: "SignGroupUser", + nickname: str, + user_id: int, + impression: Optional[float], + gold: Optional[int], + gift: str, + uid: str, + ava_bytes: BytesIO, + impression_list: List[float], + is_double: bool = False, + is_card_view: bool = False, +) -> MessageSegment: + ava_bk = CreateImg(140, 140, is_alpha=True) + ava_border = CreateImg( + 140, + 140, + background=SIGN_BORDER_PATH / 'ava_border_01.png', + ) + ava = CreateImg(102, 102, background=ava_bytes) + ava.circle() + ava_bk.paste(ava, center_type="center") + ava_bk.paste(ava_border, alpha=True, center_type="center") + + info_img = CreateImg(250, 150, color=(255, 255, 255, 0), font_size=15) + level, next_impression, previous_impression = get_level_and_next_impression( + user.impression + ) + info_img.text((0, 0), f"· 好感度等级:{level} [{lik2relation[level]}]") + info_img.text((0, 20), f"· {NICKNAME}对你的态度:{level2attitude[level]}") + info_img.text((0, 40), f"· 距离升级还差 {next_impression - user.impression:.2f} 好感度") + + bar_bk = CreateImg(220, 20, background=SIGN_RESOURCE_PATH / "bar_white.png") + bar = CreateImg(220, 20, background=SIGN_RESOURCE_PATH / "bar.png") + bar_bk.paste( + bar, + ( + -int( + 220 + * ( + (next_impression - user.impression) + / (next_impression - previous_impression) + ) + ), + 0, + ), + True, + ) + font_size = 30 + if '好感度双倍加持卡' in gift: + font_size = 20 + gift_border = CreateImg( + 270, 100, background=SIGN_BORDER_PATH / "gift_border_02.png", font_size=font_size + ) + gift_border.text((0, 0), gift, center_type="center") + + bk = CreateImg( + 876, 424, background=SIGN_BACKGROUND_PATH / "background_01.jpg", font_size=25 + ) + A = CreateImg(876, 274, background=SIGN_RESOURCE_PATH / "white.png") + line = CreateImg(2, 180, color="black") + A.transparent(2) + A.paste(ava_bk, (25, 80), True) + A.paste(line, (200, 70)) + + nickname_img = CreateImg( + 0, + 0, + plain_text=nickname, + color=(255, 255, 255, 0), + font_size=50, + font_color=(255, 255, 255), + ) + if uid: + uid = f"{uid}".rjust(12, "0") + uid = uid[:4] + " " + uid[4:8] + " " + uid[8:] + else: + uid = "XXXX XXXX XXXX" + uid_img = CreateImg( + 0, + 0, + plain_text=f"UID: {uid}", + color=(255, 255, 255, 0), + font_size=30, + font_color=(255, 255, 255), + ) + sign_day_img = CreateImg( + 0, + 0, + plain_text=f"{user.checkin_count}", + color=(255, 255, 255, 0), + font_size=40, + font_color=(211, 64, 33), + ) + lik_text1_img = CreateImg( + 0, 0, plain_text="当前", color=(255, 255, 255, 0), font_size=20 + ) + lik_text2_img = CreateImg( + 0, + 0, + plain_text=f"好感度:{user.impression:.2f}", + color=(255, 255, 255, 0), + font_size=30, + ) + watermark = CreateImg( + 0, + 0, + plain_text=f"{NICKNAME}@{datetime.now().year}", + color=(255, 255, 255, 0), + font_size=15, + font_color=(155, 155, 155), + ) + today_data = CreateImg(300, 300, color=(255, 255, 255, 0), font_size=20) + if is_card_view: + today_sign_text_img = CreateImg( + 0, 0, plain_text="", color=(255, 255, 255, 0), font_size=30 + ) + if impression_list: + impression_list.sort(reverse=True) + index = impression_list.index(user.impression) + rank_img = CreateImg( + 0, + 0, + plain_text=f"* 此群好感排名第 {index + 1} 位", + color=(255, 255, 255, 0), + font_size=30, + ) + A.paste(rank_img, ((A.w - rank_img.w - 10), 20), True) + today_data.text( + (0, 0), + f"上次签到日期:{'从未' if user.checkin_time_last == datetime.min else user.checkin_time_last.date()}", + ) + today_data.text((0, 25), f"总金币:{gold}") + today_data.text((0, 50), f'色图概率:{(70 + user.impression if user.impression < 100 else 100):.2f}%') + today_data.text((0, 75), f'开箱次数:{(20 + int(user.impression / 3))}') + _type = "view" + else: + A.paste(gift_border, (570, 140), True) + today_sign_text_img = CreateImg( + 0, 0, plain_text="今日签到", color=(255, 255, 255, 0), font_size=30 + ) + if is_double: + today_data.text((0, 0), f"好感度 + {impression / 2:.2f} × 2") + else: + today_data.text((0, 0), f"好感度 + {impression:.2f}") + today_data.text((0, 25), f"金币 + {gold}") + _type = "sign" + current_date = datetime.now() + week = current_date.isoweekday() + data = current_date.date() + hour = current_date.hour + minute = current_date.minute + second = current_date.second + data_img = CreateImg( + 0, + 0, + plain_text=f"时间:{data} {weekdays[week]} {hour}:{minute}:{second}", + color=(255, 255, 255, 0), + font_size=20, + ) + bk.paste(nickname_img, (30, 15), True) + bk.paste(uid_img, (30, 85), True) + bk.paste(A, (0, 150), alpha=True) + bk.text((30, 167), "Accumulative check-in for") + _x = bk.getsize("Accumulative check-in for")[0] + sign_day_img.w + 45 + bk.paste(sign_day_img, (346, 158), True) + bk.text((_x, 167), "days") + bk.paste(data_img, (220, 370), True) + bk.paste(lik_text1_img, (220, 240), True) + bk.paste(lik_text2_img, (262, 234), True) + bk.paste(bar_bk, (225, 275), True) + bk.paste(info_img, (220, 305), True) + bk.paste(today_sign_text_img, (550, 180), True) + bk.paste(today_data, (580, 220), True) + bk.paste(watermark, (15, 400), True) + bk.save(SIGN_TODAY_CARD_PATH / f"{user_id}_{user.belonging_group}_{_type}_{data}.png") + return image(f"{user_id}_{user.belonging_group}_{_type}_{data}.png", "sign/today_card") + + +def generate_progress_bar_pic(): + bg_2 = (254, 1, 254) + bg_1 = (0, 245, 246) + + bk = CreateImg(1000, 50, is_alpha=True) + img_x = CreateImg(50, 50, color=bg_2) + img_x.circle() + img_x.crop((25, 0, 50, 50)) + img_y = CreateImg(50, 50, color=bg_1) + img_y.circle() + img_y.crop((0, 0, 25, 50)) + A = CreateImg(950, 50) + width, height = A.size + + step_r = (bg_2[0] - bg_1[0]) / width + step_g = (bg_2[1] - bg_1[1]) / width + step_b = (bg_2[2] - bg_1[2]) / width + + for y in range(0, width): + bg_r = round(bg_1[0] + step_r * y) + bg_g = round(bg_1[1] + step_g * y) + bg_b = round(bg_1[2] + step_b * y) + for x in range(0, height): + A.point((y, x), fill=(bg_r, bg_g, bg_b)) + bk.paste(img_y, (0, 0), True) + bk.paste(A, (25, 0)) + bk.paste(img_x, (975, 0), True) + bk.save(SIGN_RESOURCE_PATH / "bar.png") + + A = CreateImg(950, 50) + bk = CreateImg(1000, 50, is_alpha=True) + img_x = CreateImg(50, 50) + img_x.circle() + img_x.crop((25, 0, 50, 50)) + img_y = CreateImg(50, 50) + img_y.circle() + img_y.crop((0, 0, 25, 50)) + bk.paste(img_y, (0, 0), True) + bk.paste(A, (25, 0)) + bk.paste(img_x, (975, 0), True) + bk.save(SIGN_RESOURCE_PATH / "bar_white.png") + + +def get_level_and_next_impression(impression: float): + if impression == 0: + return lik2level[10], 10, 0 + keys = list(lik2level.keys()) + for i in range(len(keys)): + if impression > keys[i]: + return lik2level[keys[i]], keys[i - 1], keys[i] + return lik2level[10], 10, 0 + + +def clear_sign_data_pic(): + date = datetime.now().date() + for file in os.listdir(SIGN_TODAY_CARD_PATH): + if str(date) not in file: + os.remove(SIGN_TODAY_CARD_PATH / file) + diff --git a/plugins/statistics_hook.py b/plugins/statistics_hook.py index fa274eb7..1371ee85 100644 --- a/plugins/statistics_hook.py +++ b/plugins/statistics_hook.py @@ -183,7 +183,10 @@ async def _(): for data in [_prefix_count_dict, _prefix_user_count_dict]: for x in _prefix_count_dict["day_statistics"].keys(): for key in _prefix_count_dict["day_statistics"][x].keys(): - data["day_statistics"][x][key] = 0 + try: + data["day_statistics"][x][key] = 0 + except KeyError: + pass data["day_index"] += 1 with open(DATA_PATH + "_prefix_count.json", "w", encoding="utf8") as f: json.dump(_prefix_count_dict, f, indent=4, ensure_ascii=False) diff --git a/plugins/super_cmd/__init__.py b/plugins/super_cmd/__init__.py index 46c7a951..0081e936 100644 --- a/plugins/super_cmd/__init__.py +++ b/plugins/super_cmd/__init__.py @@ -225,14 +225,15 @@ def _clear_data() -> float: size = 0 for dir_name in ['temp', 'rar', 'r18_rar']: dir_name = f'{IMAGE_PATH}/{dir_name}' - for file in os.listdir(dir_name): - try: - file_size = os.path.getsize(os.path.join(dir_name, file)) - os.remove(os.path.join(dir_name, file)) - except Exception as e: - logger.error(f"清理临时数据错误...e:{e}") - file_size = 0 - size += file_size + if os.path.exists(dir_name): + for file in os.listdir(dir_name): + try: + file_size = os.path.getsize(os.path.join(dir_name, file)) + os.remove(os.path.join(dir_name, file)) + except Exception as e: + logger.error(f"清理临时数据错误...e:{e}") + file_size = 0 + size += file_size return float(size) diff --git a/plugins/super_help/__init__.py b/plugins/super_help/__init__.py index 6fe13153..f8d7d644 100644 --- a/plugins/super_help/__init__.py +++ b/plugins/super_help/__init__.py @@ -11,7 +11,7 @@ from utils.message_builder import image result = """超级用户帮助: *:可多个类型参数 ?:可省略参数 1.添加/删除管理 [at] [level] - 2.查看群组/查看好友 + 2.所有群组/好友 3.广播- [msg] 4.更新色图 5./t命令帮助 @@ -19,14 +19,14 @@ result = """超级用户帮助: 7.开启/关闭广播通知 [群号] 8.退群 [群号] 9.自检 - 10.更新价格/更加图片 [武器箱] + 10.更新价格/更加图片 ?[武器箱] 11.更新好友信息 12.更新群群信息 13.重载原神/方舟/赛马娘/坎公骑冠剑卡池 14.添加商品 [名称]-[价格]-[描述]-[折扣]-[限时时间] 15.删除商品 [名称(序号)] 16.修改商品 -name [名称(序号)] -price [价格] -des [描述] -discount [折扣] -time [限时] - 17.节日红包 [金额] [数量] [祝福语](可省) *?[指定群 + 17.节日红包 [金额] [数量] ?[祝福语] *?[指定群] 18.更新原神今日素材 19.更新原神资源信息 20.添加pix关键词/uid/pid *[关键词/uid/pid] ?[-f](强制通过不检测) @@ -36,10 +36,13 @@ result = """超级用户帮助: 24.删除pix图片 *[pid] ?[-b](同时加入黑名单) 25.查看pix图库 [keyword] 26.pix检测更新 [update] - 27.检查更新真寻 - 28.真寻重启 - 29.添加/删除群白名单 *[群号] - 30.关闭[功能] ?[群号/private/group](有群号时禁用指定群)""" + 27.pix [-s/-r] ?[tag] + 28.检查更新真寻 + 29.真寻重启 + 30.添加/删除群白名单 *[群号] + 31.关闭[功能] ?[群号/private/group](有群号时禁用指定群) + 32.功能状态 + 33.查看群白名单""" height = len(result.split('\n')) * 24 A = CreateImg(1000, height, font_size=20) diff --git a/plugins/update_gocqhttp/__init__.py b/plugins/update_gocqhttp/__init__.py index 4b9df65d..5a21a83f 100644 --- a/plugins/update_gocqhttp/__init__.py +++ b/plugins/update_gocqhttp/__init__.py @@ -31,7 +31,7 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): await lasted_gocqhttp.finish("gocqhttp没有更新!") if _ulmt.check(event.group_id): await lasted_gocqhttp.finish("gocqhttp正在更新,请勿重复使用该命令", at_sender=True) - _ulmt.set_True(event.group_id) + _ulmt.set_true(event.group_id) try: for file in os.listdir(path): await upload_gocq_lasted(path, file, event.group_id) @@ -39,7 +39,7 @@ async def _(bot: Bot, event: GroupMessageEvent, state: T_State): await lasted_gocqhttp.send(f"gocqhttp更新了,已上传成功!\n更新内容:\n{info}") except Exception as e: logger.error(f"更新gocq错误 e:{e}") - _ulmt.set_False(event.group_id) + _ulmt.set_false(event.group_id) # 更新gocq diff --git a/plugins/what_anime/__init__.py b/plugins/what_anime/__init__.py index 2fbec594..2758699f 100644 --- a/plugins/what_anime/__init__.py +++ b/plugins/what_anime/__init__.py @@ -42,7 +42,7 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): @what_anime.got("img_url", prompt="虚空识番?来图来图GKD") async def _(bot: Bot, event: MessageEvent, state: T_State): img_url = state["img_url"][0] - _ulmt.set_True(event.user_id) + _ulmt.set_true(event.user_id) await what_anime.send("开始识别.....") anime_data_report = await get_anime(img_url) if anime_data_report: @@ -58,4 +58,4 @@ async def _(bot: Bot, event: MessageEvent, state: T_State): f"{event.group_id if isinstance(event, GroupMessageEvent) else 'private'} 识番 {img_url} 未找到!!" ) await what_anime.send(f"没有寻找到该番剧,果咩..", at_sender=True) - _ulmt.set_False(event.user_id) + _ulmt.set_false(event.user_id) diff --git a/plugins/withdraw.py b/plugins/withdraw.py new file mode 100644 index 00000000..b1abc1fd --- /dev/null +++ b/plugins/withdraw.py @@ -0,0 +1,15 @@ +from nonebot import on_command +from nonebot.adapters.cqhttp import Bot, GroupMessageEvent +from nonebot.typing import T_State +import re + + +withdraw_msg = on_command('撤回', priority=5, block=True) + + +@withdraw_msg.handle() +async def _(bot: Bot, event: GroupMessageEvent, state: T_State): + r = re.search(r'\[CQ:reply,id=(\d*)]', event.raw_message) + if r: + await bot.delete_msg(message_id=int(r.group(1)), self_id=int(bot.self_id)) + diff --git a/resources/img/sign/sign_res/background/background_01.jpg b/resources/img/sign/sign_res/background/background_01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fe48e2e0f34bf82eab9122e86779f086ba7b90e1 GIT binary patch literal 36148 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 literal 0 HcmV?d00001 diff --git a/resources/img/sign/sign_res/bar_white.png b/resources/img/sign/sign_res/bar_white.png new file mode 100644 index 0000000000000000000000000000000000000000..e04bca9849eda05101a13727adca9dc7dbae59b3 GIT binary patch literal 584 zcmeAS@N?(olHy`uVBq!ia0y~yV15B)8*#7!$@A|%8Za<0@q4;BhE&XXd&e=a*+9fK zaK)zox|bbYS&m3Z=X?r17s-%T^_AzbiSJ?FJWCd!HY>?$`43rtY!vwz7!K4=d3{&y z<2|Jdu9iS4|7~v`UEWxydZ87>TXS}8jO?p)rq~(G3=9r&r7Ql=u>lG*ScKiVxRiUB z0XqXjLvh!&FOyhf=Kxh4D80GQj;o{)Xe`6qH)Umd7mk8V*)sjj+gtOGFanvfZ&-JI zl>st-{e1B*WI=Z^kohA2>iu;)Ko&6EcGx}-WKQip**r&(;SAFn^K3zk-@b-*^n-+i)`Yw#&@%K literal 0 HcmV?d00001 diff --git a/resources/img/sign/sign_res/border/ava_border_01.png b/resources/img/sign/sign_res/border/ava_border_01.png new file mode 100644 index 0000000000000000000000000000000000000000..b9af9cc2e13154e2f37b758d66b5cb70f1d3ffb6 GIT binary patch literal 54726 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 literal 0 HcmV?d00001 diff --git a/resources/img/sign/sign_res/border/gift_border_02.png b/resources/img/sign/sign_res/border/gift_border_02.png new file mode 100644 index 0000000000000000000000000000000000000000..9d89b37662c8be366e970f45bf290e1e540e2f43 GIT binary patch literal 24931 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 literal 0 HcmV?d00001 diff --git a/resources/img/sign/sign_res/white.png b/resources/img/sign/sign_res/white.png new file mode 100644 index 0000000000000000000000000000000000000000..366c89e40db6bc7a1e15c9e01080ee723fe96798 GIT binary patch literal 3773 zcmeAS@N?(olHy`uVBq!ia0y~yU{zsYVEn?t1{6_|Q+8xv;A`=8aSW-L^Y(@zBZC6Z z0fPhkpFdnAqrz2He*TOOP#HtRfl65r&G6^&1Qrl&)CM$!fnkE73L}te=uzYZaZ`9a zL6kzW2NQ^vbQS_pCnP3;D2HRCT1SIvG)au+oY7KZv_u@OI7TbU(FVb2yJ@tIINFpR f?KX^dktXb1w1WM~y`!;apuVD~tDnm{r-UW|>I4l> literal 0 HcmV?d00001 diff --git a/services/log.py b/services/log.py index 30c76278..fa2443ad 100644 --- a/services/log.py +++ b/services/log.py @@ -1,30 +1,26 @@ -import logging -from datetime import datetime +from datetime import datetime, timedelta from configs.path_config import LOG_PATH -import sys -# CRITICAL 50 -# ERROR 40 -# WARNING 30 -# INFO 20 -# DEBUG 10 -# NOTSET 0 +from loguru import logger as logger_ +from nonebot.log import default_format, default_filter -# _handler = logging.StreamHandler(sys.stdout) -# _handler.setFormatter( -# logging.Formatter('[%(asctime)s %(name)s] %(levelname)s: %(message)s') -# ) -logger = logging.getLogger('hibiki') -logger.setLevel(level=logging.DEBUG) -formatter = logging.Formatter('[%(asctime)s] - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s') +logger = logger_ -file_handler = logging.FileHandler(LOG_PATH + str(datetime.now().date()) + '.log', mode='a', encoding='utf-8') -file_handler.setLevel(level=logging.INFO) -file_handler.setFormatter(formatter) -stream_handler = logging.StreamHandler() -stream_handler.setLevel(logging.INFO) -stream_handler.setFormatter(formatter) +logger.add( + f'{LOG_PATH}/{datetime.now().date()}.log', + level='INFO', + rotation='00:00', + format=default_format, + filter=default_filter, + retention=timedelta(days=30)) + +logger.add( + f'{LOG_PATH}/error_{datetime.now().date()}.log', + level='ERROR', + rotation='00:00', + format=default_format, + filter=default_filter, + retention=timedelta(days=30)) + -logger.addHandler(file_handler) -logger.addHandler(stream_handler) diff --git a/update_info.json b/update_info.json index 65a59212..a7f49d21 100644 --- a/update_info.json +++ b/update_info.json @@ -1,48 +1,11 @@ { "update_file": [ "configs/config.py", - "configs/path_config.py", - "plugins/aconfig", - "plugins/admin_bot_manage", - "plugins/admin_help", - "plugins/ai", - "plugins/alapi", - "plugins/auto_invite", - "plugins/ban", - "plugins/check_zhenxun_update", - "plugins/delete_img", - "plugins/fudu.py", - "plugins/genshin/query_resource_points", - "plugins/group_handle", - "plugins/help", - "plugins/hook.py", - "plugins/jitang.py", - "plugins/move_img", - "plugins/mute.py", - "plugins/nickname.py", - "plugins/one_friend", - "plugins/open_cases", - "plugins/parse_bilibili_json.py", - "plugins/pixiv", - "plugins/reimu", - "plugins/remind", - "plugins/roll.py", - "plugins/russian", - "plugins/search_anime", - "plugins/search_buff_skin_price", - "plugins/send_img", - "plugins/send_setu", - "plugins/shop/gold_redbag", - "plugins/sign_in/group_user_checkin.py", - "plugins/statistics_hook.py", - "plugins/super_help", - "plugins/update_info.py", - "plugins/update_setu", - "plugins/upload_img", - "plugins/weather", - "plugins/yiqing", - "utils/image_utils.py" + "plugins", + "utils", + "services", + "models" ], - "add_file": ["plugins/group_manager", "resources/img/other/webtop.png", "utils/static_data"], - "delete_file": ["plugins/group_level", "plugins/nonebot_plugin_manager"] + "add_file": [], + "delete_file": [] } diff --git a/utils/browser.py b/utils/browser.py index e4eee6c4..b08f728b 100644 --- a/utils/browser.py +++ b/utils/browser.py @@ -4,6 +4,7 @@ from playwright.async_api import Browser, async_playwright import nonebot from nonebot import Driver from services.log import logger +import platform driver: Driver = nonebot.get_driver() @@ -13,14 +14,16 @@ _browser: Optional[Browser] = None async def init(**kwargs) -> Optional[Browser]: - # try: - global _browser - browser = await async_playwright().start() - _browser = await browser.chromium.launch(**kwargs) - return _browser - # except NotImplementedError: - # logger.warning("win环境下 初始化playwright失败....请替换环境至linux") - # return None + if platform.system() == "Windows": + return None + try: + global _browser + browser = await async_playwright().start() + _browser = await browser.chromium.launch(**kwargs) + return _browser + except NotImplementedError: + logger.warning("win环境下 初始化playwright失败....请替换环境至linux") + return None async def get_browser(**kwargs) -> Browser: diff --git a/utils/image_utils.py b/utils/image_utils.py index 7b8fa547..00bd3730 100644 --- a/utils/image_utils.py +++ b/utils/image_utils.py @@ -45,7 +45,9 @@ def get_img_hash(image_file: str) -> ImageHash: return hash_value -def compressed_image(in_file: Union[str, Path], out_file: Union[str, Path] = None, ratio: float = 0.9): +def compressed_image( + in_file: Union[str, Path], out_file: Union[str, Path] = None, ratio: float = 0.9 +): """ 说明: 压缩图片 @@ -56,15 +58,19 @@ def compressed_image(in_file: Union[str, Path], out_file: Union[str, Path] = Non """ in_file = Path(IMAGE_PATH) / in_file if isinstance(in_file, str) else in_file if out_file: - out_file = Path(IMAGE_PATH) / out_file if isinstance(out_file, str) else out_file + out_file = ( + Path(IMAGE_PATH) / out_file if isinstance(out_file, str) else out_file + ) else: out_file = in_file h, w, d = cv2.imread(str(in_file.absolute())).shape - img = cv2.resize(cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio))) + img = cv2.resize( + cv2.imread(str(in_file.absolute())), (int(w * ratio), int(h * ratio)) + ) cv2.imwrite(str(out_file.absolute()), img) -def alpha2white_PIL(pic: Image) -> Image: +def alpha2white_pil(pic: Image) -> Image: """ 说明: 将图片透明背景转化为白色 @@ -131,16 +137,19 @@ class CreateImg: def __init__( self, - w, - h, - paste_image_width=0, - paste_image_height=0, - color="white", - image_type="RGBA", - font_size=10, - background="", - ttf="yz.ttf", - ratio=1, + w: int, + h: int, + paste_image_width: int = 0, + paste_image_height: int = 0, + color: Union[str, Tuple[int, int, int], Tuple[int, int, int, int]] = "white", + image_mode: str = "RGBA", + font_size: int = 10, + background: Union[Optional[str], BytesIO] = None, + ttf: str = "yz.ttf", + ratio: float = 1, + is_alpha: bool = False, + plain_text: Optional[str] = None, + font_color: Optional[Tuple[int, int, int]] = None, ): """ 参数: @@ -149,11 +158,13 @@ class CreateImg: :param paste_image_width: 当图片做为背景图时,设置贴图的宽度,用于贴图自动换行 :param paste_image_height: 当图片做为背景图时,设置贴图的高度,用于贴图自动换行 :param color: 生成图片的颜色 - :param image_type: 图片的类型 + :param image_mode: 图片的类型 :param font_size: 文字大小 :param background: 打开图片的路径 :param ttf: 字体,默认在 resource/ttf/ 路径下 :param ratio: 倍率压缩 + :param is_alpha: 是否背景透明 + :param plain_text: 纯文字文本 """ self.w = int(w) self.h = int(h) @@ -161,9 +172,14 @@ class CreateImg: self.paste_image_height = int(paste_image_height) self.current_w = 0 self.current_h = 0 - self.ttfont = ImageFont.truetype(TTF_PATH + ttf, int(font_size)) + self.font = ImageFont.truetype(TTF_PATH + ttf, int(font_size)) if not background: - self.markImg = Image.new(image_type, (self.w, self.h), color) + if plain_text: + ttf_w, ttf_h = self.getsize(plain_text) + self.w = self.w if self.w > ttf_w else ttf_w + self.h = self.h if self.h > ttf_h else ttf_h + self.markImg = Image.new(image_mode, (self.w, self.h), color) + self.markImg.convert(image_mode) else: if not w and not h: self.markImg = Image.open(background) @@ -181,14 +197,26 @@ class CreateImg: self.markImg = Image.open(background).resize( (self.w, self.h), Image.ANTIALIAS ) + if is_alpha: + array = self.markImg.load() + for i in range(w): + for j in range(h): + pos = array[i, j] + is_edit = (sum([1 for x in pos[0:3] if x > 240]) == 3) + if is_edit: + array[i, j] = (255, 255, 255, 0) self.draw = ImageDraw.Draw(self.markImg) self.size = self.w, self.h + if plain_text: + fill = font_color if font_color else (0, 0, 0) + self.text((0, 0), plain_text, fill) def paste( self, img: "CreateImg" or Image, pos: Tuple[int, int] = None, alpha: bool = False, + center_type: Optional[str] = None, ): """ 说明: @@ -197,7 +225,26 @@ class CreateImg: :param img: 已打开的图片文件,可以为 CreateImg 或 Image :param pos: 贴图位置(左上角) :param alpha: 图片背景是否为透明 + :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 """ + if center_type: + if center_type not in ["center", "by_height", "by_width"]: + raise ValueError( + "center_type must be 'center', 'by_width' or 'by_height'" + ) + width, height = 0, 0 + if not pos: + pos = (0, 0) + if center_type == "center": + width = int((self.w - img.w) / 2) + height = int((self.h - img.h) / 2) + elif center_type == "by_width": + width = int((self.w - img.w) / 2) + height = pos[1] + elif center_type == "by_height": + width = pos[0] + height = int((self.h - img.h) / 2) + pos = (width, height) if isinstance(img, CreateImg): img = img.markImg if self.current_w == self.w: @@ -222,10 +269,38 @@ class CreateImg: 参数: :param msg: 文字内容 """ - return self.ttfont.getsize(msg) + return self.font.getsize(msg) + + def point(self, pos: Tuple[int, int], fill: Optional[Tuple[int, int, int]] = None): + """ + 说明: + 绘制多个或单独的像素 + 参数: + :param pos: 坐标 + :param fill: 填错颜色 + """ + self.draw.point(pos, fill=fill) + + def ellipse( + self, + pos: Tuple[int, int, int, int], + fill: Optional[Tuple[int, int, int]] = None, + outline: Optional[Tuple[int, int, int]] = None, + width: int = 1, + ): + """ + 说明: + 绘制圆 + 参数: + :param pos: 坐标范围 + :param fill: 填充颜色 + :param outline: 描线颜色 + :param width: 描线宽度 + """ + self.draw.ellipse(pos, fill, outline, width) def text( - self, pos: Tuple[int, int], text: str, fill: Tuple[int, int, int] = (0, 0, 0) + self, pos: Tuple[int, int], text: str, fill: Tuple[int, int, int] = (0, 0, 0), center_type: Optional[str] = None ): """ 说明: @@ -234,8 +309,24 @@ class CreateImg: :param pos: 文字位置 :param text: 文字内容 :param fill: 文字颜色 + :param center_type: 居中类型,可能的值 center: 完全居中,by_width: 水平居中,by_height: 垂直居中 """ - self.draw.text(pos, text, fill=fill, font=self.ttfont) + if center_type: + if center_type not in ["center", "by_height", "by_width"]: + raise ValueError( + "center_type must be 'center', 'by_width' or 'by_height'" + ) + w, h = self.w, self.h + ttf_w, ttf_h = self.getsize(text) + if center_type == 'center': + w = int((w - ttf_w) / 2) + h = int((h - ttf_h) / 2) + elif center_type == 'by_width': + w = int((w - ttf_w) / 2) + elif center_type == 'by_height': + h = int((h - ttf_h) / 2) + pos = (w, h) + self.draw.text(pos, text, fill=fill, font=self.font) def save(self, path: str): """ @@ -291,13 +382,14 @@ class CreateImg: 参数: :param word: 文本内容 """ - return self.ttfont.getsize(word)[0] > self.w + return self.font.getsize(word)[0] > self.w - def transparent(self, n: int = 0): + def transparent(self, alpha_ratio: float = 1, n: int = 0): """ 说明: 图片透明化 参数: + :param alpha_ratio: 透明化程度 :param n: 透明化大小内边距 """ self.markImg = self.markImg.convert("RGBA") @@ -305,7 +397,7 @@ class CreateImg: for i in range(n, x - n): for k in range(n, y - n): color = self.markImg.getpixel((i, k)) - color = color[:-1] + (100,) + color = color[:-1] + (int(100 * alpha_ratio),) self.markImg.putpixel((i, k), color) def pic2bs4(self) -> str: diff --git a/utils/message_builder.py b/utils/message_builder.py index 452e5356..f6135741 100644 --- a/utils/message_builder.py +++ b/utils/message_builder.py @@ -1,14 +1,13 @@ from configs.path_config import IMAGE_PATH, VOICE_PATH from nonebot.adapters.cqhttp.message import MessageSegment from services.log import logger -from typing import Optional +from typing import Union from pathlib import Path import os -import ujson def image( - img_name: str = None, path: str = None, abspath: str = None, b64: str = None + img_name: Union[str, Path] = None, path: str = None, abspath: str = None, b64: str = None ) -> MessageSegment or str: """ 说明: @@ -29,6 +28,8 @@ def image( elif b64: return MessageSegment.image(b64 if "base64://" in b64 else "base64://" + b64) else: + # if isinstance(img_name, Path): + # return MessageSegment.image(img_name) if "http" in img_name: return MessageSegment.image(img_name) if len(img_name.split(".")) == 1: diff --git a/utils/static_data/__init__.py b/utils/static_data/__init__.py index 7563e9c0..c400aadf 100644 --- a/utils/static_data/__init__.py +++ b/utils/static_data/__init__.py @@ -2,10 +2,13 @@ from typing import Optional from .group_manager import GroupManager from pathlib import Path from .data_source import init +from .data_class import StaticData -# 群权限 +# 群管理 group_manager: Optional[GroupManager] = GroupManager( Path() / "data" / "manager" / "group_manager.json" ) +withdraw_message_id_manager: Optional[StaticData] = StaticData(None) + init(group_manager) diff --git a/utils/static_data/data_class.py b/utils/static_data/data_class.py index cca80a3b..0915b788 100644 --- a/utils/static_data/data_class.py +++ b/utils/static_data/data_class.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Optional from pathlib import Path import ujson as json @@ -8,12 +8,13 @@ class StaticData: 静态数据共享类 """ - def __init__(self, file: Path): - file.parent.mkdir(exist_ok=True, parents=True) - self.file = file + def __init__(self, file: Optional[Path]): self.data = {} - if file.exists(): - self.data: dict = json.load(open(file, "r", encoding="utf8")) + if file: + file.parent.mkdir(exist_ok=True, parents=True) + self.file = file + if file.exists(): + self.data: dict = json.load(open(file, "r", encoding="utf8")) def set(self, key, value): self.data[key] = value diff --git a/utils/static_data/group_manager.py b/utils/static_data/group_manager.py index d34bfc74..6f16744e 100644 --- a/utils/static_data/group_manager.py +++ b/utils/static_data/group_manager.py @@ -1,4 +1,5 @@ -from typing import Optional +from configs.config import DEFAULT_GROUP_LEVEL +from typing import Optional, List from pathlib import Path from .data_class import StaticData @@ -132,6 +133,13 @@ class GroupManager(StaticData): if group_id in self.data['super']['white_group_list']: self.data['super']['white_group_list'].remove(group_id) + def get_group_white_list(self) -> List[str]: + """ + 说明: + 获取所有群白名单 + """ + return self.data['super']['white_group_list'] + def _set_plugin_status( self, plugin_cmd: str, @@ -183,4 +191,6 @@ class GroupManager(StaticData): 参数: :param group_id: 群号 """ - self.data["group_manager"][group_id] = {"level": 5, "close_plugins": []} + self.data["group_manager"][group_id] = {"level": DEFAULT_GROUP_LEVEL, "close_plugins": []} + + diff --git a/utils/utils.py b/utils/utils.py index 668cd7e6..51d082fb 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -3,7 +3,7 @@ from collections import defaultdict from nonebot import require from configs.path_config import TXT_PATH from configs.config import SYSTEM_PROXY -from typing import List, Union +from typing import List, Union, Optional from nonebot.adapters import Bot import nonebot import pytz @@ -48,16 +48,16 @@ class UserExistLimiter: self.flag_data = defaultdict(bool) self.time = time.time() - def set_True(self, key: Union[str, int, float]): + def set_true(self, key: Union[str, int, float]): self.time = time.time() self.flag_data[key] = True - def set_False(self, key: Union[str, int, float]): + def set_false(self, key: Union[str, int, float]): self.flag_data[key] = False def check(self, key: Union[str, int, float]) -> bool: if time.time() - self.time > 30: - self.set_False(key) + self.set_false(key) return False return self.flag_data[key] @@ -169,12 +169,15 @@ def is_number(s: str) -> bool: return False -def get_bot() -> Bot: +def get_bot() -> Optional[Bot]: """ 说明: 获取 bot 对象 """ - return list(nonebot.get_bots().values())[0] + try: + return list(nonebot.get_bots().values())[0] + except IndexError: + return None def get_message_at(data: str) -> List[int]: @@ -322,3 +325,21 @@ def cn2py(word: str) -> str: for i in pypinyin.pinyin(word, style=pypinyin.NORMAL): temp += "".join(i) return temp + + +def change_picture_links(url: str, mode: str): + """ + 说明: + 根据配置改变图片大小 + 参数: + :param url: 图片原图链接 + :param mode: 模式 + """ + if mode == 'master': + img_sp = url.rsplit('.', maxsplit=1) + url = img_sp[0] + img_type = img_sp[1] + url = url.replace('original', 'master') + f'_master1200.{img_type}' + return url + +